はじめに
AES は現在も広く使われている安全な共通鍵暗号ですが、
暗号アルゴリズムそのものが安全でも、暗号モードの選択を誤ると致命的な脆弱性になります。
その代表例が ECB(Electronic Codebook)モードです。
本記事では、ECB モードを用いたシステムに対して成立する
ECB Oracle 攻撃(Byte-at-a-time 攻撃) を、原理から実践まで丁寧に解説します。
1. ECB モードとは何か?
ECB モードは、平文を 固定長ブロック(AES は 16 バイト) に分割し、
それぞれを 独立に暗号化します。
Plaintext Block 1 ──► Encrypt ──► Ciphertext Block 1
Plaintext Block 2 ──► Encrypt ──► Ciphertext Block 2
...
ECB の致命的な特徴
同じ平文ブロックは、必ず同じ暗号文ブロックになる
これが ECB の最大の欠点です。
2. なぜ ECB は危険なのか?
ECB モードでは:
- ブロック間の関連性がない
- ランダム性(IV)が存在しない
- 構造がそのまま暗号文に反映される
その結果、攻撃者が平文を制御できる場合、
暗号文を比較するだけで 内部情報を推測できてしまいます。
3. Oracle(オラクル)とは?
暗号攻撃における Oracle(オラクル) とは:
攻撃者が何度でも呼び出せる「暗号化サービス」
を意味します。
典型的な ECB Oracle は以下の形式です:
AES-ECB( PREFIX || user_input || SECRET )
-
PREFIX:攻撃者には見えない固定またはランダムな前置き -
user_input:攻撃者が完全に制御可能 -
SECRET:攻撃者が盗みたい秘密情報 -
KEY:固定・非公開
4. 攻撃のゴール
この攻撃の目的はただ一つです。
暗号鍵を知らずに、SECRET を 1 バイトずつ復元する
例:
SECRET = "FLAG{ECB_IS_BROKEN}"
5. ECB Oracle 攻撃の全体像
攻撃は以下の 4 ステップで進みます。
Step 1:ECB モードであることの確認
-
"A" * 64のような繰り返し入力を送信 - 暗号文に 同一ブロックが複数出現すれば ECB
Step 2:ブロックサイズの特定
- 入力長を 1 バイトずつ増やす
- 暗号文長が増加した瞬間の差分
- これが ブロックサイズ(通常 16 バイト)
Step 3:Prefix Offset の調整(重要)
PREFIX が存在すると、攻撃者の入力はブロック境界に揃っていません。
そのため:
-
"B"などのダミー文字を前に追加 - 自分が制御できる入力をブロック境界に揃える
- SECRET が含まれるブロック番号(base_index)を特定
この工程が ECB 攻撃で最もミスしやすい部分です。
6. バイト目の情報漏えい
発想の核心
未知の 1 バイトを「ブロックの最後」に追い込む
例(AES = 16 バイト):
AAAAAAAAAAAAAAA?
-
?が SECRET の 1 バイト目
辞書攻撃(Dictionary Attack)
- 上記入力を送信し、対象ブロックの暗号文を保存
- 次に 1 バイトを総当たり:
AAAAAAAAAAAAAAAc
- 暗号文ブロックを比較
- 一致した文字 = SECRET の 1 バイト目
7. バイト目以降はどうする?
核心となる計算式
padA = block_size - 1 - (len(known) % block_size)
target_index = base_index + (len(known) // block_size)
意味は:
- 既知文字が増えるごとに A の数を減らす
- 16 バイトごとに SECRET は次のブロックへ移動
- 比較対象の暗号文ブロックも 自動的に切り替える
ブロック遷移の例(AES = 16)
| 取得済み文字数 | 対象ブロック |
|---|---|
| 0〜15 | block 1 |
| 16〜31 | block 2 |
| 32〜47 | block 3 |
このロジックがないと、16 バイトで攻撃が止まります。
8. なぜこの攻撃が成立するのか?
理由は ECB の本質にあります。
- ECB は 「同一平文 → 同一暗号文」 を保証する
- 攻撃者は平文を意図的に一致させられる
- 暗号鍵を知らなくても比較だけで推測可能
つまりこれは:
暗号解読ではなく、設計ミスの悪用
です。
9. なぜ CBC / GCM では成立しないのか?
CBC モード
- 各ブロックが前の暗号文に依存
- 同一平文でも結果が変わる
GCM(AEAD)
- 暗号化 + 認証
- 改ざんや推測は即座に検知される
ECB は現代暗号において使用してはいけないモード
10. 防御策(最重要)
やってはいけないこと
- ECB モードの使用
-
user_input || secretのような設計 - 暗号化結果をそのまま返す API
推奨される対策
- AES-GCM / ChaCha20-Poly1305 を使用
- ユーザー入力と秘密情報を直接連結しない
- 不要な暗号化 Oracle を公開しない
まとめ
ECB Oracle 攻撃は「高度な暗号解読」ではありません。
それは「設計ミスが引き起こす必然的な情報漏えい」です。
この攻撃を理解できたなら:
- ECB の危険性
- ブロック暗号の構造
- モード選択の重要性
を本質的に理解できたと言えます。
Source Code
from Crypto.Cipher import AES
from bs4 import BeautifulSoup
import binascii
import requests
import string
import ecb_brute_oracle
def oracle_bytes(username:str)->bytes:
"""
oracle_encrypt(username) should return hex string ciphertext
"""
hex_ct = ecb_brute_oracle.oracle_encrypt(username)
return bytes.fromhex(hex_ct)
def split_blocks(ciphertext:bytes,block_size:int)-> list[bytes]:
return [ciphertext[i:i+block_size] for i in range(0,len(ciphertext),block_size)]
# -------------------------
# Detect block size (bytes)
# -------------------------
def detect_block_size(max_probe:int =256)->int:
base_len= len(oracle_bytes("A"))
prev =base_len
for n in range(2,max_probe+1):
cur=len(oracle_bytes("A"*n))
if cur>prev:
return cur-prev
prev=cur
raise RuntimeError("Failed to detect block size")
def detect_prefix_alignment(bs: int) -> tuple[int, int]:
"""
Return (pad_len, base_index)
pad_len: how many bytes to prepend to align our controlled 'A's on a block boundary
base_index: index of the first of the two equal adjacent blocks (where our A-blocks land)
"""
marker = "A" * (bs * 2) # two identical blocks
for pad_len in range(bs): # 0..bs-1
pt = ("B" * pad_len) + marker
ct = oracle_bytes(pt)
blks = split_blocks(ct, bs)
for i in range(len(blks) - 1):
if blks[i] == blks[i + 1]:
return pad_len, i
# If never found, assume no random prefix; base_index=0 is a reasonable fallback
return 0, 0
def leak_next_byte(bs: int, pad_len: int, base_index: int, known: str, charset: str) -> str | None:
"""
Leaks the next unknown byte of SECRET (as a single character), or None if not found.
"""
# how many 'A' to make the next unknown byte fall at end of a block
padA = bs - 1 - (len(known) % bs)
# prefix aligns our controlled area
prefix = ("B" * pad_len) + ("A" * padA)
# target block index rolls over every bs bytes leaked
target_index = base_index + (len(known) // bs)
# reference: encryption of prefix only
ref_ct = oracle_bytes(prefix)
ref_blocks = split_blocks(ref_ct, bs)
if target_index >= len(ref_blocks):
return None
ref_block = ref_blocks[target_index]
# dictionary match: prefix + known + guess
for ch in charset:
test_pt = prefix + known + ch
test_ct = oracle_bytes(test_pt)
test_blocks = split_blocks(test_ct, bs)
if target_index < len(test_blocks) and test_blocks[target_index] == ref_block:
return ch
return None
def leak_secret(bs: int, pad_len: int, base_index: int, max_len: int = 256) -> str:
charset = string.ascii_letters + string.digits + "{}_:-@!$.,/+=()[]<>\"'\\| "
known = ""
for _ in range(max_len):
ch = leak_next_byte(bs, pad_len, base_index, known, charset)
if ch is None:
break
known += ch
print("Leaked so far:", known)
return known
if __name__ == "__main__":
print("Testing oracle...")
print("ct(hex) for SuperUser:", ecb_brute_oracle.oracle_encrypt("SuperUser"))
print("\nDetecting block size...")
bs = detect_block_size()
print("Block size (bytes):", bs)
print("\nDetecting prefix alignment (pad_len) and base_index...")
pad_len, base_index = detect_prefix_alignment(bs)
print("pad_len:", pad_len, "base_index:", base_index)
print("\nLeaking secret...")
secret = leak_secret(bs, pad_len, base_index, max_len=256)
print("\nFinal leaked secret:", secret)
ecb_brute_oracle.py
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os
BLOCK_SIZE=16
KEY=os.urandom(16)
PREFIX =os.urandom(7)
SECRET =b"FLAG{ECB_IS_BROKEN}"
def oracle_encrypt(username:str)->str:
pt = PREFIX+username.encode("utf-8")+SECRET
pt=pad(pt,BLOCK_SIZE)
# ECB 暗号文
ct= AES.new(KEY,AES.MODE_ECB).encrypt(pt)
return ct.hex()
結果
eaking secret...
Leaked so far: F
Leaked so far: FL
Leaked so far: FLA
Leaked so far: FLAG
Leaked so far: FLAG{
Leaked so far: FLAG{E
Leaked so far: FLAG{EC
Leaked so far: FLAG{ECB
Leaked so far: FLAG{ECB_
Leaked so far: FLAG{ECB_I
Leaked so far: FLAG{ECB_IS
Leaked so far: FLAG{ECB_IS_
Leaked so far: FLAG{ECB_IS_B
Leaked so far: FLAG{ECB_IS_BR
Leaked so far: FLAG{ECB_IS_BRO
Leaked so far: FLAG{ECB_IS_BROK
Leaked so far: FLAG{ECB_IS_BROKE
Leaked so far: FLAG{ECB_IS_BROKEN
Leaked so far: FLAG{ECB_IS_BROKEN}
Final leaked secret: FLAG{ECB_IS_BROKEN}