# 逆向分析
1 | __int64 __fastcall rwmod_ioctl(int *a1, int a2, struct request *ubuf) |
uaf + 大小可控
不过注意 这里的 uaf 指的是
- 驱动申请一个 chunk
- 驱动释放这个 chunk
- 用户 syscall 申请到这个 chunk
- 驱动再次释放这个 chunk
- 用户通过另外的 syscall (或者按这题的重新申请) 写到这个 chunk
所以这里的数据写入不是传统的与驱动交互的写入 而是通过对已申请的 chunk 释放 让用户从系统调用或者重新申请实现 uaf 这个 uaf 的难度更高 而且写入的时候不能改到其他数据 或者说其他数据对控制流无影响 这对利用的结构体也有要求
第一眼想到 ldt_struct 泄漏 + seq_operations 劫持控制流 就可以秒 有点简单 所以多多尝试一下各种打法
另外需要注意一点的是 这里其实调用的 kmalloc 类型类似于 kzalloc
1 | _kmalloc((unsigned int)req.size, 3520LL) // 1101 1100 0000 |
# 漏洞利用
# exp1 - user_key_payload + seq_operations
uaf 转换为地址泄漏 可以用 add_key
[[add_key]]
断点到 0xffffffff813d8190 user_preparse
看申请情况
用驱动申请两个 chunk 一个用来存放 revoked key 用于地址泄漏 一个用来 uaf 篡改 datalen 实现越界读到 revoked key
所以要保证 revoked key 在高地址处 也就是第二个 chunk
1 | int main(int argc, char const *argv[]) |
这里要确保 description 不一样 我也不知道为什么 一样就 crash 了 怀疑 key 被复用了
另外由于 key_alloc 中有四次对象分配 其中 payload 的两次 size 比较接近 (相差 0x18 可以构造大小让他们处于不同的 slab)
剩下就是释放掉第一个 user_key_payload 结构体所在的区域 然后立刻重新申请回来形成 uaf 从而改写掉他的 datalen
然后 revoke 掉第二个 user_key_payload 这样 rcu 就被赋值 从而得到堆地址和内核基址
1 | xfree(0); /* free it again, now uaf */ |
然后就是熟悉的喷 seq_operations 了 这个不赘述了
# exp
- user_key_payload + uaf 泄漏内核基址
- seq_operations 进行控制流劫持
当然我这里是一次就能申请到就没喷 正常情况下可能需要喷一下 key
1 |
|
# exp2 - double free aaw
- user_key_payload + uaf 泄漏内核基址
- df 劫持 freelist 构造 aaw 原语 再劫持 n_tty_ops
这里没有 slab_freelist_hardened 加上无限制的 free 直接劫持 freelist 写全局变量即可提权
[[double-free-and-restore-in-slub.png]]
modprobe 比较简单 这里劫持 n_tty_ops 顺便学习一下高版本的 freelist 的应对办法
double free
先 free 两次 这样 freelist->next = freelist
第一次申请 freelist = freelist->next 没变 next 改变了
第二次申请 freelist = freelist->next = target_addr 申请的内存和第一次一样
第三次申请 就到 target_addr 了 同时 freelist = target_addr->next 这里就要让 target_addr->next 为 0
另外这里的版本是 5.19.0
freelist 不放在开头了
slub.c - mm/slub.c - Linux source code (v5.19) - Bootlin
1 | } else { |
看着很费劲 不如打印一下 一目了然
1 | $ gcc -g -o test test.c; ./test |
freelist 不是放在开头而是放在中间了 所以我们劫持 freelist 后要申请到 n_tty_ops 上方 然后让 freelist 落到 num 或者 align 的地方 这样才不会 crash 或者找个其他结构体先喷后还原 freelist
例如我这里所使用的 0x60 的结构体 用的是 kmalloc-96 s->object_size / 2
再与 8 向下对齐 0x30 也就是 offset 0x30 处
1 | struct fake_tty_ops { |
另外这里我们申请出来的页会被清空 最开始我没加 write 导致 write 被刷成 0 了 所以这里要最好确保 我们的对象和 slab->object_size 相等
另外一个注意点就是 劫持 n_tty_ops->read 从内核返回之后 rsp 为 0 此时禁止使用栈上变量!
# exp
1 |
|
# exp3 - msg_msg + shm_file_data
用 msg_seq 占住我们刚刚释放的 0x20 然后立马给它 free 了 这样就能用 shm_file_data 给它喷上了(seq_operations 也行不过不能泄露 dma 地址 虽然也不需要 dma 啦) 然后 msgrcv 读出来 注意字节要恰当 不要走到下一个 next 去了
劫持控制流还是 modprobe/n_tty_ops
# exp
1 |
|
# exp4 - ldt_struct + cred
ldt_struct+uaf 篡改 entries 可以在堆上进行暴力搜索 找到 task_struct 然后就能根据指针找到 cred 后 aaw 了
cred 搜出来大概这样
这里 0x3e8 就是 xxid 0x28 个字节全改成 0 即可
1 | pwndbg> x/40gx 0xffff888003f3ec00 |
断点到 getuid
两次 rax 取指后查看
1 | 0c:0060│ 0xffff888003f43f60 ◂— push rcx /* 0x5100000051; 'Q' */ <<- 当前的pid |
这是内存页的限制
1 | pwndbg> x/30gx 0xffff888008000000 |
但是这题我遇到了一个巨坑
首先由于有 df + 劫持 next 可以构造一个不那么稳定的 aaw ()(由于高版本 freelist 位置的缘故很难直接申请到 null 的位置 需要提前申请结构体然后释放用于恢复 freelist)
这里我劫持 ldt 泄漏出 cred 的时候 大量喷射 keys 再 aaw 再释放 理想很丰满 但是喷 keys 之后 cred 会换个位置 这就很离谱 不过解决办法也很简单 提前喷好 ldt 暴搜完之后就别寄吧再申请内存了(严重怀疑大量申请内存后会导致 cred 的迁移
- 准备 keys
- ldt 碰撞出 dma 基址
- ldt + uaf 实现 aar 扫描 dma 获取 task_struct 地址 从而获取 cred 地址
- double free => aaw 篡改 cred
- 恢复 freelist 后提个权
# exp - 修改 cred 内容
1 |
|
# exp - 直接修改 cred 指针
既然堆喷 key 会迁移我们的 cred 那么规避办法很简单 我们直接改掉 task 中的 cred 指针和 real_cred 指向 init_cred
令人满意的是 task_struct 中 cred 和 comm 之间正好有 cached_requested_key 域为 0 又恰巧的是 如果 real_cred 作为起始地址申请 0x20 的 chunk 这个地方正好是 freelist 的指针的存放位置 所以不需要恢复 freelist 了
df => aaw
uaf + ldt_struct => aar
ldt_struct 暴搜 => kdma + task_struct addr
kdma + aar => kbase
kbase => init_cred
aaw + task_struct with init_cred => 提权
1 |
|