# HITCON CTF 2018 - Super Hexagon (Part 4/7) ###### tags: `trustzone` `ctf` `pwn` `ARM` `Aarch64`, `kernel` `hypervisor` # はじめに この記事は,[CTF Advent Calendar 2020](https://adventar.org/calendars/5338) の6日目の記事です. 5日目は私の「[HITCON CTF 2018 - Super Hexagon (Part 3/7)](https://hackmd.io/@bata24/S1bHxavMU)」でした. # リンク集 - 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 # S-EL0の攻略 ## 状況確認 セキュアワールドのコードを読む前に,まずは各ELにおけるセキュアワールドへの移行に関連するコードを確認しておこう.具体的にはシステムコールなどの処理だ. ### EL0について まずはEL0のコードだ.`tc_init_trustlet()`,`tc_register_wsm()`,`tc_tci_call()`という`svc`呼び出しがあった. ![main](https://i.imgur.com/iTVoRah.png) ![load_trustlet](https://i.imgur.com/4vNkkG3.png) ![load_key](https://i.imgur.com/Fdo0K7q.png) ![save_key](https://i.imgur.com/TZZfgjV.png) それぞれのシステムコールは,`0xFF00000N`という値で呼び出されていたことを思い出そう. ![tc_init_trustlet](https://i.imgur.com/xtWHraz.png) ![tc_register_wsm](https://i.imgur.com/Qmh5GCQ.png) ![tc_tci_call](https://i.imgur.com/fGIkw9w.png) ### EL1について 次にEL1カーネルのシステムコールハンドラにおいて,システムコール番号が`0xFF00000N`の場合の処理を見てみよう. ![](https://i.imgur.com/DL3CD5U.png) そしてそこから呼ばれる処理だ.`do_smc_call()`に処理を渡しているのがわかるだろう. ![](https://i.imgur.com/ipZNHZl.png) ![](https://i.imgur.com/ru3eamb.png) ちなみに,カーネルの初期化時にも`smc`が呼ばれている. ![](https://i.imgur.com/k0ClPsb.png) ![](https://i.imgur.com/JbObVfm.png) EL1から(`hvc`ではなく)`smc`が呼ばれていることを確認したら,次へ進もう. ### EL2について 次にEL2ハイパーバイザのハイパーコールハンドラも見てみよう.こちらにもセキュアワールドの処理が見て取れる. ![](https://i.imgur.com/YLN8ehO.png) ![](https://i.imgur.com/mKmDh5A.png) `smc`番号が`0x83000003`以外の場合は,`smc`番号と引数をそのまま上位(S-EL3)に渡している点に注意しよう. ### S-EL3について 次にS-EL3 BIOSのコードのセキュアコールハンドラも見てみよう.BIOSは物理メモリ(というかFlash)の上でそのまま動いているので,IDA上でのベースアドレスは`0x0`のままで良い. `0x2400`に存在する以下のコードが割り込みベクタだ. ![](https://i.imgur.com/ku1yzlj.png) `handle_smc_w()`が呼ばれ,更に`handle_smc()`が呼ばれたところがセキュアコールのハンドラである. ![](https://i.imgur.com/cd7XDTx.png) ![](https://i.imgur.com/ja8a0R8.png) 7行目で,ノーマルワールドとセキュアワールドのどちらから呼ばれたかを判定している. `smc`番号が`TEE_OS_init`の場合は特殊な処理が行われ,ダイレクトにリターンする. `smc`番号が`TEE_OS_init`でない場合は,26目~28行目でコンテキストをセキュアワールド向けの値に設定してスタックに積み,一度この関数を抜ける. そして先の`handle_trap()`に戻り,20行目の`return_to_ctx()`経由で,設定したコンテキスト(セキュアワールド)にスイッチしているようだ(内部で`eret`が呼ばれる). ![](https://i.imgur.com/ku1yzlj.png) また先のハンドラでセキュアワールドから呼ばれた場合は, - `0x83000002`: `TEE_OS`が初期化済みであることを表示してノーマルワールドにリターン - `0x83000007`: ノーマルワールドにリターン といったことを意味していることも分かる. ここまでをまとめると,以下の通りだ.尚`0x83000002`や`0x83000007`はS-EL1から呼ばれるものと思われるため,表には含めていない. | | `TEE_OS_init()` | `tc_register_wsm()` | `tc_init_trustlet()` | `tc_tci_call()` | |:-:|:-:|:-:|:-:|:-:| | EL0 `svc`番号と引数 | - | `0xFF000003`, `wsm`, `size` | `0xFF000005`, `trustlet`, `size` | `0xFF000006`, `tci_handle` | | | - | ↓ | ↓ | ↓ | | EL1 `smc`番号と引数<br>(EL2以降の<br>`smc`番号も同様) | `0x83000001` | `0x83000003`, `wsm`, `size` | `0x83000005`, `trustlet`, `size` | `0x83000006`, `tci_handle` | | EL1呼出条件 | - | `size & 0xfff == 0`であること<br>`size <= 0x4000`であること<br>`wsm & 0xfff == 0`であること | `trustlet & 0xfff == 0`であること | `tci_handle & 0xfff == 0`であること | | | ↓ | ↓ | ↓ | ↓ | | EL2呼出条件 | 素通し | `wsm <= 0x3c000`のとき<br>`wsm += 0x40000000` | 素通し | 素通し | | | ↓ | ↓ | ↓ | ↓ | | S-EL3処理関数 | `TEE_OS_setup_w()` | - | - | - | | S-EL3呼出条件 | - | 素通し | 素通し | 素通し | | | - | ↓ | ↓ | ↓ | | S-EL1処理関数 | - | 未解析 | 未解析 | 未解析 | ## セキュアワールドのデバッグ ### セキュアワールドのアーキテクチャ S-EL3の最後で触れた`eret`だが,この命令以降は`gdb`デバッグができなくなる点について説明しておこう. 以下の`4`つのブレークポイントを記した`cmd`ファイルは,EL0で`tc_tci_call()`を発行した際,各ELを順に登っていくステップで止まるようそれぞれ仕掛けたものである. ```= # EL0 svc b *0x401b9c c # EL1 smc b *0xFFFFFFFFC0009164 c # EL2 smc b *0x40100760 c # EL3 eret b *0xfa4 c ``` ブレークポイントを仕掛けつつ実行するとわかるが,`eret`が行われた瞬間に`gdb`のデバッグは終了してしまう. ```= root@3ae0ae1ee7fd:~# gdb-multiarch -q -ex 'target remote :1234' -x cmd; kill -9 $(pgrep qemu) Remote debugging using :1234 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0xffffffffc0009144 in ?? () Breakpoint 1 at 0x401b9c Breakpoint 1, 0x0000000000401b9c in ?? () Breakpoint 2 at 0xffffffffc0009164 Breakpoint 2, 0xffffffffc0009164 in ?? () Breakpoint 3 at 0x40100760 Breakpoint 3, 0x0000000040100760 in ?? () Breakpoint 4 at 0xfa4 Breakpoint 4, 0x0000000000000fa4 in ?? () (gdb) x/10i $pc => 0xfa4: eret 0xfa8: mov x17, sp 0xfac: msr spsel, #0x1 0xfb0: str x17, [sp, #272] 0xfb4: ldr x18, [sp, #256] 0xfb8: ldp x16, x17, [sp, #280] 0xfbc: msr scr_el3, x18 0xfc0: msr spsr_el3, x16 0xfc4: msr elr_el3, x17 0xfc8: b 0xf9c (gdb) si Truncated register 8 in remote 'g' packet // これ以降gdbが解釈できなくなる Truncated register 8 in remote 'g' packet (gdb) ``` これはS-EL3内のセキュアワールドのセットアップコードを読むとわかるが,セキュアワールドのコードはAArch32で動作していることによる。27行目がそれで,`TEE_SPSR`という変数(後に`SPSR`として使われる)に`0x1D3`が入っている. ![](https://i.imgur.com/PDRmVLb.png) ARMv8の`SPSR`のビットアサインについては,問題と同時に提供されたマニュアルを読むか,[Transitioning from ARM v7 to ARM v8](https://events.static.linuxfound.org/sites/events/files/slides/KoreaLinuxForum-2014.pdf) によれば,以下のような意味である. ![](https://i.imgur.com/T8HpLbx.png) 下から`4`ビット目(0-origin)が`1`か`0`かによって,Aarch32/Aarch64を決めているようだ.従って`0x1D3`はAarch32を意味する事がわかる. ### `qemu`の修正 先の`smc`のハンドラに話を戻すと,`eret`実行時,コンテキスト的にはAarch64からAarch32にスイッチする事がわかった. ここで`gdb`はAArch32のレジスタ情報が返ってくることを期待するが,`qemu`のデバッグスタブからはAarch64のレジスタ情報が返ってきているためおかしくなっている. 修正するには`qemu`のデバッグスタブにおいて,Aarch64ではなくAarch32の情報が送信されれば良い. ここでようやく,同梱の`qemu-arm-debug.patch`が意味を持つ.以下はパッチの中身だが,`qemu`のAarch64におけるデバッグスタブでAarch32の内容を返すように変更しているのがわかる(16~27行目). ```sh= diff -Naur qemu/target/arm/cpu64.c qemu-arm-debug/target/arm/cpu64.c --- qemu/target/arm/cpu64.c 2018-10-19 18:44:40.960612534 -0700 +++ qemu-arm-debug/target/arm/cpu64.c 2018-10-19 18:44:16.709191908 -0700 @@ -452,17 +452,20 @@ return g_strdup("aarch64"); } +void arm_cpu_set_pc(CPUState *cs, vaddr value); +gchar *arm_gdb_arch_name(CPUState *cs); + static void aarch64_cpu_class_init(ObjectClass *oc, void *data) { CPUClass *cc = CPU_CLASS(oc); cc->cpu_exec_interrupt = arm_cpu_exec_interrupt; - cc->set_pc = aarch64_cpu_set_pc; - cc->gdb_read_register = aarch64_cpu_gdb_read_register; - cc->gdb_write_register = aarch64_cpu_gdb_write_register; - cc->gdb_num_core_regs = 34; - cc->gdb_core_xml_file = "aarch64-core.xml"; - cc->gdb_arch_name = aarch64_gdb_arch_name; + cc->set_pc = arm_cpu_set_pc; + cc->gdb_read_register = arm_cpu_gdb_read_register; + cc->gdb_write_register = arm_cpu_gdb_write_register; + cc->gdb_num_core_regs = 26; + cc->gdb_core_xml_file = "arm-core.xml"; + cc->gdb_arch_name = arm_gdb_arch_name; } static void aarch64_cpu_register(const ARMCPUInfo *info) diff -Naur qemu/target/arm/cpu.c qemu-arm-debug/target/arm/cpu.c --- qemu/target/arm/cpu.c 2018-10-19 18:44:40.960612534 -0700 +++ qemu-arm-debug/target/arm/cpu.c 2018-10-19 18:44:16.705192007 -0700 @@ -37,7 +37,7 @@ #include "disas/capstone.h" #include "fpu/softfloat.h" -static void arm_cpu_set_pc(CPUState *cs, vaddr value) +void arm_cpu_set_pc(CPUState *cs, vaddr value) { ARMCPU *cpu = ARM_CPU(cs); @@ -1907,7 +1907,7 @@ } #endif -static gchar *arm_gdb_arch_name(CPUState *cs) +gchar *arm_gdb_arch_name(CPUState *cs) { ARMCPU *cpu = ARM_CPU(cs); CPUARMState *env = &cpu->env; ``` ということで,このパッチを適用した`qemu`を作ろう.同梱の`qemu-system-aarch64`は`v3.0.0`なので,このバージョンの`qemu`にパッチを当てよう. ``` $ ./qemu-system-aarch64 -version QEMU emulator version 3.0.0 Copyright (c) 2003-2017 Fabrice Bellard and the QEMU Project developers ``` というわけでダウンロード&パッチ当て&ビルドを行う. ```= # 環境によって変更すること $ export SUPER_HEXAGON_BASE=/path/to/super_hexagon # ダウンロード&パッチ当て&ビルド $ wget https://download.qemu.org/qemu-3.0.0.tar.bz2 $ tar xf qemu-3.0.0.tar.bz2 $ cd qemu-3.0.0 && mkdir build && cd build $ cp $SUPER_HEXAGON_BASE/{qemu.patch,qemu-arm-debug.patch} . $ patch -p1 -d .. < qemu.patch $ patch -p1 -d .. < qemu-arm-debug.patch $ ../configure --target-list=aarch64-softmmu --static $ make # オリジナルのqemuと区別するため,末尾に-debugをつけておく $ mv aarch64-softmmu/qemu-system-aarch64{,-debug} $ cp aarch64-softmmu/qemu-system-aarch64-debug $SUPER_HEXAGON_BASE/super_hexagon/share/ # 起動スクリプトを変更 $ vi $SUPER_HEXAGON_BASE/super_hexagon/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 # ↑は全部コメント化 # 以下追加."-serial null"は残すと動かないので消す /home/super_hexagon/qemu-system-aarch64-debug -nographic -machine hitcon -cpu hitcon -bios /home/super_hexagon/bios.bin -monitor /dev/null 2>/dev/null -s # gdbスクリプトを32ビット版へ差し替え $ mv ~/.gdbinit{,_64} $ wget https://gist.githubusercontent.com/bata24/106c878aabbfab181b4fae2bde357c36/raw/3e716d292e1644ec68216fa6b740b01ad986a1cb/gdbinit_generic_32.txt -O /root/.gdbinit ------------------------------------ ``` 尚,この自前ビルドした`qemu-system-aarch64-debug`でターミナルから直接起動すると問題が起きた.どうやらローカルエコーがオフになっているらしい. ![](https://i.imgur.com/9BbKMvE.png) ネットワーク越しにアクセスすれば問題ないので,気にしてはいけない. ![](https://i.imgur.com/bRI4rWq.png) セキュアワールドを正しくデバッグできるか動作確認を行いたいところだが,今回のパッチ当てによってノーマルワールドはデバッグできなくなってしまっている.S-EL0やS-EL1内で無限ループを発生させた状態でないと,まともに停止させられないので,`bios.bin`のS-EL1のコードに適当に無限ループするパッチを当ててみよう. 以下は,とりあえず割り込みベクタっぽいところに無限ループのパッチを当てたものである. ![](https://i.imgur.com/Wl1FB8X.png) このパッチを当てた`bios_patched.bin`に差し替えて`qemu-system-aarch64-debug`配下で動かし,`gdb`でアタッチすると,確かに命令が正しく表示されることを確認した.また元の命令に差し替えてから`c`(コンティニュー)すると正しく再開した. ```= root@c75b104b0609:~# gdb-multiarch -q -ex 'target remote :1234'; kill -9 $(pgrep qemu) Remote debugging using :1234 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x0e400000 in ?? () (gdb) this ##### register ########################################################### r0 0xe400000 239075328 r1 0x1 1 r2 0x2 2 r3 0x0 0 r4 0x0 0 r5 0x0 0 r6 0x0 0 r7 0x0 0 r8 0x0 0 r9 0x0 0 r10 0x0 0 r11 0x0 0 r12 0x0 0 sp 0x0 0x0 lr 0x0 0 pc 0xe400000 0xe400000 cpsr 0x1d3 467 fpsr 0x0 0 fpcr 0x0 0 ##### disassemble ######################################################## 0xe3ffff0: andeq r0, r0, r0 0xe3ffff4: andeq r0, r0, r0 0xe3ffff8: andeq r0, r0, r0 0xe3ffffc: andeq r0, r0, r0 => 0xe400000: b 0xe400000 0xe400004: b 0xe401530 0xe400008: b 0xe4015b4 0xe40000c: b 0xe40155c 0xe400010: b 0xe401588 0xe400014: b 0xe400014 0xe400018: b 0xe400018 0xe40001c: b 0xe40001c 0xe400020: b 0xe4015dc 0xe400024: nop {0} ##### stack ############################################################## 0x0: 0xd2810600 0xf2a618a0 0xd51e1000 0xd5033fdf 0x10: 0x1000ff80 0xd51ec000 0xd5033fdf 0xd2820141 0x20: 0xd53e1000 0xaa010000 0xd51e1000 0xd5033fdf 0x30: 0xd2804700 0xd51e1100 0xd2900000 0xf2a00020 0x40: 0xd51e1320 0xd50344ff 0xd2800000 0xd51e1140 0x50: 0x58000340 0x58000361 0x940003eb 0x58000360 0x60: 0x58000381 0x580003a2 0x94000423 0x580003a0 0x70: 0x580003c1 0x580003a2 0x9400041f 0x580003a0 0x80: 0x580003c1 0x580003e2 0x9400041b 0x580003e0 0x90: 0x58000401 0x580002a2 0x94000417 0xd50040bf 0xa0: 0x580003c0 0x9100001f 0x9400011b 0x940001d2 0xb0: 0x140003be 0x00000000 0x0e002000 0x00000000 0xc0: 0x00202000 0x00000000 0x0e000000 0x00000000 0xd0: 0x00002850 0x00000000 0x00000068 0x00000000 0xe0: 0x40100000 0x00000000 0x00010000 0x00000000 0xf0: 0x0e400000 0x00000000 0x00020000 0x00000000 ************************************************************************** (gdb) set *(void**)0xe400000=0xea00053b (gdb) c Continuing. ``` ちなみにオリジナルの`qemu-system-aarch64`では,ブレークした瞬間の表示とステップ実行はできるものの,(元の命令に差し替えるなどしてから)`c`(コンティニュー)すると,切断されてしまう. ```= root@c75b104b0609:~# gdb-multiarch -q -ex 'target remote :1234'; kill -9 $(pgrep qemu) Remote debugging using :1234 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x0e400000 in ?? () (gdb) this ##### register ########################################################### r0 0xe400000 239075328 r1 0x1 1 r2 0x2 2 r3 0x0 0 r4 0x0 0 r5 0x0 0 r6 0x0 0 r7 0x0 0 r8 0x0 0 r9 0x0 0 r10 0x0 0 r11 0x0 0 r12 0x0 0 sp 0x0 0x0 lr 0x0 0 pc 0xe400000 0xe400000 cpsr 0x1d3 467 fpsr 0x0 0 fpcr 0x0 0 ##### disassemble ######################################################## 0xe3ffff0: andeq r0, r0, r0 0xe3ffff4: andeq r0, r0, r0 0xe3ffff8: andeq r0, r0, r0 0xe3ffffc: andeq r0, r0, r0 => 0xe400000: b 0xe400000 0xe400004: b 0xe401530 0xe400008: b 0xe4015b4 0xe40000c: b 0xe40155c 0xe400010: b 0xe401588 0xe400014: b 0xe400014 0xe400018: b 0xe400018 0xe40001c: b 0xe40001c 0xe400020: b 0xe4015dc 0xe400024: nop {0} ##### stack ############################################################## 0x0: 0xd2810600 0xf2a618a0 0xd51e1000 0xd5033fdf 0x10: 0x1000ff80 0xd51ec000 0xd5033fdf 0xd2820141 0x20: 0xd53e1000 0xaa010000 0xd51e1000 0xd5033fdf 0x30: 0xd2804700 0xd51e1100 0xd2900000 0xf2a00020 0x40: 0xd51e1320 0xd50344ff 0xd2800000 0xd51e1140 0x50: 0x58000340 0x58000361 0x940003eb 0x58000360 0x60: 0x58000381 0x580003a2 0x94000423 0x580003a0 0x70: 0x580003c1 0x580003a2 0x9400041f 0x580003a0 0x80: 0x580003c1 0x580003e2 0x9400041b 0x580003e0 0x90: 0x58000401 0x580002a2 0x94000417 0xd50040bf 0xa0: 0x580003c0 0x9100001f 0x9400011b 0x940001d2 0xb0: 0x140003be 0x00000000 0x0e002000 0x00000000 0xc0: 0x00202000 0x00000000 0x0e000000 0x00000000 0xd0: 0x00002850 0x00000000 0x00000068 0x00000000 0xe0: 0x40100000 0x00000000 0x00010000 0x00000000 0xf0: 0x0e400000 0x00000000 0x00020000 0x00000000 ************************************************************************** (gdb) set *(void**)0xe400000=0xea00053b (gdb) c Continuing. Remote connection closed (gdb) ``` ※`this`コマンドは自前で書いたスクリプトで,簡易pedaっぽい表示を実現しているだけである よって`qemu-system-aarch64-debug`を自前ビルドしておかないと,まともにデバッグできないと言える. また上記はS-EL1における`gdb`デバッグの動作確認を示したものだが,同様にS-EL0でも無限ループが起きるようにパッチすれば`gdb`でアタッチしてデバッグすることが可能である.但し後述の通り,S-EL1がS-EL0のコード(`trustlet`と呼ぶ)をロードする際にはハッシュチェックが存在する.S-EL0のコード部分に無限ループパッチを当てる場合,S-EL1のハッシュチェックを無効化するパッチも併せて当てる必要があることを覚えておこう. この他,AArch32をデバッグしている際に`info register`コマンドによってシステムレジスタの一覧を表示すると,明らかにおかしなレジスタ情報が表示される(しかもレジスタ一覧の出力途中にエラーが発生する).これはAArch64のシステムレジスタ一覧が送られてきているように見えており,おそらく同梱のパッチは不完全なのだと考えられるが,システムレジスタを参照しなければ(汎用レジスタだけを参照すれば)デバッグ自体はできるので気にしてはいけない. ## セキュワールドの初動調査 ### S-EL0について EL0の解析において,S-EL0のコードらしきもの(`TA_BIN`)が手に入っていたことを思い出そう.まずは最初にこのファイルの中身を見てみよう. ![](https://i.imgur.com/hRw2Zrz.png) 先頭`0x20`バイト程がヘッダらしきデータで,その後ろはカスタムコードに見える.このままではフォーマットすらわからないので,このフォーマットを解釈してロードする処理が書かれているであろうS-EL1のコードも読む必要がある. ### S-EL1について S-EL1カーネルは、仮想アドレスを持つ可能性があるが,現時点でそのアドレスは分からない.物理メモリは`0x0E400000`であることが分かっているので,ベースアドレスを`0x0E400000`として解析を進めると,`switch`文でジャンプ先のアドレスを得るために固定アドレスを参照していることが分かった. ![](https://i.imgur.com/4Pizaco.png) ジャンプ先は`0x08000000+α`のような形であるため,おそらくベースアドレスは`0x08000000`だ.IDA上ではこのアドレスへrebaseしておこう. さてS-EL1のコードの中で,S-EL0のコードをロードしている箇所を探そう.ここからS-EL0のフォーマットがわかるはずだ. 調査方針には色々あるが,今回は暗号系シグネチャをベースに探した.なぜならセキュアワールドで動くS-EL0のコード(`trustlet`)は,たいてい偽造したコードが動かないようにホワイトリストと突合されている可能性が高いからだ.つまり署名やハッシュの比較などが行われていると思われる. IDAには,こういったハッシュや各種暗号の計算に使われる`const`値テーブルを探索してくれるプラグインがある.`IDA_Signsrch`と呼ばれるツールで,オリジナルは2012年にC++で書かれたものだ.同じ作者によって2018年にIDA v7.1へ対応なされたが,その後L4ysによって`python`版へ移植されている. - オリジナル: http://www.macromonkey.com/ida-signsrch-released/ - 7.1対応版: http://www.macromonkey.com/updated-ida-pro-plugins-to-7-1/ - python版: https://github.com/L4ys/IDASignsrch これを使うとSHA-256のテーブルが存在することがわかる. ![](https://i.imgur.com/Yd8c5Gb.png) ここから遡っていくとSHA-256の計算コードが特定できる.また固定値のハッシュと突合されていることもわかる. ![](https://i.imgur.com/tGTs7al.png) このチェック関数を呼び出しているところが,trustletをロードするコードであった.最初にハッシュチェックを行い,正しければ`.text`/`.data`/`.bss`/`stack`をそれぞれ個別にロードしているように見える. ![](https://i.imgur.com/vkYr2FB.png) 使われている構造体は以下の通りだ.これでS-EL0のバイナリのフォーマットがわかった. ![](https://i.imgur.com/laYmLj6.png) ## S-EL0コードの解析 ### バイナリの切り出し フォーマットがわかったので,S-EL0のバイナリをIDAでパースできるよう切り出そう. 以下のようなスクリプトで,`S-EL0_TA_BIN`を`S-EL0_TA_BIN.text`と`S-EL0_TA_BIN.data`に分割する. :::spoiler Click {%gist bata24/125d3fd042d52a909836b9e3ca196778 %} ::: ---- IDAで`S-EL0_TA_BIN.text`を開き,CPUは`ARM Little Endian`を指定して`0x1000`にロードする.チームNASA RejectsのWrite-upでは,S-EL0などのコードが`ARM Big endian`であると書かれているが,おそらく勘違いだと思われる. ![](https://i.imgur.com/K5ZkpRB.png) 続いて`Additional binary file...`で`S-EL0_TA_BIN.data`を`0x2000`にロードする. ![](https://i.imgur.com/VoLgNhq.png) ![](https://i.imgur.com/LjckZ8i.png) `.bss`セグメントも追加しておく. ![](https://i.imgur.com/sIHYyjW.png) 後は普通に解析していけば良い.`.text`領域は`Thumb-2`モードであることに注意. ### 動作内容の把握 ロードとセーブしかない単純なノートサービスだ. 利用されている構造体はこちら. ![](https://i.imgur.com/DJwQKeR.png) `DB`構造体にノートの内容を保存する.また`malloc()`と`free()`が実装されているので,`CHUNK`構造体や`ARENA`構造体が存在する. 全体的なコードをざっと貼っておく.ノートの読み書きは以下の通りだ. ![cmd_handler_w](https://i.imgur.com/H9uc50r.png) ![cmd_handler](https://i.imgur.com/1krgCDq.png) ![do_abort](https://i.imgur.com/ufHZ5jy.png) ![load_db](https://i.imgur.com/uTncXKk.png) ![store_db](https://i.imgur.com/0ucGUNU.png) また`malloc()`と`free()`が実装されている.`glibc`のヒープ機構から`fastbins`や`bins`を削ったような形だ. ![malloc](https://i.imgur.com/mX6yCl0.png) ![free](https://i.imgur.com/fdfPQD5.png) ![init_arena](https://i.imgur.com/TVtrTdR.png) ![init_top_chunk](https://i.imgur.com/CPP4Y6L.png) `svc`は以下の`4`つが実装されている. ![](https://i.imgur.com/nj33VXZ.png) またデータ領域は以下の通りだ. ![](https://i.imgur.com/AJt2Z1E.png) ### 脆弱性 独自`malloc()`に脆弱性がある. ![](https://i.imgur.com/KxPaVrJ.png) `malloc()`の冒頭で,確保したいサイズを最小サイズに切り上げる処理があるが,ここで整数オーバーフローが発生する. もし`wanted_size`として`0xfffffff0`などを渡すと,実際の確保サイズは`0x20`になってしまい,そこへの`memcpy()`が発生する.つまりヒープBOFが発生するのだ. このパターンの脆弱性は,本来ならば`memcpy(dst, src, 0xfffffff0)`のように大量のコピーが走ってSEGVが発生するため悪用が難しいものであるが,面白いことにS-EL0ではSEGVが発生しても単にノーマルワールドに戻るだけである.以下はSEGV発生時のハンドラで,エラーメッセージは表示するものの,何事もなかったかのようにノーマルワールドに戻っているのが分かる(ここら辺の正確な動作はS-EL1カーネルも読む必要があるが,ここでは省略する). ![](https://i.imgur.com/VYlw4vY.png) 従ってS-EL0で後続のメモリチャンクを書き換えて,SEGVを発生させたあとにEL0へ戻り,改めてS-EL0を呼び出し直せば,S-EL0のエントリーポイントから再度実行が行われる. この脆弱性はチームPPPが利用した.おそらくこれが想定解と思われる. ## 任意コード実行へ S-EL0のヒープを図示しながら解説しよう. まずはEL0の任意コード実行を使って,3回ほど`0x10`サイズのデータを書き込む.内部的にはヘッダのサイズを加味した`0x20`サイズのチャンクが3つ確保され,それぞれ`db[2]`、`db[3]`、`db[4]`用の領域になる. 尚EL0の任意コード実行を行う時点で,Stager用途の領域を`db[1]`に保存しているため,すでに`0x60`サイズの領域が確保されているところから始まる点に注意しよう. ```= 0x100070 +------------------------+ db[1] | (prev_size) 0x00000000 | | size 0x00000061 | | (next) 0xaaaaaaaa | | (prev) ... | <- EL0 stagerを無理やりdecode("hex") | ... | した壊れたデータが元々入ってる 0x1000d0 +------------------------+ db[2] | (prev_size) 0x00000000 | | size 0x00000021 | | (next) 0xaaaaaaaa | | (prev) 0xbbbbbbbb | | ... | 0x1000f0 +------------------------+ db[3] | (prev_size) 0x00000000 | | size 0x00000021 | | (next) 0xaaaaaaaa | | (prev) 0xbbbbbbbb | | ... | 0x100110 +------------------------+ db[4] | (prev_size) 0x00000000 | | size 0x00000021 | | (next) 0xaaaaaaaa | | (prev) 0xbbbbbbbb | | ... | 0x100130 +------------------------+ top | (prev_size) 0x00000000 | <---------- [g_arena.top] | size 0x0007ff41 | | (next) 0x00000000 | | (prev) 0x00000000 | +------------------------+ ``` ここで`db[4]`に`0x20`サイズのデータを書き込む.`0x100110`にあった`db[4]`の領域は`free()`され,`0x30`サイズのチャンクが新たに確保し直される. ```= 0x100070 +------------------------+ db[1] | (prev_size) 0x00000000 | | size 0x00000061 | | (next) 0xaaaaaaaa | | (prev) ... | <- EL0 stagerを無理やりdecode("hex") | ... | した壊れたデータが入ってる 0x1000d0 +------------------------+ db[2] | (prev_size) 0x00000000 | | size 0x00000021 | | (next) 0xaaaaaaaa | | (prev) 0xbbbbbbbb | | ... | 0x1000f0 +------------------------+ db[3] | (prev_size) 0x00000000 | | size 0x00000021 | | (next) 0xaaaaaaaa | | (prev) 0xbbbbbbbb | | ... | 0x100110 +------------------------+ unused | (prev_size) 0x00000000 | <---------- [g_arena.free_list] | size 0x00000021 | ^ | next 0x00100050 | -----------------+ | prev 0x00100050 | -----------------+ | ... | 0x100130 +------------------------+ db[4] | prev_size 0x00000020 | | size 0x00000030 | | (next) 0xaaaaaaaa | | (prev) 0xbbbbbbbb | | ... | 0x100160 +------------------------+ top | (prev_size) 0x00000000 | <---------- [g_arena.top] | size 0x0007ff11 | | (next) 0x00000000 | | (prev) 0x00000000 | +------------------------+ ``` さらに`db[2]`に`0x20`サイズのデータを書き込む.同様に`free()`が呼ばれたあと`0x30`サイズのチャンクが確保し直される.尚,図に書き起こすのが辛かったため,以下の図ではフリーリストの`prev`要素の矢印を省略している. ```= 0x100070 +------------------------+ db[1] | (prev_size) 0x00000000 | | size 0x00000061 | | (next) 0xaaaaaaaa | | (prev) ... | <- EL0 stagerを無理やりdecode("hex") | ... | した壊れたデータが入ってる 0x1000d0 +------------------------+ unused | (prev_size) 0x00000000 | <--+ | size 0x00000021 | | | next 0x00100050 | ---|-------------------------------+ | prev 0x00100110 | | | | ... | | | 0x1000f0 +------------------------+ | | db[3] | (prev_size) 0x00000020 | | | | size 0x00000020 | | | | (next) 0xaaaaaaaa | | | | (prev) 0xbbbbbbbb | | | | ... | | | 0x100110 +------------------------+ | | unused | (prev_size) 0x00000000 | <--|------- [g_arena.free_list] <--+ | size 0x00000021 | | | next 0x001000d0 | ---+ | prev 0x00100050 | | ... | 0x100130 +------------------------+ db[4] | prev_size 0x00000020 | | size 0x00000030 | | (next) 0xaaaaaaaa | | (prev) 0xbbbbbbbb | | ... | 0x100160 +------------------------+ db[2] | (prev_size) 0x00000000 | | size 0x00000031 | | (next) 0xaaaaaaaa | | (prev) 0xbbbbbbbb | | ... | 0x100190 +------------------------+ top | (prev_size) 0x00000000 | <---------- [g_arena.top] | size 0x0007ff41 | | (next) 0x00000000 | | (prev) 0x00000000 | +------------------------+ ``` さてここで,`db[5]`に新たにサイズ`0xfffffff0`のデータを書き込むとしよう.`malloc()`内のバグにより,チャンクサイズは`0x20`として確保されるので,フリーリストから確保されるはずだ.フリーリストからの確保は,`glibc`の`unsorted_bin`と同じくリストの後ろから確保されるので,`0x1000d0`のチャンクが返されるはずだ. ```= 0x100070 +------------------------+ db[1] | (prev_size) 0x00000000 | | size 0x00000061 | | (next) 0xaaaaaaaa | | (prev) ... | <- EL0 stagerを無理やりdecode("hex") | ... | した壊れたデータが入ってる 0x1000d0 +------------------------+ db[5] | (prev_size) 0x00000000 | | size 0x00000021 | | next 0xdeadbeef | <- フリーリストから辿られて,ここが返るはず | prev 0xdeadbeef | | ... | 0x1000f0 +------------------------+ db[3] | (prev_size) 0xdeadbeef | | size 0xdeadbeef | | (next) 0xdeadbeef | | (prev) 0xdeadbeef | | ... | 0x100110 +------------------------+ unused | (prev_size) 0xdeadbeef | <---------- [g_arena.free_list] | size 0xdeadbeef | ^ | next 0xdeadbeef | -----------------+ | prev 0xdeadbeef | -----------------+ | ... | 0x100130 +------------------------+ db[4] | prev_size 0xdeadbeef | | size 0xdeadbeef | | (next) 0xdeadbeef | | (prev) 0xdeadbeef | | ... | 0x100160 +------------------------+ db[2] | (prev_size) 0xdeadbeef | | size 0xdeadbeef | | (next) 0xdeadbeef | | (prev) 0xdeadbeef | | ... | 0x100190 +------------------------+ top | (prev_size) 0xdeadbeef | <---------- [g_arena.top] | size 0xdeadbeef | | (next) 0xdeadbeef | | (prev) 0xdeadbeef | +------------------------+ ``` `0x1000d0`のチャンクはサイズ`0x20`であるが,確保サイズは本来`0xfffffff0`なので,`malloc()`を抜けた後の`memcpy()`でヒープBOFが起きる.これによって後続のチャンクを全て破壊することができるようになった. ![](https://i.imgur.com/STAZ6ji.png) ヒープBOFを使えば,`0x100110`にある解放済みチャンクの`next`/`prev`を書換えることができる.ここに任意のアドレスと値を仕込んでおくことで.次回の`malloc()`時にフリーリストからの確保が行われた瞬間,以下のコードによってunlink attackが行われる. ![](https://i.imgur.com/Son4dsM.png) さてunlink attackを利用する上で知っておくべき事がある.S-EL0における各種メモリの属性だ.以下は先にも挙げた,S-EL1カーネルにおけるS-EL0のメモリマッピング関連の処理である. ![](https://i.imgur.com/qaifkTX.png) `.text`領域の`prot`は`10(=0b1010)`だが,`.bss`領域の`prot`は`14(=0b1110)`としてマッピングされている.違いは下から`2`ビット目(0-origin)で,これは以下のコードを読めば分かる. ![](https://i.imgur.com/nztkKMu.png) コードによれば,このビットは`UXN`を意味している.従って`.bss`領域がS-EL0で実行不可能と設定されているだけである.逆にそれ以外の違いはないので`.text`領域は`.bss`領域と同様に`RW`属性であると分かる. 話を元に戻そう.unlink attackは一度しか使えず(頑張れば何度も使える可能性はあるが),`4`バイトの任意書き込みが可能である.なるべく一度のパッチで済ませたいので,書き換え先を色々考えたところ,`load_db()`の`memcpy()`の第一引数をコード領域に修正するのが簡単で良いと判断した. つまり以下のコードを ![before](https://i.imgur.com/OZ3dJuk.png) 次のように修正するのだ(`0x10d6`は`memcpy()`から返ってきたあとの最初の命令のアドレス). ![after](https://i.imgur.com/70ZDT1U.png) こうすることで,`db[i]`にあるデータを`load_db()`のコード領域に書き込む事になり,任意コード実行に持ち込むことができる. S-EL0を乗っ取るだけなら,EL1やEL2を攻略する必要がないので,`exp_el0.py`をベースに作った. :::spoiler Click {%gist bata24/6d62b507d657d70102ab0669c4b36752 %} ::: ---- # 続く 明日は私の[HITCON CTF 2018 - Super Hexagon (Part 5/7)](https://hackmd.io/@bata24/By9QPlFMU)です.