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

NDIAS Automotive IoT CTF問題解説 buried_key

Avatar for ishii ishii
July 03, 2026
70

NDIAS Automotive IoT CTF問題解説 buried_key

Avatar for ishii

ishii

July 03, 2026

Transcript

  1. Challenge: Buried Key • 問題文 • あるバイナリが AES-128 ベースのホワイトボックス暗号化を行っている。 •

    鍵を取り出してフラグを復号せよ。 • 配布ファイル : buried_key • Linux x86-64 ELF binary (statically linked) • ゴール:以下の暗号文を AES-128-ECB(PKCS7 パディング)で復号せよ。 ba720d90a039d801adf22f86d2901b91 8f36fbbfa6459accb1c5315d1bd4cb39 (32バイト=2ブロック) 1. バイナリから鍵を抜く 2. AES-128-ECB 復号(PKCS7) FLAG{…} $ ./buried_key <32 hex chars> 2
  2. 初動調査: 暗号化 oracle っぽい • 見える性質: • 32 hex chars

    を受け取る • 16 byte を処理する • 16 byte を hex で返す • ここまでは「AESっぽい」 • ただし、まだ鍵もモードも分からない。 $ ./buried_key 00000000000000000000000000000000 88f98d5e605eac3ee60fb40913896498 $ ./buried_key 00112233445566778899aabbccddeeff 0657dd0ddf3394181a952ad847ac6f94 3
  3. decomp / symbol で見る:Whitebox AES っぽい • main関数をデコンパイルすると以下の処理が見える • 以下の表が見える(not

    stripped なので): • この時点で立つ仮説: • 普通のAESライブラリ呼び出しではない。 鍵が値として置かれているのではなく、表に焼かれた Whitebox AES では? argv[1] -> 32 hex check -> 16 byte parse -> 9 rounds: TboxTyi / XorTables -> final: Tbox10 / FinalDec -> hex output FinalDec Tbox10 TboxTyi XorTables SR_INV 4
  4. 普通のAESとWhiteboxの違い • Whitebox は 攻撃者がバイナリ全体を持ち、メモリを読めても鍵が取り出しにくくする 暗号化コード 鍵 暗号文 鍵が値として存在 →

    メモリを見れば抜ける 鍵を埋め込んだ ルックアップテーブル 鍵は表に焼いてあり、 値として存在していない 普通のAES Whitebox AES 暗号文 表引き 5
  5. AESの暗号化処理 • 16バイトの平文を 4×4 の「状態(state)」に並べ、同じ加工を10回くり返す。 ラウンド1 ラウンド 10 State 暗号文

    ラウンド2 ... ラウンド9 1. SubBytes : バイトを置換 2. ShiftRows : 位置を動かす 3. MixColumns : 列に行列を掛ける 4. AddRoundKey : 鍵をXOR ラウンド 1~9 1. SubBytes : バイトを置換 2. ShiftRows : 位置を動かす 3. AddRoundKey :鍵をXOR ラウンド 10 6
  6. MixColumns • 各列に固定行列を掛ける (GF(2^8) 上) • GF(2^8) Galoir field •

    8ビット係数の多項式として扱って計算する、という意味。整数の掛け算とは別物 1. SubBytes 2. ShiftRows 3. MixColumns 4. AddRoundKey = 04 d4 bf 5d 30 State´ State 0xd4 = 11010100₂ = 𝑥7+ 𝑥6+𝑥4+𝑥2 02*0xd4 = 𝑥(𝑥7+ 𝑥6+𝑥4+𝑥2) = 𝑥8+ 𝑥7+𝑥5+𝑥3 = 𝑥7+ 𝑥5+𝑥4+𝑥+1 = 10110011₂ = 0xb3 0x04 = 02*0xd4 ⊕ 03*0xbf ⊕ 0x5d ⊕ 0x30 9
  7. AddRoundKey • Stateと、その回のラウンドキー(RK)を、同じ位置どうしで XOR する • RKはAESの共通鍵(128bit) k0 から生成されるもの •

    k0をラウンド0~10(128bit * 11)のラウンドキーに拡張 04 10 14 1. SubBytes 2. ShiftRows 3. MixColumns 4. AddRoundKey ⊕ State State´ RK 10
  8. AddRoundKey • 共通鍵(128bit) k0 から11個のラウンドキー (16 byte × 11 =

    176 byte)を順に作る • 鍵スケジュールという 1. SubBytes 2. ShiftRows 3. MixColumns 4. AddRoundKey k0 = 2b7e1516 28aed2a6 abf71588 09cf4f3c (= w0 w1 w2 w3 = RK0) w4 を作る: RotWord(w3=09cf4f3c) = cf4f3c09 … 1バイト左回転 SubWord(cf4f3c09) = 8a84eb01 … 各バイトを SBox で置換 Rcon[1] = 01000000 … 標準 w4 = w0 ⊕ 8a84eb01 ⊕ Rcon[1] = 2b7e1516 ⊕ 8a84eb01 ⊕ 01000000 = a0fafe17 w5 = w4 ⊕ w1 = 88542cb1 w6 = w5 ⊕ w2 = 23a33939 w7 = w6 ⊕ w3 = 2a6c7605 RK1 = a0fafe17 88542cb1 23a33939 2a6c7605 RK2..10も同様に作成 11 Rcon
  9. 普通のAES Whitebox ルックアップテーブルのイメージ • ルックアップテーブルT = SboxにRKを組み込んだもの • T[byte_index][input_byte] =

    Sbox[input_byte ^ RK[byte_index]] を事前に計算 State 2b RK 1. SubBytes :Sbox 2. ShiftRows 3. Mixcolumns 4. AddRoundKey 1. SubBytes :Sbox 2. ShiftRows 3. Mixcolumns 4. AddRoundKey T uint8_t T[16][256]; // T[byte_index][input_byte] 例: 左上の要素の場合 T[0][0x01] = Sbox[0x01 ^ 0x2b] = Sbox[0x2a] = 0xe5 12 01 ⊕ e5 State´ Sbox e5 T 0 … 16 255 鍵は表に焼いてあり、 値として存在しない
  10. Whitebox AES への3つの攻撃 攻撃 何をするか 代表的なツール DFA (Differential fault analysis)

    表を壊して実行し、暗号文の差分から鍵を復 元する Deadpool[1]、phoenixAES DCA (Differential Computation Analysis) 実行中の中間値を記録し、統計で鍵を当てる Daredevil[1]、Tracer[1] テーブル解析(BGE) 表を分解して鍵を取り出す 自作スクリプト DCAとBGEは符号化があるとが難易度が上がる T[i][x] = Sbox[x ^ RK[i]]だと簡単にRKを戻せるため 符号化という入出力にランダムな全単射を入れることがある [1]https://github.com/sidechannelmarvels ※本問題も符号化が存在する。 問題文:バイナリの入出力には内部変換が含まれるため、バイナリ の出力は通常の AES-128 とは一致しません。 13
  11. DFA (Differential fault analysis) • AES white-box 実装に fault を入れて、

    正常暗号文 C と fault 暗号文 C' の差分 から RK を復元する • DFAではDeadpool、phoenixAESというツールが有用 • CTFの問題を解くだけであれば、数学的な原理は知る必要はあまりない • ただどの表を壊す必要があるのかを理解しておくとよい 暗号化処理 ⊕ 入力 正常暗号文 C fault暗号文 C′ 暗号化処理 入力 fault を入れる: 表を1バイト書き換える 15 GOAL:RK復元
  12. DFA の目標 • 最終ゴール: RK10…を復元 • 最初の一歩: ラウンド9でfaultを入れる ラウンド1 ラウンド

    10 暗号文 ラウンド2 ... ラウンド9 基本的にはツールがやってくれる Deadpool: ラウンド9にfaultを入れたバイナリを作成、暗号文を集める phoenixAES: 集めた暗号文からRKを算出する AESの暗号化処理 ※RK10が復元できるとRK9 RK9が復元できるとRK8 … が可能になる faultを入れる 16 入力
  13. Faultを入れると暗号文はどう変わるのか • DFAではラウンド 9 の MixColumnsの入力Stateに fault ε を入れる •

    最後の暗号文 C’は4バイトだけ変化する 1. SubBytes 2. ShiftRows 3. Mixcolumns 4. AddRoundKey ラウンド 9 ε × 2ε ε ε 3ε 1. SubBytes 2. ShiftRows 3. AddRoundKey ラウンド 10 2ε ε ε 3ε MixColumns Mixcolumns 後 暗号文C’ ShiftRows 列に拡散 この4バイトだけ変化す るとRKが復元できる 17
  14. Faultを入れる場所を決定する • ラウンド 9 の MixColumnsの入力に fault ε を入れたい •

    Buried_keyの処理と Faultを入れる候補のテーブル FinalDec Tbox10 TboxTyi XorTables SR_INV argv[1] -> 32 hex check -> 16 byte parse -> 9 rounds: TboxTyi / XorTables -> 10 round: Tbox10 / FinalDec -> hex output XorTables(uint8)→ 良い 1エントリ = 中間の1バイト 書き換えると state の1バイトだけが狂う =クリーンな単一バイトのfault。 TboxTyi(uint32)→ 悪い 1エントリは MixColumns 後の4バイトの塊 1バイト書き換えると4バイト同時に狂う。 18 2つのテーブルにdeadpool をかけてみればよい
  15. Deadpool + phoenixAESでRKを復元 • Deadpoolはtargetファイルを書き換えて実行し、暗号文を集める • phoenixAESが集めた暗号文からRKを復元する import deadpool_dfa import

    phoenixAES engine = deadpool_dfa. Acquisition ( goldendata = golden, # 正常コピー(壊さない基準) targetdata = target, # 毎回壊されるファイル targetbin = "./buried_key", # 起動する実行ファイル dfa = phoenixAES, # 良し悪しの判定 + 最後の鍵計算 # addresses = tables_range, # 壊す範囲を指定 ) tracefiles=engine.run() for tracefile in tracefiles: if phoenixAES.crack(tracefile): print(phoenixAES.crack(tracefile)) # RKを出力 break https://github.com/SideChannelMarvels/Deadpool/blob/master/README_dfa.md 19
  16. Deadpool + phoenixAESでRKを復元 • Deadpoolが暗号文を集める • phoenixAESが集めた暗号文からRKを復元する $ ./buried_key 74657374746573747465737474657374

    a0bec87433681b16166a39329ca5581f #暗号文 $ solver.py … (testtesttesttest) Lvl 013 [0x000F9938-0x000F9940[ xor 0x11 74657374746573747465737474657374 -> 85BEC87433681B0E166AD8329CE5581F GoodEncFault Column:0 Logged Lvl 013 [0x000F9938-0x000F9940[ xor 0x5B 74657374746573747465737474657374 -> 00BEC87433681BF4166A7F329C08581F GoodEncFault Column:0 Logged Lvl 013 [0x000F9938-0x000F9940[ xor 0x2F 74657374746573747465737474657374 -> 32BEC87433681B83166AA4329C01581F GoodEncFault Column:0 Logged Lvl 013 [0x000F9938-0x000F9940[ xor 0xF3 74657374746573747465737474657374 -> 41BEC87433681B47166A5A329CB5581F GoodEncFault Column:0 Logged … # Column: 1~3も同様に実施 RK[10] = 24943C23338A6D3A6BD98491F71F370E 0 13 10 7 暗号文 RK[10]が復元出来たら、鍵スケジュールでk0まで一気に復元ができる 20
  17. 鍵スケジュールでk0を逆算 • RK[10] が分かればk0 を鍵スケジュールを逆算することが可能。 • 次のRKは、前の鍵を「rotate → Sbox →

    Rcon と XOR」で作る。逆向きも同じ式。 • RK[10] = 24943C23338A6D3A6BD98491F71F370E • k0まで逆算すると k0 = E4499AC8E6FED4732F374D316A42B134 21
  18. 暗号文を復号する → 失敗 • 暗号文をAES-128-ECB / PKCS7 で復号する。 • 暗号文:

    ba720d90a039d801adf22f86d2901b918f36fbbfa6459accb1c5315d1bd4cb39 • 復号してみると明らかにFLAGではない • RK10から標準Rconで鍵スケジュールを逆算したが、標準Rconではない可能性がある。 • → DFAでRK9..RK3まで復元して、Rconを確認してみる。 00000000 c1 42 26 bb a9 c9 4e bc 43 f5 c7 f7 f4 6e 10 48 |.B&...N.C....n.H| 00000010 91 47 59 b7 27 74 a1 71 68 0f b5 85 51 57 70 db |.GY.'t.qh...QWp.| 22
  19. 残りのRKをDFAで復元する • RK[10] が分かれば最終ラウンドを計算でほどき、1つ手前に DFA。RK[3] まで8回くり返す。 • AES鍵 k0まではDFAでは復元できない RK[10]

    RK[9] … RK[3] RK[2] RK[1] RK[0] =k0 DFA でとれる DFA で取れない engine = deadpool_dfa. Acquisition ( goldendata = golden, # 正常コピー(壊さない基準) targetdata = target, # 毎回壊されるファイル targetbin = "./buried_key", # 起動する実行ファイル dfa = phoenixAES, # 良し悪しの判定 + 最後の鍵計算 # addresses = xortables_range, # 壊す範囲を XorTables に限定 ) tracefiles=engine.run(lastroundkeys=復元済みRK) for tracefile in tracefiles: if phoenixAES.crack(tracefile): print(phoenixAES.crack(tracefile, lastroundkeys=復元済みRK)) break RK3 = fd4e7a78f61c3846850692dbcc1692a7 RK4 = 14012633e21d1e75671b8caeab0d1e09 RK5 = 84732751666e39240175b58aaa78ab83 RK6 = b611cbfdd07ff2d9d10a47537b72ecd0 RK7 = f1dfbbdc21a04905f0aa0e568bd8e286 RK8 = 9e47ffe1bfe7b6e44f4db8b2c4955a34 RK9 = a8f9e7fd171e51195853e9ab9cc6b39f RK10 = 24943c23338a6d3a6bd98491f71f370e 23
  20. Rcon を鍵から逆算する • 既知の RK[3..10] を入れれば Rcon が出る • W

    はRKの 4バイト word。 Rcon[r] = ( W[4r] ⊕ W[4r-4] ⊕ SubWord(RotWord(W[4r-1])) ) 既知の RK[3..10] から Rcon[4..10] が出る: Rcon[4..10] = AE 47 8E 07 0E 1C 38 これは xtime(GF(2^8) の ×2)の連鎖。さかのぼると: 53 → A6 → 57 → AE → 47 → 8E → 07 → 0E → 1C → 38 → Rcon[1] = 0x53 (標準は 0x01) RK10 = 24943c23 338a6d3a 6bd98491 f71f370e (= W[40] W[41] W[42] W[43] RK9 = a8f9e7fd 171e5119 5853e9ab 9cc6b39f (= W[36] W[37] W[38] W[39] 24
  21. 本物の Rcon で k0 まで戻す • Rcon[1] = 0x53 を初項に、鍵スケジュールを最後まで逆算する。

    • W[0..3]、つまり最初の鍵 k0 が出る。 • 暗号文をAES-128-ECB / PKCS7 で復号する。 • 暗号文: ba720d90a039d801adf22f86d2901b918f36fbbfa6459accb1c5315d1bd4cb39 k0 = deadbeefcafebabe1337c0de42424242 FLAG{custom_rcon_is_the_key} 25
  22. まとめ • Buried Key • WhiteboxからAES鍵 k0を復元し、与えられた暗号文をAES-128-ECB / PKCS7 で復号

    • DFAによる解法 • 山1:DFA • Deadpool_dfaを使用して、表にfaultを入れ、暗号文の差からRKを剥がす (RK[10] → RK[3]) • 山2:Rcon • RK[10] … RK[3] から、非標準の Rcon(0x53)を復元。 • その後、非標準の Rconを使用して 、鍵スケジュールを逆算し k0を復元 RK10を復号する詳しい導出 26
  23. 暗号文からRKを復元する • 正常暗号文 C : • 𝑥について整理する. k=RK10と仮定する • 𝑥と𝑥’の差分を考える

    𝐶 = 𝑆 𝑥 ⊕ 𝑅𝐾10 ※ShiftRowsを省略 𝐶’ = 𝑆 𝑥’ ⊕ 𝑅𝐾10 𝐶 = 𝑆 𝑥 ⊕ 𝑅𝐾10 𝐶 ⊕ 𝑘 = 𝑆 𝑥 ⊕ 𝑅𝐾10 ⊕ 𝑘 𝑥 = 𝑆−1 𝐶 ⊕ 𝑘 • Fault暗号文 C’ : 𝑥 ⊕ 𝑥’ = 𝑆−1 𝐶 ⊕ 𝑘 ⊕ 𝑆−1 𝐶’ ⊕ 𝑘 1. SubBytes :Sbox 2. ShiftRows 3. AddRoundKey ラウンド 10 x 暗号文C x’ 暗号文 C’ ⊕ 𝑘する Sboxの逆引き 𝑥 ⊕ 𝑥’がわかれば k を計算できそう 2ε ε ε 3ε 出力: 入力State: 𝑥 ⊕ 𝑥’ には綺麗な差分構造が出る 27
  24. 𝑥 ⊕ 𝑥’ がMix 後の差分 𝑚 ⊕ 𝑚’ になる •

    𝑥をMix後の値で考える 𝑥 ⊕ 𝑥’ = 𝑚 ⊕ 𝑅𝐾9 ⊕ 𝑚’ ⊕ 𝑅𝐾9 = 𝑚 ⊕ 𝑚’ 3. Mixcolumns 4. AddRoundKey ラウンド 9 m m’ x x’ Mixcolumns 後の差分 𝒎 ⊕ 𝒎’ 𝑥 = 𝑚 ⊕ 𝑅𝐾9, 𝑥’ = 𝑚’ ⊕ 𝑅𝐾9, 𝑥 ⊕ 𝑥’ = 𝑆−1 𝐶 ⊕ 𝑘 ⊕ 𝑆−1 𝐶’ ⊕ 𝑘 差分をとる 𝑚 ⊕ 𝑚’ = 𝑆−1 𝐶 ⊕ 𝑘 ⊕ 𝑆−1 𝐶’ ⊕ 𝑘 ε × 2ε ε ε 3ε MixColumns 列に拡散 28
  25. 各鍵候補について差分を計算する • 𝑚 ⊕ 𝑚’ = 𝐷𝑖 (k) とおく •

    𝐷𝑖 (k) = 𝑆−1 𝐶𝑖 ⊕ k ⊕ 𝑆−1 𝐶’𝑖 ⊕ k 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 2ε ε ε 3ε Mixcolumns 後の差分 𝒎 ⊕ 𝒎’ for k0 in 0..255: d0 = D0 (k0) ε = d0÷2 // GF(2^8) 上の割り算 εに対して: D13 (k13) == ε になる k13 を探す D10 (k10) == ε になる k10 を探す D7 (k7) == 3ε になる k7 を探す 見つかった組(k0, k13, k10, k7)を候補として残す 𝑘が正しい鍵なら 具体的に考えてみる: 𝑘0 = 0 としてd0を計算 d0 = 0x0e 2ε = 0x0e ε = 0x07 𝑘13 =0…255 を入れてみて成り立つものがあれば、鍵候補 D13 (k) = 𝑆−1 𝐶𝑖 ⊕ k ⊕ 𝑆−1 𝐶’𝑖 ⊕ k = 0x07 もし、存在しなければ 𝑘0 = 0 の仮定が間違い → 𝑘0 = 1 と仮定して再度計算する i=0,13,10,7が 変化しているとき 𝐷0 𝐷13 𝐷10 𝐷7 このパターンが出るkを総当たりする 29
  26. RK10全体を復元する 0 4 8 12 1 5 9 13 2

    6 10 14 3 7 11 15 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fault ε を入れる εを入れる場所を変える fault暗号文 C’ RK10 RK10全体を 復元可能 30
  27. 補足 • 𝑥について整理する. k=RK10と仮定する • k=RKだと綺麗な差分構造が出る • k=RK10ではないとき 𝐶 =

    𝑆 𝑥 ⊕ 𝑅𝐾10 𝐶 ⊕ 𝑘 = 𝑆 𝑥 ⊕ 𝑅𝐾10 ⊕ 𝑘 𝑆−1 𝐶 ⊕ 𝑘 = 𝑥 ⊕ 𝑘する Sboxの逆引き 𝑆−1 𝐶 ⊕ 𝑘 = 𝑆−1(𝑆 𝑥 ⊕ 𝑅𝐾10 ⊕ k) ≠ 𝑥 𝑆−1 𝐶 ⊕ 𝑘 ⊕ 𝑆−1 𝐶’ ⊕ 𝑘 = 𝑥 ⊕ 𝑥’ 2ε ε ε 3ε 𝑆−1 𝐶 ⊕ 𝑘 ⊕ 𝑆−1 𝐶’ ⊕ 𝑘 = 𝑆−1(𝑆 𝑥 ⊕ δ) ⊕ 𝑆−1(𝑆 𝑥’ ⊕ δ) ≠ 𝑥 ⊕ 𝑥’ 綺麗な差分構造が出ない 31