ブログ未満のなにか

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

最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて

はじめに

これは CTF Advent Calendar 2018 - Adventar の8日目の記事です。 7日目は、@bata_24 さんの 「WCTF 2018 - klist Writeup - HackMD 」でした。

この記事は、タイトル通りに私が個人的に「よく使うなぁ・他のwriteupでよく見るなぁ」と感じたテクニックの紹介です。 ですが紹介なので、glibc heapの挙動について詳細な説明は特にしないです。 テクニックが何故通用するのかは各自でソースコード読んで理解してください。 また、glibcのバージョンによっては通用しないものもあるので注意してください。 テクニックの名前自体はクソ適当です。

fastbin attack with 0x70 chunk

概要

結構前から汎用的に使われているテクニック。 fastbinに繋がれたチャンクをmalloc()などで取り出す時に、チェックが甘いことに起因する。 libcのdata, bssにあるデータを利用して、その付近のメモリをmalloc()で取得するテクニック。

使用条件

  • UAFやheap bofなどで、free済みのチャンクのfdを任意に制御できる
  • 0x59から0x68までのサイズでmalloc()を呼べる
  • libcのアドレスが分かっている(これが絶対かは問題による)

やり方

x64のlinuxでは、libcなどのライブラリが配置されるアドレスは、0x7fXX_XXXXXXXXとなっている。 アドレスをズラして見ると、0x7fと見ることができ、fastbinに繋げられるchunkのsizeとしてvalidになる。 つまり、fastbin関連のチェックをすり抜けられる。

__malloc_hookをターゲットとした例を説明する。 今回の__malloc_hookが存在するアドレスは、0x7ffff7dd1b10となっている。 それよりも前のメモリがどうなっているか調べると、いくつか0x7fXX_XXXXXXXXとなる値が存在する。 これを数byteズラして見ると、0x7fをサイズとして利用できる。

__malloc_hookのアドレス
gdb-peda$ p &__malloc_hook
$3 = (void *(**)(size_t, const void *)) 0x7ffff7dd1b10 <__malloc_hook>

適当に数byte前を見てみる
gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20
0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260  0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20  0x00007ffff7a92a00
0x7ffff7dd1b10 <__malloc_hook>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b20 <main_arena>:  0x0000000000000000  0x0000000000000000

適当にズラして0x7fとする
gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20-3
0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000  0x000000000000007f
0x7ffff7dd1afd: 0xfff7a92e20000000  0xfff7a92a0000007f
0x7ffff7dd1b0d <__realloc_hook+5>:    0x000000000000007f  0x0000000000000000
0x7ffff7dd1b1d: 0x0000000000000000  0x0000000000000000

今、0x70のチャンクがfastbinに繋がっている。 このチャンクのfdに先ほどのアドレス(0x7ffff7dd1aed)を書き込み、リストに繋げる。 そしてmalloc()していくと、その領域を取ることができる。

sizeが0x70のchunkが1つ存在しfreeされて、fastbinに繋がっている。
gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x602000 --> 0x0
(0x80)     fastbin[6]: 0x0
                  top: 0x6020e0 (size : 0x20f20)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0

そのchunkのfdを、uafなどで上書きできたと想定して、先ほどのアドレスを書き込む
gdb-peda$ set {long}0x602010=0x7ffff7dd1aed

エラーと言われるが気にしない
gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x602000 --> 0x7ffff7dd1aed (size error (0x78)) --> 0xfff7a92e20000000 (invaild memory)
(0x80)     fastbin[6]: 0x0
                  top: 0x6020e0 (size : 0x20f20)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0

先ほどのメモリがmalloc()で返ってくる
gdb-peda$ call malloc(0x68)
$5 = (void *) 0x602010
gdb-peda$ call malloc(0x68)
$6 = (void *) 0x7ffff7dd1afd

unsotedbin attack to libc data region

概要

これも結構前からあるテクニック。 unsortedbin attackは、結果として任意の箇所に0x7fXX_XXXXXXXXとなる値を書き込むことができるが、どこに対して書き込むのかという話。 stdin/stdoutのバッファのポインタを書き換えて、バッファの範囲を変えると良い。

前提条件

  • unsortedbin attackができる
  • libcのアドレスが分かっている

やり方

