はまやんはまやんはまやん

hamayanhamayan's blog

CTFのCryptoにおけるAESまとめ

CTFにおけるCrypto入門とまとめの1つです。

AES

  • AES = ブロック暗号コンペで選定されたRijandaelという対称暗号
    • AESの欠点「ちょうど16bytesしか暗号化できない」
    • 表記方法: AES-128-CBC
      • AES: 暗号プリミティブ名
      • 128: 鍵長(bit)
      • CBC: 暗号利用モード
  • ブロック暗号
    • 一定長のブロックに分けて暗号化
    • 構成要素
      • パディング方式: ブロック毎に分けたときに端数をどのようにブロックに丸めるかを決めたもの
      • ブロック暗号利用モード: 分けたブロックに対してどのようにブロック暗号を適用していくかを決めたもの
      • 暗号化方式: ブロック暗号のコア部分。ブロックをどのように暗号化するかを決めたもの

Rijandael暗号

パディング方式

  • ゼロパディング: paddingとしてゼロ0x00埋めする方式
  • PKCS#7 pkcs7 Padding
    • PKCS(: Public-Key Cryptography Standards)の7番目として決められた、暗号メッセージ構文に関する仕様の中で言及されているパディング方式
    • 1ブロックは255バイトまで
    • パディングとして0x00ではなくて、パディング長を入れこむ
      • 仮に8bytesでアラインメントするなら…
      • AAAAなら4つ入れる必要があるのでAAAA\x04\x04\x04\x04みたいな感じ
      • ピッタリなら新しくブロックを作ってパディング長を入れる。BBBBBBBBならBBBBBBBB\x08\x08\x08\x08\x08\x08\x08\x08

ブロック暗号モード

必ずしも+AESではないみたい。

ECBモード

  • ECB:Electronic CodeBlock ブロックごとに単純に暗号化する
    • 平文のブロックで同じものがあれば同じような暗号文が生成されてしまう
    • 暗号化 encrypt([平文block])
    • 復号化 decrypt([暗号block])
    • 鍵はあるけどivがないならECBかも
  • 攻撃テク
    • ECBでは16バイトのブロックごとに暗号化されるので、暗号化文字列を切り貼りして16バイト単位で差し替えが可能
      • ブロック長を意識して暗号文を作成して、切り貼りしていくことで任意の復号文を作る
      • 例えばjson形式を暗号化していたりすればスペースは無視されるので、スペースとかを利用して任意の文字だけを含むブロックを作ったりしながら頑張る
      • https://qiita.com/kusano_k/items/8a63b97d1427ef2e3369#imaginary
    • ECBではブロックごとに平文と鍵が同じであれば、同じ暗号文が出力される
      • 同一の鍵で平文が操作できるときに、全通りのブロックを作って比較して特定みたいなことができる
        • SECCON Beginner CTF 2025 01-Translator
    • 同じ平文で同じ暗号文になるので頻度分析に脆弱

