关于 kernel-UAF 的学习总结
终于来到了关于内核的学习,目前打算浅尝一下内核的基础知识和漏洞。之后每个学习的新漏洞都单独写一篇文章,每篇学到的新的前置知识都放到对应的文章中吧,暂时先不做汇总。
CISCN2017_Pwn_babydriver
前置知识
将 rootfs.cpio 文件系统映像解包,因为静态分析需要解包得到的 ko 文件
hen rootfs.cpio |
解包脚本 hen
|
打包脚本 gen
|
使用下面的命令,从 bzImage 文件中提取 vmlinux
/usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux bzImage > vmlinux |
调试
sudo gdb vmlinux |
下面的命令导入符号表,这个 ko 文件是刚刚解压 rootfs.cpio 得到的,后面这个 0xffffffffc0000000 需要在启动内核后,输入 lsmod 查看驱动的基地址从而得到。
add-symbol-file /home/zikh/Desktop/babydriver/core/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000 |
最后用下面的命令连接,调试程序
target remote localhost:1234 |

设置断点需要用驱动的基地址加上 ida 中的偏移的位置打断点即可,这个基地址仅仅是和 text 段的地址相同,假设你现在想查看 bss 段上的某个变量,那么需要获取到 bss 段的基地址以及变量在 bss 段上的偏移。
假设要查看 0xd90 这个地址装载到内存中的实际地址。首先获取它在 bss 段上的偏移,发现 bss 段基地址为 0xd00 因此这个地址在 bss 段上偏移为 0x90
获取 bss 段的基地址 (如下)

因此 babydevice_t 结构体地址是 0xffffffffc00024d0 ,验证如下

内核提权
如果攻击者能够修改某个进程中的 cred 结构体中的 gid 和 uid euid等字段为 0,也就是能控制 cred 结构体的话,那么攻击者就获得了 root 权限,如果再开启一个 shell 的话,执行的任何命令也都是拥有 root 权限
题目链接
https://github.com/cc-sir/ctf-challenge/blob/master/2017CISCN%20babydriver/babydriver.tar
解压文件
tar -xvf babydriver.tar |
boot.sh 文件
因为我的虚拟机不支持 kvm ,所以把原本 -enable-kvm 这段代码删了,为了方便之后使用 gdb 进行调试,加上了 -gdb tcp::1234 这段代码
|
然后运行 boot.sh 启动即可。
逆向分析
babyopen

申请了 0x40 的堆空间,并返回申请的内存首地址记录在 babydevice_t 结构体的 device_buf 字段
将 0x40 赋值为 babydevice_t 结构体的 device_buf 字段。需要注意的是 babydevice_t 结构体位于 bss 段上,这个全局变量就会存在被覆盖的可能,也就是说我连续 open 两次,那么第二次申请出来的内存块地址则会覆盖第一次申请的内存块地址。
babyioctl
该函数定义了一个 0x10001 的命令,先将 babydevice_t 结构体中的 device_buf 给释放掉,然后重新申请了一块内存,因为 v3 是 rdx 寄存器所赋值的,也就是 babyioctl 函数的第三个参数,而 v3 又给了 v4 ,这个内存大小是我们可控的。
babyread

该函数显示检查了 device_buf 是否为空,如果为空的话返回 -1 ,如果 device_buf_len 大于 write 函数的第三个参数则将 device_buf 中的数据 copy 到用户区 buffer 空间中
这里 ida 生成的伪代码是有点问题的,正常情况是 copy_to_user(buffer, babydev_struct.device_buf, v4);
babywrite

这个函数和 babywrite 是相反的,将数据从用户区的 buffer 复制到内核中的 device_buf 。
babyrelease
该函数可以将 device_buf 这个堆块给释放掉,但是释放内存后,未将指针置空,产生了 UAF 漏洞。
利用思路
连续 open 两次,分配出 fd1 和 fd2 ,此时 fd2 将 fd1 的堆块地址覆盖掉了。再使用 ioctl 函数去执行那个 0x10001 的指令,将 fd1 释放掉 (其实释放的是 fd2 ),再申请一个 0xa8 的堆块出来(用于伪造 cred 结构体 ),接着再用 release (也就是 close ) 函数将 fd1 释放掉(此时释放的是刚刚申请出来 0xa8 的那个堆块)
调用 fork 函数,创建一个子进程出来,并让父进程 wait。子进程产生时,就需要申请一个 0xa8 的堆块用来当做 cred 结构体,这时就会申请出来刚刚的我们释放掉的堆块。因为最后 release 是对 fd1 操作的,此时 fd2 是依然可以被写入数据的,向 fd2 中写入数据就等同于向子进程刚刚申请 cred 结构体中写入数据。此时父进程中 device_buf 记录的就是刚刚子进程申请堆块的地址。
将其 cred 结构体前 0x28 个字节覆盖成 \x00 执行 system("/bin/sh") 即可开启一个 root 权限下的 shell ,也就完成了所谓的内核提权。
上述思路的重点在于,release 操作对一个文件使用后,就无法再用 write 等函数进行该文件的操作了。但 fd1 和 fd2 其实都同时指向了device_buf (无论 device_buf 是哪个堆块地址)。因此用 release 函数释放 fd1 将申请的 0xa8 堆块给 free 掉,通过 write 函数对 fd2 操作依然可以写入数据。
EXP
|