よくやる/見るのが、stdinが持つbufのポインタに対して。 pwnで出題されるバイナリは、入出力のバッファリングを無効にしているケースが多い。 無効といっても、stdin/stdoutが持つメンバである_shortbufをバッファとして使用している。 scanf()などが呼ばれると、_shortbufにデータが書き込まれている。 バッファを指すポインタをunsortedbin attackで上書きして、バッファと見なす範囲を広げて不正に上書きできる状態を作り出すのが、このテクニックの肝である。

例を上げて流れを説明する。 例では、stdinの_IO_write_endをunsortedbin attackで上書きする。

setbuf(stdin, NULL)を実行した後のstdinの状態は以下のようになっている。 read/write用のバッファは、stdinの内部に持っている_shortbufを指している。

gdb-peda$ x/32gx 0x7ffff7dd18e0
0x7ffff7dd18e0 <_IO_2_1_stdin_>:  0x00000000fbad208b  0x00007ffff7dd1963
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1900 <_IO_2_1_stdin_+32>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1910 <_IO_2_1_stdin_+48>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1920 <_IO_2_1_stdin_+64>:   0x00007ffff7dd1964  0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>:  0x0000000000000000  0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>:  0x0000000000000000  0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>:  0xffffffffffffffff  0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>:  0x00007ffff7dd19c0  0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>:  0x0000000000000000  0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>:  0x0000000000000000  0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>:  0x0000000000000000  0x00007ffff7dd06e0
0x7ffff7dd19c0 <_IO_wide_data_0>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd19d0 <_IO_wide_data_0+16>:  0x0000000000000000  0x0000000000000000

gdb-peda$ p *(struct _IO_FILE *) 0x7ffff7dd18e0
$5 = {
  _flags = 0xfbad208b,
  _IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_buf_end = 0x7ffff7dd1964 <_IO_2_1_stdin_+132> "",
  _IO_save_base = 0x0,
  _IO_backup_base = 0x0,
  _IO_save_end = 0x0,
  _markers = 0x0,
  _chain = 0x0,
  _fileno = 0x0,
  _flags2 = 0x0,
  _old_offset = 0xffffffffffffffff,
  _cur_column = 0x0,
  _vtable_offset = 0x0,
  _shortbuf = "",
  _lock = 0x7ffff7dd3790 <_IO_stdfile_0_lock>,
  _offset = 0xffffffffffffffff,
  _codecvt = 0x0,
  _wide_data = 0x7ffff7dd19c0 <_IO_wide_data_0>,
  _freeres_list = 0x0,
  _freeres_buf = 0x0,
  __pad5 = 0x0,
  _mode = 0x0,
  _unused2 = '\000' <repeats 19 times>
}

今、0x110byteのサイズを持つチャンクがunsortedbinに繋がっている。 このチャンクのbkを書き換えて、malloc(0x100)を実行すると、unsortedbin attackが発生する。 stdinの内部に書き込まれた0x7ffff7dd1b78をバッファの終端として使用するようになるため、大きくlibcの中身に書き込める状態を作れた。

unsortedbinが1つ存在している。
gdb-peda$ parseheap
addr                prev                size                 status              fd                bk
0x602000            0x0                 0x110                Freed     0x7ffff7dd1b78    0x7ffff7dd1b78

bkを上書きする
gdb-peda$ set {long}0x602018=0x7ffff7dd1910

gdb-peda$ parseheap
addr                prev                size                 status              fd                bk
0x602000            0x0                 0x110                Freed     0x7ffff7dd1b78    0x7ffff7dd1910

malloc()を呼んで、unsortedbin attackを起こす
gdb-peda$ call malloc(0x100)
$7 = (void *) 0x602010

stdinの状態を確認すると、_IO_buf_endが書き換わっている。
gdb-peda$ x/20gx 0x7ffff7dd18e0
0x7ffff7dd18e0 <_IO_2_1_stdin_>:  0x00000000fbad208b  0x00007ffff7dd1963
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1900 <_IO_2_1_stdin_+32>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1910 <_IO_2_1_stdin_+48>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1920 <_IO_2_1_stdin_+64>:   0x00007ffff7dd1b78  0x0000000000000000

