Kqueue
1 | !/bin/bash |
- kaslr
1 | !/bin/sh |
- perf_event_paranoid:控制非特权用户对性能事件系统的使用(无CAP_SYS_ADMIN),预设值为 “2”
- dmesg_restrict
- kptr_restrict
经过测试,重新打包会导致程序权限出现问题(不知道为什么),这里只好把其他题目的文件系统拿来用
逆向分析
1 | __int64 __fastcall kqueue_ioctl(__int64 fd, unsigned int cmd, __int64 ptr) |
第一眼看上去程序很乱,但切入点在这一句:
1 | copy_from_user(v10, ptr, 24LL) |
- 由此我们可以判断 v10 是一个结构体,并且大小为 24 字节
1 | 00000000 Node struc ; (sizeof=0x18, mappedto_3) ; XREF: delete_kqueue/r |
- 先对 v10 设置一个结构体,具体的条目可以之后再确定
- 例如:通过
_kmalloc可以确定哪个条目代表 size(通过提示信息也可以判断一些条目)
1 | chunk = (char *)_kmalloc(24LL * (unsigned int)(tmp.size1 + 1) + 0x20, 0xCC0LL); |
- 其实这里可以看出来程序还有一个结构体 chunkP,大小不确定
- 对于大小不确定并且在堆上的结构体,我们可以尽量多设置一些条目,然后根据情况进行修改
复原结构体后,随便打开一个参数很多的函数:
1 | __int64 __fastcall create_kqueue(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7) |
- 发现
a1~a6根本没有用上(从a7(v10)开始有用),可以用 IDA 删除这些多余的参数 v10是一个结构体(不是结构体指针),出现在RBP+0x8的位置- IDA 分析时不会把
v10当做是一个结构体,而是把它拆分为多个使用过的结构体条目(没有使用过的结构体条目会被直接忽略)
1 | char request; // [rsp+3Eh] [rbp+Eh] |
request_2就是Node->queue_idxrequest就是Node->entry_idxrequest_3就是Node->data- 由于
Node->data_size和Node->max_entries没有使用,于是被 IDA 忽略了
对于这种情况建议不要把形参修成一个结构体,而是就把它设置为多个结构体条目:
1 | unsigned __int16 entry_idx; // [rsp+3Eh] [rbp+Eh] |
分析全局,在内核堆上的结构如下:
1 | ---------------- |
- 这一整片内存的大小是确定的
- 程序根据
queue->max_entries来确定kqueue_entry的个数 - 每个
queue和kqueue_entry都有一个data条目,指向一片大小为request.data_size的堆空间
漏洞分析
在 create_kqueue 中:
1 | num = request.max_entries + 1; |
- 局部变量
num的类型为unsigned int,对其加一可能会导致整形溢出 - 写入
0xffffffff会导致request.max_entries + 1溢出为0x0,导致不初始化queue_entry的同时使得queue->max_entries = 0xffffffff
1 | queue_size = 24LL * (request.max_entries + 1) + 0x20; |
- 同理,
request.max_entries + 1的溢出会导致queue_size变为0x20
在 save_kqueue_entries 中:
1 | new_queue = (Queue *)kzalloc(queue->queue_size, 0xCC0); |
- 而
request.data_size是我们输入的数据,过大会导致new_queue堆溢出
入侵思路
首先 save_kqueue_entries 的 new_queue 是有堆溢出的
结合 create_kqueue 中的整形溢出,就会使 new_queue 从 kmalloc-32 中申请空间,然后就可以在 kmalloc-32 中进行溢出
对于 kmalloc-32 我们可以使用 seq_operations:
1 | struct seq_operations { |
- 当我们 Read 一个 stat 文件时,内核会调用其
proc_ops->proc_read指针(其默认值为seq_read函数) - 进而调用
seq_operations->start
程序没有提供泄露内核基地址函数(ldt_struct 结构体使用 kmalloc-16,也无法使用),但程序没有开 smap smep,可以注入 shellcode 并在内核栈上找恰当的数据以获得内核基址
- PS:在
rsp+0x8的位置可以泄露内核基地址
于是我们可以堆喷覆写 kmalloc-32 上的 seq_operations->start 为 shellcode
这一步的调试比较麻烦,我的建议是:
- 直接在
shellcode打断点进行调试(0x401D1F)
当然也可以用常规的调试方式:
- 关闭 Kaslr ,开启 Root,在
seq_read(旧的内核版本用的是seq_read_iter)打上断点,然后对着源码进行调试
1 | / |
- 另外还可以利用 IDA 去分析 vmlinux,以确定准确的断点(vmlinux 可以用
extract-vmlinux来生成) - 这部分可以对照源码来看
1 | .text:FFFFFFFF812010F0 |
下面简述一下定位的过程:
- 先找函数指针,按 x 交叉引用看看哪个变量使用了函数指针:
1 | __int64 (__fastcall **v8)(__int64 *, __int64); // rax |
- 最后确定调用
seq_operations->start的代码位置:(0xFFFFFFFF8120115B)
1 | v8 = (__int64 (__fastcall **)(__int64 *, __int64))v5[11]; |
调试信息如下:
1 | ► 0x401d1f push rbp |
完整 exp 如下:
1 |
|
小结:
这个题其实是给了源码的,但我看到题目把结构体作为形参传入时,就知道 IDA 分析出来肯定很乱
1 | static noinline long create_kqueue(request_t request){} |
于是打算用这个题来练一练内核模块的逆向
这个题目有3个不同的结构体,当时把结构体的范围搞错了,导致程序的功能难以理解
最后还调试了一下 seq_read 的执行流程(真的很慢)