# HITCON CTF 2018 - Super Hexagon (Part 2/7) ###### tags: `trustzone` `ctf` `pwn` `ARM` `Aarch64`, `kernel` `hypervisor` # はじめに この記事は,[CTF Advent Calendar 2020](https://adventar.org/calendars/5338) の4日目の記事です. 3日目は私の「[HITCON CTF 2018 - Super Hexagon (Part 1/7)](https://hackmd.io/@bata24/HyMQI7PuB)」でした. # リンク集 - 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 # EL1の攻略 ## メモリマップの特定 ### qemuパッチからの情報 まず,配布された`qemu.patch`の中には以下の記述がある. ```= +static const MemMapEntry memmap[] = { + /* Space up to 0x8000000 is reserved for a boot ROM */ + [VIRT_FLASH] = { 0, 0x08000000 }, + [VIRT_CPUPERIPHS] = { 0x08000000, 0x00020000 }, + [VIRT_UART] = { 0x09000000, 0x00001000 }, + [VIRT_SECURE_MEM] = { 0x0e000000, 0x01000000 }, + [VIRT_MEM] = { 0x40000000, RAMLIMIT_BYTES }, +}; ``` つまり図にするとこういうことだ.なお,これは物理メモリの話である. ![](https://i.imgur.com/DSEaufz.png) `PERIPHERALS`は周辺装置用領域,`UART`はUART通信用領域である.結論から言えばいずれも攻略には関係ないため気にしなくて良い.重要なのは`FLASH`,`SECURE_MEM`,`NON_SECURE_MEM`である.読み進めていくとこれらがどのように使われるか分かってくるので,意識しながら解析をしていく必要がある. ### 解析の便利ツール `bios.bin`を読むと,先頭には以下のコードが有る.`MRS`や`MSR`命令が多数出てきている. これらはAArch64におけるシステムレジスタへのアクセス命令だが,何をしているか分かりにくいので[highlight_arm_system_insn](https://github.com/gdelugre/ida-arm-system-highlight/blob/master/highlight_arm_system_insn.py)を使おう.特殊な命令は色付き強調され,またコメントにどのレジスタへのアクセスかを表示してくれるので読みやすくなる. ![highlight_arm_system_insnを当てたもの](https://i.imgur.com/EGHg5Ss.png) また[AMIE](https://github.com/NeatMonster/AMIE) も使うと良い.`MRS`や`MSR`命令を更に簡略化した記載へ変更してくれる. ![highlight_arm_system_insnとAMIEを当てたもの](https://i.imgur.com/x1RIY1a.png) ※IDAユーザ向けに書いとくと,idb内のアセンブリ文字列にパッチを当てるわけではなく,ディスアセンブル画面とデコンパイル画面での"表示"を修正してくれるものである.つまりidbそのものに変化はなく,AMIEの無い環境では元のアセンブリが表示される. ※今回の問題で使われている`FLAG<n>`レジスタのような本来存在しないレジスタに対する操作は上手く"表示"されないため注意. ### `bios.bin`の調査 先に示した`bios.bin`の先頭のコードを見ると,`SCTLR_EL3`(System Control Register EL3),`SCR_EL3`(Secure Configuration Register EL3)と言ったレジスタへのアクセスがある.これらが出現するということはここはS-EL3のコードだろう.つまりセキュアモニタのコードだ. この領域をデコンパイルすると,以下のようになっている. なお`bzero()`や`memcpy()`と既に名前がついているが,これは解析の結果つけたものだ.というのも,ファームウェアでは開始直後にシステムレジスタの設定を行い,続いて`bzero()`や`memset()`によるRAM初期化,`memcpy()`や`memmove()`によるRAMへのコピーなどが行われることが多いからだ.従って関数をざっと読めば,これらの関数はすぐに名前がつけられる. ![](https://i.imgur.com/2CanAwF.png) 1つの`bzero()`と,4つの`memcpy()`が存在するのが見えるだろう.おそらくメモリにコードをロードしている部分だ.ロード元のデータを一つずつ見ていこう. `0x10000`には,`VBAR_EL2`へのアクセスを含むコードがある.これはおそらくEL2ハイパーバイザのコードだ. ![](https://i.imgur.com/1yM4TiF.png) `0xB0000`には,`TTBR0_EL1`や`TTBR1_EL1`へのアクセスを含むコードがある.これはおそらくEL1カーネルのコードだ. ![](https://i.imgur.com/V1usPa4.png) `0x20000`には,ちょっと何をやっているかわからないコードらしきものがある. ![](https://i.imgur.com/EtnQfTw.png) この領域の中身はそのままだと解釈が難しいが,各4バイト命令の末尾を見てみると,どれも`0xEA`で終わっている.つまりこれはAArch32のコードの可能性が高い(これは後ほど確認する).AArch64で解釈しているために,おかしくなっているのだと思われる. ### バイナリの抽出 とりあえず,それぞれ抽出しておこう.単にシェルスクリプトで切り出す場合はこちら. :::spoiler Click {%gist bata24/8ab0b5c866fe06bc356d63ff73ee47ed %} ::: ---- IDA上で,idbから切り出しつつ切り出した部分を削除したい場合はこちら. :::spoiler Click (IDA7.1向けなので,最近のIDAでは動作しない可能性があります) {%gist bata24/d5f8d23f4ddcdef0303e595ca82fed8a %} ::: ---- 上記のIDAスクリプトについて少し解説をしておこう. - 切り出した部分を別途保存し別のidbで解析する場合,切り出し元における当該データはもう不要である - 解析の邪魔になるため,削除しておいたほうが何かと都合が良い - 但し完全に削除すると,どこかから参照されていた場合にその参照先が消えることになる可能性がある - つまり理想としては,参照先としては残し,実データだけを削除するのが望ましい - このスクリプトは,指定した範囲のデータを別ファイルに保存しつつ,IDA上では空セグメントに置き換えるような処理をしている - 切り出し元のidbから実データが削除されるため,idbが少し軽くなるメリットが有る - `binwalk`で抽出済みの`EL0_ELF`も併せて削っている - `0x2850`~には`0x68`バイトの`TEE_OS`用セットアップデータ(`TEE_OS`はS-EL1カーネルの意味)が有り,その後ろには未使用領域が長く続いていたため,併せて削っている また`bios.bin`も他のELと同様にELをファイル名に含めるようリネームしておこう. ```= $ cp bios.bin S-EL3_bios ``` これ以降,解析には`S-EL3_bios`を用い,`qemu`の起動にはこれまで通り`bios.bin`を用いるものとする. ### `0x20000`にあったデータの確認 おかしかった`0x20000`~のデータを切り出し,別途IDAに読み込ませてAArch32として解釈させると,正しい命令が現れる.これはARMのリセットベクトル周辺のように見える.どうやら推測は正しそうだ. BIOS(`0x0`~),ELF(`0xBC010`~),S-EL0のコード(`TA_BIN`),カーネル(`0xB0000`~),ハイパーバイザ(`0x10000`~)は既に分かっているので,消去法により,これはS-EL1のコードだろう. ![](https://i.imgur.com/IUqqpo5.png) ### 物理メモリマップ `bios.bin`の先頭にあるセットアップコードを再掲しておく.コピー先の領域は物理メモリのはずだ. ![](https://i.imgur.com/2CanAwF.png) これと先程の物理メモリの図(↓)を組み合わせてみよう. ![](https://i.imgur.com/DSEaufz.png) セキュアモニタは最初に動くコードであり,この時点では特にアドレス変換がされていないため,アドレス`0x0`にあるはずだ.物理メモリの先頭つまり青の領域にS-EL3(セキュアモニタ)があると考えられる.また`memcpy()`のコピー先アドレスから,緑の領域の中ほどにS-EL1(セキュアOS)があり,赤の領域の先頭にEL1(カーネル),少し間を開けてEL2(ハイパーバイザ)が配置されていることがわかる. ![](https://i.imgur.com/RJgFZP0.png) これで物理メモリの配置が特定できた. ### 仮想メモリマップ さて今はEL0からEL1に昇格したいので,EL1の仮想アドレスを知っておこう. `gdb`で`read()`の`svc`命令付近にブレークポイントを仕掛け,命令を一つ進めると,`0xffffffffc000a404`に行き当たった. ```= $ docker exec -it super_hexagon_super_hexagon_1 bash (docker内部で) root@490f646242c9:~# gdb-multiarch -q -ex 'target remote :1234' Remote debugging using :1234 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x0000000000401b58 in ?? () (gdb) b *0x401b54 ★read()内のsvc命令にブレークポイント Breakpoint 1 at 0x401b54 (gdb) c Continuing. Breakpoint 1, 0x0000000000401b54 in ?? () (gdb) ni ★1命令進めると,svc割り込みに飛ぶ 0xffffffffc000a404 in ?? () (gdb) x/10i $pc ★ここはカーネルのコード => 0xffffffffc000a404: b 0xffffffffc000a80c 0xffffffffc000a408: .inst 0x00000000 ; undefined 0xffffffffc000a40c: .inst 0x00000000 ; undefined 0xffffffffc000a410: .inst 0x00000000 ; undefined 0xffffffffc000a414: .inst 0x00000000 ; undefined 0xffffffffc000a418: .inst 0x00000000 ; undefined 0xffffffffc000a41c: .inst 0x00000000 ; undefined 0xffffffffc000a420: .inst 0x00000000 ; undefined 0xffffffffc000a424: .inst 0x00000000 ; undefined 0xffffffffc000a428: .inst 0x00000000 ; undefined (gdb) ``` これはIDAでは以下の位置に相当する. ![](https://i.imgur.com/u9I2FIB.png) つまり,EL1(カーネル)は`0xffffffffc0000000`にマップされているのだとわかる.この環境にkASLRはないので,アドレスは毎回固定だ.IDA上ではrebaseしておこう. ## AArch64のシステムレジスタについて ここで,今回の攻略で重要となるAArch64のシステムレジスタについて述べておこう. 汎用レジスタと違ってあまり馴染みはないが,これらを知らないと解くことは難しいだろうからだ. ### `TTBR` (Translation Table Base Register) / `TCR` (Translation Control Register) これはMMU変換テーブルのベースレジスタと,その設定を表すレジスタだ.EL0を除き例外レベル毎に存在するので,まずは一覧を示しておこう.なお表中のHVはハイパーバイザの略である. | 現在の例外レベル | 管理対象の例外レベル | `TTBR`レジスタ名 | `TCR`レジスタ名 | 今回の利用可否 | |:-:|:-:|:-:|:-:|:-:| | EL0 | なし | なし | なし | - | | EL1 | EL0 | `TTBR0_EL1` | `TCR_EL1` | ✔ | | EL1 | EL1 | `TTBR1_EL1` | 同上 | ✔ | | EL2 | EL2(HV未利用時) | `TTBR0_EL2` | `TCR_EL2` | ✖ | | EL2 | EL2(HV利用時) | `VTTBR_EL2` | `VTCR_EL2` | ✔ | | S-EL3 | S-EL3 | `TTBR0_EL3` | `TCR_EL3` | ✔ | 参考: https://qiita.com/eggman/items/a8862d165cc0b4965f70 参考: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.100403_0200_00_en/kks1469702719702.html もう少し詳しく解説しておこう. EL1(カーネル)では仮想メモリと物理メモリの変換,つまりMMUの管理が必要である. EL1カーネル自身のMMU変換テーブルと,EL0ユーザランドのMMU変換テーブルを両方保持する必要があるので,そのベースアドレスを保持するレジスタが2つ用意されている. - `TTBR0_EL1`: Translation Table Base Register 0, EL1 - EL1における,ユーザランド向けの,仮想メモリ->物理メモリへの変換テーブル(の先頭アドレス). - `TTBR1_EL1`: Translation Table Base Register 1, EL1 - EL1における,カーネル向けの,仮想メモリ->物理メモリへの変換テーブル(の先頭アドレス). それぞれの変換テーブルが管理する対象領域は,以下のような仮想メモリの領域となる. ![](https://i.imgur.com/nUBr0no.png) またEL1における変換テーブルは,`TCR`の設定に従う. - `TCR_EL1`: Translation Control Register, EL1 - EL1における,`TTBR0_EL1`,`TTBR1_EL0`の詳細設定 `TTBR`と`TCR`の実際の値は次のようになっていた. ```= (gdb) i r TTBR0_EL1 TTBR0_EL1 0x20000 0x20000 (gdb) i r TTBR1_EL1 TTBR1_EL1 0x1b000 0x1b000 (gdb) i r TCR_EL1 TCR_EL1 0x6080100010 0x6080100010 (gdb) ``` 他のレベル向けのレジスタも一応示しておこう. ```= ---- EL2 ---- (gdb) i r TTBR0_EL2 TTBR0_EL2 0x0 0x0 // 非hypervisorとして使う場合に使われる,EL2でのテーブルベース(今回未使用) (gdb) i r VTTBR_EL2 VTTBR_EL2 0x40106000 0x40106000 // hypervisorとして使う場合に使われる,EL2でのテーブルベース (gdb) i r TCR_EL2 TCR_EL2 0x0 0x0 // 非hypervisorとして使う場合に使われる,EL2でのテーブル設定(今回未使用) (gdb) i r VTCR_EL2 VTCR_EL2 0x80000027 0x80000027 // hypervisorとして使う場合に使われる,EL2でのテーブル設定 ---- S-EL3 ---- (gdb) i r TTBR0_EL3 TTBR0_EL3 0xe203000 0xe203000 // S-EL3でのテーブルベース (gdb) i r TCR_EL3 TCR_EL3 0x100022 0x100022 // S-EL3でのテーブル設定 (gdb) ``` #### ページテーブル構造について `TTBR`や`TCR`を解説したので,ページテーブルについても書いておこう.[チームKernel SandersのWrite-up](https://hernan.de/blog/2018/10/30/super-hexagon-a-journey-from-el0-to-s-el3/)に,良さげな解説があったので転記しておく. - ページングが有効なCPUでは,仮想アドレスによるメモリ操作を受け取ると,適切なページテーブルを参照してページウォーク(仮想アドレスから物理アドレスへの変換)を実行する. - 仮想アドレスを物理アドレスに解決するために複数のテーブル参照が必要なため,ページウォークは高価な操作であり,これがCPUが`TLB`(Translation Lookaside Buffer)と呼ばれる変換キャッシュを採用している理由である.これはAArch64でも同様である. - AArch64の各例外レベルには,EL0を除いて1つ以上の変換テーブルレジスタが存在する.つまり少なくとも`3`つ(EL1, EL2, S-EL3)の異なる仮想メモリ空間が存在できることを意味する.今回の問題でもこれは同様である. - S-EL3では,ブート時に`TTBR0_EL3`と`TCR_EL3`を初期化する. - EL2では,`VTTBR_EL2`と`VTCR_EL2`を初期化する(ハイパーバイザとして構成されている特殊なケースであるため`V`の付いたこれらレジスタが使われる). - EL1では,`TTBR0_EL0`(ユーザ空間用)と`TTBR1_EL1`(カーネル用)と`TCR_EL1`を初期化する. - 各レジスタは大まかに次のような役割がある. - `TTBR`は特定のELにおける,ページテーブルの物理ベースアドレスを保持するものである. - `VTTBR`はハイパーバイザ利用時に,追加で利用するページテーブルの物理ベースアドレスを保持するものである. - `TCR`は,ページグラニュール(`TG0=4KB,16KB,64KB`)や仮想範囲サイズ(`T0SZ`)などのページテーブルの詳細を変更するために使用される. - 実際のページテーブル構造は,ページのアクセス許可が記述されるとともに,最終的に物理アドレス値に変換されるような複数レベルのツリー構造となっている. #### ページテーブルのフォーマット さて,カーネル向けのページテーブルである`TTBR1_EL1`のフォーマットを見ておこう.https://static.docs.arm.com/ddi0557/ab/DDI0557A_b_armv8_1_supplement.pdf の P37あたりからだ. 仮想アドレスは本来`64`ビットだが,実際に使われるのは下位`48`ビットだ.つまり仮想アドレス`48`ビットを,物理アドレス`48`ビットに変換する必要がある. ARMv8には`4`つのレベルのテーブルがある.`48`ビットの仮想アドレスを`[9,9,9,9,12]`ビットに分解し,各レベルで前から`9`ビットずつをそれぞれ変換に使う.最後のレベルのテーブルを解釈した段階で`9×4=36`ビット分を消費している.ここで得られる物理アドレスは`4KB`ページ単位のアドレスなので,末尾が`0x000`である(例えば`0x1234000`のような物理アドレスになっている).残った`12`ビットを使ってページ内オフセットを表す.これで`48`ビット分のアドレス変換を実現している. 以下の図で,左下の`TTBR_ELx(x=0,1)`レジスタから,仮想アドレスの一部`a`, `b`, `c`, `d`をインデックスと見なして`4`つのテーブルを辿り,最後に`D_Page`が特定される,というように考えればよいだろう. ![](https://i.imgur.com/tm4P2Qf.png) レベル0, 1, 2では,以下のようなページテーブルエントリが用いられる. ![](https://i.imgur.com/F2F8Xrp.png) 最後のレベル3では,以下のようなページテーブルエントリが用いられる. ![](https://i.imgur.com/yOHjPTH.png) 重要なのは,テーブルには`Invalid`/`D_Block`/`D_Table`/`D_Page`の`4`種類のエントリが存在し,それは下位`2`ビットによって決定されることだ. なお`D_Block`型と`D_Page`型は,同じ末尾ビット`0b11`を持つが,利用されるレベルが異なるので解釈に困ることはない. - `Invalid`型エントリは,その名の通り無効なページテーブルである. - (レベル0~2の)`D_Block`型エントリは,変換終了時に用いられる,(ページよりも大きい)メモリの大きな領域を表すページテーブルである. - `D_Table`型エントリは,次レベルのテーブルへのポインタである. - (レベル3の)`D_Page`型エントリは,変換終了時に用いられるページテーブルである. この内レベル3の出力である`D_Page`型エントリの`Upper attributes`と`Lower attributes`がとても重要だ.それぞれ次のような構造である. ![](https://i.imgur.com/txOBce6.png) 以下のような属性を持っている. - `PBHA`: Page-based hardware attributes bits (ARMv8.2-TTPBHA) - `UXN`: Unprivileged eXecute Never.EL0におけるEL0ページの実行可否. - `PXN`: Privileged eXecute Never.EL1におけるEL0ページの実行可否. - `Contiguous`: 連続したページの1つであることを示すヒント - `DBM`: Dirty Bit Modifier (ARMv8.1-TTHM) - `nG`: not Global - `AF`: Access flag - `SH`: Shareability field - `AP`: Data Access Permission - `NS`: Non-Secure bit - `AttrIndx`: Stage1 memory attributes index, for MAIR_ELx 詳細な説明は以下を参考にするとよいだろう. - https://armv8-ref.codingbelief.com/en/chapter_d4/d43_3_memory_attribute_fields_in_the_vmsav8-64_translation_table_formats_descriptors.html - https://www.cnblogs.com/-9-8/p/8417770.html #### 実際のページテーブルの閲覧 ページテーブルの内容を閲覧するには,IDA上でMMU変換テーブルを頑張って解読しても良いのだが,もっと簡単な方法がある. チームKernel Sandersが今回の問題を解く過程で作成した`gdb`スクリプトで,以下で入手可能だ. - https://github.com/grant-h/gdbscripts/blob/master/aarch64/aarch64-pagewalk.py ただし若干中途半端な状態でcommitしてあるらしく,EL2が正しく表示されないバグがあるので,パッチを当てておこう. ```= (docker内部で) $ cd ~ $ wget https://raw.githubusercontent.com/grant-h/gdbscripts/master/aarch64/aarch64-pagewalk.py $ wget https://gist.githubusercontent.com/bata24/bb8eddeed7f185423ddce2d3a24b78e5/raw/4752ace02a821d02753ba30a77ecb127409535e9/diff.patch $ patch < diff.patch ``` このスクリプトは`CPSR`レジスタが保持する例外レベル情報(EL)を利用して現在のELを判定している.従ってカーネル空間を実行している状態でスクリプトを実行すると,EL1のページテーブル2つ(カーネル用/ユーザ空間用)の内容を確認することができる. 左側が仮想アドレスで,右側が物理アドレスとそのページの属性のようだ. ```= (gdb) source aarch64-pagewalk.py (gdb) pagewalk CPSR: EL1 IPA Size: 32-bits EL1 Kernel Region Min: 0xffff000000000000 EL1 Kernel Page Size: 4KB EL1 User Region Max: 0x0000ffffffffffff EL1 User Page Size: 4KB User Mode Page Tables Entries/table: 512 Levels: 4 0000000000400000: 0x000000000002c000 [PXN ELx/RO] 0000000000401000: 0x000000000002d000 [PXN ELx/RO] 0000000000402000: 0x000000000002e000 [PXN ELx/RO] 0000000000412000: 0x000000000002f000 [PXN UXN ELx/RW] 00007ffeffffd000: 0x0000000000034000 [PXN UXN ELx/RW] 00007ffeffffe000: 0x0000000000033000 [PXN UXN ELx/RW] 00007ffefffff000: 0x0000000000032000 [PXN UXN ELx/RW] 00007fff7fffe000: 0x0000000000030000 [PXN UXN ELx/RW] 00007fff7ffff000: 0x0000000000031000 [PXN UXN ELx/RW] Kernel Mode Page Tables Entries/table: 512 Levels: 4 ffffffffc0000000: 0x0000000000000000 [UXN EL1/RO] ffffffffc0001000: 0x0000000000001000 [UXN EL1/RO] ffffffffc0002000: 0x0000000000002000 [UXN EL1/RO] ffffffffc0003000: 0x0000000000003000 [UXN EL1/RO] ffffffffc0004000: 0x0000000000004000 [UXN EL1/RO] ffffffffc0005000: 0x0000000000005000 [UXN EL1/RO] ffffffffc0006000: 0x0000000000006000 [UXN EL1/RO] ffffffffc0007000: 0x0000000000007000 [UXN EL1/RO] ffffffffc0008000: 0x0000000000008000 [UXN EL1/RO] ffffffffc0009000: 0x0000000000009000 [UXN EL1/RO] ffffffffc000a000: 0x000000000000a000 [UXN EL1/RO] ffffffffc000c000: 0x000000000000c000 [PXN UXN EL1/RW] ffffffffc000d000: 0x000000000000d000 [PXN UXN EL1/RW] ffffffffc000e000: 0x000000000000e000 [PXN UXN EL1/RW] ffffffffc000f000: 0x000000000000f000 [PXN UXN EL1/RW] ffffffffc0010000: 0x0000000000010000 [PXN UXN EL1/RW] ffffffffc0011000: 0x0000000000011000 [PXN UXN EL1/RW] ffffffffc0012000: 0x0000000000012000 [PXN UXN EL1/RW] ffffffffc0013000: 0x0000000000013000 [PXN UXN EL1/RW] ffffffffc0014000: 0x0000000000014000 [PXN UXN EL1/RW] ffffffffc0015000: 0x0000000000015000 [PXN UXN EL1/RW] ffffffffc0016000: 0x0000000000016000 [PXN UXN EL1/RW] ffffffffc0017000: 0x0000000000017000 [PXN UXN EL1/RW] ffffffffc0018000: 0x0000000000018000 [PXN UXN EL1/RW] ffffffffc0019000: 0x0000000000019000 [PXN UXN EL1/RW] ffffffffc001a000: 0x000000000001a000 [PXN UXN EL1/RW] ffffffffc001b000: 0x000000000001b000 [PXN UXN EL1/RW] ffffffffc001c000: 0x000000000001c000 [PXN UXN EL1/RW] ffffffffc001d000: 0x000000000001d000 [PXN UXN EL1/RW] ffffffffc001e000: 0x000000000001e000 [PXN UXN EL1/RW] ffffffffc001f000: 0x000000000001f000 [PXN UXN EL1/RW] ffffffffc0020000: 0x0000000000020000 [PXN UXN EL1/RW] ffffffffc0021000: 0x0000000000021000 [PXN UXN EL1/RW] ffffffffc0022000: 0x0000000000022000 [PXN UXN EL1/RW] ffffffffc0023000: 0x0000000000023000 [PXN UXN EL1/RW] ffffffffc0024000: 0x0000000000024000 [PXN UXN EL1/RW] ffffffffc0025000: 0x0000000000025000 [PXN UXN EL1/RW] ffffffffc0026000: 0x0000000000026000 [PXN UXN EL1/RW] ffffffffc0027000: 0x0000000000027000 [PXN UXN EL1/RW] ffffffffc0028000: 0x0000000000028000 [PXN UXN EL1/RW] ffffffffc0029000: 0x0000000000029000 [PXN UXN EL1/RW] ffffffffc002a000: 0x000000000002a000 [PXN UXN EL1/RW] ffffffffc002b000: 0x000000000002b000 [PXN UXN EL1/RW] ffffffffc002c000: 0x000000000002c000 [PXN UXN EL1/RW] ffffffffc002d000: 0x000000000002d000 [PXN UXN EL1/RW] ffffffffc002e000: 0x000000000002e000 [PXN UXN EL1/RW] ffffffffc002f000: 0x000000000002f000 [PXN UXN EL1/RW] ffffffffc0030000: 0x0000000000030000 [PXN UXN EL1/RW] ffffffffc0031000: 0x0000000000031000 [PXN UXN EL1/RW] ffffffffc0032000: 0x0000000000032000 [PXN UXN EL1/RW] ffffffffc0033000: 0x0000000000033000 [PXN UXN EL1/RW] ffffffffc0034000: 0x0000000000034000 [PXN UXN EL1/RW] ffffffffc0035000: 0x0000000000035000 [PXN UXN EL1/RW] ffffffffc0036000: 0x0000000000036000 [PXN UXN EL1/RW] ffffffffc0037000: 0x0000000000037000 [PXN UXN EL1/RW] ffffffffc0038000: 0x0000000000038000 [PXN UXN EL1/RW] ffffffffc0039000: 0x0000000000039000 [PXN UXN EL1/RW] ffffffffc003a000: 0x000000000003a000 [PXN UXN EL1/RW] ffffffffc9000000: 0x000000000003b000 [PXN UXN EL1/RW] (gdb) ``` ちなみに,現在のELに関係なく,引数として表示したいページテーブルのELを指定することもできる(EL1,EL2のみ). ### `VBAR` (Vector Base Address Register) 次は割り込みベクトルを保持するレジスタだ. | 例外レベル | `VBAR`レジスタ名 | 今回の利用可否 | |:-:|:-:|:-:| | EL0 | なし | - | | EL1 | `VBAR` | ✔ | | EL2 | `VBAR_EL2` | ✔ | | S-EL3 | `VBAR_EL3` | ✔ | 参考: https://bunkyu3.hatenablog.com/entry/2019/02/02/023926 - `VBAR`: Vector Base Address Register(, EL1) - EL1での割り込みベクトル. - この環境では`VBAR_EL1`のように末尾に`_EL1`がつかないので注意. - 環境によっては付く場合もあるようだ. - `VBAR_EL2`: Vector Base Address Register, EL2 - EL2での割り込みベクトル. - `VBAR_EL3`: Vector Base Address Register, EL3 - S-EL3での割り込みベクトル. `gdb`で見ると,以下のような値となっていた. ```= (gdb) i r VBAR VBAR 0xffffffffc000a000 0xffffffffc000a000 (gdb) i r VBAR_EL2 VBAR_EL2 0x40101800 0x40101800 (gdb) i r VBAR_EL3 VBAR_EL3 0x2000 0x2000 (gdb) ``` なお,AArch64での割り込みベクトルはAArch32とは大きく異なり,命令が複数実行できるように間隔が広くなっていることに注意しよう.具体的には,割込一つ当たりの空間は`0x80`バイトで,最大`32`個(`=0x80/4`バイト)の命令が実行できるようになっている. - 参考: AArch32(Cortex-A)の割り込みベクトル - `0x0`にあるが,設定で高位アドレスに移動可能.`VBAR`で指定することも出来る. - 割込ベクトルは1つ当たり`4`バイト.「ハンドラへジャンプする命令」を書き込んでおく. - 参考: AArch32(Cortex-M)の割り込みベクトル - `0x0`にある.ベクタテーブルオフセットレジスタを使って指定することも出来る. - 割込ベクトルは1つ当たり`4`バイト.「ハンドラのアドレス」を書き込んでおく. - 参考: AArch64の割り込みベクトル - `VBAR`を使って保持する. - 割込ベクトルは1つ当たり`0x80`バイト.複数命令が書き込まれる. 今回のケース(AArch64)ではこんな感じであった.`ALIGN 0x80`されているのがわかるだろう. ![](https://i.imgur.com/GFazmAR.png) そしてそのような割り込み用の領域が`4`つ(`Sync`, `IRQ`, `FIQ`, `SError`)で一組となる. 割り込み元のELやモードによって4種類のバリエーションがあるため、合計で`16`個の割り込みベクトルが存在する. ![](https://i.imgur.com/vPUqSKu.png) 例えば割り込みが発生して現在EL1になったとすると,この図は以下のようになる. - `Current EL with SP_EL0`: EL1からの割り込みで,EL0のスタックポインタを使用する - `Current EL with SP_ELx, x>0`: EL1からの割り込みで,EL1のスタックポインタを使用する - `Lower EL from AArch64`: EL0(AArch64 state)からの割り込み - `Lower EL from AArch32`: EL0(AArch32 state)からの割り込み カーネルの解析で重要なシステムコールのハンドラを探すには,見るべき`VBAR`は`Lower EL from AArch64`であり,それはオフセット`0x400`に存在するものだ(上から`9`個目). ![](https://i.imgur.com/qtEcppz.png) ## カーネルの解析 ### システムコールハンドラの特定 先程`gdb`で値を調べたら`VBAR`が`0xffffffffc000a000`であったと述べた.ここからオフセット`0x400`,つまり`0xffffffffc000a400`が見るべきハンドラだ. ![](https://i.imgur.com/VcoJHEz.png) ジャンプ先で更に3つの関数を呼び出す. ![](https://i.imgur.com/rEQNYo0.png) 真ん中の関数を読んでいくと,どうやらシステムコールの分岐であることがわかる. ![](https://i.imgur.com/dVPuNka.png) 理由は簡単で,ここで`0x3f`, `0x40`, `0x5d`, `0xde`, `0xe2`と比較しているが,これはまさにEL0のELFにおける,`svc`に渡すシステムコール番号の値と一致しているからだ. 参考までに,以下は`ELF_EL0`の該当する箇所である. ![](https://i.imgur.com/dETE9wW.png) EL1カーネルに戻ろう.システムコールハンドラの全体像はこんな感じになる. ![](https://i.imgur.com/P05PGeH.png) 尚`ARG1`~`ARG3`は共用体として定義し,見やすくしておいた. ![](https://i.imgur.com/fQv3OMC.png) システムコール一覧をまとめると次のようになる. | | `read()` | `write()` | `mmap()` | `mprotect()` | `exit()` | |:-:|:-:|:-:|:-:|:-:|:-:| | EL0<br>`svc`番号と<br>引数 | `0x3f`, `fd`, `buf`, `len` | `0x40`, `fd`, `buf`, `len` | `0xde`, `addr`, `length`, `prot` | `0xe2`, `addr`, `length`, `prot` | `0x5d` | | | ↓ | ↓ | ↓ | ↓ | ↓ | | EL1<br>処理概要 | `stdin`から<br>`1`バイト読み込む<br>(`fd`は無視) | `stdout`へ<br>`len`バイト書き出す<br>(`fd`は無視) | メモリを確保<br>(第4~6引数は無視) | メモリの<br>属性変更 | 終了<br>(第1引数は無視) | ### 脆弱性 `read()`システムコールでは,`buf`で指定したアドレスへ,チェック無しに代入している.つまりカーネル空間のアドレスへの任意書き込みが可能だ. ![](https://i.imgur.com/x0xBIA1.png) 但し注意しなければいけないのは,`read_char()`と書いてあるとおり,一度の呼び出しでは`1`バイトだけしか書き換えることができない.また当然だが`RW`な領域しか書き換える事はできない. ## フラグ奪取 最終的に`print_flag()`を実行したいので,`$PC`を奪う必要がある.ただしNXは有効なので,コード領域を書き換えることはできない. 必然的に,関数ポインタかスタックのリターンアドレスを`1`バイト書き換えてROPに持ち込むことになるだろう.関数ポインタでちょうど良いのは見つからないので,結局スタックのアドレスを上書きすることになる. チームBalsnとチームKernel Sandersはスタック上のリターンアドレスを次のように変更した(`[..]`の中身を`1`バイト書き換えた). - `Balsn` - `0xffffffffc0019bb8`: `0xffffffffc000[a8]30` -> `0xffffffffc000[8f]30` - `Kernel Sanders` - `0xffffffffc0019bb8`: `0xffffffffc000[a8]30` -> `0xffffffffc000[94]30` 使ったガジェットは異なるが,どちらもほぼ同じことを行っており,これは次のような考え方による. - スタック上にある本来のリターンアドレスは`0xffffffffc000a830`だが,`print_flag()`は`0xffffffffc0008408`にある. ```= 0xffffffffc0008c54 in ?? () (gdb) x/64xg $sp 0xffffffffc0019bb0: 0x00007fff7ffffe40 0xffffffffc000a830 ★ret-addr 0xffffffffc0019bc0: 0x0000000000412650 0x0000000000000000 0xffffffffc0019bd0: 0x0000000000000000 0x0000000000000000 0xffffffffc0019be0: 0x0000000000000000 0x0000000000000000 0xffffffffc0019bf0: 0x0000000000000000 0xffffffffc0008034 0xffffffffc0019c00: 0x0000000000000000 0x0000000000000000 0xffffffffc0019c10: 0x0000000000000000 0x0000000000000000 0xffffffffc0019c20: 0x0000000000000000 0x0000000000000000 0xffffffffc0019c30: 0x0000000000000000 0x0000000000000000 0xffffffffc0019c40: 0x0000000000000000 0x0000000000000000 0xffffffffc0019c50: 0x0000000000000000 0x0000000000000000 ``` - 末尾`2`バイトが違うので,一度の上書きでは達成できない.そこで,`0xffffffffc000a830`と`1`バイトしか違わないアドレスから,いい感じのトランポリンを探してきて挟む. - トランポリンには`0xffffffffc0008f30`を採用する.これはスタック上の値を`$X30`にロードして,そこにリターンする.つまり`ret2ret`のようなガジェットだ. ![](https://i.imgur.com/Zl0oO4S.png) - トランポリンには`0xffffffffc0009430`を採用することもできる.動作は同じだ. ![](https://i.imgur.com/4ptykY7.png) - これらを使うともう少し下方のスタック,つまり`0xffffffffc0019c08`に書かれているアドレスへリターンすることになる. ```= 0xffffffffc0008c54 in ?? () (gdb) x/64xg $sp 0xffffffffc0019bb0: 0x00007fff7ffffe40 0xffffffffc000a830 ★ret-addrを上記のどちらかに改変すると 0xffffffffc0019bc0: 0x0000000000412650 0x0000000000000000 0xffffffffc0019bd0: 0x0000000000000000 0x0000000000000000 0xffffffffc0019be0: 0x0000000000000000 0x0000000000000000 0xffffffffc0019bf0: 0x0000000000000000 0xffffffffc0008034 0xffffffffc0019c00: 0x0000000000000000 0x0000000000000000 ★ここが$X30にロードされる 0xffffffffc0019c10: 0x0000000000000000 0x0000000000000000 0xffffffffc0019c20: 0x0000000000000000 0x0000000000000000 0xffffffffc0019c30: 0x0000000000000000 0x0000000000000000 0xffffffffc0019c40: 0x0000000000000000 0x0000000000000000 0xffffffffc0019c50: 0x0000000000000000 0x0000000000000000 ``` - つまり事前に`0xffffffffc0019c08`へ`print_flag()`のアドレスを書いておき,その後`0xffffffffc0019bb8+1`の`1`バイトを変更すると,制御を奪うことが出来る. - `0xffffffffc0019c08`に書いた値は,システムコールを複数回呼び出す間,破壊されずにずっと残っている. 以上を踏まえて実装したのが下記のコードである. :::spoiler Click {%gist bata24/08ec5bb523911dbf1e4a78a3c7e59262 %} ::: ---- EL0のstager後に送りつけるシェルコード内で,EL1のフラグを表示するようなコードを仕込んでいるのがわかるだろう. ## 任意コード実行へ EL1権限で任意のシェルコードを実行するためには,ページテーブルを書き換える必要がある. ### 攻略方針1 まずはチームKernel Sandersが取った手法を見てみよう. `TTBRx_EL1`には物理メモリのアドレスが書かれている.仮想メモリではカーネルのアドレス,つまり`0xffffffffc0000000`に対応するはずなので`0xffffffffc0000000`を加算したところにページテーブル(レベル0)がある. ```= // EL1向けのTTBRの値(物理メモリアドレス) (gdb) i r TTBR0_EL1 TTBR1_EL1 TTBR0_EL1 0x20000 0x20000 TTBR1_EL1 0x1b000 0x1b000 // 仮想メモリしか見えていないので,そのままでは物理メモリのアドレスへアクセスできない (gdb) x/20gx $TTBR0_EL1 0x20000: Cannot access memory at address 0x20000 (gdb) x/20gx $TTBR1_EL1 0x1b000: Cannot access memory at address 0x1b000 // 仮想メモリのアドレスへアクセスすることで代用 (レベル0) (gdb) x/20gx 0xffffffffc0000000 + $TTBR1_EL1 // カーネル向けページテーブル 0xffffffffc001b000: 0x0000000000000000 0x0000000000000000 ★先頭付近にはエントリがない(かなり後ろの方にある) 0xffffffffc001b010: 0x0000000000000000 0x0000000000000000 0xffffffffc001b020: 0x0000000000000000 0x0000000000000000 0xffffffffc001b030: 0x0000000000000000 0x0000000000000000 0xffffffffc001b040: 0x0000000000000000 0x0000000000000000 0xffffffffc001b050: 0x0000000000000000 0x0000000000000000 0xffffffffc001b060: 0x0000000000000000 0x0000000000000000 0xffffffffc001b070: 0x0000000000000000 0x0000000000000000 0xffffffffc001b080: 0x0000000000000000 0x0000000000000000 0xffffffffc001b090: 0x0000000000000000 0x0000000000000000 ★長いので省略 (gdb) x/20gx 0xffffffffc0000000 + $TTBR0_EL1 // ユーザランド向けページテーブル 0xffffffffc0020000: 0x0000000000021003 0x0000000000000000 ★エントリが先頭に1つある 0xffffffffc0020010: 0x0000000000000000 0x0000000000000000 0xffffffffc0020020: 0x0000000000000000 0x0000000000000000 0xffffffffc0020030: 0x0000000000000000 0x0000000000000000 0xffffffffc0020040: 0x0000000000000000 0x0000000000000000 0xffffffffc0020050: 0x0000000000000000 0x0000000000000000 0xffffffffc0020060: 0x0000000000000000 0x0000000000000000 0xffffffffc0020070: 0x0000000000000000 0x0000000000000000 0xffffffffc0020080: 0x0000000000000000 0x0000000000000000 0xffffffffc0020090: 0x0000000000000000 0x0000000000000000 ★長いので省略 (gdb) ``` ユーザランド向けページテーブルのレベル0には,`0x21003`というエントリがあるので,更に追いかけてみよう.尚アドレス末尾の`3`は`D_Table`型を表すフラグなので無視すること. ```= (gdb) x/20gx 0xffffffffc0000000 + 0x21000 // レベル1 0xffffffffc0021000: 0x0000000000022003 0x0000000000000000 0xffffffffc0021010: 0x0000000000000000 0x0000000000000000 0xffffffffc0021020: 0x0000000000000000 0x0000000000000000 0xffffffffc0021030: 0x0000000000000000 0x0000000000000000 0xffffffffc0021040: 0x0000000000000000 0x0000000000000000 0xffffffffc0021050: 0x0000000000000000 0x0000000000000000 0xffffffffc0021060: 0x0000000000000000 0x0000000000000000 0xffffffffc0021070: 0x0000000000000000 0x0000000000000000 0xffffffffc0021080: 0x0000000000000000 0x0000000000000000 0xffffffffc0021090: 0x0000000000000000 0x0000000000000000 (gdb) x/20gx 0xffffffffc0000000 + 0x22000 // レベル2 0xffffffffc0022000: 0x0000000000000000 0x0000000000000000 0xffffffffc0022010: 0x0000000000023003 0x0000000000000000 0xffffffffc0022020: 0x0000000000000000 0x0000000000000000 0xffffffffc0022030: 0x0000000000000000 0x0000000000000000 0xffffffffc0022040: 0x0000000000000000 0x0000000000000000 0xffffffffc0022050: 0x0000000000000000 0x0000000000000000 0xffffffffc0022060: 0x0000000000000000 0x0000000000000000 0xffffffffc0022070: 0x0000000000000000 0x0000000000000000 0xffffffffc0022080: 0x0000000000000000 0x0000000000000000 0xffffffffc0022090: 0x0000000000000000 0x0000000000000000 (gdb) x/20gx 0xffffffffc0000000 + 0x23000 // レベル3 0xffffffffc0023000: 0x002000000002c4c3 ★ 0x002000000002d4c3 ★ 0xffffffffc0023010: 0x002000000002e4c3 ★ 0x0000000000000000 0xffffffffc0023020: 0x0000000000000000 0x0000000000000000 0xffffffffc0023030: 0x0000000000000000 0x0000000000000000 0xffffffffc0023040: 0x0000000000000000 0x0000000000000000 0xffffffffc0023050: 0x0000000000000000 0x0000000000000000 0xffffffffc0023060: 0x0000000000000000 0x0000000000000000 0xffffffffc0023070: 0x0000000000000000 0x0000000000000000 0xffffffffc0023080: 0x0000000000000000 0x0000000000000000 0xffffffffc0023090: 0x006000000002f443 ★ 0x0000000000000000 (gdb) ``` さて,先程使った`pagewalk`の結果をもう一度見てみよう.★がそれぞれ対応するのがわかるだろう.ついでにその他の領域も使われ方を書いておいた. ```= (gdb) source aarch64-pagewalk.py (gdb) pagewalk CPSR: EL1 IPA Size: 32-bits EL1 Kernel Region Min: 0xffff000000000000 EL1 Kernel Page Size: 4KB EL1 User Region Max: 0x0000ffffffffffff EL1 User Page Size: 4KB User Mode Page Tables Entries/table: 512 Levels: 4 0000000000400000: 0x000000000002c000 [PXN ELx/RO] ★ // .text 0000000000401000: 0x000000000002d000 [PXN ELx/RO] ★ // .text 0000000000402000: 0x000000000002e000 [PXN ELx/RO] ★ // .text 0000000000412000: 0x000000000002f000 [PXN UXN ELx/RW] ★ // .text + .bss 00007ffeffffd000: 0x0000000000034000 [PXN UXN ELx/RW] // mmaped area for buf 00007ffeffffe000: 0x0000000000033000 [PXN UXN ELx/RW] // mmaped area for tci_buf 00007ffefffff000: 0x0000000000032000 [PXN UXN ELx/RW] // mmaped area for wsm 00007fff7fffe000: 0x0000000000030000 [PXN UXN ELx/RW] // stack 00007fff7ffff000: 0x0000000000031000 [PXN UXN ELx/RW] // stack Kernel Mode Page Tables Entries/table: 512 Levels: 4 ``` さて,ユーザランド(EL0)のメモリをカーネル(EL1)が実行するには,`PXN`フラグを落とせば良い.`PXN`フラグは,ページテーブルエントリの下から`53`ビット目に対応する. 例えば手動で`PXN`フラグを消してみよう.するとちゃんと`pagewalk`の結果から`PXN`が消えた.つまり,この領域をカーネルも実行出来るようになるはずだ.これと同じことを,EL1シェルコードを書き込んだアドレスに対して行えば良い. ```= (gdb) set *(void**)0xffffffffc0023000=0x000000000002c4c3 (gdb) pagewalk CPSR: EL1 IPA Size: 32-bits EL1 Kernel Region Min: 0xffff000000000000 EL1 Kernel Page Size: 4KB EL1 User Region Max: 0x0000ffffffffffff EL1 User Page Size: 4KB User Mode Page Tables Entries/table: 512 Levels: 4 0000000000400000: 0x000000000002c000 [ELx/RO] ★PXNがないのでカーネルも実行可能 0000000000401000: 0x000000000002d000 [PXN ELx/RO] 0000000000402000: 0x000000000002e000 [PXN ELx/RO] 0000000000412000: 0x000000000002f000 [PXN UXN ELx/RW] 00007ffeffffd000: 0x0000000000034000 [PXN UXN ELx/RW] 00007ffeffffe000: 0x0000000000033000 [PXN UXN ELx/RW] 00007ffefffff000: 0x0000000000032000 [PXN UXN ELx/RW] 00007fff7fffe000: 0x0000000000030000 [PXN UXN ELx/RW] 00007fff7ffff000: 0x0000000000031000 [PXN UXN ELx/RW] ... ``` ということで,方針は簡単だ. 1. `mmap()`で適当にメモリを確保(`RW-`) 2. EL1シェルコードを書き込む 3. `mprotect()`でパーミッション変更(`RW-`->`R-X`) 4. ページテーブルの`PXN`フラグを削除 5. `mmap()`で適当にメモリを確保(TLB更新?) 6. スタック上にシェルコードへのアドレスを書き込む(`8`バイト変更が必要) 7. スタック上のリターンアドレスをトランポリンに変更(`1`バイト変更で達成可能) ステップ5について説明しておこう.実はこの操作はローカルではなくてもうまくいく.しかしチームKernel Sandersによれば,リモートでは必要(?)だったようだ. ステップ4で`PXN`フラグを削除しても,それは単にEL1のページテーブルを更新しただけである.CPUの`TLB`キャッシュには反映されておらず,結果として`PXN`は効いたままになってしまうらしいのだ.全てを同期するにはEL2へのハイパーコールを呼べばいいらしいが,この時点ではハイパーコールの引数が未解析なので,組み立てるのが難しい.そこで`mmap()`を発行すると内部でハイパーコールが呼ばれることを利用して同期させると上手くいくそうだ. ### 攻略方針2 チームNASA Rejectsが取った手法もほぼ同様で,ページテーブルエントリを偽造するのだが,詳細が違うので解説しておく. こちらは`PXN`フラグを落とすのではなく,元々`PXN`が落ちているページの向け先を変更することで,EL1でのシェルコード実行を達成している. 彼らのコードを見ると,`0xffffffffc001e000`に,`\x83\x54\x03`を書き込んでいた.これはつまり,カーネル用のページテーブルの最初のエントリを変更していることになる. ```= (gdb) pagewalk CPSR: EL1 IPA Size: 32-bits EL1 Kernel Region Min: 0xffff000000000000 EL1 Kernel Page Size: 4KB EL1 User Region Max: 0x0000ffffffffffff EL1 User Page Size: 4KB User Mode Page Tables Entries/table: 512 Levels: 4 0000000000400000: 0x000000000002c000 [PXN ELx/RO] 0000000000401000: 0x000000000002d000 [PXN ELx/RO] 0000000000402000: 0x000000000002e000 [PXN ELx/RO] 0000000000412000: 0x000000000002f000 [PXN UXN ELx/RW] 00007ffeffffc000: 0x0000000000035000 [PXN ELx/RO] 00007ffeffffd000: 0x0000000000034000 [PXN ELx/RO] 00007ffeffffe000: 0x0000000000033000 [PXN UXN ELx/RW] 00007ffefffff000: 0x0000000000032000 [PXN UXN ELx/RW] 00007fff7fffe000: 0x0000000000030000 [PXN UXN ELx/RW] 00007fff7ffff000: 0x0000000000031000 [PXN UXN ELx/RW] Kernel Mode Page Tables Entries/table: 512 Levels: 4 ffffffffc0000000: 0x0000000000000000 [UXN EL1/RO] ★ ffffffffc0001000: 0x0000000000001000 [UXN EL1/RO] ffffffffc0002000: 0x0000000000002000 [UXN EL1/RO] ffffffffc0003000: 0x0000000000003000 [UXN EL1/RO] ffffffffc0004000: 0x0000000000004000 [UXN EL1/RO] ffffffffc0005000: 0x0000000000005000 [UXN EL1/RO] ... (gdb) ddh 0xffffffffc001e000 // 頑張って探すと,★を意味するエントリはここだった 0xffffffffc001e000: 0x0040000000000483 ★ 0x0040000000001483 0xffffffffc001e010: 0x0040000000002483 0x0040000000003483 0xffffffffc001e020: 0x0040000000004483 0x0040000000005483 0xffffffffc001e030: 0x0040000000006483 0x0040000000007483 0xffffffffc001e040: 0x0040000000008483 0x0040000000009483 0xffffffffc001e050: 0x004000000000a483 0x0000000000000000 0xffffffffc001e060: 0x006000000000c403 0x006000000000d403 0xffffffffc001e070: 0x006000000000e403 0x006000000000f403 ``` `0x0040000000000483`を`0x0040000000035483`に変更したことで,仮想メモリの`0xffffffffc0000000`は物理メモリの`0x35000`を指すことになり,ここに書かれたコードがEL1で実行される.尚,物理メモリ`0x35000`は,ユーザランドの仮想メモリ`00007ffeffffc000`でもある. || ページテーブル<br>(レベル3)<br>のアドレス | 実際の値 | 物理メモリ<br>(値を解釈して<br>得られる) | 仮想メモリ<br>(ページテーブルの<br>各レベルのオフセット<br>をもとに計算される) | |:-:|:-:|:-:|:-:|:-:| |変更前| `0xffffffffc001e000` | `0x0040000000000483` | `0x0` | `0xffffffffc0000000` | |変更後| `0xffffffffc001e000` | `0x0040000000035483` | `0x35000` | `0xffffffffc0000000` | ```= (gdb) set *(void**)0xffffffffc001e000=0x0040000000035483 (gdb) pagewalk CPSR: EL1 IPA Size: 32-bits EL1 Kernel Region Min: 0xffff000000000000 EL1 Kernel Page Size: 4KB EL1 User Region Max: 0x0000ffffffffffff EL1 User Page Size: 4KB User Mode Page Tables Entries/table: 512 Levels: 4 0000000000400000: 0x000000000002c000 [PXN ELx/RO] 0000000000401000: 0x000000000002d000 [PXN ELx/RO] 0000000000402000: 0x000000000002e000 [PXN ELx/RO] 0000000000412000: 0x000000000002f000 [PXN UXN ELx/RW] 00007ffeffffc000: 0x0000000000035000 [PXN ELx/RO] ★物理メモリの同じ場所を指している 00007ffeffffd000: 0x0000000000034000 [PXN ELx/RO] 00007ffeffffe000: 0x0000000000033000 [PXN UXN ELx/RW] 00007ffefffff000: 0x0000000000032000 [PXN UXN ELx/RW] 00007fff7fffe000: 0x0000000000030000 [PXN UXN ELx/RW] 00007fff7ffff000: 0x0000000000031000 [PXN UXN ELx/RW] Kernel Mode Page Tables Entries/table: 512 Levels: 4 ffffffffc0000000: 0x0000000000035000 [UXN EL1/RO] ★物理メモリの同じ場所を指している ffffffffc0001000: 0x0000000000001000 [UXN EL1/RO] ffffffffc0002000: 0x0000000000002000 [UXN EL1/RO] ``` ### 攻略方針3 次はチームPPPが取った手法を見てみよう.こちらもページテーブルエントリを変更するという点では同じだが,かなり遠回りなことをしている. - `svc`ハンドラ周辺のページを,ユーザランドにまるごとコピー - ユーザランドにコピーした領域のうち,`svc`ハンドラを自身で用意したコードに差し替え - こうすると`svc`ハンドラ以外が実行されてもコケず,`svc`割り込みが発生したときに自身のコードが動作することになる - ページテーブルエントリを偽造し,`svc`ハンドラをユーザランドの偽造`svc`ハンドラへ向け直す 言われてみれば確かにできなくもないが,面倒なので今回は実施しない. ### exploit EL1で任意コード実行を行うexploitの実装は以下のようになる.今回は,わかりやすく実装することができる攻略方針1を採用している. 尚`TLB`キャッシュの更新のための`mmap()`は実装していない(必要に応じて実装するだけなので今回は省略した). :::spoiler Click {%gist bata24/2e23b9415a4018e8e3ce8e718a2b875c %} ::: ---- # 続く 明日は私の[HITCON CTF 2018 - Super Hexagon (Part 3/7)](https://hackmd.io/@bata24/S1bHxavMU)です.