gdb-peda$ p *(struct _IO_FILE *) 0x7ffff7dd18e0
$8 = {
  _flags = 0xfbad208b,
  _IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_buf_end = 0x7ffff7dd1b78 <main_arena+88> "\200!`",

_IO_str_jumps

stdin/stdoutなどにはvtableのポインタがあり、制御を奪取する際の狙い目である。 そのためかポインタが正常なものかチェックが入るようになった。 回避方法として、_IO_str_jumpsは手軽で汎用性が高い。

詳しくは以下のリンク先を参照

veritas501.space

おわりに

ということで、以上が個人的によく使うテクニックでした。

明日は、@N4NU さんの「各種OSのUserlandにおけるPwn入門」です。 windowslinux以外のosについても紹介されそうなタイトルで、楽しみですね。

あと宣伝ですが、1人1日1writeup advent calendarというのを細々とやっています。 そちらも良かったら読んでいってください。 ただ既にwriteupのストックはないし、修論などで禿げそうなのでadvent calendarは失敗しました。

hama.hatenadiary.jp

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

CODE GRAY CTF sured writeup

はじめに

一人writeup advent calendarの5日目です。1日1問分のwriteupを目標に頑張っていきます。 5日目の問題は、CODE GRAY CTFで出題された「sured」。race conditionを使った問題で初めての人にオススメです。

初期調査

セキュリティ機構は以下の通りで、特筆すべきところはcanaryがなくstackでbofがあれば、簡単に制御を奪えること。

[root@ubuntu] ~/ctf/CODEGLAY/sured
# checksec ./23016_sured
[*] '/root/ctf/CODEGLAY/sured/23016_sured'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

解析

実行するとこちらからの入力を受け付けており、入力した文字列を返してくる。 LEAVEという文字列を送ると処理が終了する。

[root@ubuntu] ~/ctf/CODEGLAY/sured
# ./23016_sured
Hello, visitor. Leave your comment!
comment:
hoge
Your comment: hoge
comment:
hogehoge
Your comment: hogehoge
comment:
LEAVE
Your comment: LEAVE
Goodbye visitor!

このバイナリは、起動してからスレッドを1つ作成している。 メインをスレッドA、作成したもう片方をスレッドBとすると、それぞれのスレッドは以下の処理を行う。

スレッドAは、標準入力から文字列を読み込む。 読み込んだ文字列はstd::stringとして保存され、文字列の長さをbss領域に保存する。 文字列の読み込みと長さの保存をループで繰り返すようになっている。

スレッドBでは、入力された文字列がLEAVEで始まるかどうか確認している。 bssに保存されている文字列の長さによって、文字列をstackにコピーする処理が異なっている。 長さが0x100byte以下なら、長さ分だけコピーし、0x100byteより大きいなら、0x100byteだけコピーする。 コピーされた文字列がLEAVEで始まる場合、全体の処理を終える。

重要な処理だけのコードは以下のようになっていた。

std::string buf;
int n;

int thread(void) {
    char dest[0x100];
    do {
        char* data = buf.data();
        if (n <= 0x100) {
            memcpy(dest, data, n);
        } else {
            qmemcpy(dest, data, 0x100);
        }
    } while(strncmp(dest, "LEAVE", 5));
    return 1;
}

int main(void) {
    int result = 0;
    // threadを開始する処理、threadの返り値がresultに入る。
    std::thread::_M_start_thread(); ?????

    std::cout << "Hello, visitor. Leave your comment!" << std::endl;

    while(result) {
        std::cout << "comment: " << std::endl;
        std::cin >> buf;
        n = buf.length();
        std::cout << "Your comment: " << buf.data() << std::endl;
    }
    std::cout << "Goodbye visitor!" << std::endl;
    return 0;
}

exploit

各スレッドでは文字列とその長さを扱っているが、それぞれの処理で文字列に対するロックがないためrace conditionが発生する。 race conditonを起こすには以下のようにして長い文字列と短い文字列を交互に送信する。

  1. はじめに0x100byte以下の短い文字列を入力する。
  2. スレッドBで長さのチェックが行われる。入力文字列が0x100byte以下であるため、文字列の長さnでmemcpy()が実行される方へと分岐する。
  3. 長さのチェックが終わってからmemcpy()が実行されるまでの間に、0x100byteを超える長い文字列を入力する
  4. チェック後のnが書き換わり0x100byteを超えた値で、memcpy()が実行される。
  5. stackに用意された配列は0x100byteなので、それ以上の大きさだとstack overflowとなる。

race conditionが上手く行くかどうかネットワークの状況により、成功率は高くない。 リモートではたまたま1度だけ上手く行った時にフラグを取ることができた。

CODE GRAY CTF sured · GitHub

[root@ubuntu] ~/ctf/CODEGLAY/sured
# python exploit.py
[*] '/root/ctf/CODEGLAY/sured/23016_sured'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './23016_sured': pid 12741
[*] Pwning
[*] Switching to interactive mode
Hello, visitor. Leave your comment!
comment:
Your comment: 1
comment:
Your comment: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx$
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
comment:

(長いので省略)

Your comment: LEAVE
comment:
$ id
uid=0(root) gid=0(root) groups=0(root)

フラグ: FLAG{h4v3_4_v4ri0us_p3r5p3ct1v3_v13w_c4n_f17d_bu9}

Sharif CTF 2018 kdb writeup

はじめに

一人writeup advent calendarの4日目です。1日1問分のwriteupを目標に頑張っていきます。 4日目の問題は、Sharif CTF 2018で出題された「kdb」。kUAF (kernel Use After Free) を起点としたkernel exploit問題で初めての人にオススメです。

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

配布ファイルは以下の通り。 run.shを中身を見てみると、smep、kaslrが有効になっている。

% ls
bzImage rootfs.cpio run.sh* 

% cat run.sh
#!/bin/sh

qemu-system-x86_64 -cpu kvm64,+smep -m 64M -kernel ./bzImage -initrd ./rootfs.cpio -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" -smp cores=2,threads=1,sockets=1 -monitor /dev/null -nographic 2>/dev/null

解析

攻略対象は/dev/kdb/dev/kdbに対する操作はopenやreleaseなどが定義されているが、ioctl()がメインの処理となっている。 ioctl(fd, cmd, args);と言った感じでユーザ空間から呼んでやると以下の処理を実行する。 cmdによって実行する内容が異なっている。

  • 0x13371338: alloc_buf()を呼んでbufを新規作成。argsは先頭20byteは作成されたbufのnameに書き込まれる。次の8byteはsizeで、kmalloc()を呼ぶ時の引数となっている。

  • 0x13371339: find_cbuf()でbufの探索。argsは先頭20byteで指定した文字列と合致するnameを持つbufを探す。argsの次の8byteは使われない。そのまた次の8byteがユーザ空間のポインタとなっている。copy_to_user()で見つけたbufのptrの中身をsize分だけ書き出す。

  • 0x1337133a: 0x13371339の逆の処理で、copy_from_user()で書き込む。

  • 0x1337133d: find_cbuf()で探索したbufが存在した場合、free_buf()で解放する。
  • 0x1337133f: argsにはname[0x20]; user_ptr; len;ように30byte分設定しておく。find_cbuf()でbufを探索し、見つかったbufのsizeが、lenより小さい場合、kfree(ptr)してから新しくkmalloc(len)で領域を確保する。その後copy_from_user()で書き込む。lenが超えていない場合は、元のptrにそのままcopy_from_user()で書き込む。

使用される構造体は以下の通り。双方向のリンクドリストとなっている。

struct buf {
    char name[0x20];
    char* ptr;
    long size;
    struct buf *next;
    struct buf *prev;
};

exploit

ioctl(0x1337133F)には、kfree()した後にポインタを適切に処理せずに終了するパスが存在する。 つまり、ioctl()を呼ぶ時の引数を工夫することで、kUAFがおこせる。 具体的には、まず引数で設定するサイズはオブジェクトのサイズよりも大きくしてkfree()を実行させる。 その後、書き込む元のユーザランドのポインタとサイズの合計がユーザ空間内に収まっているかチェックが行われる。 このチェックに引っかかるとkfree()したポインタはそのままでretrunするためkUAFとなる。

この問題環境はkaslrが有効なので、まずはカーネル空間のアドレスをリークする必要がある。 取得した領域は初期化されず、さらに取得した領域から自由に読み込みができるので、未初期化の空間からカーネル空間のアドレスを特定できそうなアドレスを得る。 具体的には、/dev/ptmxのオープン時に作成されるtty_struct構造体がもつメンバであるopsのアドレスからカーネル空間に関するリークができる。 opsはptm_unix98_opsを指しており、これはカーネル空間のdata(?)を指している。 そのため、そのアドレスからカーネルがマップされているベースのアドレスが計算できる。

リーク後は、ropでcommit_creds(prepare_kernel_commit(0));を実行後にswapgs+iretqでユーザ空間へ戻り、シェルを起動する。 このropの流れは、今までのwriteupのものと同じなので詳細については省略する。

gist.github.com

f:id:hama7230:20181120215120p:plain

NCSTISC 2018 babydriver writeup

はじめに

一人writeup advent calendarの3日目です。1日1問分のwriteupを目標に頑張っていきます。 3日目の問題は、NCSTISC CTF 2018で出題された「babydriver」。kUAF (kernel Use After Free) を起点としたkernel exploit問題で初めての人にオススメです。

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

配布されるファイルは以下の通り。boot.shが起動スクリプトで、起動するとqemuが立ち上がる。 boot.shの中身を見ると、smepが有効になっている。また、起動してカーネルのバージョンを確認すると、4.4系であった。

% ls
boot.sh* bzImage* rootfs.cpio*

% cat  boot.sh
#! /bin/sh

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep

[In qemu]
/ $ uname -a
Linux (none) 4.4.72 #1 SMP Thu Jun 15 19:52:50 PDT 2017 x86_64 GNU/Linux

/ $ cat /proc/cpuinfo | grep flags
flags           : fpu vme 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 pni cx16 x2apic hypervisor smep

解析

rootfs.cpioを展開すると、/lib/modules/4.4.72/babydriver.koに攻略対象となるカーネルモジュールがある。 babydriver_init()がモジュールの初期化関数で、/dev/babydevを作成し登録している。 デバイスに対する動作は、以下の通りとなっている。

  • babyopen : ユーザから/dev/babydevをopen()すると呼ばれる。カネールモジュールのbss領域にあるbabydev_structを初期化する (構造は以下の通り)。device_bufにkmem_cache_alloc_trace()で得られたポインタを入れ、device_buf_lenに0x40を入れる。kmem_cache_alloc_trace()で取得されるチャンクのサイズは0x40となっている。

  • babyrelease :/dev/babydevをclose()すると呼ばれる。kfree(babydev_struct.device_buf)を呼ぶ。

  • babywrite : /dev/babydevにwrite()すると呼ばれる。device_buf_lenがwrite()で書き込むサイズよりも大きい場合、copy_from_user(device_buf, user_buf, len)が呼ばれる。

  • babyread : /dev/babydevにread()すると呼ばれる。device_buf_lenがread()で読み込むサイズよりも大きい場合、copy_to_user(user_buf, device_buf, len)が呼ばれる。

  • babyioctl : /dev/babydevにioctl()すると呼ばれる。cmdが0x10001で呼び出すと処理が実行される。ioctl(fd, 0x10001, size)といった感じで呼ぶと、元々のbabydev_struct.device_bufをkfree()して、新しくkmalloc(size)で取得されたポインタを入れる。device_buf_lenにはsizeが入る。

struct babydev_struct {
    char* device_buf;
    long device_buf_len;
}

exploit

babydev_strudtはモジュールのbssで管理されており、/dev/babydevを複数回開いてそれぞれのfd経由で呼び出しても同じ箇所を使用する。 そこで/dev/babydevを2度open()し、片方をclose()する。 すると、close()する際にbabyrelease()が呼ばれて、babydev_struct.device_bufはkfree()されるが、もう片方のfd経由ではbabydev_struct.device_bufを参照している状態にあり、kUAFとなる。

kUAFを起こせるで、適当なオブジェクトを被せて制御を奪っていく。 今回は/dev/ptmxが存在するので、これを使う。 /dev/ptmxを開いた時、tty_struct構造体用の領域がkmalloc()で取得される。 このtty_struct構造体にはopsというメンバを持っており、これはtty_operationsで定義される関数テーブルとなっている。opsを書き換えることで、制御が奪える。 tty_struct構造体は、kmalloc-256に該当するオブジェクトなので、ioctl()で事前にサイズを256byteに変更しておく。

// https://elixir.bootlin.com/linux/v4.4.72/source/include/linux/tty.h#L259
struct tty_struct {
    int    magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops; // これを書き換えたい
    int index;

    あとは省略
}

今回はsmapが無効なので、偽のtty_operationsをユーザ空間に用意できる。 kUAFで重複している/dev/ptmxが指すopsメンバを、ユーザ空間に用意した偽のものを指すように書き換える。

root権限を取るまでの流れは、基本に忠実にropで実行してく。 ユーザ空間に予め偽のstackとrop chainを用意して、そこへpivotさせる。 ropでは、commit_creds(prepare_kernel_cred(0))を呼び出し、swapgs+iretqでユーザ空間へ復帰し、シェルを起動する。 pivotする先は、0x01740100で固定なのであらかじめmmapでその近辺を確保しておく。

kUAFで重複している/dev/ptmxopsを書き換えたままなので、そのままだとroot権限のコマンドを1回実行したらカーネルパニックとなってしまう (実際になった)。 なので、不正に上書きしたopsを正常なものに書き戻しておく必要がある。 exploit内では、一度正常な状態のttyを保存してから不正な状態に書き換えている。 そして上手くroot権限が取れた後に、正常な状態に書き戻している。

gist.github.com

参照URL

SEC-T CTF gh0st writeup

はじめに

一人writeup advent calendarの2日目です。 1日1問分のwriteupを目標に頑張っていきます。 2日目の問題は、SEC-T CTF 2018で出題された「gh0st」。 out-of-boundsを起点としたkernel exploit問題で初めての人にオススメです。

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

配布ファイルを以下の通り。 READMEを読むと、kaslr, smep, smapがオフになっているらしい。 initは圧縮されたcpioファイルなので展開する。 展開するとgh0st.koが存在する。 このモジュールが攻略対象。

% ls
README       gdb.cmd*     init         run.sh*      tiny.kernel* vmlinux.bin*

% cat README
First complete your exploit locally using the given qemu environment, it comes with symbols etc.

./run.sh -debug

gdb
target remote :1234
file ./vmlinux.bin

Then pwn the remote system by connecting to the service and f.ex. dump your exploit bin via gzip/b64.

NX is ON, KASLR/SMEP/SMAP is OFF!

解析

gh0st.koでは、簡易バイトコードを実行できる。 バイトコードそのものはカーネルモジュールのbss領域にbytecodeとして保持される。 また、バイトコード実行の管理領域として、stack_ptrなどがbss領域に存在する。

/dev/gh0st に対する主な挙動は以下の通りであった。

  • write: bytecodeへユーザ空間から0x1000byteのコピー

  • ioctl(0x1337B4B3): writeを呼び出してbytecodeに0x1000byte書き込み、bytecodeの内容がフォーマットに合っているか確認する。フォーマット通りであった場合、指定したサイズでkmalloc()を呼び出し、結果をstack_ptrとorig_stack_ptrへ書き込む。bytecodeのフォーマットは以下の通り。

    • header: 4byte。"BFBF"である必要がある。
    • size: 4byte。kmalloc()で呼び出すサイズの指定。スタックの大きさ。
    • out: 0x100byte。命令用のバッファ。
    • in: 0x100byte。命令用のバッファ。
    • inst: 0x418byte。命令のバイトコード。使用できる命令は下述の6つ。
  • ioctl(0xAC1DC0DE): 先に0x1337B4B3を呼び出す必要がある。bytecodeの命令に従って処理を実行する。命令は0x418回実行されると終了する。実行できる命令は以下の6つ。out_counter、in_counterは0で初期化されてから実行される。

    • '+', '>': stack_ptrをインクリメント
    • '-', '<': stack_ptrをデクリメント
    • ',': stack_ptrが指す先へout[out_counter]を書き込む。その後out_counterをインクリメントする。out_counterが0x100まで行ったら処理終了
    • '.': stack_ptrが指す先からin[in_counter]を読み込む。その後in_counterをインクリメントする。in_counterが0x100まで行ったら処理終了

exploit

stack_ptrの操作に制限がないため、out-of-boundsで読み書きができる。 読みはできるが、ユーザ空間へ渡す方法が (簡単には) ないので、書き込みだけを使っていく。 stack_ptrが指す先はkmalloc()で取得された領域である。 そのため、その前後には同様にkmalloc()を用いて確保されたオブジェクトが存在している。

今回のカーネルはSLOBアロケータを使用しているため、近いサイズのオブジェクトは隣接して配置される可能性が高い。 具体的には、256byte未満、256byte以上1024byte未満、1024byte以上の3種類に分けられ、それぞれは別の領域でアロケートされる。 事前にターゲットとなるオブジェクトを配置しておき、ioctl(0x1337B4B3)でkmalloc()を呼び出す。 もしターゲットのオブジェクトとkmalloc()で取得された領域が連続している場合、stack_ptrをズラすことでターゲットのオブジェクトへ任意の書き込みが行える。

ターゲットとするオブジェクトは、ファイルを展開した際に使用されるfile構造体にした(Linux source code: include/linux/fs.h (v4.17) - Bootlin)。 理由は単純でメンバのf_opは関数テーブルを指しているので、これを上書きすれば制御を奪えるからである。 具体的な流れは、stack_ptrを適用な回数デクリメントさせて、真上に存在する(であろう)file構造体のf_opを指す。 書き込みを用いてf_opをこちらが制御できるアドレスで上書きする。

実際に書き込むアドレスは、gh0stのbytecodeの中にした。 自由に制御できるうえに、今回はKASLRが無効なのでbytecodeのアドレスは固定となっている。 あらかじめbytecode中に偽のfile_operationsを用意しておく。 ユーザ空間からread()を実行すると、f_op->read()が実行されるので、readの位置に呼び出したいアドレスを書いておく。 file構造体を狙うため、適当なファイルを複数回開いてからgh0stでkmalloc()を呼べば隣接する確率が高くなる(0x100回もすれば100%上手くいった)。

root権限を取るまでの流れだが、基本に忠実にcommit_creds(prepare_kernel_commit(0));を呼び出す。SMAPが無効なのでユーザ空間内にstackをpivotさせてROPを実行する。 (あとから考えるとSMEPも無効なのだから直接ユーザ空間内でもよかった)

gist.github.com

f:id:hama7230:20181118154849p:plain

参考URL

Blaze CTF 2018 blazeme writeup

はじめに

一人writeup advent calendarの1日目です。 1日1問分のwriteupを目標に頑張っていきます。 1日目の問題は、Blaze CTF 2018で出題された「blazeme」。 stack overflowを起点としたkernel exploit問題で初めての人にオススメです。

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

配布されたファイルは以下の通りで、blazeme.cが攻略対象のデバイスソースコードである。 run.shが起動スクリプトになっている。 カーネルのバージョンは4.15で、smepやsmapは無効になっている。

% ls
blazeme.c    bzImage      rootfs.ext2  run.sh

% cat run.sh
#!/bin/sh
/usr/bin/qemu-system-x86_64 -kernel bzImage -smp 1 -hda rootfs.ext2 -boot c -m 64M -append "root=/dev/sda nokaslr rw ip=10.0.2.15:10.0.2.2:10.0.2.2 console=tty1 console=ttyAMA0" -net nic,model=ne2k_pci -net user -nographic
WARNING: Image format was not specified for 'rootfs.ext2' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]

