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 这两种利用都复现一下