HITCON CTF 2019 Quals - dadadb Write-up (+ call/jmp tracer) - HackMD
# HITCON CTF 2019 Quals - dadadb Write-up (+ call/jmp tracer) ###### tags: `ctf` `pwn` `windows` `intel pin` # 概要 この記事は,[CTF Advent Calendar 2019](https://adventar.org/calendars/4241) の2日目の記事です. 1日目はhamaくんの「[CTFでのLinuxのユーザランド以外の問題についてまとめる](https://hama.hatenadiary.jp/entry/2019/12/01/231213)」でした. # はじめに 今回はHITCON CTF 2019 Qualsで出題された,dadadbという問題について解説します. 解いたチームは6チームだけ(`A*O*E`,`PPP`,`r3kapig`,`Shellphish`,`binja`,`Never Stop Exploiting`)なので,かなり難しい部類だと思います. ![](https://i.imgur.com/o5W8nEh.png) またあまり情報を得ることはできなかったのですが,解く過程で作成した,pinベースの調査ツールについても供養がてらご紹介しようと思います. # 問題文 ``` It’s ddaa’s database of doctoral dissertation. Try to get some magic doctoral dissertation in C:\dadadb\flag.txt nc 13.230.51.176 4869 Note: The service is running on Windows Server 2019 Microsoft Windows [Version 10.0.17763.805] ``` ファイルは[こちら](https://github.com/scwuaptx/CTF/raw/master/2019-writeup/hitcon/dadadb/challenge/dadadb-122988ea411bf6b367fd82e8386aabcb0cefc947.zip)からDLできます. # 準備 WindowsのPwn問ですが,最初にやることは大して難しく有りません. - 問題文に書かれている通りWindows Server 2019の評価版環境を用意し,Microsoft Upgradeで最新にする - 当時の最新が`Version 10.0.17763.805`だった - 必要なツールを入れる - 個人的には,デバッガとしてx64dbgがあれば十分だと思います - その他としてProcess Explorerやサクラエディタ,clinkなど好きなツールを導入しておきましょう - OSが評価版なので問題を解いたら結局破棄する環境です,頑張って作り込む必要はありません # 初動解析 ざっくり見ていきましょう. まずは`main`を見つけます. ![](https://i.imgur.com/LIdg7jO.png) `login`,`add_note`,`show_note`,`delete_note`を実装したノート系サービスです. `show_note`,`delete_note`はインライン展開されているようですね.ここにバグは有りません. ![](https://i.imgur.com/GWhX0OM.png) ちなみにノートはこんな感じの構造体です. ![](https://i.imgur.com/ncQE9lB.png) また`NOTE_ARRAY`は256個のテーブルで,`note->key`の0バイト目によって振り分けられ,個別のリンクリストを持ちます. ![](https://i.imgur.com/ag0iTb7.png) `login`はこんな感じ.同梱の`user.txt`を見て比較しているだけなので,ここも大したことは有りません. ![](https://i.imgur.com/TSEldJI.png) `add_note`を見てみましょう.ここにバグがあります. ![](https://i.imgur.com/f8uOWJ2.png) さてこの関数は`add_note`と命名していますが,実際には`edit_note`的な機能も兼ねています. ノートは`key`と`content`を持っていて, - 新たな`key`を指定: 新たなノートの`content`にデータを登録(`add_note`相当) - 既存の`key`を指定: 既存のノートの`content`のデータを更新(`edit_note`相当) という感じです.但し既存のノートの`content`を更新する際,毎回`HeapFree(content)`が走ります. さて,バグはかなり自明です. 内容を更新する際に新たな`content`の`size`が尋ねられますが,その`size`は使わず過去に登録した`content_size`を使い回して`FileRead`します. つまり一度非常に大きな`content`を登録しておき,更新する際に小さい`size`を指定しながら大量のデータを送信すると,ヒープBOFが発生します. また`content`は確保後にゼロクリアされないので,`show_note`を用いればヒープのデータリークも可能ですね. あとはここからどの様にシェルを奪うか,という方針を考える必要があります. # 各種リーク リークは簡単にできるので,先に抜いておきましょう. こんな感じのコードで,まずはヒープのベースアドレスが抜けます. ```python= s, f = sock(HOST, PORT) login() tag = pQ(0) print "[+] setup" add("Aa", 0x400, "A"*0x2f) # create add("Aa", 0x10, "A"*0xf) # re-create add(tag, 0x20, "z"*0xf) # victim print "[+] first leak" leak_data = show("Aa") leak = uQ(leak_data[0x20:0x28]) dbg("leak") heap = leak - 0x960 dbg("heap") ``` 続いてこんな感じのコードで,各種メモリのアドレスが抜けます.単に`content`のアドレスをヒープBOFで差し替えて`show_note`で抜いているだけです. ```python= print "[+] leak dll, pie, peb, and stack" def leak(addr, size=0x20, leak_data=leak_data): forge = leak_data[:0x20] + pQ(addr) + pQ(size) + leak_data[0x30:] add("Aa", 0x10, forge) r = show(tag) return uQ(r[:8]), r ntdll = leak(heap + 0x2c0)[0] - 0x163d10 ntdll &= ~0xfff dbg("ntdll") assert leak(ntdll)[1].startswith("MZ") # debug dadadb = leak(ntdll + 0x15f000 + 0x62c8)[0] - 0xf8 dbg("dadadb") assert leak(dadadb)[1].startswith("MZ") # debug cookie = leak(dadadb + 0x5008)[0] dbg("cookie") kernel32 = leak(ntdll + 0x15f000 + 0x6fd8)[0] - 0x3d8d0 kernel32 &= ~0xfff dbg("kernel32") assert leak(kernel32)[1].startswith("MZ") # debug encoding = leak(heap + 0x88)[0] dbg("encoding") peb = leak(ntdll + 0x165308)[0] - 0x80 dbg("peb") ucrtbase = leak(ntdll + 0x178548)[0] dbg("ucrtbase") assert leak(ucrtbase)[1].startswith("MZ") # debug stackbase = leak(peb + 0x1010)[0] dbg("stackbase") r = leak(stackbase + STACK_START_OFS, 0x1000)[1] while not "e\0x\0e\0" in r : r += menu() menu() stack = stackbase + STACK_START_OFS + len(r) + 0x102 dbg("stack") print "[+] fix addr" leak(heap + 0x960) # for fix to delete leak_data = show("Aa") ``` 抜いたのは`ntdll.dll`のベース, `dadadb.exe`のベース,`cookie(stack canary)`,`kernel32.dll`のベース,`_HEAP->encoding`,`PEB`のベース,`ucrtbase.dll`のベース,スタックベース,スタックの実際に利用している付近のアドレスです(この時点では,使うかわからないけど先を見据えて色々抜いただけです). Windowsでは,ヒープのベースアドレスには`_HEAP`と呼ばれる`main_arena`のような管理用の構造体が存在し,こいつは`ntdll.dll`内のアドレスを保持しています.`ntdll.dll`のベースアドレスが判明すれば,後は芋づる式に分かります. - ヒープのベースアドレス特定(=`_HEAP`を特定) - `_HEAP`は,`ntdll.dll`内のアドレスを保持 - `ntdll.dll`は,実行したバイナリ(=`dadadb.exe`)内のアドレスを保持 - `dadadb.exe`は,`.data`に`__security_cookie`を保持(起動後に値が書き換わる) - `ntdll.dll`は,`kernel32.dll`内のアドレスを保持 - `ntdll.dll`は,`PEB`内のアドレスを保持 - `PEB`は,スタックの開始~終了アドレスを保持 - スタック全体のうち,実際に使っているのがどこかは不明 - `ntdll.dll`は,`ucrtbase.dll`内のアドレスを保持 - `_HEAP`は,ヒープの各チャンクのヘッダをXOR暗号化する`encoding`を保持 `STACK_START_OFS`はとりあえずローカルでは`0x2000`にしとくと良さ気な確率でスタックのアドレスが抜けました(本番では`0x4000`だと上手く行った).雑に書いたので,スタックの実際使われている部分を特定するためのオフセットを決め打ちして若干確率的になっていますが,綺麗に書くならスタックの先頭から少しずつチェックしていけば100%の信頼度で抜くことも可能です. 尚,最後はヒープの破損状況を元の状態に戻しているだけです. # 任意書き込みへ さて,任意書き込みはどうすればよいでしょうか. 私はこの時点で,以下の2つの方針が思いついていました. 1. `next`を偽造し,任意のメモリ領域をノートのリンクリストにつなげる - 任意のメモリ領域をノートとして扱い,`content_size`相当の位置に任意の8byteを書き込む - どのメモリ領域を指定するかはこの時点ではいいアイデアが浮かんでいない 2. ノートの`content`を偽造して,強制freeから何か別のテクニックに持ち込む - この時点ではいいアイデアが浮かんでいない この内,まずは1つ目ができないかを調査することにしました.もっとも簡単なのは,RWな関数ポインタがどこかにあった場合,それを差し替えることです.その関数ポインタが呼ばれれば後はROPで何とかなるでしょう.ASLRは突破できていますから,メモリのどこに存在しても対応は可能なはずです. では,どこに関数ポインタがあるのでしょうか.またその関数ポインタは実際に呼ばれるものでしょうか. # 関数ポインタの探索ツール 方針1が可能かどうかを調査すべく,関数ポインタを探索するツールをintel pinをベースに作成しました. ## Linux版 実は元々linuxベースのツールを作ってあったので,まずはそれを紹介します. やってることは単純で,pin配下で`call [reg/mem]`や`jmp [reg/mem]`をトレースし,メモリマップ情報と突き合わせてアドレスを表示するだけです. https://gist.github.com/bata24/221f45b05740e38e81d03afc85111da5 実際に使うとこんな感じ.Linux版は色をつけて見やすくしてあります. ![](https://i.imgur.com/6XW9xR8.jpg) ## Windows版 Linux版の上記ツールを改造して,Windowsにも対応させることにしました. ただしLinuxでは`/proc/self/maps`を使えばすぐにメモリマップが確認可能ですが,Windowsではメモリマップを簡単に取得することはできません. いろいろなデバッガのコードを参考にしながら,なんとか同様の機能を実装しました. https://gist.github.com/bata24/cb63e647825fe7998aa23ac52f33b9c5 # 方針1 このツールでdadadbにアタッチします. - 上のプロンプトは`AppJailLauncher.exe`の待ち受けです. - 環境変数を使って,後ほど`MyPinTool.dll`がインジェクションされたときに,`LOG=1(ログ保存モード)`,`FIRST=1(同じアドレスは2回目以降表示しない)`,として動作するようにしています. - ファイルの作成制限を解除するため,`/nojail`付きにしています(`log.log`をオープンできるようにする). - 終了したときに環境変数を削除するようにしています. - 下のプロンプトはpinによるインジェクションです. - その下は,ProcessExplorerでアタッチすべきPIDを調べているところです. - インジェクションに成功すると,`%TEMP%\log.log`が作られます. ![](https://i.imgur.com/56IA7Xd.png) logの中に,実際に呼ばれ得る`call [reg/mem]`や`jmp [reg/mem]`が記録されています. この後色々なパターンを試したのですが,結果として面白い関数ポインタは見つからず,方針1は断念したのでした. 尚,この問題を解いていて改めて感じたことは,Windowsは思ったより堅く,簡単に奪えるような関数ポインタは存在しないということです. # 方針1.5 実はこの後,方針2を考える前にもう一つ調査していたことがあります.それは`add_note`の以下のコードを考察していて思いついたものです. ![](https://i.imgur.com/r7cFQgh.png) まず事前に`note->next`を偽造するなどで,利用される`note`がスタックのどこかを指すよう仕込んでおいたと仮定しましょう.58行目でスタック上の`note`が選択された状況を考えてみてください. `note(=stack上のどこか)->content`が`NULL`なら,60行目の`HeapFree()`を通過して77行目の処理へ到達するはずです(Windowsでは`HeapFree(NULL)`は未定義動作ですが,Linuxの`free(NULL)`と同様,コード上は何もせずリターンするようです). 77行目ではヒープから`content`が確保されますが,その後`printf()`が一度呼ばれてから`ReadFile()`しています.この付近のコードは,アセンブリで読むと以下のようになっています. ![](https://i.imgur.com/CmMEpOG.png) `HeapAlloc()`で確保したアドレス(=`rax`)は,一度`note->content`に保存しているのが確認できます(=`mov [rdi], rax`).そして`printf()`を呼び出してから再度取り出し,そこに`content`としてデータを読み込むのです. この動きを元に,以下のような流れで攻略できるのではないかと考えました. - `HeapAlloc()`で`note->content`にヒープのアドレスが書き込まれる - 但し`note`がスタックのどこかを指すと仮定しているので,実際はスタック上にヒープのアドレスが書き込まれる - `printf()`の内部で,スタックにスタックのアドレスを積む処理が存在する(?) - 実際にこんな処理があるかどうかはわからない - これらのアドレスを重ねておけば,`printf()`によってスタック上の`note->content`が書き換わる - つまり`note->content`がスタックのアドレスを指す - スタックのアドレスに対して大量のヒープBOFが発生,つまりスタックBOF - cookieが抜けているので,ROP可能 この方針1.5はとても魅力的に見えました.少なくともどうすればよいか見当がついていない方針2よりは,探索する価値があると思ったのです. そこで,小一時間かけてデバッグしたのですが,この方針は結局NGでした.確かに,`printf()`の処理においてスタックにスタックのアドレスを積む処理はあったのですが,そのようなアドレスを`note`として指定しようとすると,前段の`HeapFree()`を突破できなかったのです(`note->content`が非NULL). 残念ながら方針1.5も諦めました. # 方針2 さて当初は攻略の見当がつかなかった方針2ですが,方針1.5を調査している最中に1つ閃きました.よくよく考えれば,`note->content`を頑張って差し替える必要はないのです.`HeapAlloc()`からスタックのアドレスが返ってくれば,やりたいこと(スタックBOF)は達成できるのです. そして,`HeapAlloc()`からスタックのアドレスを返すテクニックは,Linuxでは知られています.そう,`House of spirit`ですね. ということで,Windowsでの`House of spirit`を試したところ,なんと上手く行ってしまいました. Windowsのヒープのチャンクの仕組みは,以前スライドで公開したので,お持ちの方は見てみると良いでしょう(現在は公開停止しています).ちなみに該当する部分だけ引っ張ってくると,こんな感じです. 尚当時のスライドは32bit環境なのでチャンクのヘッダは8バイトですが,64bit環境ではこれが0x10バイトでalignされます.また`Size`や`PreviousSize`は,32bit環境では8バイト単位の値を持ちますが,これも64bit環境では0x10バイト単位の値を保持するよう変更されています. ![](https://i.imgur.com/DPeI6K4.png) ![](https://i.imgur.com/edy0QGF.png) これを元に,ヒープのチャンクを偽造しておきます.ここでは3つのチャンクを作っていて,0x10, 0x20,0x10のサイズのチャンクを3つ続けて作っています.2つ目のチャンク(0x20サイズ)が本命のチャンクで,前後に偽造したチャンクを付加している状況ですね. ```python= print "[+] create fake heap on to stack " def create_heap_header(size, prev_size): busy = 1 unused = 0x10 # size = size/0x10 prev_size = prev_size/0x10 raw = size | (busy<<16) | (((size&0xff)^((size>>8)&0xff)^busy)<<24) | (prev_size<<32) | (unused<<56) return encoding ^ raw fake_chunk = tag + pQ(create_heap_header(0x10, 0x10)) # 1st fake_chunk += pQ(0) + pQ(create_heap_header(0x20, 0x10)) # 2nd fake_chunk += pQ(0) + pQ(0) fake_chunk += pQ(0) + pQ(create_heap_header(0x10, 0x20)) # 3rd ``` `fake_chunk`は64バイト以下にしなければなりません.これを書き込みたいのはスタックで,スタックへ書き込むには`add_note`の`key`として書き込む必要があるからです.`add_note`の`key`は64バイトまでしか受け付けないので,その制約を回避するためギッチギチに詰め込んでいます. 面白いのは,Windows 64bitのチャンクサイズは最低0x20バイト(チャンクヘッダ0x10バイト+ユーザ利用領域0x10バイト)であるにもかかわらず,前後においた1つ目のチャンクと3つ目のチャンクは0x10バイト(=ユーザ利用領域を持たない変なサイズ)であっても問題なく通過することです.おそらく`kernel32.dll`の`HeapFree()`時における前後チャンクの併合処理では,`prev_size`や`size`で得られる位置を`encoding`で復号し,チャンクヘッダの`busy`が1であれば併合しない,というだけの処理なのでしょう.サイズが適切かどうかまでは見ていないようです. さてこの偽造チャンク3つを`add_note`で`key`として送ると,`key`は一旦スタック上に保存されるため,スタック上に偽造チャンクが3つできることになります.そして2つ目のチャンクのアドレスを指定して`HeapFree()`が行われるとエラーなく通過し,次回の`HeapAlloc()`にて2つ目のチャンク(=スタックのアドレス)が返ります. あとは単なるスタックBOFですので,ROPで`VirtualProtect()`を呼び,シェルコードを呼ぶだけですね.`dadadb.exe`は`AppJailLauncher.exe`配下で動くはずなので,`seccomp`配下で動いているようなイメージですね.とはいえ流石にフラグは読むことができるはずなので,余計なことはせず`open`/`read`/`write`型のシェルコードにするのが適切でしょう. # Exploit 最終的なexploitは以下のようになりました. 途中で方針1や方針1.5に寄り道したせいで多少無駄な事もやっていますが(`next`を差し替えた後にリンクリスト上の前のノートを`delete`するなど),気にしてはいけません. {%gist bata24/e36842df06b7e62127405d39e2a5ed7e %} # 終わりに Windowsのexploitは苦手なのですが,少しだけ慣れることができた気がします. あと,後日談としてMicrosoftに,最新のWindowsの`kernel32.dll`でもHouse of spiritが可能な旨を報告しておきました.まぁバグじゃないのですぐは直さないと返信は頂きましたが,将来直すかもしれないね,とのことでした.また記事にしても良いと許可も貰ったのでこの記事を書きました. 明日は私の[Google CTF 2019 Quals - sandstone Write-up](https://hackmd.io/@bata24/SkkrkDlaB)です.