Upgrade to Pro — share decks privately, control downloads, hide ads and more …

NDIAS CTF 問題解説会資料 Root of (Dis)trust

Avatar for smz smz
July 03, 2026
53

NDIAS CTF 問題解説会資料 Root of (Dis)trust

NDIAS CTF 問題解説会資料
- cartagaitai勉強会#5 (2026/07)
Root of (Dis)trust

Avatar for smz

smz

July 03, 2026

Transcript

  1. Root of (Dis)trustの問題概要 • 2ステージ構成 • Stage1(Normal World でシェル奪取) •

    Stage2(Secure World で OEM ルート鍵奪取) • 舞台は Arm TrustZone / OP-TEE • 配布物に Docker + QEMU のローカル環境が同梱 • Normal World シリアル:nc localhost 54320(Stage1/2 の操作) • Secure World コンソール:nc localhost 54321(OP-TEE デバッグログ) • フラグはダミー、本物のフラグはリモートサーバーのみ • Stage2で使用するTA のソース(ecu_keystore_ta.c)も配布
  2. Root of (Dis)trust ー Stage 1 • 問題文 次世代 ECU

    のデジタルキープロビジョニングシステムが手に入りました。 シリアルコンソール上で認証デーモンが動作しています。 入力処理に何か問題がありそうです。 接続: nc <host> 13337
  3. 初動調査 • 問題サーバーに接続してみると認証トークン入力を求めるプログラムが動作している === TEE Security Gateway v1.2 === [DEBUG]

    libc puts @ 0xffff8256ebe0 Authentication token: • 配布物のrootfs.cpio.ubootを展開して探索すると、 /home/user/ecu_auth_daemonが見つかる cpio-root/home/user$ ls ecu_auth_daemon flag1 tz_gate ※tz_gateは後述。 RELRO STACK CANARY NX PIE RPATH Partial RELRO No canary found NX enabled No PIE No RPATH • バイナリの脆弱性緩和機能は、スタックカナリア無し、NX有効
  4. バイナリの脆弱性緩和機構 機構 説明 今回の設定 NX データ領域(スタックなど)を実行不可にして、そこに書き込ん だコードが動作しないようにする仕組み 有効 Stack Canary

    リターンアドレスの手前に番兵値(カナリア)を置き、関数終了 時に壊れていたら異常終了させる。バッファオーバーフロー (BOF)による戻り先の書き換えを検知する仕組み 無し ASLR ライブラリなどのロードアドレスを実行のたびにランダム化し、 攻撃者が飛び先の番地を決め打ちできないようにする。 OS/実行時側の機構 (有効)※ • ※今回は、問題サーバーに接続すると表示されるlibcのアドレスが毎回異なることから ASLR有効であると判断できる • 逆に、この表示される puts アドレスから libc ベースを逆算すれば、ASLR を実質的に無 効化してガジェットのアドレスを確定できる
  5. ecu_auth_daemonの脆弱性 void authenticate(void) { undefined1 auStack_40 [64]; read(0,auStack_40,0x100); return; }

    • 64バイトのバッファに256バイト(0x100)readする単純なバッファオーバーフロー • 差の192バイトが溢れ、スタック上に保存されたレジスタ値を上書きできる(スタックカ ナリア無し)
  6. Return-oriented Programming • NXがあるので、スタック上にシェルコードを置いても実行できない • そこで既に実行可能な形で存在するretで終わる既存コード(ガジェット)を使って目的 の処理を組み立てる • ret:スタックから次の番地を取り出して飛ぶ命令 ※簡略化した説明、正確には後述

    ガジェット1のアドレス 命令A; ret 命令B; ret 命令C; ret ガジェット2のアドレス ガジェット3のアドレス スタック ガジェット (既存コードの断片、末尾ret) BOF等で書き換える 関数の戻りアドレス スタックから次のアドレスを取り出して飛ぶ スタックから次のアドレスを取り出して飛ぶ 制御が飛ぶ 制御が飛ぶ 制御が飛ぶ 高位アドレス↓
  7. AArch64のROP事情 レジスタ 説明 x0~x7 関数の引数はここで渡す。特にx0=第1引数。 x29 (FP) フレームポインタ。スタックフレームの基準。x30とペアで退避・復元される(stp/ldp x29,x30)。 x30

    (LR) リンクレジスタ。 関数の戻り先が入る特別なレジスタでretはここへ飛ぶ。(x86-64では 戻り先はスタックに積まれる) x19〜x28 関数呼び出しをまたいでも値が保たれる、不揮発レジスタ(callee-saved) • AArch64では、ret命令はスタックを見ずx30に飛ぶ • そのため、ROPを実現するにはスタックからx30を補充する命令(ldp)が必要 • 通常、関数エピローグはldp x29,x30,...; retとなるので、ガジェットには困らない • 不揮発レジスタは、呼び出される側が保存・復元の責任を持つため、スタックから値を 補充するガジェットを見つけやすい 前提:AArch64の主なレジスタ
  8. libcからガジェット探索 • ゴールはsystem(“/bin/sh”) を呼ぶこと。AArch64 では第1引数はx0なので、 x0に/bin/shを入れてsystemを呼ぶ • x0 は揮発レジスタで、スタックから直接復元するガジェットが少ないため、x19/x20 (不揮発レジスタ)を中継に使うのがポイント

    • ①GADGET_LOAD:ldp x19,x20,[sp,#16]; ldp x29,x30,[sp],#32; ret • スタックから x19(=/bin/sh), x20(=system) を不揮発レジスタにロード、x30 を補 充して次へ • ②GADGET_CALL:mov x0,x19; blr x20 • x0=x19(/bin/sh) にして x20(system) を呼ぶ
  9. 実際のペイロード構成 • ※これは一例。ガジェットの選び方やオフセットは他にもあり得る 'A'*64 buf+0 x29 junk buf+64 GADGET_LOAD (mainのsaved

    x30) x29 junk GADGET_CALL /bin/sh アドレス system() アドレス buf+72 buf+80 buf+88 buf+96 buf+104 ROP開始地点
  10. シェル取得 [*] system : 0xffffa8cca620 [*] /bin/sh : 0xffffa8dcf2c0 [*]

    ROP chain triggered ... [+] Stage 1: Shell acquired! [*] Switching to interactive mode / $ $ whoami whoami user / $ $ cat /home/user/flag1 cat /home/user/flag1 FLAG{r0p_ch41n_unl0ck3d_th3_g4t3}
  11. Root of (Dis)trust ー Stage 2 • 問題文 侵入に成功しました。しかし OEM

    ルート鍵(この ECU が生成するすべてのアンロックトークン に署名するマスターシークレット)は TEE の外に出ることはありません。ベンダーは「絶対に触 れない」と主張しています。Normal World から ARM TrustZone Secure World へ攻撃を 展開し、TEE セキュアストレージから鍵を抜き出してください。 ヒント: エクスプロイトはシリアル接続の TTY 越しにバイナリデータを送信します。rawバイト列 を送る前にターミナルの設定が必要になる場合があります。
  12. Arm TrustZone入門 • Normal World / Secure Worldを実現するハードウェア機構 • CPUの「NSビット」でどちらの状態にあるかを示す

    • メモリや周辺デバイスへのアクセスがNSビットで制御されるため、Normal Worldか らSecure Worldの資源にアクセスできない • Normal WorldとSecure Worldをつなぐ方法はSMC命令 • EL3のセキュアモニタが取り次ぐ • World(NS) と 権限(EL) は直交する別の概念であることに注意 • Secure=高権限ではない。同じEL1でもNormal側(Linux)とSecure側(OP-TEE)が 存在する
  13. Normal WorldからSecure Worldへ • Stage1で得たのはNormal Worldのシェル • Stage2のフラグ(OEMルート鍵)はSecure World側のTrusted Application

    (TA)が握る • OP-TEEはSecure Worldで動くTEE OS。TAはこの上で動く • Client Application(CA)を起動し、SMC命令経由でTAを呼ぶのがSecure Worldで動くTAへの唯一の窓口 SMC Linux OP-TEE tz_gate ecu_keystore_ta Secure World ecu_auth_daemon Trusted Application (TA) Client Application (CA) Normal World ※EL3(セキュアモニタ)を省略した概念図です OEMルート鍵
  14. tz_gateでTAと対話してみる • Stage1のシェルからtz_gateを起動すると、以下のメニューが出る === Automotive Digital Key Provisioning System ===

    1. Get Debug Info 2. Init Key Slot 3. Delete Key Slot 4. Import Certificate 5. Generate Unlock Token 0. Exit • Normal World から Secure World を操作できる唯一の窓口 • 各メニューを選ぶと、対応するTAコマンドが SMC 経由で呼ばれる • ⇒TAのソースコードは配布されているので、対応する処理を読んでみる
  15. tz_gateメニューとTAコマンドの対応 メニュー TAコマンド(関数) 額面の動作 1. Get Debug Info cmd_get_debug_info デバッグ情報を返す

    2. Init Key Slot cmd_init_key_slot 鍵スロットを確保(malloc) 3. Delete Key Slot cmd_delete_key_slot 鍵スロットを解放(free) 4. Import Certificate cmd_import_certificate 証明書を書き込む(memmove) 5. Generate Unlock Token cmd_generate_unlock_token トークンを生成
  16. TAの脆弱性① OEM鍵ダンプ関数の存在 /* [DEBUG ONLY] Dump OEM Root Key. *

    TODO: Remove this before the final vehicle release!!! * Edit: Actually, it's not connected to the TA dispatcher, * so it's uncallable from the Normal World anyway. Safe to ignore. :) */ static TEE_Result debug_dump_oem_root_key(void *challenge, uint32_t len) { TEE_ObjectHandle object = TEE_HANDLE_NULL; TEE_ObjectInfo object_info; TEE_Result res; uint32_t read_bytes = 0; IMSG("[CRITICAL WARNING] Executing Factory Key Dump API!"); (略) • 呼べたら「勝ち」な関数は存在するが、どこからも呼ばれず、tz_gateからも呼び出せな い
  17. TAの脆弱性② ヒープBOF static TEE_Result cmd_import_certificate(uint32_t param_types, TEE_Param params[4]) { uint32_t

    exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_VALUE_INPUT, TEE_PARAM_TYPE_MEMREF_INPUT, TEE_PARAM_TYPE_NONE, TEE_PARAM_TYPE_NONE); if (param_types != exp_param_types) return TEE_ERROR_BAD_PARAMETERS; uint32_t slot_id = params[0].value.a; uint32_t cert_size = params[0].value.b; if (slot_id >= MAX_KEY_SLOTS || key_cert_slots[slot_id] == NULL) return TEE_ERROR_BAD_PARAMETERS; TEE_MemMove(key_cert_slots[slot_id], params[1].memref.buffer, cert_size); return TEE_SUCCESS; } • cmd_import_certificate関数で、ユーザーが指定する書き込みサイズを検証せずに key_cert_slots (ヒープ領域に確保)にTEE_MemMove
  18. 呼べないダンプ関数をどう呼ぶか • tz_gateの「5. Generate Unlock Token」メニューで呼び出されるTAの関数は、 ポインタ経由で署名関数を呼ぶ • active_signer[0]->generate_signature の関数ポインタ呼び出しを乗っ取れば、

    dump を呼べるはず struct TokenSigner { uint32_t crypto_suite_id; TEE_Result (*generate_signature)(void *challenge, uint32_t len); }; static TEE_Result cmd_generate_unlock_token(uint32_t param_types, TEE_Param params[4]) { (略) if (active_signer[0] && active_signer[0]->generate_signature) { return active_signer[0]->generate_signature(params[0].memref.buffer, params[0].memref.size); } return TEE_ERROR_ACCESS_DENIED; }
  19. free時の隣接結合とunlink • free済みチャンク B の直前(メモリ上)に、使用中チャンク X があるとする • Xを free

    すると、連続する空き領域(Xと B)を結合して大きな空きにチャンクにする ため、B を一旦リストから外す(=unlink) • Xを free したのに、直下の B が unlink される点が重要 チャンクA チャンクB チャンクC A.flink B.blink B.flink C.blink チャンクX 使用中 ①空きチャンク(B)に隣接する チャンク(X)をfreeする ②大きな空きチャンクとして結 合するため、一旦Bをunlink ③XとBを一つの空きチャンク に統合
  20. 古典的なUnlink Attackの原理 • blinkを書きたいアドレス(周辺)に、flinkを書きたい値に設定したチャンクB’を偽造 できたとする(手段はBOF、UAF、double-free問わない) • B’に隣接する使用中チャンクXをfreeすると、B’がunlinkされ任意アドレスへの書き 込みが実現する 書きたいアドレス (周辺)

    チャンクB’ 書きたい値 B’.blink B’.flink Xをfreeして、B’がunlink 書きたいアドレス (周辺) 書きたい値 チャンクB’ B’.blink(=書きたいアドレス周辺)->flinkをB’.flink(=書きたい値)に書き換え チャンクX 使用中
  21. 古典的手法がOP-TEE(BGET)では通用 • glibcは、古典的unlinkをsafe-unlinking検証を導入することで対策済み • 一方、OP-TEEのTAヒープアロケータはBGET(1972年設計) • BGETにも検証(assert)はあるが、リリースビルドでは消える • glibcでは通用しない手法が、BGETを使用するOP-TEE TAでは今も通用する

    /* The buffer is free. Remove it from the free list and add its size to that of our buffer. */ assert(BH((char *) bn + bn->bh.bsize)->prevfree == bn->bh.bsize); assert(bn->ql.blink->ql.flink == bn); assert(bn->ql.flink->ql.blink == bn); bn->ql.blink->ql.flink = bn->ql.flink; bn->ql.flink->ql.blink = bn->ql.blink; bget.cの繋ぎ替え箇所。assertはあるがリリースビルドで消える
  22. 攻撃フローの全体像 Step 内容 tz_gateメニュー 一言メモ 1 隣接する2チャンクを確保 2 (Init Key

    Slot) slot 1(free する)と slot 0(偽装対象の隣 接)を並べる 2 アドレスをリーク 1 (Get Debug Info) win / target / slot1 addr を取得 3 偽装ペイロードを構築 — slot 0 のヘッダ(flink/blink)を偽装する 4 Heap BOF で書き込み 4 (Import Certificate) slot 1 への import で slot 0 のヘッダを上 書き 5 free で unlink 発火 3 (Delete Key Slot) slot 1 を free → slot 0 が unlink → 任意 書き込み 6 乗っ取った関数ポインタ実行 5 (Generate Unlock Token) active_signer が偽物を指す → win(dump )が呼ばれる
  23. Step2:都合の良いリーク • TAにおいてもASLRは有効 • tz_gateのメニュー1(Get Debug Info)を選ぶと3つのアドレスをまとめて返す static TEE_Result cmd_get_debug_info(uint32_t

    param_types, TEE_Param params[4]) { (略) leak_data[0] = (uint64_t)(uintptr_t)&debug_dump_oem_root_key; leak_data[1] = (uint64_t)(uintptr_t)&active_signer[0]; leak_data[2] = (slot_id < MAX_KEY_SLOTS) ? (uint64_t)(uintptr_t)key_cert_slots[slot_id] : 0; • &debug_dump_oem_root_key: よびたい関数(TA脆弱性①) • &active_signer[0]: 書き換えたい標的 • key_cert_slots[slot_id] : 確保したslotのアドレス
  24. Step3:偽装ペイロードの構築 【スロット1のデータ(埋め)】 【後ろの隣接(B')の偽ヘッダ】 prevfree = 0 bsize = 48(正の値) flink

    = 偽TokenSigner のアドレス blink = active_signer[0] のアドレス - 16 【偽TokenSigner】 generate_signature = dump関数のアドレス slot1_addr +0 slot1_addr +32 slot1_addr +64 • bgetのソースコードをよみつつ偽のヘッダを作る • active_signer[0] が指す先を、署名関数をdump関数に付け替えた偽Signerに書 き換える
  25. OEM Root Keyの取得 [*] Launching tz_gate ... [*] [Stage2] Step

    1: Allocating adjacent chunks ... [*] [Stage2] Step 2: Leaking addresses ... [*] win=0x40068140, target=0x40081720, heap=0x4008cf10 [*] [Stage2] Step 3: Crafting payload (size=80) [*] [Stage2] Step 4: Triggering Heap BOF via import_cert ... [*] [Stage2] Step 5: Freeing chunk to trigger BGET unlink ... [*] [Stage2] Step 6: Executing hijacked generate_signature ... [+] FLAG 2: FLAG{trustz0n3_f3ll_0em_k3y_1s_m1n3}
  26. まとめ • 本問題は、ROPとUnlink Attackというpwnの2つの基本的手法を、1問で体験でき るよう設計した • 舞台を Arm TrustZone /

    OP-TEE とし、「Normal World 陥落を前提に Secure Worldで鍵を守る」設計思想によって、Stage1(Normal World制圧)が Stage2(Secure World侵入)への必然的な踏み台になるよう構成した • Stage2では、BGET(OP-TEEのアロケータ)がglibcのような堅牢な対策を持たな いため、古典的なUnlink Attackがそのまま通用する点がポイント • assertでsafe unlink相当のチェックを行うが、NDEBUGビルドでは無効化される
  27. 参考文献 • Stage2の背景・先行研究 • F. Fleischer, M. Busch, P. Kuhrt,

    "Memory Corruption Attacks within Android TEEs: A Case Study Based on OP-TEE", ARES 2020. • P. Kuhrt, "BGET Explained", https://phi1010.github.io/2020-11-02- bget-exploitation-2/ • Unlink Attackについて • bata_24,「katagaitai CTF勉強会 #1 pwnables編 - DEFCON CTF 2014 pwn1 heap」, https://speakerdeck.com/bata_24/katagaitai-ctf-number-1