Welcome to Buildroot
buildroot login: blazeme
Password:
login: can't change directory to '/home/blazeme'
$ uname -a
Linux buildroot 4.15.0 #1 SMP Wed Apr 18 01:12:17 PDT 2018 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 nopl cpuid pni cx16 hypervisor lahf_lm svm 3dnowprefetch retpoline rsb_ctxsw vmmcall

解析

blazeme_init()で、blazemeデバイスを作成し登録している。 blazeme_fopsはblazemeに対する操作を定義しており、read, write, open, releaseが登録されている。 これらはカーネル空間(カーネルモジュール)の関数なので、ユーザ空間から呼び出すには、

  • blazeme_open() => open('/dev/blazeme', flag, mode);
  • blazeme_close() => cloes(blazeme_fd);
  • blazeme_read() => read(blazeme_fd, buf, size);
  • blazeme_write() => write(blazeme_fd, buf, size);

といった感じになる。(blazeme_fdはopen()で得られたfd、他の引数については適切なものを設定)

blazeme_write()では、kmalloc(64)で取得した領域にユーザ領域からcopy_from_user()で書き込みを行っている。 その後、strncat()でカーネルのスタックへと書き込んでいる。

脆弱性はblazeme_write()のstrncat()でstack overflowとなることである。 今回のカーネルはSLUBアロケータを使用している。 SLUBアロケータでのチャンクはglibc mallocのチャンクでのサイズに相当するメタデータは存在しない。 そのため取得した領域を一杯まで埋めてstrlen()をすると、次のチャンクの内容も巻き込んで長さが求められる。 blazeme_write()でkmalloc()で取得されたポインタはエラーがなければkfree()されないので、複数回blazeme_write()を呼び出すことでチャンクを隣接して配置できる可能性がある。 上手くチャンクが隣接し、blazeme_write()のローカル変数として用意されたスタックのバッファのサイズを超えればstack overflowが発生する。

