Kernel_pwn FG_KASLR in ROP
第一次遇到FG_KASLR机制,算是KASLR的加强版,加大了一些ROP的难度,但仍有bypass的方法。题目来自hxpCTF2020 kernel_rop。
FG_KASLR
首先了解下FG_KASLR:
全称是Function Granular KASLR
,译为函数颗粒化地址随机分布。我们知道,开启一般的KASLR会使得每次目标文件加载到的内存起始地址会随机化,但只要能leak一个在相应内存区域的地址,我们就能通过其offset固定的原因得到任何一个在此内存区域的地址。
文档对FG_KASLR描述为:
1 | This patch set is an implementation of finer grained kernel address space |
和KASLR不同,FG_KASLR做了更随机化。它在load的时候按照函数级别的细粒度来重排内核代码,这样做它的boot time增加了1秒,但使得我们通过offset的方法得到真实地址的方法”破灭“。即使我们leak出了一个在.text段的地址,但FG_KASLR是函数级上的随机化,对函数的重排使得offset也变得随机化,我们也就无法通过leak_addr+offset
来确定地址了。
这是我一开始看到这个机制的想法,但当我做到一道开了FG_KASLR的kernel_pwn之后,才发现我too yuang too simple了。
hxpCTF2020 kernel_rop
拿到题目后发现内核版本为vmlinuz: Linux kernel x86 boot executable bzImage, version 5.9.0-rc6+
算是比较新了。
接着查看etc/init.d/
里的rcS初始化脚本看到insmod了hackme.ko
,应该就是有漏洞的模块了。
模块提供了hackme_read()
和hackme_write()
功能,漏洞在write功能里:
1 | ssize_t __fastcall hackme_write(file *f, const char *data, size_t size, loff_t *off) |
_memcpy(tmp, hackme_buf, v5);
造成栈溢出。因此利用方法就比较明确了,先利用read功能leak地址,找gadgets,之后rop执行commit_creds(prepare_kernel_cred(0))
提权。
首先leak地址:
1 | read(global_fd, leak, sizeof(leak)); |
传入一个leak数组,看下都读到了啥:
1 | 0: 0xffff888007201020 |
多重复leak几次后会发现leak[38]
处的0xffffffff8100a157
是没变化的,而且这个地址是kernel image里的地址,因此通过这个地址我们容易得到image_base的地址。
接着我们先手动root下,通过/proc/kallsyms
去找找函数地址
1 | 38: 0xffffffffa820a157 |
上面是两次启动qemu后先leak地址在通过/proc/kallsyms
查看函数地址得到的结果。commit_creds
函数和image_base
的offset分别为0xa93f90和0x42e6c0,可以看到,我们无法通过image_base+offset
来得到commit_creds()
的地址,这也是开启了FG_KASLR的效果。
但在计算__ksymtab_commit_creds
和image_base
的偏移时发现:
1 | >>> hex(0xffffffffa9187d90-0xffffffffa8200000) |
offset是固定的。可以看到,FG_KASLR并没有做到Kernel地址的完全随机化,没有受到影响的区域如下:
1、存在于.text_base
到__x86_retpoline_r15(.text+400dc6)
的函数没有受到影响。显然commit_creds
和prepare_kernel_cred()
没有包含在内,但我们还是可以在这里找一些gadget的。
2、KPIT trampolineswapgs_restore_regs_and_return_to_usermode()
没有受到影响。
3.内核符号表(kernel symbol table)ksymtab开始于.text+0xf85198
没有受到影响。
这也说明了我们计算__ksymtab_commit_creds
的偏移固定的原因。ksymtab的结构如下
1 | struct kernel_symbol { |
其中value_offset
是ksymtab到对应函数的偏移,以commit_creds函数为例就是__ksymtab_commit_creds + value_offset = commit_creds
。
使用到的gadgets有:
1 | pop_rax_ret = image_base + 0x4d11; |
思路如下:
1、通过read功能leak出image_base
和stack里的cookie
,分别对应leak[38]
和leak[2]
。
2、通过image_base+offset
计算__ksymtab_commit_creds
,__ksymtab_prepare_kernel_cred()
以及gadgets地址。
3、构造ROP,利用gadget将ksymtab的value_offset
存放rax,再利用KPTI trampoline
返回userland得到其值,计算得真实函数地址,再做一次类似的ROP从而得到commit_creds()
和prepare_kernel_cred()
函数地址。
4、由于有KPTI而且我们的gadget里没有能类似mov rdi, rax
的功能,只能分解利用的过程。我们的ROP应使用KPTI trampoline
将提权分成两个stage:1.ROP执行prepare_kernel_cred(0)
,执行结果于rax,利用KPTI trampoline
返回userland得到rax值(即cred_struct_va
)。2.ROP执行commit_cred(cred_struct_va)
,利用KPTI trampoline
返回userland执行system('/bin/sh');
。
exp:
1 |
|
参考:
https://lkmidas.github.io/posts/20210205-linux-kernel-pwn-part-3/
https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/