# HITCON CTF 2018 - Super Hexagon (Part 5/7) ###### tags: `trustzone` `ctf` `pwn` `ARM` `Aarch64`, `kernel` `hypervisor` # はじめに この記事は,[CTF Advent Calendar 2020](https://adventar.org/calendars/5338) の7日目の記事です. 6日目は私の「[HITCON CTF 2018 - Super Hexagon (Part 4/7)](https://hackmd.io/@bata24/BJHBSc0g8)」でした. # リンク集 - 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の攻略(別解) チームNASA RejectsはS-EL0で脆弱性を見つけられなかったらしい. 代わりにS-EL1カーネルで脆弱性を見つけ,S-EL0の処理を奪うために直接S-EL1へ攻撃を仕掛けたとのことだ.S-EL0を攻略するためだけにS-EL1を全て解析するのは少し変な気もするが,こちらの手法も見てみよう. ## S-EL1カーネルの解析 ### 動作内容の把握 #### 割込経路の把握 改めてS-EL1のコードに戻ろう. S-EL1のコードには,S-EL3からの`eret`割り込みと,S-EL0からの`svc`割り込みの,2種類のハンドラが存在する.以下の図の黄色い線を見てもらうとわかるが,S-EL3からS-EL1へ割り込む経路①と,S-EL0からS-EL1へ割り込む経路②だ. ![](https://i.imgur.com/x96PoQS.png) ##### 割込経路② 最初にS-EL0からの`svc`割り込み(割込経路②)のハンドラを見てみよう.まずアドレス的には`0x08000008`が割り込みベクタだ.一般的なAarch32の割り込みベクタと同じで,オフセット`0x08`が`svc`割り込みを意味しているようなので、分かりやすい. ![](https://i.imgur.com/GQvGoLu.png) 最終的にたどり着く`0x08000A30`が割り込みハンドラだ.`4`つのシステムコールが定義されている. ![](https://i.imgur.com/rY3KVBL.png) ##### 割込経路① 次はS-EL3からの`eret`割り込み(割込経路①)のハンドラを見てみよう.まずアドレス的には`0x08000020`が割り込みベクタだ. ![](https://i.imgur.com/4vnZNtq.png) 割り込みベクタは本来`0x1c`までの`8`つである. ![](https://i.imgur.com/5IfX5p2.png) しかし,S-EL1などにおいてはその後ろに追加で割り込みベクタが存在することがあるようだ.このあたりはマニュアルのp4622~をみればなんとなくわかるだろう. ![](https://i.imgur.com/X5LcujA.png) その割り込みベクタから色々呼ばれて,最終的にたどり着く`0x0800087C`が割り込みハンドラだ.以前見つけたS-EL0のイメージをロードする部分もこちらにある. ![](https://i.imgur.com/EDIe1MH.png) #### 割込経路①のシステムコールの把握 ではまず割込経路①の各割り込みハンドラを調査しよう.尚EL0/EL1/EL2/S-EL3における呼び出しについては,[一つ前の記事](https://hackmd.io/gsKHQrOTR_6om_TCZ6J7Zg)に画像を添付してあるのでそちらを参照してほしい. 存在するシステムコールは全部で`4`つで,またEL0では使われていなかった`sel1_unmap_from_sel0()`という新たなシステムコールが存在することがわかる.割込経路①は呼び出しが複雑だ.呼び出しの流れをまとめておこう.尚,便宜上`TEE_OS_init()`も表には含めておく. | | `TEE_OS_init()` | `tc_register_wsm()` | `tc_unmap()` (仮) | `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` | 呼び出し機構はなし<br>強制的に呼び出すなら<br>`0x83000004`, `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処理関数 | - | `sel1_mmap_world_shared_memory()` | `sel1_unmap_from_sel0()` | `sel1_load_trusted_app()` | `sel1_call_trusted_app()` | | S-EL1処理概要 | - | EL0の仮想メモリを<br>S-EL0にマップ | EL0の仮想メモリを<br>S-EL0からunmap | S-EL0に<br>trustletをロード | S-EL0へ<br>処理を渡す | S-EL1に新たなシステムコール`sel1_unmap_from_sel0()`が見つかっているが,これを呼ぶにはEL1(もしくはEL2)から直接呼びだせば良い.EL0から呼び出してもEL1にはハンドラがないためハンドリングされないが,EL1から呼び出すとEL2やS-EL3では特に`smc`番号が検証されずそのまま上位に渡されるので,呼び出すことが可能だ. ではこれら`4`つのシステムコールの処理を解析してみよう. まず`sel1_mmap_world_shared_memory()`は以下の通り. ![](https://i.imgur.com/J58uAKF.png) ![](https://i.imgur.com/b7wrQqj.png) ![](https://i.imgur.com/JEgxiu0.png) ![](https://i.imgur.com/u0c4BhN.png) ![](https://i.imgur.com/WNdB0Kj.png) どうやら二段階のページテーブルを持つアドレス管理機構だ.ページテーブルを確認して,要求サイズに合致した未割り当ての連続した領域が存在すれば,そのページテーブルを有効にするといった処理が行われている. ただし処理の内容はビットマップに利用フラグを立てているだけで,実際にメモリを確保するような処理は見当たらない.おそらくこの処理で割り当てるメモリ領域は,S-EL1の起動時にまとめて確保されているのだろう.初期化コードを探すと以下に見つかった.`0x400000`バイトの領域を事前に確保して,`page pool`として保持しているようだ. ![](https://i.imgur.com/d8s4eSr.png) 図にするとこういう感じだろう. ```= 0x8000000 +-----------------------------+ | S-EL1 .text | 0x8003000 +-----------------------------+ | S-EL1 .data | | | | | | | | | | | | | 0x8087000 +-----------------------------+ | some area ? | | | | | 0x8097000 +-----------------------------+ | page pool | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 0x8497000 +-----------------------------+ ``` またこちらは`sel1_unmap_from_sel0()`のコードだ. ![](https://i.imgur.com/70KtPi0.png) ![](https://i.imgur.com/xcTigG5.png) ![](https://i.imgur.com/6Mlk0bI.png) ![](https://i.imgur.com/2QdV5LU.png) ページテーブルにおける対応付けを削除し,利用していたメモリを`page pool`に返している事がわかる. こちらは`sel1_load_trusted_app()`のコードだ.S-EL0のバイナリフォーマットを特定する段階で確認しているが,改めて貼っておく. ![](https://i.imgur.com/q37Yawv.png) ![](https://i.imgur.com/D7Xz5xC.png) こちらは`sel1_call_trusted_app()`のコードだ.引数の`cmd_buf_addr`を,S-EL0の`.bss_end-4`のアドレスに書き込んでいる事がわかる. ![](https://i.imgur.com/2kZ5x3i.png) ![](https://i.imgur.com/FhMqtmJ.png) #### 割込経路②のシステムコールの把握 次は以下の割込経路②の各割り込みハンドラを調査しよう. ![](https://i.imgur.com/rY3KVBL.png) 尚,割込経路②のシステムコールを呼び出すには,`tc_tci_call()`経由で一旦S-EL0に処理を渡してから呼び出すしか方法がない.従ってEL0~EL2からは,これらの各システムコールをトリガすることはできるものの,その引数までは任意に指定できない点に注意しよう. ![](https://i.imgur.com/6o3o6uN.png) `sel1_return_val_to_normal_world()`のコードは次の通り.単に`smc`命令によってS-EL3の`smc`ハンドラに戻り,そのままノーマルワールドへリターンする. ![](https://i.imgur.com/f3HDHpO.png) `sel1_signal_syscall()`のコードは次の通り.SEGV時にリスタートするアドレスを設定している. ![](https://i.imgur.com/qUP1B3l.png) `sel1_mmap_syscall()`のコードは次の通り.一部のコードは`sel1_mmap_world_shared_memory()`と共通である(`sel1_find_config_virtpage()`,`sel1_pagetable_set_prot()`が該当). ![](https://i.imgur.com/deOZe0y.png) ![](https://i.imgur.com/QOm0B6V.png) ![](https://i.imgur.com/S8RKW2U.png) ![](https://i.imgur.com/Xom3jsF.png) ![](https://i.imgur.com/8PYGQhn.png) `sel1_munmap_syscall()`のコードは次の通り.こちらも一部のコードが`sel1_unmap_from_sel0()`と共通である(`sel1_unmap_from_sel0_inner()`が該当). ![](https://i.imgur.com/uSibGcm.png) ### 脆弱性 S-EL1には,気づきにくいロジックバグが存在する.割込経路①も②も`mmap()`/`munmap()`のようなシステムコール/セキュアコールを持っているが,それらが利用するアドレスは共通の`page pool`なのだ. 以下はそれぞれの割込経路における`mmap()`相当の処理だ. 割込経路①ではノーマルワールドの物理アドレスが渡される.このページは確保済みとして扱われ,それをS-EL0に対応付けた仮想アドレスを返す処理をしている. ![](https://i.imgur.com/J58uAKF.png) 割込経路②では`addr`が渡されるものの,処理においては特に利用していない.つまりこの時点で利用したいサイズの物理ページは確保されていないため,自身で物理ページを確保してからS-EL0の仮想アドレスに対応付ける処理をしている. ![](https://i.imgur.com/deOZe0y.png) また以下はそれぞれの割込経路における`munmap()`相当の処理だ.内部の処理は全く同じである. ![](https://i.imgur.com/70KtPi0.png) ![](https://i.imgur.com/uSibGcm.png) つまり同一の`page pool`を使っているのが分かる. またASLRも無いので,すべての処理は決定的である.従って,S-EL0が確保して利用しているメモリを,EL1側から強制的に解放してマッピングし直す,といったことが可能だ.これによってS-EL0が確保している物理ページの内容を破壊することができる. ## 任意コード実行へ EL1とS-EL1,S-EL0の関係を図示しながら解説したものが以下の図だ. ```= [EL1] [S-EL1] step1: ----------------------------------> a = map(0x41000) ※tci_buf用。 ※step2の後にstore_db@S-EL0で行われる0x40000バイトのmemcpy()に耐えるサイズが必要 [EL1] [S-EL0] [S-EL1] step2: -------> B = malloc(0x3ffe1) -----> b = map(0x40000) ※0x40000以上ならmalloc@S-EL0内でmmapが呼ばれS-EL1へリダイレクトされる ※この時点では,EL1からS-EL0のB(=S-EL1のb)へはアクセスできない ※Bはdb[0]@S-EL0に入れておくとする [EL1] [S-EL1] step3: ----------------------------------> unmap(b, 0x1000) ※S-EL0へmmapされたメモリの先頭0x1000を強制解放 ※EL1からBやbにアクセスはできないが,bのアドレスは決定的なので指定可能 ※この時点でS-EL0のB[:0x1000]はアクセス不可能に [EL1] [S-EL1] step4: ----------------------------------> c = map(0x1000) ※step3で解放した領域にEL1から別メモリをマッピング ※この時点でS-EL0のB[:0x1000]へのアクセスが復活 ※またB[:0x1000](=b[:0x1000])とcは物理メモリが同じ ※cはEL1からアクセス可能 ``` 後は`c`を使って`B[:0x1000]`を以下の様に書き換えておけば良い. この状態で`db[0]`に対して`store_db()`を行えば,既存の領域`B`つまり以下の先頭チャンクが`free()`されることになる.つまり直下のチャンクとマージが起きることで,unlink attackが発動する.書き換え先はスタック上の戻りアドレスにしよう. ```= 0x237d000 +------------------------+ (used) | (prev_size) 0x00000000 | | size 0x00000021 | | (next) ... | | (prev) ... | 0x237d020 +------------------------+ (freed) | (prev_size) ... | | size 0x00000021 | | (next) sc_addr+1 | ※sc_addrはshellcodeのアドレスつまり0x237d048 | (prev) stack_addr | 0x237d040 +------------------------+ (don't care) | (prev_size) ... | | size 0x00000410 | 0x237d048 | (next) shellcode | ※shellcodeは0x400バイト固定とする | (prev) shellcode | | shellcode | | ... | +------------------------+ ``` 尚リターン先は`0x237d048`,つまりこの偽造したチャンクの一部を指している.チャンクがある`.bss`領域は本来`RW-`であり実行できないのではと思うかもしれないが,この領域はS-EL0の`.bss`ではなく,EL1からの強制解放/再確保操作によって差し替えられた領域である.EL1から`sel1_mmap_world_shared_memory()`で確保するときの`prot`は`0b0010`,つまり`UXN`も`PXN`も設定されていない.従って,S-EL0では`RWX`として見える事になり,そのままシェルコードが実行可能である. ![](https://i.imgur.com/3r93LVD.png) ![](https://i.imgur.com/6t2wCPV.png) 尚この別解は,`tc_unmap()`を利用するため,EL1から操作する必要がある.従って`exp_el1-2.py`をベースに作った. :::spoiler Click {%gist bata24/5cb2648f2b6f371098c710fb565c403a %} ::: ---- # 続く 明日は私の[HITCON CTF 2018 - Super Hexagon (Part 6/7)](https://hackmd.io/@bata24/H1N-W6vf8)です.