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_idx
request
就是Node->entry_idx
request_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
的执行流程(真的很慢)