CBCモード

  • CBC:Cipher Block Chaining 暗号化する前に平文を1つ前のブロックとXORして暗号化する
    • 最初のブロックは1つ前というのが無いのでIV(初期化ベクトル)というのを初期状態として用意する
      • IVは復号時にも必要になるが、鍵という扱いはされないみたい
      • 人に依って意見は分かれそうな雰囲気があるが、個人的には鍵がいくつもあるメリットもないので秘密ではないと考えておくスタンスでよさそう
    • 暗号化 encrypt([平文block] xor [IVか1つ前暗号block])
    • 復号化 decrypt([暗号block]) xor [IVか1つ前暗号block]
  • ivを外部注入できる場合
    • 最終的な復号文の先頭16bytesを自由に変更することができる
    • 復号時に先頭16bytesにAESの復号化処理がかかった後、xor ivされる。ここでAという風に復号化され、Bに復号化したい場合は、ivをiv xor A xor Bとしてやればxorでうまく相殺されて、Bに書きかえることが可能になる
  • Bit-flipping attack
    • CBCモードでは復号時に、decrypt関数に通した後1つ前の暗号のblockとxorをするので、とある暗号ブロックを1bitフリップさせることで、その次の暗号ブロックの対応bitもフリップさせられる
      • 例えば復号したら"test1"になるようにしておいて、その前の暗号ブロックの対応ビット部分を"test1" xor "admin"にしておけば復元したらadminになっている。
      • 副作用で色々壊れるかもしれないが、それでも問題なければ攻撃につなげることが可能
    • picoCTF2021 [Web Exploitation] writeup - 好奇心の足跡
    • https://tryhackme.com/r/room/flip 練習に便利
  • Padding Oracle Attack
    • Bit-flipping attackの発展形
    • CBCモードにおいて復号するときに、PKCS#7パディングが不正であるエラーが発生する場合に有効なテクニック
      • 名前にもあるように復元後のパディングチェックが成功したか失敗したかをオラクルとして攻撃を進めていく
    • 復元への動作原理
      • 暗号文の最終ブロックを特定することをまず考える
        • 最後から2番目のブロックを特定したい場合は最終ブロックを削除して同じことを行えばできるので、最後ブロックの復元だけ考えればいい
      • 最終ブロックc[N]を特定するために、最終ブロックc[N-1]部分を改変してpayloadを送る
        • c[1],c[2]...,改変c[N-1],c[N]
        • c[N-1]前まではどうでもいいが、c[N]はそのままにしておくこと
      • c[N]は元のままなので、復号用関数に通した結果は解析したいものと同じになるdec(c[N])
        • 攻撃で求めるのはdec(c[N])の内容になる。この内容が分かれば平文p[N]がdec(c[N]) xor c[N-1]で求まる
      • c[N-1]を00...0000から00...00ffまで全探索してc[1],c[2]...,改変c[N-1],c[N]のようにして送る
        • この改変後のc[N-1]をc'[N-1]としておこう
        • 送った後のN番目のブロックの復号文はdec(c[N]) xor c'[N-1]となっている
        • そして重要なのが『paddingを検証をしているとこのうち少なくとも1つはpadding検証は成功で返ってくるはずである』
          • ここの理解が難しい!
          • xorしているので、N番目のブロックの復元後は??????00~???????ffが均等に現れてくるはずである
          • この復元後に対してpaddingチェックが確実にOKとなるのは???????01の時だけである
            • N番目の元々の原文が何であるかというのは全く関係ない。paddingチェックという観点だけで見ると末尾が0x01になっていればチェックは通る
            • (もしかしたらたまたま末尾が0x0202みたいになってて成功する場合もあるかも)
          • つまり、255通りのうち成功で返ってきているならば、その入力において復号したdec(c[N]) xor c'[N-1]の末尾1byteが0x01である可能性が非常に高いと言える
          • もっと言えば、255通りのうち1つだけ成功で返ってきているならば確実にその入力において復号したdec(c[N]) xor c'[N-1]の末尾1byteは0x01である
        • これで dec(c[N]) xor c'[N-1] = ???????01となるc'[N-1]が特定できたので、dec(c[N])の末尾1byteは0x01 xor c'[N-1][:-1]であると特定できた
        • ここまで理解できていればだいぶ完全理解まで近い
      • これでdec(c[N])の末尾1byteは分かったのでは次は末尾2byte目を特定しよう
        • c[N-1]を00...0000XXから00...00ffXXまで全探索してc[1],c[2]...,改変c[N-1],c[N]のようにして送る
          • XXはなんだと思うかもしれないが、padding検証のことを加味して考えてみる
          • 末尾1byte特定の場合は最終的に0x01で終わることを考えたが、次は末尾が0x0202で終わることを考えよう
          • この場合のXX部分は、既にdec(c[N])の末尾1byteが特定されているので、0x02にするためには XX = dec(c[N])[:-1] xor 0x02を置けばいい
          • これでXXの1つ上のbyteを00からffまで全探索すれば末尾が0x0202となる1つだけがpadding検証は成功した状態で返ってくることになる
        • padding検証が成功でかえってきた入力を見れば、末尾1byte目の特定と同じ要領で末尾2byte目まで特定できたことになる
      • このような感じでブロックサイズ16byteのdec(c[N])を特定していく
        • dec(c[N])が特定できればもともとのc[N-1]を使って、平文p[N]を求めることができた
      • ivが用意されていればそれもうまく弄って全文復元可能
    • 改ざんへの実行原理
      • まずは復元時と同様にして、改ざんしたいブロックのdec(c[x])を求める
      • そしたら、改ざんしたい値に合わせてc[x-1]を(改ざん後としたい文字列) xor dec(c[x])にすれば改ざん完了
        • こうするとx-1番目の内容も変わってしまうが、変わった後のdec(c[x])も求めることができるので、それを使ってさらに前の文章も修正できる
      • ivが用意されていればそれもうまく弄って全文改ざん可能
      • https://writeups.thebusfactoris1.online/posts/2025-09-05-decryption-execution-service-writeup
    • Padding Oracle Attack 分かりやすく解説したい - Attack All Around
    • 実装
  • ivが隠匿されていて、暗号化を任意の値で行えて結果が取得できる場合にivを特定可能
    1. b'A'*16+b'\x00'*16+b'A'*16というのを復号する
    2. 復号はdecrypt([暗号block]) xor [IVか1つ前暗号block]のように行われるので...
      • 1ブロック目の復号結果はdecrypt(b'A'*16) xor IV
      • 3ブロック目の復号結果はdecrypt(b'A'*16) xor b'\x00'*16よりdecrypt(b'A'*16)
    3. つまり、1と3ブロック目の復号結果をxorするとIVが得られる
      • decrypt(b'A'*16) xor IV xor decrypt(b'A'*16) = IV
    4. https://github.com/rerrorctf/writeups/blob/main/2025_02_07_BITSCTF25/crypto/alice_n_bob_in_wonderland/writeup.md
  • 同じ鍵が使われていて、何度も復号化できる場合は、復号先を任意の値に変更可能
  • Key reuse https://github.com/jvdsn/crypto-attacks/tree/master/attacks/cbc_and_cbc_mac
    • EAM, ETM, MTEの鍵が再利用されていると脆弱瀬

