ブログ未満のなにか

ブログなのか誰にも分からない

0CTF 2017 Finals cred_jar writeup

はじめに

一人writeup advent calendarの6日目です。 1日1問分のwriteupを目標に頑張っていきます。 6日目の問題は、0CTF 2017 Finalsで出題された「cred_jar」。 race conditionを起点としたkernel exploit問題で初めての人にオススメです。

カーネルの情報 (セキュリティ機構など)

カーネルのバージョンが4.12で、smep, kaslrが有効になっている。 kernel exploitでよく使われる/dev/ptmxが存在しない。

% cat boot.sh
#!/bin/bash

qemu-system-x86_64 -initrd rootfs -kernel bzImage -append 'console=hvc0 root=/dev/ram rdinit=/linuxrc oops=panic panic=1 quiet kaslr' -m 128M -chardev stdio,id=k33n -device virtio-serial -device virtconsole,chardev=k33n -display none -monitor none -smp cores=1,threads=1 -cpu kvm64,+smep

[in qemu]
/ $ uname -a
Linux (none) 4.12.0-rc1 #25 SMP Fri May 26 16:51:03 CST 2017 x86_64 GNU/Linux
/ $ cat /proc/cpuinfo | grep flags
flags       : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl cpuid pni cx16 hypervisor smep
/ $ ls /dev/
console   cred_jar

解析

攻略対象のデバイス/dev/cred_jar。 デバイスに対する操作は以下のように定義されている。

struct file_operations cred_jar_fops = {
    .open    = cred_jar_open,
    .release = cred_jar_release,
    .read    = cred_jar_read,
    .write   = cred_jar_write,
    .ioctl   = cred_jar_ioctl
};

cred_jar_ioctl()がメインとなっている箇所で、指定するcmdによって下の構造体jarに関する操作が行える。 行える処理は、0x7401:alloc、0x7402:get、0x7403:get id、0x7404:change id、の4つである。 file構造体のprivate_dataで、jarへのポインタを保存している。

allocは、指定したサイズとjar構造体の大きさの合計でkamlloc()して領域を確保する。 確保したら、メンバを適切に初期化して、リンクドリストに繋ぎ、private_dataにポインタを入れる。

id指定で、jarを選択する。指定したidを持つjarが存在したら、private_dataにポインタを入れる。 一度getすると、他のjarを選び直すことはできない。 read/writeは、選択したjarのbufを読み書きできる (bofはない)。

get idとchange idはそのままの処理。get idは選択しているjarのidを得られる。change idは選択しているjarのidを変更できる。

refcountはそのjarへの参照数である。 allocした時点で、参照数は1。 getされたら参照数は増えて、jarを選択しているfdがclose()されたら参照数は減る。

struct jar {
    int refcount;
    int id;
    struct jar *next;
    struct jar *prev;
    unsigned long size;
    char buf;
};

exploit

getはget_cred_jar_ctx()が実行され、close()するときにはput_cred_jar_ctx()が実行される。

struct jar* get_cred_jar_ctx(int id) {
    struct jar* ptr;
    mutex_lock(&ctx_lock);

    if ( ctx_list != &ctx_list ) {
        ptr = ctx_list;
        while(1) {

            if (ptr == &ctx_list)
                break;
            if (ptr->id == id ) {
                _InterlockedIncrement(ptr->count);
                break;
            }
                        
            ptr = ptr-next;
        }
    }
    ptr = 0LL;
    mutex_unlock(&ctx_lock);
    return ptr;
}

int put_cred_jar_ctx(struct jar *jar) {
    if (jar) {
        if (!_InterlockedDecrement(jar->count)) {
            mutex_lock(&ctx_lock);
            // ここにunlinkの処理
            mutex_unlock(&ctx_lock);
            kzfree(a1);
        }
    }
    return 0;
}

put_cred_jar_ctx()は、refcountをデクリメントし0になった場合 (他からも参照されていない状態)、kfree()を実行する。 しかし、refcountの確認をした後にmutex_lock()が走るので、race conditionとなる可能性がある。

対してget_cred_jar_ctx()は、まずmutex_lock()が走る。 ロック後に、指定したidを持つjarをリストを辿って検索する。 指定のidを持つjarが見つかったら、refcountをインクリメントする。 そしてreturnする前にmutex_unlock()が走る。

put_cred_jar_ctx()とget_cred_jar_ctx()を上手く使うことで、参照している状態にあるjarをkfree()することができる。

race conditionが発生してkUAFとなる具体的な流れを詳しく説明する。 ここで、get_cred_jar_ctx()にて目的のidを持つjar Xを探索している途中に、jar Xを参照するfdがclose()されてput_cred_jar_ctx()が実行されたと仮定する。 jar Xはclose()したfdだけが参照している状態、つまりrefcountは1であるとする。

get_cred_jar_ctx()でmutex_lock()が実行されているが、put_cred_jar_ctx()のmutex_lock()は_InterlockedDecrement()後にあるため、jar Xを参照するrefcountのデクリメント自体は問題なく実行される。jar Xのrefcountは元々1だったので、デクリメントされて0になる。jar Xをリンクドリストから外しkfree()を実行する方向へ分岐が進むが、mutex_lock()があるため、get_cred_jar_ctx()の処理が終わるまで、put_cred_jar_ctx()を実行しているスレッドは待機する。 get_cred_jar_ctx()ではjar Xを見つけて、mutex_unlock()が走り、file->private_dataにjar Xへのポインタが格納される。 もう片方のput_cred_jar_ctx()では、mutex_lock()の停止から復帰してkfree()が実行される。 しかしjar Xへの参照はget側で残ったままなので、kUAFとなる。

雑に説明すると、同じjarに対して、スレッドAでget_cred_jar_ctx()、スレッドBでput_cred_jar_ctx()を実行すると、スレッドAでgetしたjarがなぜか知らないうちにkfree()されているという感じ。

SLUBアロケータでは、kfree()したチャンクの先頭8byteはfree済みのチャンクを繋ぐためのポインタになっている。 そのためkUAFが起きた場合、idには設定したものと異なる値が入っているので、idを確認して違う値だった場合はrace conditionに成功してkUAFが起きたと判断できる。

race conditionを起こしてkUAFが起こせるので、別のオブジェクトをラップさせてroot権限を取っていく。 まずはカーネルのアドレスのリークを狙う。 今回のカーネルは、/dev/ptmxがいないので、共有メモリのshmget()を呼んだときに割り当てられるfile構造体を使用した。 file構造体はkmalloc-256から取られるので、alloc_cred_jar()に渡すサイズは256-0x20にする。(関連する部分のコードは次のリンク先に Linux source code: ipc/shm.c (v4.12) - Bootlin, Linux source code: fs/file_table.c (v4.12) - Bootlin)_

file構造体のf_opからカーネルのベースアドレスを求めることができ、さらにf_opを書き換えれば制御の奪取が可能である。 smapは無効なので、ユーザ空間に偽のfile_operationsを用意できる。

struct file {
    union {
        struct llist_node  fu_llist;
        struct rcu_head    fu_rcuhead;
    } f_u;
    struct path        f_path;
    struct inode       *f_inode;   /* cached value */
    const struct file_operations  *f_op; // このポインタの値を知ればカーネルのベースアドレスが求まるし、書き換えれば制御が奪える。

    以下省略

偽のfile_operationsのioctlに、ユーザ空間へとstackをpivotするガジェットを入れてropした。 pivot先に目的のrop chainを用意しておきcommit_creds(prepare_kernel_commit(0))を実行して、root権限を取った。

gist.github.com

f:id:hama7230:20181205190753j:plain