Kernel Pwn && SCTF flyingpwn WP
周末做了下SCTF的flyingpwn,顺带把kernelpwn总结一下
下面介绍知识的时候都以flyingpwn为例
基础知识
一般题目会给下面几个文件
boot.sh是qemu的启动脚本
1 | qemu-system-x86_64 \ |
Kernel 保护机制
boot.sh中主要关注的就是开了哪些kernel保护机制
- Kernel stack cookies (or canaries) - this is exactly the same as stack canaries on userland. It is enabled in the kernel at compile time and cannot be disabled.
- Kernel address space layout randomization (KASLR) - also like
ASLR
on userland, it randomizes the base address where the kernel is loaded each time the system is booted. It can be enabled/disabled by addingkaslr
ornokaslr
under-append
option.- Supervisor mode execution protection (SMEP) - this feature marks all the userland pages in the page table as non-executable when the process is in kernel-mode. In the kernel, this is enabled by setting the
20th bit
of Control RegisterCR4
. On boot, it can be enabled by adding+smep
to-cpu
, and disabled by addingnosmep
to-append
.- Supervisor Mode Access Prevention (SMAP) - complementing
SMEP
, this feature marks all the userland pages in the page table as non-accessible when the process is in kernel-mode, which means they cannot be read or written as well. In the kernel, this is enabled by setting the21st bit
of Control RegisterCR4
. On boot, it can be enabled by adding+smap
to-cpu
, and disabled by addingnosmap
to-append
.- Kernel page-table isolation (KPTI) - when this feature is active, the kernel separates user-space and kernel-space page tables entirely, instead of using just one set of page tables that contains both user-space and kernel-space addresses. One set of page tables includes both kernel-space and user-space addresses same as before, but it is only used when the system is running in kernel mode. The second set of page tables for use in user mode contains a copy of user-space and a minimal set of kernel-space addresses. It can be enabled/disabled by adding
kpti=1
ornopti
under-append
option.
KASLR
这里虽然明确是说开启了smep,而没有开smap,kaslr没说,但是默认是开启的,所以这里kernel base会随机化,对此我们需要想办法leak,不过有些题确实有爆破的打法,开了KASLR后kernelbase有512个可能的加载地址,我们需要用默认的基地址0xffffffff81000000去不停的打。
不过我们调试的时候为了方便,可以在这里手动加上nokaslr来关闭随机化
SMEP和SMAP
关于SMEP和SMAP的区别,一个是不可执行Execute,一个是不可Access,开了SMAP限制就比较严格,如果只开了SMEP,虽然我们不可以直接去执行用户空间的代码,但是我们依然可以访问用户空间内存,比较常见的利用思路是栈迁移到用户空间,然后打kernel ROP。
另外SMEP是受CR4的第20bit控制的,kernel中有一个函数是native_write_CR4(),其内部会执行mov cr4,rdi,只要我们设置rdi来清空cr4的第20bit就可以关闭SMEP
但是遗憾的是,在新版本的kernel中,CR4已经被锁死了
1 | void native_write_cr4(unsigned long val) |
1
2 > pinned CR4 bits changed: 0x100000!?
>
如果修改CR4,在Kernel log中就会打印上面这个错误,所以利用思路只剩内核ROP。
kernel crash
这个是我在搜索信息的时候发现的不起眼的选项
1 | oops=panic panic=1 |
这个选项其实非常关键
当内核发生crash时,会提示这样的信息
1 | [<ffffffffc011b47d>] ? do_ioctl+0x34d/0x4c0 [vuln_driver] |
我们可以借此计算kernelbase,要利用这个方法,我们必须得保证crash时,内核不会重启,而这句的意思就是将oops类型的错误当作panic错误处理,而panic会使得1s后重启内核,所以这句可以限制kaslr的利用
core and thread
在boot.sh中指定了是两核两线程,多核这个会影响到内核slab的分配,在一个CPU上运行的进程是共享slab池的,不同的CPU上的进程不共享slab,因此有时为了避免这个影响,我们得手动设置下亲和性
1 | set_thread_aff(0) |
Kernel extract
bzImage是内核镜像的压缩格式文件,解压脚本如下,用法为
1 | $ ./extract-image.sh ./vmlinuz > vmlinux |
1 |
|
提取出vmlinux之后,直接用ROPGadget获得可用gadgets备用
1 | ROPgadget --binary ./vmlinux > gadgets.txt |
文件系统
提取文件系统直接用binwalk就行,
1 | binwalk -Me rootfs.img |
另一个常用的操作是重打包文件系统,因为我们一般要修改init文件和放exp程序进去
重打包脚本如下
1 | find . -print0 \ |
将该脚本放到rootfs根目录下,运行下面命令,即可在上层目录生成重打包的文件系统
1 | sh cpio.sh ../rootfs.cpio |
在文件系统中,一般能够找到题目驱动,比如flying.ko,我们一般还要关注init文件,这个文件是系统的启动文件
1 |
|
比较关键的几句是
1 | poweroff -d 240 -f & |
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict1
2
3
4
5
6
这两句设置dmesg限制,当我们为非root用户时是看不到kernel debug message的,我们也没法调用dmesg命令
为了调试方便,我们一般都设置成0,但是注意这里的限制不影响printk,像题目中使用printk打印信息我们还是能看到的(做题的时候以为看不到,导致思路朝盲pwn上想浪费了大量时间)
![image-20211229113232699](.\image-20211229113232699.png)
setsid /bin/cttyhack setuidgid 1000 /bin/sh1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这句设置我们登录的shell是1000用户组,为了调试方便,我们这里可以改成0
### 内核符号地址
获取内核版本号
![image-20211229114930507](.\image-20211229114930507.png)
当我们把shell权限设置成root后,就可以通过读取内核符号表的方式来获取到函数地址,命令为
```bash
cat /proc/kallsyms | grep func_name
内核基地址的符号
1 | cat /proc/kallsyms | grep startup_64 |
驱动基地址的获取
1 | cat /proc/modules | grep driver_name |
Kmalloc
内核中申请内存一般是用kmalloc,kmalloc的标准分配方式很类似glibc的方式,也是把要分配的内存按大小划分成各种chunk,只不过内核中叫slab,同类的free slab也是按链表的方式链接在一起的
题目中的这个分配函数是kmalloc中的内部函数
根据请求的分配的内存大小,kmalloc_index获得该size对应的index,然后按index分配,一般来说,块的大小按2的阶计算,kmalloc_caches数组中的下标其实也是表示了块的大小即2^n字节,但是实际的如下图
像exploit中比较常用的结构体,tty_operation是1024,cred结构体是192
针对freelist本身似乎也有一种exploit的方式,但是具体还有待研究
Kernoob: kmalloc without SMAP (kirin-say.top)
调试
一般是用连接到qemu的方式调试,在启动脚本上加上-s
然后gdb这边用下面命令就可以连接上去,但是调试内核有个问题是,如果装了gdb插件比如peda之类的
1 | gdb ./vmlinux -q |
调试倒也能调试,一个是非常慢,另一个会出错误,所以需要禁用掉插件,一般注释掉这个文件里的内容即可
1 | gedit ~/.gdbinit |
题目分析
这个驱动没有read函数
IOCTL函数
0x6666是释放申请的sctf_buf,这里释放之后并没有清空sctf_buf,存在UAF
0x7777是打印sctf_buf里的内容,仔细看这里,是经典的格式化字符串漏洞的模式,这里做题的时候我竟然没考虑到
0x5555 是给sctf_buf申请内存
write函数是往sctf_buf里面写入内容,这里特点是倒着写的
漏洞分析
首先就是刚才提到的UAF,我们释放了sctf_buf之后没有清空,
申请内存块用的是
kmem_cache_alloc_trace(kmalloc_caches[7], 0xCC0LL);
对照前面的大小表,index为7的大小为128,一般这种UAF加slab的攻击方法是堆喷到内核中某个关键结构体,通过覆写其内容来exploit,这种攻击方式需要我们攻击的关键结构体的大小和我们这里能write的内存块是相同的index
这里关键结构体的选取已经有大佬做总结了,这里我参考pzhxbz的翻译
(翻译)kernel pwn中能利用的一些结构体 – pzhxbz的技术笔记本
可以找到一个结构体叫subprocess_info,通过覆写其内容我们可以实现控制流劫持,只不过不知道是不是内核版本的不同,上文中提到的要覆写的位置和题目中内核是有出入的,下面我会给出分析
subprocess_info
结构体如下
1 | struct subprocess_info { |
触发方式为执行下面这句代码
1 | socket(22, AF_INET, 0); |
socket会按下面的流程执行代码
1 | int call_usermodehelper(const char *path, char **argv, char **envp, int wait) |
所以如果我们能修改掉cleanup指针,就可以实现控制流劫持
在IDA中找到对应部分,可以看到应该修改的cleanup指针位于0x60也就是[12]的偏移处,至于info->path的值,实践后发现,这个值不用改,反正这题write也是倒着写的,所以我们只要改掉cleanup就行了
泄露kernel base
由于存在格式化字符串漏洞,所以直接打印栈上的值就可以leak base
不过这里也有个细节是泄露用的 %lld
1 | strcpy(buf, "data : %lld %lld %lld %lld %lld \xff%lld %lld %lld %lld %lld %lld %lld %lld %lld %lld %lld %lld\n"); |
这里不能像做libc一样,用%p,因为printk的%p有拓展用法
exploit
泄露完base之后我们利用条件竞争来劫持控制流到如下gadget
这句代码可以实现栈迁移到用户空间,这里还有个小细节是,我们知道内核栈一般是0xffffffff开头的,如果我们直接mov esp,比如这里,rsp会变成0xffffffff83000000,栈依然在我们无法控制的内核空间中,所以为什么呢
这里其实用到了x64下的一个特性,mov esp,时会清空高位,所以我们赋值完之后,rsp 就等于 esp(太细节了orz)
ROP布局代码
1 | void setup_chunk_rop(unsigned long kernel_base) |
可以看到我们就是在0x83000000的位置布局的rop,这里为什么要在0x8300 0000前后都分配内存,因为
ROP链是经典的布置了,基本上都是这样,没什么好说的,找到地址填上去就行了
条件竞争的部分
1 | ret = ioctl(fd, UAF, 0); |
先free掉内存,然后创建一个线程,不停的往其0x60位置写入stack pivot的gadget,因为其实倒着写的,所以这里其实用的是0x80 - 0x60 = 0x20
1 | void *memset_buf(void *tmp) |
后面用socket去创建subprocess_info,只要能在创建subprocess_info后,cleanup前写入gadget就能成功,出乎意料,这个成功率还是很高的。
这里还有个小问题是,因为新开的线程会不停的写入值,因为会不停的打印write,我们获得的shell能够正常使用吗(被write的log信息干扰),这里实际打过后会发现可以正常获得shell的,我猜测跟shell的前台进程和后台进程有关,execve(“/bin/sh”)执行后,这个新进程就变成前台进程了,因此write的log信息并不会显示出来
完整的exp
1 |
|
编译命令为
1 | gcc -osploit -pthread -static -Os suexp.c -lutil -masm=intel -s |
pthread库肯定是要链接上的,编译成static也是为了避免依赖库的问题,-masm=intel是为了支持intel汇编格式,这里如果你用的不是intel汇编就可以不加这句。
总结
感谢SU的wp,上面exp基本是拷贝SU的exp,只能说有些队wp原来还能写一句话,现在连一句话都不贴了,麻
SCTF 2021 SU Write-Up | TEAM-SU
Refs:
Learning Linux Kernel Exploitation - Part 1 - Midas Blog (lkmidas.github.io)
Kernel Pwn 学习之路 - 番外 - 安全客,安全资讯平台 (anquanke.com)
Kmalloc申请内存源码分析 - 云+社区 - 腾讯云 (tencent.com)
(翻译)kernel pwn中能利用的一些结构体 – pzhxbz的技术笔记本
CVE-2016-6187复现以及struct subprocess_info的劫持-Pwn影二つ的博客 (kagehutatsu.com)
workqueue.h - include/linux/workqueue.h - Linux source code (v5.15.11) - Bootlin 在线看内核代码的网站