#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define DEVICE_NAME "blazeme"

#define ERR_BLAZEME_OK (1)
#define ERR_BLAZEME_MALLOC_FAIL (2)

#define KBUF_LEN (64)

dev_t dev = 0;
static struct cdev cdev;
static struct class *blazeme_class;

ssize_t blazeme_read(struct file *file, char __user *buf, size_t count,
                                loff_t *ppos);
ssize_t blazeme_write(struct file *file, const char __user *buf,
        size_t count, loff_t *ppos);

int blazeme_open(struct inode *inode, struct file *file);
int blazeme_close(struct inode *inode, struct file *file);

char *kbuf;

struct file_operations blazeme_fops =
{
    .owner           = THIS_MODULE,
    .read            = blazeme_read,
    .write           = blazeme_write,
    .open            = blazeme_open,
    .release         = blazeme_close,
};

ssize_t blazeme_read(struct file *file, char __user *buf, size_t count,
                                loff_t *ppos) {
    int len = count;
    ssize_t ret = ERR_BLAZEME_OK;

    if (len > KBUF_LEN || kbuf == NULL) {
        ret = ERR_BLAZEME_OK;
        goto out;
    }

    if (copy_to_user(buf, kbuf, len)) {
        goto out;
    }

    return (ssize_t)len;

out:
    return ret;
}

