babydriver
先进行解压:
1 | mv rootfs.cpio rootfs.cpio.gz |
- 这个
rootfs.cpio
其实是个压缩包,不过它省略了后缀“.gz”,这里需要先改名后解压
1 | !/bin/bash |
- smep
1 | !/bin/sh |
漏洞分析
1 | void __fastcall babyopen(inode *inode, FILE *fp) |
- 伪条件竞争引发的 UAF 漏洞,即当我们同时打开两个设备,第二次会覆盖第一次分配的空间
1 | void __fastcall babyioctl(FILE *fp, unsigned int command, __int64 arg) |
- 可以再释放后再申请(改写大小)
babydriver 是我们入门内核的第一道题目,现在来看看它的两个变种
变种一:添加 KPTI 和 kaslr
1 | !/bin/bash |
- smep
- kaslr
- KPTI(在
append
中添加pti=on
)
KPTI(Kernel Page Table Isolation)就是内核页表隔离(内核版本 4.15 以上),使内核与用户态进程使用两套独立的页表
- 在 Linux 中,寄存器 CR3 用于存储当前的 PGD 地址(四级页表结构:
PGD->PUD->PMD->PTE
) - 如果开启了 KPTI,则在内核态与用户态切换时会同时切换 CR3
- 为了提高切换的速度,内核将内核空间的 PGD 与用户空间的 PGD 两张页全局目录表放在一段连续的内存中(两张表,一张一页4k,总计8k,内核空间的在低地址,用户空间的在高地址)
- 只需要将 CR3 的第 13 位取反便能完成页表切换的操作
KPTI:会使 ret2usr
失效,在用户空间中构造 fake tty_operations
也会失效,在 ROP 中需要切换 CR3 的 gadget
KPTI pass:使用 seq_operations + pt_regs
结构体 seq_operations
的条目如下:
1 | struct seq_operations { |
- 当我们打开一个 stat 文件时(如
/proc/self/stat
)便会在内核空间中分配一个seq_operations
结构体 - 当我们 read 一个 stat 文件时,内核会调用其
proc_ops
的proc_read_iter
指针,然后调用seq_operations->start
函数指针
结构体 pt_regs
的条目如下:
1 | struct pt_regs { |
- 在系统调用当中有很多的寄存器其实是不一定能用上的,比如 r8 ~ r15
- 只需要寻找到一条形如
add rsp, val; ret
的 gadget 便能够完成 ROP
于是泄露思路如下:
- 先执行两次
open("/dev/babydev", O_RDWR)
- 执行
ioctl(fd[0], 0x10001, 0x20)
,修改内核结构体的大小为 0x20(使seq_operations
可以被放入这里) - 释放
fd[0]
,并且执行open("/proc/self/stat", O_RDONLY)
(内核结构体被释放,又被申请回来存放seq_operations
) - 此时
fd[1]
仍然指向内核结构体,于是用read(fd[1], leak_data, 0x10)
泄露内核基地址
使用如下命令来查找需要的偏移:(使用前先关闭 kaslr,并开启 root 权限)
1 | / |
接下来需要把 seq_operations->start
覆盖为一个 gadget(类似于 add rsp, offset;...; ret;
)
在开启 KPTI 内核,提权返回到用户态(iretq/sysret
)之前如果不设置CR3寄存器的值,就会导致进程找不到当前程序的正确页表,引发段错误
常规设置需要从上到下执行3个 gadget:(改写 CR3 的第13位)
pop rdi; ret;
(RDI 写入0x6f0
)mov cr4, rdi; ret;
swapgs; pop rbp; ret;
使用 pt_regs
在内核态写 ROP 链,就没有那么多空间来放入 gadget,于是我们用 swapgs_restore_regs_and_return_to_usermode
函数一次搞定(低版本的内核没有这个函数)
- 需要的栈布局如下:
1 | swapgs_restore_regs_and_return_to_usermode + 22 |
- 只要把
swapgs_restore_regs_and_return_to_usermode + 22
放在 R10 的位置就可以符合条件
综合上面的内容,我们可以得到劫持控制流的思路:
- 执行
write(fd[1], &magic_addr, 8)
写入形如add rsp, 0x148; ...; ret;
的 gadget - 通过
pt_regs
构造 ROP 链(R10 写上swapgs_restore_regs_and_return_to_usermode + 22
) - 在 gadget 打上断点,然后计算该 gadget 到
pt_regs
结构体的偏移,寻找准确的 gadget
在实际的调试中,我发现 pt_regs
的内容没有那么规整(可能是 sys_read 破坏了 pt_regs
原本的结构),下面给一个调试案例:
1 | __asm__( |
GDB 部分内存如下:
1 | pwndbg> telescope 0xffff8800027cbdd8 |
- 注意
0xbbbb2222 -> 0xbbbb1111
和0x11110000 -> 0x88888888
- ROP 的构造思路:
- 在
0xbbbb2222-0x33333333
分别放入pop_rdi_ret
init_cred
commit_creds
- 在
0x44444444
放入add_rsp_offset_ret
以跳转到0x11110000
- 在
0x11110000
中放入swapgs_restore_regs_and_return_to_usermode + 22
- 在
seq_operations->start
中写入的 gadget 需要把栈迁移的 ROP 链上
- 在
本题目给的内核版本是 4.4.72,没有 KPTI 和 swapgs_restore_regs_and_return_to_usermode
函数,因此没法完成演示
下面给出非完整的 exp:
1 |
|
小结:
主要是参考 墨晚鸢 大佬的博客:
他给出的参考题目应该是改过内核版本的,我手上没有对应版本的题目,最后只能将就了
之后抽时间学一下 ldt_struct
的利用