# DEFCON CTF 2022 - teedium-wallet (1/3) ###### tags: `trustzone` `ctf` `pwn` `ARM` # リンク集 - Part1/3 (事前準備/TIPS) - https://hackmd.io/@bata24/BJ3nuVEu5 - Part2/3 (解析) - https://hackmd.io/@bata24/ry1_YS-c5 - Part3/3 (攻略) - https://hackmd.io/@bata24/HkZkcrW95 # Write-up - https://github.com/Nautilus-Institute/quals-2022/tree/main/teedium-wallet (公式ソース) - http://repwn.com/archives/36/ - https://github.com/5lipper/ctf/tree/master/defcon30-quals/teedium - https://github.com/BrieflyX/ctf-pwns/tree/master/escape/TeediumWallet # ファイル https://github.com/Nautilus-Institute/quals-2022/blob/main/teedium-wallet/teedium_wallet_dist.tar.gz 中身は以下の通り. ```= -rw-rw-r-- 1000/1000 112 2022-05-29 02:52 secure-world-wallet/README # docker環境のセットアップ用 # Ubuntu 20.04ベースで,必要な各種ファイルをdocker内にコピーし,qemu_run.shを叩くだけ -rw-rw-r-- 1000/1000 303 2022-05-29 02:50 secure-world-wallet/Dockerfile -rwxrwxr-x 1000/1000 86 2022-05-29 02:50 secure-world-wallet/docker_run.sh # docker内でのqemu起動用 # TrustZoneを使うために,ブートローダが複数存在することがわかる -rwxrwxr-x 1000/1000 23353 2022-05-29 02:50 secure-world-wallet/bl1.bin -rwxrwxr-x 1000/1000 25037 2022-05-29 02:50 secure-world-wallet/bl2.bin -rw-rw-r-- 1000/1000 28 2022-05-29 02:50 secure-world-wallet/bl32.bin -rw-rw-r-- 1000/1000 591880 2022-05-29 02:50 secure-world-wallet/bl32_extra1.bin -rw-rw-r-- 1000/1000 0 2022-05-29 02:50 secure-world-wallet/bl32_extra2.bin -rwxrwxr-x 1000/1000 599300 2022-05-29 02:50 secure-world-wallet/bl33.bin -rwxrwxr-x 1000/1000 5102592 2022-05-29 02:50 secure-world-wallet/zImage -rw-rw-r-- 1000/1000 5853672 2022-05-29 02:50 secure-world-wallet/rootfs.cpio.gz -rwxrwxr-x 1000/1000 103707048 2022-05-29 02:50 secure-world-wallet/qemu-system-arm -rwxrwxr-x 1000/1000 660 2022-05-29 02:54 secure-world-wallet/qemu_run.sh # 今回の攻撃対象であるTrust Appが単体で抜き出しておいてある # 当然rootfs.cpio.gzにも含まれている -rw-rw-r-- 1000/1000 178676 2022-05-29 02:50 secure-world-wallet/7dc089d2-883b-4f7b-8154-ea1db9f1e7c3.ta ``` ## ブートローダの構成 ブートローダの構成は以下の通り. ![](https://i.imgur.com/kv9yKGr.png) 今回のケースではBL31が同梱されていないので,セキュアモニタはない. BL32は同梱されているので,セキュアワールドが動作していることがわかる. BL33では,特にハイパーバイザらしきものはなかった.つまりノーマルワールドは,素直にカーネル+ユーザランドの構成である. ## Trusted Appの切り出し Trusted App(以下TA)である`7dc089d2-883b-4f7b-8154-ea1db9f1e7c3.ta`は,ELFに特殊なヘッダがくっついたものだ.ヘッダの構造は下記の通り(P41). https://ispranproceedings.elpub.ru/jour/article/download/1495/1325 但し頑張って切り出す必要はなく,binwalkで取り出すほうが早い. ```bash= root@Ubuntu2204:~# binwalk --run-as=root --dd=".*" -eM ./7dc089d2-883b-4f7b-8154-ea1db9f1e7c3.ta root@Ubuntu2204:~# file _7dc089d2-883b-4f7b-8154-ea1db9f1e7c3.ta.extracted/* _7dc089d2-883b-4f7b-8154-ea1db9f1e7c3.ta.extracted/148: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), static-pie linked, stripped _7dc089d2-883b-4f7b-8154-ea1db9f1e7c3.ta.extracted/27444: OpenPGP Public Key root@Ubuntu2204:~# ``` 以降は,`_7dc089d2-883b-4f7b-8154-ea1db9f1e7c3.ta.extracted/148`を`extracted.elf`として扱っていく. # 初動調査 `qemu_run.sh`を起動してみる.起動すると一般ユーザ権限でログインでき,`/dev`配下に`tee0`/`teepriv0`というデバイスファイルが存在する.また`tee-supplicant`も動いている. ```bash= ~ $ id uid=1001(test) gid=1003(test) groups=1003(test) ~ $ ls -l /dev total 0 ... crw-rw---- 1 root test 250, 0 Jun 1 01:05 tee0 crw-rw---- 1 root tee 250, 16 Jun 1 01:05 teepriv0 crw-rw---- 1 root root 4, 64 Jun 1 01:05 ttyS0 ~ $ lsmod Module Size Used by Not tainted ~ $ ps -ef PID USER COMMAND 1 root {init} /bin/sh /init ... 54 tee /usr/sbin/tee-supplicant /dev/teepriv0 55 test /bin/sh 56 root [optee_bus_scan] 60 test ps -ef ~ $ ``` `tee-supplicant`はOP-TEEプロジェクトが開発しているTrustZoneとの通信用アプリ.`tee0`や`teepriv0`を使って通信する. 以下に配布ファイルのどれがどんな役割を持つのかまとめてみた. | レイヤ | 概要 | どの配布ファイルか | ソース | |:-:|:-:|:-:|:-:| | Normal-PL0 | TZ通信アプリ | `rootfs.cpio.gz` 内の `tee-supplicant` | [optee_client](https://github.com/OP-TEE/optee_client) | | Normal-PL1 | TZ通信ドライバ | `zImage`内に組み込まれている(関数名:`optee_*`) | [linux-kernel](https://github.com/torvalds/linux/tree/master/drivers/tee) | | Secure-PL0 | Trusted App | `rootfs.cpio.gz` 内の `7dc089d2-883b-4f7b-8154-ea1db9f1e7c3.ta` | (雛形が)[optee_os](https://github.com/OP-TEE/optee_os) | | Secure-PL1 | Trusted OS | `bl32.bin`, `bl32_extra1.bin`, `bl32_extra2.bin` | [optee_os](https://github.com/OP-TEE/optee_os) | # TIPS ## ビルド OPTEEは,ARM32とARM64のTrustZoneを扱うためのフレームワーク.Linux kernelにも4.12からメインラインに取り込まれている. Qemu環境での構築にも対応しており,以下のURLにあるコマンドを叩けば,簡単に環境が構築できる. https://optee.readthedocs.io/en/latest/building/devices/qemu.html ## セキュアワールドの出力 OPTEE-OSを基にビルドしたqemuイメージは,シリアル出力を2つ持つ.1つはノーマルワールドで,もう1つはセキュアワールド用らしい. `qemu_run.sh`の起動コマンドでは2つ目のシリアル出力が`-serial /dev/null`と指定されているが,どこかに情報を吐き出してもらったほうがデバッグしやすくなるのは明白だ. 2つのシリアル出力を両方とも`stdio`に出力することはできないので,`-serial telnet::4444,server,nowait`などに書き換えよう.ついでに`-S -s`も付与しておくと良い. 変更前 ```bash= #!/bin/bash # semihosting needed for bootloaders - bl2.bin, bl32.bin, bl32_extra1.bin, bl33.bin - QEMU is built to disable semihosting after boot ./qemu-system-arm \ -nographic \ -serial chardev:serial0 \ ★1つ目 -serial /dev/null \ ★2つ目 -monitor /dev/null \ -chardev stdio,signal=off,id=serial0 \ ★1つ目の参照先デバイス -smp 2 \ -machine virt,secure=on \ -cpu cortex-a15 \ -semihosting-config enable=on,target=native \ -m 1057 \ -bios bl1.bin \ -object rng-random,filename=/dev/urandom,id=rng0 \ -device virtio-rng-pci,rng=rng0,max-bytes=1024,period=1000 \ -netdev user,id=vmnic -device virtio-net-device,netdev=vmnic \ -no-reboot ``` 変更後 ```bash= #!/bin/bash # semihosting needed for bootloaders - bl2.bin, bl32.bin, bl32_extra1.bin, bl33.bin - QEMU is built to disable semihosting after boot ./qemu-system-arm \ -nographic \ -serial chardev:serial0 \ -serial telnet::4444,server,nowait \ ★2つ目を変更 -monitor /dev/null \ -chardev stdio,signal=off,id=serial0 \ -smp 2 \ -machine virt,secure=on \ -cpu cortex-a15 \ -semihosting-config enable=on,target=native \ -m 1057 \ -bios bl1.bin \ -object rng-random,filename=/dev/urandom,id=rng0 \ -device virtio-rng-pci,rng=rng0,max-bytes=1024,period=1000 \ -netdev user,id=vmnic -device virtio-net-device,netdev=vmnic \ -no-reboot -S -s ★ついでにgdbスタブも付与 ``` 得られる情報 ```bash= root@Ubuntu2204:~# telnet localhost 4444 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. D/TC:0 add_phys_mem:555 ROUNDDOWN(0x09040000, CORE_MMU_PGDIR_SIZE) type IO_SEC 0x09000000 size 0x00100000 D/TC:0 add_phys_mem:555 ROUNDDOWN(0x0e000000, CORE_MMU_PGDIR_SIZE) type IO_SEC 0x0e000000 size 0x00100000 D/TC:0 add_phys_mem:555 ROUNDDOWN((0x08000000 + 0), CORE_MMU_PGDIR_SIZE) type IO_SEC 0x08000000 size 0x00100000 D/TC:0 add_phys_mem:555 ROUNDDOWN((0x08000000 + 0x10000), CORE_MMU_PGDIR_SIZE) type IO_SEC 0x08000000 size 0x00100000 D/TC:0 add_phys_mem:569 Physical mem map overlaps 0x8000000 D/TC:0 add_phys_mem:555 VCORE_UNPG_RX_PA type TEE_RAM_RX 0x0e100000 size 0x0008e000 D/TC:0 add_phys_mem:555 VCORE_UNPG_RW_PA type TEE_RAM_RW 0x0e18e000 size 0x00172000 D/TC:0 add_phys_mem:555 TA_RAM_START type TA_RAM 0x0e300000 size 0x00d00000 D/TC:0 add_phys_mem:555 TEE_SHMEM_START type NSEC_SHM 0x7fe00000 size 0x00200000 D/TC:0 add_va_space:595 type RES_VASPACE size 0x00a00000 D/TC:0 add_va_space:595 type SHM_VASPACE size 0x02000000 D/TC:0 init_mem_map:1155 Mapping core at 0xe3107000 offs 0xd5007000 D/TC:0 dump_mmap_table:718 type IDENTITY_MAP_RX va 0x0e100000..0x0e100fff pa 0x0e100000..0x0e100fff size 0x00001000 (smallpg) D/TC:0 dump_mmap_table:718 type NSEC_SHM va 0xdf200000..0xdf3fffff pa 0x7fe00000..0x7fffffff size 0x00200000 (pgdir) D/TC:0 dump_mmap_table:718 type TA_RAM va 0xdf400000..0xe00fffff pa 0x0e300000..0x0effffff size 0x00d00000 (pgdir) D/TC:0 dump_mmap_table:718 type IO_SEC va 0xe0200000..0xe02fffff pa 0x0e000000..0x0e0fffff size 0x00100000 (pgdir) D/TC:0 dump_mmap_table:718 type IO_SEC va 0xe0300000..0xe03fffff pa 0x09000000..0x090fffff size 0x00100000 (pgdir) D/TC:0 dump_mmap_table:718 type IO_SEC va 0xe0400000..0xe04fffff pa 0x08000000..0x080fffff size 0x00100000 (pgdir) D/TC:0 dump_mmap_table:718 type RES_VASPACE va 0xe0500000..0xe0efffff pa 0x00000000..0x009fffff size 0x00a00000 (pgdir) D/TC:0 dump_mmap_table:718 type SHM_VASPACE va 0xe1000000..0xe2ffffff pa 0x00000000..0x01ffffff size 0x02000000 (pgdir) D/TC:0 dump_mmap_table:718 type TEE_RAM_RX va 0xe3107000..0xe3194fff pa 0x0e100000..0x0e18dfff size 0x0008e000 (smallpg) D/TC:0 dump_mmap_table:718 type TEE_RAM_RW va 0xe3195000..0xe3306fff pa 0x0e18e000..0x0e2fffff size 0x00172000 (smallpg) D/TC:0 core_mmu_alloc_l2:276 L2 table used: 1/6 D/TC:0 core_mmu_alloc_l2:276 L2 table used: 2/6 D/TC:0 core_mmu_alloc_l2:276 L2 table used: 3/6 D/TC:0 core_mmu_alloc_l2:276 L2 table used: 4/6 ... ``` # セキュアワールドのデバッグ ## デバッグの問題点 セキュアワールドのデバッグにおいては,以下3つの問題が存在する. 1. ノーマルワールドをデバッグ中は,セキュアワールドのメモリにアクセスできない 2. ノーマルワールドをデバッグ中は,セキュアワールドのメモリにブレークポイントを仕掛けられない - 正確にはブレークポイント自体はどこにでも仕掛けられるのだが,ASLRのせいで仕掛けるべきアドレスがわからない 3. セキュアワールドのTAは単体でのデバッグが出来ない - Trusted OSごとデバッグする必要がある ## セキュアワールドのASLR セキュアワールドはノーマルワールドとはまた別のASLRを持っている. OPTEE-OSの場合,`CFG_CORE_ASLR`と`CFG_TA_ASLR`というビルドコンフィグが該当する.それぞれTrusted OSのASLRと,TAのASLRである. - `CFG_CORE_ASLR` - Trusted OSが起動する度,そのOSがロードされる仮想アドレスが変わる - kASLRと同じく,リブートするまでアドレスは不変 - `CFG_TA_ASLR` - TAが呼び出されて起動する度,そのTAのコードやデータがロードされる仮想アドレスが変わる - TAは基本的にスタティックリンクなので,正確にはASLR+PIEに近い - スタックの仮想アドレスは固定 - TAをロードする`ldelf`のコードやデータの仮想アドレスは固定 OPTEE-OSを自分でビルドする場合はこれらをオフにすればよいのだが(`make run CFG_CORE_ASLR=n CFG_TA_ASLR=n`),CTFの問題のように環境一式が与えられた場合は,簡単には無効化することができない.何故ならASLRの有効無効はビルド時に完全にコード中に埋め込まれるようで,後から切り替える仕組みが存在しないからだ. 理論的には,ASLRに関するシードを固定値になるよう動的にパッチを当てれば良いが(Write-upの記事では`CFG_CORE_ASLR`つまりOSのASLRのみこの方法で無効化している模様),今回はOSもTAもASLR有効な状態でのデバッグ手法の確立を考えた. ASLRの実現自体はノーマルワールドと同じで,ランダム要素を含むアドレスをTrusted OSが生成し,それを管理するページテーブルを作成してMMUに登録することで実現している.ただしpagewalk用のページテーブル自体もセキュアメモリに配置されている点が異なる. 尚,MMUのpagewalkベースとなるレジスタは,ARMv7の仕様書上はノーマルワールドでもセキュアワールドでも`TTBR0_EL1`や`TTBR1_EL1`である.しかしQemuでは少し実装が異なるようで,ノーマルワールドでは`TTBR0_EL1`や`TTBR1_EL1`を使い,セキュアワールドでは`TTBR0_EL1_S`や`TTBR1_EL1_S`という`_S`サフィックス付きレジスタを使っているように見受けられる. ``` gef> sysreg TTBR ---------------------------------------------------------- System registers ---------------------------------------------------------- HTTBR = 0x0 | TTBR0_EL1_S = 0xe1ac06a | TTBR1_EL1_S = 0xe1ac06a HTTBR_S = 0x0 | TTBR0_S = 0xe1ac06a | TTBR1_S = 0xe1ac06a TTBR0 = 0x411d006a | TTBR1 = 0x4000406a | VTTBR = 0x0 TTBR0_EL1 = 0x411d006a | TTBR1_EL1 = 0x4000406a | VTTBR_S = 0x0 gef> ``` このようなレジスタ分離があるのはARMv7だけで,ARMv8では統一されたらしく`_S`付きレジスタは見当たらない.このあたり全く情報がない上に,Qemuのソースを見てもよくわからないため,識者の解説を求む. ## 公式なデバッグ手法 公式記事では,Trusted OSやTAのデバッグ手法についてはASLRを無効化した上で,シンボルを付与してデバッグするようなことが書いてある. - https://github.com/ForgeRock/optee-build/blob/master/docs/debug.md - https://optee.readthedocs.io/en/latest/building/devices/qemu.html しかし我々はASLRが有効な状態のターゲットを,シンボル無しにデバッグしたいので,このままでは上手くいかず,少し手順を修正する必要がある. 尚,遷移の全体像を最初に知っておくと後の流れがわかりやすい. :::spoiler ノーマルワールドからセキュアワールドへの遷移の参考情報 https://github.com/OP-TEE/optee_os/issues/2168 ![](https://i.imgur.com/myGqbhD.png) ::: ### Trusted OSのデバッグ 今回はあまり使わないが,Trusted OSのデバッグ手法を書いておく. Trusted OSに処理が渡った瞬間から追いかけたいなら,`tee_entry_std`にブレークポイントを仕掛ければ良い. `tee_entry_std`は以下のようなコードである. https://github.com/OP-TEE/optee_os/blob/master/core/tee/entry_std.c ![](https://i.imgur.com/ofmP5Zy.png) 今回のケースでは`tee_entry_std`は`bl32_extra1.bin`にある. (BL32関連のファイルは3つあるが,`bl32.bin`も`bl32_extra2.bin`もファイルサイズは明らかに小さいので,Trusted OSは含まれない.従って`bl32_extra1.bin`だけを見れば良い) IDAで探すには,文字列検索から`tee_entry_std`を探し,その参照を調べれば一発で見つかる.今回の環境ではオフセット`0x1e3d0`だ. ![](https://i.imgur.com/ZtLzuoy.png) ではBL32のベースアドレスはどこだろうか.これは起動時のログと,実際のファイルサイズを突き合わせれば良い. ```bash= root@Ubuntu2204:~# ./qemu_run.sh ... INFO: BL1: RAM 0xe04e000 - 0xe056000 ... INFO: Loading image id=1 at address 0xe01b000 INFO: Image id=1 loaded: 0xe01b000 - 0xe0211cd VERBOSE: BL1: BL2 memory layout address = 0xe001000 ... INFO: Entry point address = 0xe01b000 ... INFO: Loading image id=4 at address 0xe100000 INFO: Image id=4 loaded: 0xe100000 - 0xe10001c INFO: OPTEE ep=0xe100000 INFO: OPTEE header info: INFO: magic=0x4554504f INFO: version=0x2 INFO: arch=0x0 INFO: flags=0x0 INFO: nb_images=0x1 INFO: BL2: Loading image id 21 ... INFO: Loading image id=21 at address 0xe100000 INFO: Image id=21 loaded: 0xe100000 - 0xe190808 ★ bl32_extra1.bin (0x90808バイト) INFO: BL2: Skip loading image id 22 INFO: BL2: Loading image id 5 ... INFO: Loading image id=5 at address 0x60000000 INFO: Image id=5 loaded: 0x60000000 - 0x60092504 NOTICE: BL1: Booting BL32 INFO: Entry point address = 0xe100000 INFO: SPSR = 0x1d3 ... ``` BL32(`bl32_extra1.bin`)は`0xe100000`にロードされているので,`tee_entry_std`は`0xe11e3d0`にあると計算できる.しかしこのアドレスは調べたところ物理アドレス(もしくは仮想アドレス=物理アドレスとなっている起動初期の段階)なので,ここにブレークポイントを仕掛けても,MMUが有効になりASLRが適用されたタイミング以降では停止しない. さてASLRのアドレスをどうやって知るかだが,セキュアワールド向けのシリアル出力を有効にしていると,起動時に幾つかの情報が得られると述べた.ここにはASLR適用後のメモリマップ情報も出力されているので,ここからブレークポイントを仕掛けるべきアドレスが分かる. ```bash= root@Ubuntu2204:~# telnet localhost 4444 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. D/TC:0 add_phys_mem:555 ROUNDDOWN(0x09040000, CORE_MMU_PGDIR_SIZE) type IO_SEC 0x09000000 size 0x00100000 D/TC:0 add_phys_mem:555 ROUNDDOWN(0x0e000000, CORE_MMU_PGDIR_SIZE) type IO_SEC 0x0e000000 size 0x00100000 D/TC:0 add_phys_mem:555 ROUNDDOWN((0x08000000 + 0), CORE_MMU_PGDIR_SIZE) type IO_SEC 0x08000000 size 0x00100000 D/TC:0 add_phys_mem:555 ROUNDDOWN((0x08000000 + 0x10000), CORE_MMU_PGDIR_SIZE) type IO_SEC 0x08000000 size 0x00100000 D/TC:0 add_phys_mem:569 Physical mem map overlaps 0x8000000 D/TC:0 add_phys_mem:555 VCORE_UNPG_RX_PA type TEE_RAM_RX 0x0e100000 size 0x0008e000 D/TC:0 add_phys_mem:555 VCORE_UNPG_RW_PA type TEE_RAM_RW 0x0e18e000 size 0x00172000 D/TC:0 add_phys_mem:555 TA_RAM_START type TA_RAM 0x0e300000 size 0x00d00000 D/TC:0 add_phys_mem:555 TEE_SHMEM_START type NSEC_SHM 0x7fe00000 size 0x00200000 D/TC:0 add_va_space:595 type RES_VASPACE size 0x00a00000 D/TC:0 add_va_space:595 type SHM_VASPACE size 0x02000000 D/TC:0 init_mem_map:1155 Mapping core at 0xa2ee000 offs 0xfc1ee000 D/TC:0 dump_mmap_table:718 type TEE_RAM_RX va 0x0a2ee000..0x0a37bfff pa 0x0e100000..0x0e18dfff size 0x0008e000 (smallpg) ★ D/TC:0 dump_mmap_table:718 type TEE_RAM_RW va 0x0a37c000..0x0a4edfff pa 0x0e18e000..0x0e2fffff size 0x00172000 (smallpg) D/TC:0 dump_mmap_table:718 type SHM_VASPACE va 0x0a500000..0x0c4fffff pa 0x00000000..0x01ffffff size 0x02000000 (pgdir) D/TC:0 dump_mmap_table:718 type RES_VASPACE va 0x0c500000..0x0cefffff pa 0x00000000..0x009fffff size 0x00a00000 (pgdir) D/TC:0 dump_mmap_table:718 type IO_SEC va 0x0cf00000..0x0cffffff pa 0x08000000..0x080fffff size 0x00100000 (pgdir) D/TC:0 dump_mmap_table:718 type IO_SEC va 0x0d000000..0x0d0fffff pa 0x09000000..0x090fffff size 0x00100000 (pgdir) D/TC:0 dump_mmap_table:718 type IO_SEC va 0x0d100000..0x0d1fffff pa 0x0e000000..0x0e0fffff size 0x00100000 (pgdir) D/TC:0 dump_mmap_table:718 type TA_RAM va 0x0d200000..0x0defffff pa 0x0e300000..0x0effffff size 0x00d00000 (pgdir) D/TC:0 dump_mmap_table:718 type NSEC_SHM va 0x0df00000..0x0e0fffff pa 0x7fe00000..0x7fffffff size 0x00200000 (pgdir) D/TC:0 dump_mmap_table:718 type IDENTITY_MAP_RX va 0x0e100000..0x0e100fff pa 0x0e100000..0x0e100fff size 0x00001000 (smallpg) D/TC:0 core_mmu_alloc_l2:276 L2 table used: 1/6 D/TC:0 core_mmu_alloc_l2:276 L2 table used: 2/6 D/TC:0 core_mmu_alloc_l2:276 L2 table used: 3/6 D/TC:0 core_mmu_alloc_l2:276 L2 table used: 4/6 I/TC: ``` 17行目の`smallpg`と書かれているところがそれで,上のケースでは仮想メモリ`0x0a2ee000`が物理メモリ`0x0e100000`に対応していることがわかる(ASLRが有効だと毎回変わる).後はオフセットを計算してからブレークポイントを仕掛ければ良い. ```bash= [term1] ./qemu_run.sh [term2] gdb-multiarch -ex 'target remote localhost:1234' ※tee_entry_std gef> b *(0x0a2ee000 + 0x1e3d0) ``` これでシンボルがなくてもTrusted OSに処理が渡った瞬間に停止できるようになる. 尚,オフセットさえ求められれば`tee_entry_std`以外にブレークポイントを仕掛けることももちろん可能である. ### TAのデバッグ TAをデバッグするには,Trusted OSの`thread_enter_user_mode`にブレークポイントを仕掛け,TAがどこにロードされたかを調べた上で改めてブレークポイントを仕掛け直すことになる. `bl32_extra1.bin`にある関数のうち,文字列の`!have_spinlock()`を参照している点から追跡し,`sub_E137C78`が`thread_enter_user_mode`であると推測した.オフセットは`0x37c78`である. :::spoiler `!have_spinlock`から`thread_enter_user_mode`を求める流れ ```c= int assert_have_no_spinlock() { int result; // r0 result = have_spinlock(); if ( result ) ↓★ここが文字列を参照してる assert("!have_spinlock()", "core/include/kernel/spinlock.h", 0x19, "assert_have_no_spinlock"); return result; } int __fastcall thread_set_exceptions(__int16 a1) { unsigned int v3; // [sp+Ch] [bp+4h] v3 = sub_3788E(); if ( (a1 & 2) == 0 ) assert_have_no_spinlock(); return sub_378AA((a1 << 6) & 0x1C0 | v3 & 0xFFFFFE3F); } unsigned int __fastcall thread_mask_exceptions(char a1) { unsigned int v3; // [sp+Ch] [bp+4h] v3 = thread_get_exceptions(); thread_set_exceptions(v3 | a1 & 7); return v3; } int __fastcall thread_enter_user_mode(int a1, int a2, int a3, int a4, int a5, int a6, unsigned __int8 a7, _DWORD *a8, _DWORD *a9) { int v9; // lr int v10; // r0 int v11; // r3 int v17; // [sp+2Ch] [bp+Ch] BYREF int v18; // [sp+30h] [bp+10h] int v19; // [sp+34h] [bp+14h] int v20; // [sp+38h] [bp+18h] unsigned int v21; // [sp+3Ch] [bp+1Ch] int v22; // [sp+44h] [bp+24h] v22 = v9; v17 = 0; v21 = 0; v20 = 0; v19 = 0; v10 = tee_ta_update_session_utime_resume(); v18 = thread_get_pauth_keys(v10); if ( (unsigned __int8)get_spsr(a7, a6, &v17) != 1 ) { *a8 = 1; *a9 = 0xBADBADBA; v11 = 0; } else { v21 = thread_mask_exceptions(7); v19 = thread_get_ctx_regs(); set_ctx_regs(v19, a1, a2, a3, a4, a5, a6, v17, v18); v20 = _thread_enter_user_mode(v19, a8, a9); thread_unmask_exceptions(v21); v11 = v20; } return v11; } ``` ::: ![](https://i.imgur.com/7UoB3XP.png) つまり,まず`thread_enter_user_mode`にブレークポイントを仕掛けるには以下のようにすれば良い. ```bash= [term1] ./qemu_run.sh [term2] gdb-multiarch -ex 'target remote localhost:1234' ※thread_enter_user_mode gef> b *(0x0a2ee000 + 0x37C78) gef> c gef> c # 一度停止するが,もう一度continueしておく ``` このときTrusted OS/Appのシリアル出力の結果は以下の通りである. ```bash= # 1度目の停止 D/TC:? 0 ldelf_load_ldelf:95 ldelf load address 0x104000 # 2度目の停止 D/LD: ldelf:134 Loading TS 7dc089d2-883b-4f7b-8154-ea1db9f1e7c3 D/TC:? 0 ldelf_syscall_open_bin:142 Lookup user TA ELF 7dc089d2-883b-4f7b-8154-ea1db9f1e7c3 (early TA) D/TC:? 0 ldelf_syscall_open_bin:146 res=0xffff0008 D/TC:? 0 ldelf_syscall_open_bin:142 Lookup user TA ELF 7dc089d2-883b-4f7b-8154-ea1db9f1e7c3 (Secure Storage TA) D/TC:? 0 ldelf_syscall_open_bin:146 res=0xffff0008 D/TC:? 0 ldelf_syscall_open_bin:142 Lookup user TA ELF 7dc089d2-883b-4f7b-8154-ea1db9f1e7c3 (REE) D/TC:? 0 ree_fs_ta_open:146 Opening REE TA from disk... D/TC:? 0 ldelf_syscall_open_bin:146 res=0 D/LD: ldelf:168 ELF (7dc089d2-883b-4f7b-8154-ea1db9f1e7c3) at 0x173000 ★ ``` TAがロードされたアドレスは`0x173000`であると表示されているので,あとはこれをベースとして計算すれば良い.例えばTAの`TA_InvokeCommandEntryPoint`にブレークポイントを仕掛けるのなら,`extracted.elf`を解析すればオフセット`0x2784`にあることが分かるので,以下のようにすれば良い. ```bash= gef> b *(0x173000 + 0x2784) gef> c ``` なお,厳密にはTAのエントリポイントは`TA_InvokeCommandEntryPoint`ではないが,ノーマルワールドからの呼び出しがコマンドによって分岐する箇所なので,実質的に`TA_InvokeCommandEntryPoint`に仕掛けるのが良いと思われる. :::spoiler TAのエントリポイント付近の呼び出しの流れ ```c= // attributes: thunk __int64 __fastcall _ta_entry(unsigned int a1, int a2, int a3, int a4) { __int64 v4; // r0 __int64 v5; // r2 LODWORD(v4) = _ta_entry_c(a1, a2, a3, a4); return v4 / v5; } int __fastcall _ta_entry_c(unsigned int a1, int a2, int a3, int a4) { int v4; // lr int v6[5]; // [sp+8h] [bp+0h] BYREF int v7; // [sp+1Ch] [bp+14h] v6[4] = (int)v6; v7 = v4; v6[3] = a1; v6[2] = a2; v6[1] = a3; v6[0] = a4; v7 = _utee_entry(a1, a2, (struct #111 *)a3, a4); utee_return(v7); return tahead_get_trace_level(); } TEE_Result __cdecl _utee_entry(unsigned int func, unsigned int session_id, struct utee_params *up, unsigned int cmd_id) { int res; // [sp+14h] [bp+Ch] if ( func == 2 ) { res = entry_invoke_command(session_id, up, cmd_id); } else if ( func > 2 ) { res = 0xFFFFFFFF; TEE_Panic(0); } else if ( func ) { res = entry_close_session(session_id); } else { res = entry_open_session(session_id, (struct #111 *)up); } ta_header_save_params(0, 0); return res; } TEE_Result __cdecl entry_invoke_command(unsigned int session_id, struct utee_params *up, unsigned int cmd_id) { int v3; // lr TEE_Param params[4]; // [sp+14h] [bp+Ch] BYREF uint32_t param_type; // [sp+34h] [bp+2Ch] BYREF int ret; // [sp+38h] [bp+30h] struct #114 *session; // [sp+3Ch] [bp+34h] int v12; // [sp+44h] [bp+3Ch] v12 = v3; session = ta_header_get_session(session_id); if ( !session ) return 0xFFFF0007; from_utee_params(params, &param_type, up); ta_header_save_params(param_type, params); ret = TA_InvokeCommandEntryPoint(*((void **)session + 1), cmd_id, param_type, params); to_utee_params(up, param_type, params); return ret; } TEE_Result __cdecl TA_InvokeCommandEntryPoint(void *sess_ctx, uint32_t cmd_id, uint32_t param_types, TEE_Param params[4]) { int ret; // r3 switch ( cmd_id ) { case 0u: ret = create_wallet(param_types, params); break; case 1u: ret = get_address_for_wallet(param_types, params); break; case 2u: ret = import_key(param_types, params); break; case 3u: ret = sign_transaction(param_types, params); break; default: ret = TEE_ERROR_BAD_PARAMETERS; break; } return ret; } ``` ::: ### 1つ目のブレークポイントの手間を減らす方法 シリアル出力の結果を都度参照するのは,毎回つなぎ直す必要も有り,また手計算も発生するので若干手間である.gdb内部からなんとかしてTrusted OSのマップされるASLRを特定することができれば,ブレークポイントを仕掛ける手間を減らせるはずである.これを達成できたのでメモしておく. まずqemu-systemが保持する仮想メモリ中のどこかにはセキュアワールド用のメモリが存在するはずである.直接そちらを読み書きすることで,ノーマルワールドにいながらセキュアワールドのメモリの読み書きを擬似的に達成することができる.アドレスの特定には`monitor info mtree`コマンドと`monitor gpa2hva`コマンドを使えば良い. ```bash= gef> monitor info mtree -f ... 000000000a003e00-000000000a003fff (prio 0, i/o): virtio-mmio 000000000e000000-000000000effffff (prio 0, ram): virt.secure-ram ★名称は固定っぽい 0000000010000000-0000000010000fff (prio 0, i/o): virtio-pci-common-virtio-rng ... gef> monitor gpa2hva 0xe000000 Host virtual address for 0xe000000 (virt.secure-ram) is 0x7fc886200000 ★ホストの仮想アドレス gef> ``` あとはホスト(qemu-system)の仮想メモリを`/proc/<PID>/mem`経由で読み取れば良い.この手法は自動化できるので,`xsm`/`wsm`コマンドとして,https://github.com/bata24/gef に追加しておいた. `xsm`によって,ノーマルワールドにいながらセキュアワールドのメモリが読めるようになったことから,セキュアメモリ向けの`pagewalk`も実現できるようになり,この結果ノーマルワールドにいてもセキュアワールドのメモリマップが特定できるようになる.つまりシリアル出力の結果を利用しなくても,メモリマップがわかるようになったということだ. さらにこれによって仮想アドレス/物理アドレスの相互変換(`v2p`/`p2v`)も実現することができるため,物理アドレスさえ知っていれば,ブレークポイントを仕掛けるべき仮想アドレスが分かるようになる. ここまでの一連の流れを使って,物理アドレスを使って仮想アドレスにブレークポイントを仕掛ける機能を,`bsm`コマンドとして,https://github.com/bata24/gef に追加しておいた. ```bash= [term1] ./qemu_run.sh [term2] gdb-multiarch -ex 'target remote localhost:1234' ※tee_entry_std gef> bsm 0xe11e3d0 ※thread_enter_user_mode gef> bsm 0xe137c78 ``` 尚,内部的に`p2v`で仮想アドレスを求めてブレークポイントを仕掛けるこの手法は,ARMv7(ARM32)でしか使えないことに注意する. これが実現できているのは,ARMv7だとqemuが`TTBR0_EL1_S` / `TTBR1_EL1_S`といったセキュアワールド用のレジスタをノーマルワールドでも見れるように提供してくれるからである.しかしARMv8(AArch64)ではそれらが提供されないため,ノーマルワールドにいながらセキュアワールドの`pagewalk`を行うことができない.よって`p2v`も動かないからである. →少し実行に時間がかかるものの,ARMv8にも対応した.qemu-systemのセキュアワールド向けメモリを走査することで,ARMv8でもpagewalk結果を擬似的に得ることができたためである. ### TAへのブレークポイントの手間を減らす方法 ノーマルワールドにいるとき,セキュアワールド向けの`pagewalk`をしてもTrusted OS用のメモリマップしか表示されない. ```bash= gef> vmm -S -q [+] Redirect to pagewalk ----------------------------------------------------------------------------- $TTBR0_EL1_S ----------------------------------------------------------------------------- [*] No virtual mappings found ----------------------------------------------------------------------------- $TTBR1_EL1_S ----------------------------------------------------------------------------- ------------------------------------------------------------------------------ Memory map ------------------------------------------------------------------------------ Virtual address start-end Physical address start-end Total size Page size Count Flags 000000000e100000-000000000e101000 000000000e100000-000000000e101000 0x1000 0x1000 1 [PL0/--- PL1/R-X] 000000007cd00000-000000007cf00000 000000007fe00000-0000000080000000 0x200000 0x100000 2 [PL0/--- PL1/RW- NS] 000000007cf00000-000000007dc00000 000000000e300000-000000000f000000 0xd00000 0x100000 13 [PL0/--- PL1/RW-] 000000007dd00000-000000007de00000 000000000e000000-000000000e100000 0x100000 0x100000 1 [PL0/--- PL1/RW-] 000000007de00000-000000007df00000 0000000009000000-0000000009100000 0x100000 0x100000 1 [PL0/--- PL1/RW-] 000000007df00000-000000007e000000 0000000008000000-0000000008100000 0x100000 0x100000 1 [PL0/--- PL1/RW-] 0000000080cc2000-0000000080d50000 000000000e100000-000000000e18e000 0x8e000 0x1000 142 [PL0/--- PL1/R-X] 0000000080d50000-0000000080ec2000 000000000e18e000-000000000e300000 0x172000 0x1000 370 [PL0/--- PL1/RW-] gef> ``` しかし`thread_enter_user_mode`で停止したときに`pagewalk`を行うとTA用のメモリも見える.`R-X`のついている領域も見えるが,最初に停止したタイミングではTAをロードするための`ldelf`が実行されているだけであり,まだTAはロードされていない. 尚`ldelf`はTrusted OSが提供する機能で,コード自体はTrusted OS(`bl32_extra1.bin`)に埋め込まれている. ```bash= gef> vmm -S -q [+] Redirect to pagewalk ----------------------------------------------------------------------------- $TTBR0_EL1_S ----------------------------------------------------------------------------- ------------------------------------------------------------------------------ Memory map ------------------------------------------------------------------------------ Virtual address start-end Physical address start-end Total size Page size Count Flags 0000000000100000-0000000000101000 000000000e100000-000000000e101000 0x1000 0x1000 1 [PL0/--- PL1/R-X] ★PL1からPL0へ移行するときに,参照先TTBRを切り替えた後PL1権限で実行するコードなので,PL0からはアクセスできない 0000000000102000-0000000000104000 000000000e300000-000000000e302000 0x2000 0x1000 2 [PL0/RW- PL1/RW-] 0000000000104000-000000000010f000 000000000e302000-000000000e30d000 0xb000 0x1000 11 [PL0/R-X PL1/R-X] ★ldelfのコード 000000000010f000-0000000000110000 000000000e30d000-000000000e30e000 0x1000 0x1000 1 [PL0/RW- PL1/RW-] ★ldelfのデータ ----------------------------------------------------------------------------- $TTBR1_EL1_S ----------------------------------------------------------------------------- ------------------------------------------------------------------------------ Memory map ------------------------------------------------------------------------------ Virtual address start-end Physical address start-end Total size Page size Count Flags 000000000e100000-000000000e101000 000000000e100000-000000000e101000 0x1000 0x1000 1 [PL0/--- PL1/R-X] 000000007cd00000-000000007cf00000 000000007fe00000-0000000080000000 0x200000 0x100000 2 [PL0/--- PL1/RW- NS] 000000007cf00000-000000007dc00000 000000000e300000-000000000f000000 0xd00000 0x100000 13 [PL0/--- PL1/RW-] 000000007dd00000-000000007de00000 000000000e000000-000000000e100000 0x100000 0x100000 1 [PL0/--- PL1/RW-] 000000007de00000-000000007df00000 0000000009000000-0000000009100000 0x100000 0x100000 1 [PL0/--- PL1/RW-] 000000007df00000-000000007e000000 0000000008000000-0000000008100000 0x100000 0x100000 1 [PL0/--- PL1/RW-] 000000007eb00000-000000007eb01000 0000000041a77000-0000000041a78000 0x1000 0x1000 1 [PL0/--- PL1/RW- NS] 0000000080cc2000-0000000080d50000 000000000e100000-000000000e18e000 0x8e000 0x1000 142 [PL0/--- PL1/R-X] 0000000080d50000-0000000080ec2000 000000000e18e000-000000000e300000 0x172000 0x1000 370 [PL0/--- PL1/RW-] gef> ``` `thread_enter_user_mode`は何度もヒットするのだが,2回目に停止したときにTAがロードされていた.以下の例ではTAのロードされたアドレスが`0x178000`であるが,ASLRが有効だとこのアドレスは毎回変わる. ```bash= gef> c Continuing. Thread 1 hit Breakpoint 1, 0x80cf9c78 in ?? () gef> vmm -S -q [+] Redirect to pagewalk ----------------------------------------------------------------------------- $TTBR0_EL1_S ----------------------------------------------------------------------------- ------------------------------------------------------------------------------ Memory map ------------------------------------------------------------------------------ Virtual address start-end Physical address start-end Total size Page size Count Flags 0000000000100000-0000000000101000 000000000e100000-000000000e101000 0x1000 0x1000 1 [PL0/--- PL1/R-X] ★PL1からPL0へ移行するときに,参照先TTBRを切り替えた後PL1権限で実行するコードなので,PL0からはアクセスできない 0000000000102000-0000000000104000 000000000e300000-000000000e302000 0x2000 0x1000 2 [PL0/RW- PL1/RW-] 0000000000104000-000000000010f000 000000000e302000-000000000e30d000 0xb000 0x1000 11 [PL0/R-X PL1/R-X] ★ldelfのコード 000000000010f000-0000000000114000 000000000e30d000-000000000e312000 0x5000 0x1000 5 [PL0/RW- PL1/RW-] ★ldelfのデータ 0000000000114000-0000000000115000 000000000e312000-000000000e313000 0x1000 0x1000 1 [PL0/R-- PL1/R--] 0000000000115000-0000000000116000 000000000e34b000-000000000e34c000 0x1000 0x1000 1 [PL0/RW- PL1/RW-] ★スタック 0000000000178000-00000000001a2000 000000000e313000-000000000e33d000 0x2a000 0x1000 42 [PL0/R-X PL1/R-X] ★TAのコード 00000000001a2000-00000000001b0000 000000000e33d000-000000000e34b000 0xe000 0x1000 14 [PL0/RW- PL1/RW-] ★TAのデータ ----------------------------------------------------------------------------- $TTBR1_EL1_S ----------------------------------------------------------------------------- ------------------------------------------------------------------------------ Memory map ------------------------------------------------------------------------------ Virtual address start-end Physical address start-end Total size Page size Count Flags 000000000e100000-000000000e101000 000000000e100000-000000000e101000 0x1000 0x1000 1 [PL0/--- PL1/R-X] 000000007cd00000-000000007cf00000 000000007fe00000-0000000080000000 0x200000 0x100000 2 [PL0/--- PL1/RW- NS] 000000007cf00000-000000007dc00000 000000000e300000-000000000f000000 0xd00000 0x100000 13 [PL0/--- PL1/RW-] 000000007dd00000-000000007de00000 000000000e000000-000000000e100000 0x100000 0x100000 1 [PL0/--- PL1/RW-] 000000007de00000-000000007df00000 0000000009000000-0000000009100000 0x100000 0x100000 1 [PL0/--- PL1/RW-] 000000007df00000-000000007e000000 0000000008000000-0000000008100000 0x100000 0x100000 1 [PL0/--- PL1/RW-] 000000007eb00000-000000007eb01000 0000000041a77000-0000000041a78000 0x1000 0x1000 1 [PL0/--- PL1/RW- NS] 000000007eb01000-000000007eb02000 000000004184f000-0000000041850000 0x1000 0x1000 1 [PL0/--- PL1/RW- NS] 000000007eb02000-000000007eb03000 0000000040b6e000-0000000040b6f000 0x1000 0x1000 1 [PL0/--- PL1/RW- NS] 0000000080cc2000-0000000080d50000 000000000e100000-000000000e18e000 0x8e000 0x1000 142 [PL0/--- PL1/R-X] 0000000080d50000-0000000080ec2000 000000000e18e000-000000000e300000 0x172000 0x1000 370 [PL0/--- PL1/RW-] gef> ``` ということは,`thread_enter_user_mode`にブレークポイントを仕掛けて2度目のヒット時には`pagewalk`でTAのアドレスが分かることになる.`thread_enter_user_mode`のアドレスさえわかればこれは自動化できるので,`optee-break-ta`コマンドとして https://github.com/bata24/gef に追加しておいた. ```bash= gef> optee-break-ta -v 0xe137c78 0x2784 [+] thread_enter_user_mode @ OPTEE-OS: 0xe137c78 [+] breakpoint target offset of TA: 0x2784 [+] phys2virt: 0xe137c78 -> 0x80cf9c78 [+] Temporarily breakpoint at 0x80cf9c78 gef> c Continuing. [+] TA address: 0x163000 Breakpoint 1 at 0x165784 Thread 1 hit Breakpoint 1, 0x00165784 in ?? () ``` # 実行テスト ## テストコマンド作成 以下がテスト用プログラムの雛形,ビルド手順である. ::: spoiler 雛形 {%gist bata24/2419cc74ac53a7f0f75cc4de4f198e40 %} ::: ::: spoiler ビルドコマンド ```bash= apt install -y arm-linux-gnueabihf-gcc git clone https://github.com/OP-TEE/optee_client.git /tmp/optee_client cd /tmp/optee_client make arm-linux-gnueabihf-gcc exploit.c -o exploit -I/tmp -L/tmp/optee_client/out/libteec -lteec -lpthread -static ``` ::: ## テスト環境作成 その1 `rootfs.cpio.gz`を一時的に解凍し,exploitを含めた新しい`rootfs_new.cpio.gz`を作る方法. 生成された`rootfs_new.cpio.gz`を`qemu_run.sh`と同じディレクトリに`rootfs.cpio.gz`という名前で配置し直して起動すれば良い. {%gist bata24/517d786941851d83cb25ff5658a6d281 %} 私の場合は,qemuの起動に必要なファイル一式があるディレクトリに`rev`ディレクトリを作成し,その中に`rootfs.cpio.gz`のコピーと`build.sh`, `exploit.c`を配置した上で,以下のコマンドを実行することで実現している. ```bash= (cd rev && ./build.sh && mv rootfs_new.cpio.gz ../rootfs.cpio.gz) && ./qemu_run.sh ``` ## テスト環境作成 その2 今回の環境では,ゲストに`virtio-9p-device`が存在するため,ホストのディレクトリをマウントすることもできる. まず最初に`rootfs.cpio.gz`内の`/init`にパッチを当てて,rootでログインするようにする. 以下のコマンドで`rootfs_new.cpio.gz`が生成されるので,それを使うように差し替えておく. {%gist bata24/f846a19169b9fc50d6a5197b000f3cc0 %} `qemu_run.sh`の起動コマンドに以下を追加する(パスは適宜変更する). 尚,VMwareホストのディレクトリをマウントしたディレクトリは,この方法では共有できない.あくまでQemuホストのネイティブディレクトリを指定すること. ```bash= -fsdev local,security_model=none,id=fsdev0,path=/host/dir \ -device virtio-9p-device,fsdev=fsdev0,mount_tag=hostshare ``` ゲストが起動したら,root権限があるので以下を実行すれば良い. ```bash= mount hostshare -t 9p /mnt/host ``` # その他参考になる情報 TEE ClientのAPI仕様が書かれたドキュメントもあるので,目を通しておくと良い. https://globalplatform.org/wp-content/uploads/2010/07/TEE_Client_API_Specification-V1.0.pdf # 次へ - Part2/3 (解析) - https://hackmd.io/@bata24/ry1_YS-c5