ssize_t blazeme_write(struct file *file,
                        const char __user *buf,
                        size_t count, loff_t *ppos) {
    char str[512] = "Hello ";
    ssize_t ret = ERR_BLAZEME_OK;

    if (buf == NULL) {
        printk(KERN_INFO "blazeme_write get a null ptr: buffer\n");
        ret = ERR_BLAZEME_OK;
        goto out;
    }

    if (count > KBUF_LEN) {
        printk(KERN_INFO "blazeme_wrtie invaild paramter count (%zu)\n", count);
        ret = ERR_BLAZEME_OK;
        goto out;
    }

    kbuf = NULL;
    kbuf = kmalloc(KBUF_LEN, GFP_KERNEL);
    if (kbuf == NULL) {
        printk(KERN_INFO "blazeme_write malloc fail\n");
        ret = ERR_BLAZEME_MALLOC_FAIL;
        goto out;
    }

    if (copy_from_user(kbuf, buf, count)) {
        kfree(kbuf);
        kbuf = NULL;
        goto out;
    }

    if (kbuf != NULL) {
        strncat(str, kbuf, strlen(kbuf));
        printk(KERN_INFO "%s", str);
    }

    return (ssize_t)count;

out:
    return ret;
}

int blazeme_open(struct inode *inode, struct file *file) {
    return 0;
}

