之前学习过一会kernelpwn,但感觉学习的不是很深入很扎实,导致现在好多东西也都忘了,那就再入门一次吧。
heap-overflow
就像用户态的堆溢出可以进行漏洞利用,同样的内核态的上堆溢出也可以进行漏洞利用,内核态的堆是通过slub/slab进行管理的,他把空闲队列以链表的形式组织,所以在我现在认为堆溢出一共有两种利用方式,一种是通过溢出覆盖下一个堆的next来达到任意地址申请,另一种是heap spray
,我们可以利用内核本身的一些结构体,这些结构体里有函数指针,通过溢出可以覆盖到函数指针,进而进行rip的劫持。
例题:InCTF2021 - Kqueue
启动脚本
1 |
|
通过脚本可知cpu使用了kvm进行虚拟,虚拟机开了kaslr,没开kpti
文件系统启动脚本
由于题目的文件系统由buildroot构建,我不是很懂,所以换了个文件系统,不影响做题,就是加载完自写内核模块然后设置权限。
1 |
|
内核模块
题目直接给了源码,可以直接阅读源码,通过阅读源码可以逆出这个ko模块在内核实现了一种队列管理(私以为不是很写的不是很高效),这是ioctl()
函数源码
1 | static noinline long kqueue_ioctl(struct file *file, unsigned int cmd, unsigned long arg){ |
传入不同的cmd执行不同的函数,这些函数看到函数名就能知道是干啥的,非常奇怪的是他会对传入的request
结构体进行检查,如果不符合要求就会执行err
函数,但是这个函数不会exit(),所以检查相当于没检查.
1 | static long err(char* msg){ |
其中比较重要的就是create_kqueue()
函数和save_kqueue_entries()
函数
1 | static noinline long create_kqueue(request_t request){ |
在creat_kqueue
中存在一个一个整数溢出__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space)
,request.max_entries
是一个uint32_t变量,所以如果传入的是0xffffffff,拿相加就会变成0,然后在save_kqueue
中存在validate(memcpy(new_queue,queue->data,request.data_size));
,如果在create_kqueue
中整数溢出了,那这里的new_queue
就是一个queue
大小是0x20,但是memcpy的大小是我们可以控制的,所以到了这步就可以溢出0x20的堆了,溢出0x20的堆改怎么利用呢。这里就用到了第二种思路。
当打开一个stat文件比如(“/proc/self/stat”)的时候就会在内核分配一个seq_operations
结构体,这个结构体的大小是0x20,记录着四个函数指针,当我们向stat使用read函数读的时候就会执行seq_operations
结构体的start函数指针,所以我们可以利用堆喷让这个结构体置于new_queue
堆块的下面,然后溢出覆盖第一个函数指针,最后read(seq_fd,data,0x20)就可以劫持rip了。
1 | struct seq_operations { |
由于没有开smep所以可以直接把rip劫持到用户态,但注意此时的栈依旧在内核上,里面有内核代码地址信息,可以通过写shellcode从栈上得到commit_creds
和prepare_kernel_cred
地址,然后提权后返回用户态getshell
脚本
1 |
|
1 | / $ id |
heap spray
内核堆喷,依我之见主要针对的是UAF和堆溢出,利用大量的堆喷来得到UAF所指的堆或者申请到可以堆溢出的堆的下一个堆,Kqueue就是这个思路,下面的例题notebook是上一个思路。
启动脚本
从启动脚本中可以看见开起了kaslr,smep,smap,然后设置了两个cpu,每个cpu两个核,所以最多可以跑四个线程。然后从/sys/devices/system/cpu/vulnerabilities/*
文件中可以看出开启了KPTI.
1 |
|
1 | / $ cat /sys/devices/system/cpu/vulnerabilities/* |
文件系统启动脚本
1 |
|
这道题虽然是堆喷,但是最关键的还是条件竞争,而且是我现在水平来看是比较难以察觉但是理解了又觉得十分厉害的利用,条件竞争主要还是使用copy
函数,漏洞就出在add
和edit
函数中
1 | __int64 __fastcall noteedit(size_t idx, size_t newsize, void *buf) |
解法一:userfaultfd+uaf+tty_struct+rop
1 |
|
解法二:userfaultfd+uaf+tty_struct+work_for_cpu_fn
在开启了多核支持的内核中都会有这个work_for_cpu_fn
,它就相当于一个gadget,执行rdi+0x20,参数是rdi+0x28,把返回值保存在rdi+0x30的地方,假如我控制tty_struct
的tty_operations
的的ioctl
函数指针为work_for_cpu_fn
,在调用ioctl(tty_fd,0x200,0x200)
就会调用tty_operations
的ioctl
也就是work_for_cpu_fn
函数,此时rdi就是tty_struct,而此时tty_struct能劫持的话就能任意函数执行了,我们可以执行prepare_kernel_cred(0)
然后把返回值存储在tty_struct+0x30
的地方,执行完work_for_cpu_fn
之后内核自动返回用户态,就不用我们自己构造了,然后再执行一次commit_creds(tty_struct+0x30)
就可以得到root权限了,最后返回用户态执行system("/bin/sh")
;
1 | struct work_for_cpu { |
exp
1 |
|
解法二看别人博客是稳定性更好,但是我跑好几次才能成功一次,远没有解法一来的稳定,想深究就得嗯调(不知道代码在哪),哎。
这道题debug了快两天,其中比较坑的是就是关于tty_struct
和tty_operations
的伪造了,tty_struct
如果伪造的有问题的话就会直接引起kernel painc,所以得memcpy(fake_tty_struct,tty_struct,0x50)
然后再伪造成功率高一些(关键是找不到这块的源码在哪,不能看代码分析,只能嗯调,还是太菜了)。然后是关于tty_operation
的伪造,本来的想法是直接在tty_operation
上构造好完整的rop执行得了,但是一旦在write
指针后面写东西的话就会崩掉。最后也只能跳到一个堆上再执行rop了。关键还是自己对内核不熟悉,就算想看代码都不知道在哪,非常折磨人,学完内核利用的基础知识点之后得赶紧把内核学习和内核代码阅读提上日程了。
总结:通过这道题真的能感觉到内核利用中同步引发的条件竞争的魅力,代码看着完全没有问题,但是通过阻塞就能硬生生构造出一个uaf,死高一,以后在漏洞利用的时候要多注意注意条件竞争。
条件竞争
double fetch
取值两次,第一次进行合法效验,第二次进行数据操作,在这两次中间可以通过多线程改变数据的值,进而让第一次效验失效传入恶意数据。
例题 0CTF2018 Final - baby kernel
这道题之前做过直接放脚本了
脚本
1 |
|
userfaultfd
userfaultfd是linux系统在用户态的一个缺页处理机制,用户可以自定义函数来处理此类事件,就像kvm一样,linux系统已经规定好了userfaultfd的接口,用户可以利用规定的接口对某一虚拟内存进行监视,并且可以注册一个函数,当监视内存发生缺页异常的时候就会去执行这个注册函数。
使用userfaultfd的代码分为两部分,一部分是监控并注册函数,一部分是自定义处理函数的定义
注册模板
其中register_userfault
函数的第一个参数就是受监控内存,第二个参数就是自定义处理函数
1 | void err_exit(char* err_msg) |
自定义处理函数模板
可以把自定义处理函数分为两部分,前半部分就是模板操作
1 | // 轮询uffd读到的信息需要存在一个struct uffd_msg对象中 |
后半部分是真正的处理代码
1 | for (;;) { // 此线程不断进行polling,所以是死循环 |
理清楚了userfaultfd,那对于条件竞争又有什么用处呢,可以先看下面这段代码,如果这段代码没有加锁,那么当copy_from_user
函数要向ptr但还没写入时,此时另一个线程把ptr给free掉然后再把这个堆块申请回来,这个堆块是比较特殊的堆块,比如tty_struct
,这样我们就控制了tty_struct,进而可以控制程序流了,但是这样概率会很小,如果访问user_buf发生了缺页异常,那就会停下来去执行缺页异常处理函数,处理完再继续执行copy_from_user
函数,如果我们在缺页异常处理函数中再sleep
一下,那空档期就非常长了,在这段时间里释放在申请,最后成功写的概率就很大了。
1 | if (ptr) { |
说白了使用userfaultfd就是为了提高条件竞争成功的概率。
但是需要注意的是5.11版本以后,非特权用户就被禁止使用userfaultfd了
例题 d3ctf2019-knote
启动脚本
虚拟了两个cpu,每个cpu一个核,所以可以支持两个线程。保护除了kpti没开以外其他的都开了。
1 |
|
文件系统启动脚本
1 |
|
这道题就是在内核实现了一个菜单堆,有add,get,del,edit四个功能,其中del和add是加了读写锁的,没法条件竞争,但是get和edit是可以的,所以可以通过get进行条件竞争来完成内核地址泄露,在这个内核版本中tty_struct大小是0x2e0,所以可以先申请一个0x2e0的堆,然后在get的时候条件竞争,在缺页处理的时候先把这个堆块free掉,然后再打开一个ptmx
,就会在内核申请一个0x2e0的堆,有概率申请到刚free的堆块,缺页处理完再进行拷贝的话就能得到tty_struct
的内容了,就能得到内核地址了,然后在edit的时候再次进行条件竞争就可以任意地址写了,但是怎么利用这个任意地址提升权限呢
modprobe_path
当在用户态使用execve
执行一个非法的文件的时候,内核会有如下调用链
1 | entry_SYSCALL_64() |
最后一个函数定义于/kernel/kmod.c
,下面就是题目内核版本对应的call_modprobe()
源码,在函数的最后会调用call_usermodehelper_exec()
函数,将modprobe_path
作为可执行文件路径,以root权限将其执行,modprobe_path
默认存储着执行路径/sbin/modprobe
.
所以可以通过任意地址写接触modprobe_path
,将其改写为我们构造的恶意脚本的路径,然后执行一个非法文件触发上述调用链,最后以root用户执行恶意脚本,思路就是这样
1 | static int call_modprobe(char *module_name, int wait) |
exp
1 |
|
假如恶意脚本是cat /flag
在终端是没有显示的,可能最后执行call_usermodehelper_exec()
的时候对应的终端已经不是当前终端了,所以得执行chmod 777 flag
然后再读才能得到flag.
结果
1 | /fake: line 1: ����: not found |
总结
通过这一道题大概了了解了userfaultfd和modprobe_path的思路和基本用法,收获满满。
setxattr+userfaultfd堆占位
和sendmsg一样都是堆喷,不同的是这个堆内核堆的大小没有限制,而sendmsg申请的内核堆的最小字节是44,所以比起sendmsg来说这个更具有一般性。
1 | static long |
可以看到在setattr()
函数中value和size我们都可以控制,并且可以分配任意大小的object并写入,但该object在setattr执行结束会被释放,被释放后我们就无法控制这个堆了,不过好在setattr
函数是在kmalloc完之后才执行copy_from_user
函数,而user_from_user
函数可以阻塞的,我们可以先mmap一个两页大小的内存,然后向第一个页的末尾填充数据,第二页再用userfaultfd缺页处理,然后把第一页的末尾传入setattr()
函数,这样copy完第一页的末尾后再copy第二页的时候就会陷入缺页中断了,然后就达到了chunk既可控又不会被释放的目的了。
例题 SECCON 2020 kstack
qemu启动脚本
1 |
|
开了kaslr和smep,也开了KPTI
1 | / $ cat /sys/devices/system/cpu/vulnerabilities/* |
文件系统启动脚本
1 |
|
还是不太能找到条件竞争的洞,就比如这道题,感觉这种条件竞争分析的时候就得在可以阻塞的位置停下来,思考如果这时候阻塞,其他线程进来会发生什么事情,然后还有思考阻塞前程序做了什么,阻塞后程序做了什么,这么会比较好分析出条件竞争如何利用,通过这道题还学到了两手,一手是在阻塞的空窗期我们想要执行某些任务,可以直接放在userfaultfd的处理函数中,这样可以完美利用空窗期,如果需要并发的话,完全可以在处理函数中创建线程然后使用wait函数等待所有线程全部结束运行然后再去处理缺页,二手是原来内核也可以doublefree,这道题的利用就是先doublefree一个0x20的堆,然后打开一个stat申请一个0x20的seq_operation,此时下次申请一个0x20的堆还是会申请到seq_opsration,也就能改seq_operation的start指针了,最后使用一个add rsp,xxx
的gadget迁移到pt_regs上进行rop.
脚本
1 |
|