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 是我们入门内核的第一道题目,现在来看看它的两个变种
变种二:去除 Read 功能(变种一在上篇博客)
没有 Read,可以使用 ldt_struct
结构体来泄露内核基地址
- LDT 是局部段描述符表,里面存放的是进程的段描述符,段寄存器里存放的段选择子便是段描述符表中段描述符的索引
- 结构体
ldt_struct
就是用于描述局部段描述符表的
在保护模式中,段寄存器存放的就是16的段选择子,然后通过段选择子的信息定位到 GDT/LDT 表
1 | [----[0-1]-------[2]------[3-15]---] |
- Index:CPU 将索引号乘8再加上GDT或者LDT的基地址,就可以找到目标段描述符
- TL:其值为“0”查找 GDT 表,其值为“1”查找 LDT 表
- RPL:请求特权级别(可以用于判断当前代码是否处于内核态)
结构体 ldt_struct
的条目如下:
1 | struct ldt_struct { |
- 该结构体大小为 0x10,从
kmalloc-16
中取
在局部段描述符表中有许多的段描述符,用 desc_struct
进行描述:
1 | struct desc_struct { |
Linux 提供了 modify_ldt
系统调用,用于获取或修改当前进程的 LDT
- 内核源码如下:
1 | SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr , |
- 其中我们可以利用的两个函数就是
read_ldt
和write_ldt
1 | static int read_ldt(void __user *ptr, unsigned long bytecount) |
- 劫持
mm->context.ldt->entries
就可以实现任意读(ldt_struct->entries
)
1 | static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode) |
- 在
write_ldt
中会调用alloc_ldt_struct
,然后执行一个kmalloc
(可以被 UAF 控制)
利用 modify_ldt
泄露内核地址的思路如下:
- 申请并释放有 UAF 的堆块
- 执行
write_ldt
,使在alloc_ldt_struct
中申请的ldt_struct
填充 UAF 堆块 - 利用 UAF 控制
ldt_struct->entries
,然后使用read_ldt
把数据读到用户态
在实际的利用中,只能在 ldt_struct->entries
中爆破数据
- 命中无效的地址:
copy_to_user
返回非 0 值,此时read_ldt
的返回值便是-EFAULT
- 命中内核空间:
read_ldt
执行成功
但我们不能直接爆破内核基地址,只能先爆破线性映射区 direct mapping area
(kmalloc 使用的空间),然后通过 read_ldt
在堆上读取一些可利用的内核指针并泄露内核基地址
- 通常情况下内核会开启
hardened usercopy
保护,当copy_to_user
的源地址为内核.text
段(包括_stext
和_etext
)时会引起kernel panic
- 一般情况下
page_offset_base + 0x9d000
处固定存放着secondary_startup_64
函数的地址(kernel_base = secondary_startup_64 - 0x40
)
下面看一个爆破的案例:
1 | fd[0] = open("/dev/babydev", O_RDWR); |
但本题目不能使用 modify_ldt + ldt_struct
来泄露内核地址
- 该题目的内核版本是 4.4.72,其
write_ldt
函数的代码有些不同(至少在 4.20.1 才会有alloc_ldt_struct
函数)
1 | static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode) |
- 发现不会调用
alloc_ldt_struct
函数,取而代之的是alloc_ldt
函数
1 | static int alloc_ldt(mm_context_t *pc, int mincount, int reload) |
- 而在
alloc_ldt
中使用的是vmalloc
(使用VMALLOC_START ~ VMALLOC_END
之间的区域) - 那么这里就不会复用 UAF 堆块了
小结:
由于该内核版本没有 swapgs_restore_regs_and_return_to_usermode
,本想泄露出 kernel_base 就 OK 的,但是怎么都泄露不出来,后来看源码才发现没有 alloc_ldt_struct
函数
之后找一个符合条件的内核题目,把 pt_regs + seq_operations
和 ldt_struct + modify_ldt
这两种利用都复现一下