int blazeme_close(struct inode *inode, struct file *file) {
    return 0;
}

int blazeme_init(void) {
    int ret = 0;

    ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
    if (ret) {
        printk("blazeme_init failed alloc: %d\n", ret);
        return ret;
    }

    memset(&cdev, 0, sizeof(struct cdev));

    cdev_init(&cdev, &blazeme_fops);
    cdev.owner = THIS_MODULE;
    cdev.ops = &blazeme_fops;

    ret = cdev_add(&cdev, dev, 1);
    if (ret) {
        printk("blazeme_init, cdev_add fail\n");
        return ret;
    }

    blazeme_class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(blazeme_class)) {
        printk("blazeme_init, class create failed!\n");
        return ret;
    }

    dev = device_create(blazeme_class, NULL, dev, NULL, DEVICE_NAME);
    if (IS_ERR(&cdev)) {
        ret = PTR_ERR(&cdev);
        printk("blazeme_init device create failed\n");

        class_destroy(blazeme_class);
        cdev_del(&cdev);
        unregister_chrdev_region(&dev, 1);

        return ret;
    }

    return 0;
}

void blazeme_exit(void)
{
    cdev_del(&cdev);
    class_destroy(blazeme_class);
    unregister_chrdev_region(&dev, 1);
}

module_init(blazeme_init);
module_exit(blazeme_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("BLAZECTF 2018 crixer");
MODULE_DESCRIPTION("BLAZECTF CTF 2018 Challenge Kernel Module");

exoloit

今回はcanaryもないので、stack overflowでリターンアドレスを簡単に書き換えることができる。 kaslr、smap、smepが無効となっているので、ret2usrが容易にできる。 ただ今回は練習を兼ねて、stack pivotからのROPを行なった。 切り替え先の偽のスタックをユーザ空間に予め用意してROPガジェットを書き込んでおき、任意のROPに持ち込む。 ROPでは、commit_creds(prepare_kernel_cred(0));を呼んでroot権限を得てから、swapgsとiretqでユーザ空間へ戻っている。 stack pivotのガジェットのアドレスで埋めたバッファで、write()を無限に呼ぶことでstack overflowを起こした。

gist.github.com

f:id:hama7230:20181127021431p:plain

参考リンク