CFB: Cipher FeedBack Mode / OFB: Output-FeedBack mode

  • CFB: Cipher FeedBack Mode
    • 暗号復号化
      • 暗号化 encrypt([IVか1つ前暗号block]) xor [平文block]
      • 復号化 encrypt([IVか1つ前暗号block]) xor [暗号block]
      • 暗号化と復号化が全く同じ
    • レプレイ攻撃可能
  • OFB: Output-FeedBack mode

CTR / GCM

  • CTR: CounTeR カウンタを使ってブロック暗号をストリーム暗号として使う
    • カウンタ/疑似乱数 = [nonce(毎回作り直すランダムな値)] + [ブロックごとにインクリメントするブロック番号]
    • 疑問点: ストリーム暗号になってる?
    • 例えばnonceが123456789であれば、ブロックごとに1234567890001, 1234567890002, 1234567890003,...を使うイメージ
    • 暗号化 encrypt([疑似乱数]) xor [平文block]
    • 復号化 encrypt([疑似乱数]) xor [暗号block]
    • 攻撃
      • 同一KEY かつ 同一NONCEが使われている場合は Enc(x) xor Enc(y)はencrypt([疑似乱数])部分が相殺されて、x xor yとなる。
        • 平文が分かっていれば、Enc(x) ^ x ^ yにすればyの暗号化に変換可能
      • 暗号化と復号化は同一の処理であるため、カウンタが同じであれば暗号化処理を復号化処理として利用可能
      • カウンタが何等かの問題でループする場合
        • 同じKEY, NONCEを使っている場合、カウンタをいい感じにループさせて暗号化時に使った初期カウンタと同じものが用意できれば復号可能
    • CTRはストリーム暗号として使えるので、平文はブロックサイズにpaddingされてなくてもよく、また、暗号文もpaddingされずに同じサイズ長で出てくる
      • (全部でそうなのか分からないが、少なくとも、pycryptodomeだとそうなった。Counterでブロック長が指定されていても出力サイズはpaddingされない)
    • 他攻撃 https://github.com/jvdsn/crypto-attacks/tree/master/attacks/ctr
  • GCM: Galois/Counter Mode, ガロア認証(GHASH関数)+カウンターモードで暗号化。暗号化/復号化と同時にデータが正しい人に作成されて改ざんされてないことも確認可能

XCBモードへの攻撃

XTS: XEX encryption mode with tweak and ciphertext stealing

  • AES-CFB8: Advanced Encryption Standard –Cipher Feed Back 8 bit
    • ZeroLogon CVE-2020-1472 で悪用された。昔のADではこれに加えてivが0で固定だったため、平文が全部0のものを与えると暗号文をとある1文字から成るものに強制できた
  • PCBCモード + Padding Oracle Attack

他、攻撃メモ