Files
revkit/tools/decrypt_key_response.py
tkgstratorandClaude Sonnet 4.6 e6856a96f0 feat(tools): Phase 2 KDF 仮説検証ツール群を追加・拡張
find_phase2_hmac_chain.py に以下のセクションを追加:
  - pre_appboot HMAC 中間値 (19def2f9/e60e376f/58c4e3d1) を PSK 代替として使用
  - enc_key_0 (0817065e) を起点とした逆算セクション
  - Phase 3 整合性確認: enc_key_0→enc_key_1, sign_key_0a→sign_key_1 の
    KDF チェーンが正しく一致することを確認 (sign_key_0 = 91f752f7 を特定)
  - enc_key_0 の DH 由来ブルートフォース

find_phase2_kdf_variants.py に以下のセクションを追加:
  - enc_key_0 (0817065e) を別ターゲットとした全 KDF バリアント試行
  - AES Key Wrap (RFC 3394) による key 33.6 アンラップ試行

tools/decrypt_key_response.py を新規作成:
  - key 33.6 (96B) を [IV:16][CT:48][HMAC:32] 構造として仮定し復号試行
  - 仮説 A/A2/B/C の全パターン (IV なし/あり, wrap_key 66 候補)
  - 全 232 件の appboot レスポンスファイルへの一括適用
  - HMAC 署名検証: HMAC(sign_key, IV||CT) == key336[64:96] の独立確認
  - 平文のオフセット探索 (enc_key が先頭以外にある場合)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 15:50:08 +00:00

565 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Phase 2 初期セッション鍵の復号試行ツール
key 33.6 (96 bytes) の暗号文を候補鍵で復号し、
平文が既知のセッション鍵 (enc_key_0, sign_key_0) を含むか検証する。
構造仮説:
仮説 A: [CT:64B][HMAC:32B] — IV なし (zeros または別フィールド)
仮説 B: [IV:16B][CT:48B][HMAC:32B] — 最有力 (MSL 標準構造)
仮説 C: [IV:16B][CT:64B][HMAC:16B] — HMAC が 16B に短縮
テストベクター:
DH shared_secret = 052a8bfe... (128 bytes)
PSK = 027617984f6227539a630b897c017d69
nonce (hardcode) = 809f82a7addf548d3ea9dd067ff9bb91
server nonce = e73104a8f4a9ed430d90a330d7978432
client nonce = a97e47477522ab39e39b322bdf818031
既知セッション鍵 (ターゲット):
enc_key_0 = 0817065e29e6d1c8668473af9e13b3c2 (DH 直後最初の AES 鍵)
enc_key_1 = 97b99f4e88e8e73779aa20ac11877c5d (Phase 3 KDF 出力)
sign_key_0 = 91f752f76d7ab4c2dc6e5b3ec1c0e5a16864421fe449be5457459602e298ebc1
sign_key_1 = d45443fa11efec622c83b27c55f7a73143bdfa0d51820ac597b9e3fb5c28dbb0
"""
from __future__ import annotations
import hashlib
import hmac as hmac_mod
import struct
from pathlib import Path
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
# ===================================================================
# テストベクター
# ===================================================================
DH_SHARED = bytes.fromhex(
"052a8bfe9f1a1a9becdd67672338191b"
"d7b5aff7fffe1f4cfbd97a0b14f8d59a"
"f54697a0bc1cf96ad6f6e84af98ffd1c"
"ebbc0fb5b04360878710f215f1261129"
"652808fd11f5164d84a501b40e63ba91"
"fcdcc932b6dbd61017099673f552db6e"
"94ff19934bfdef21b9701c4c9d312b78"
"f2cb8911e446313c2cb0567f7de865b3"
)
# バイナリ埋め込み固定値 (NFWebCrypto.framework @ 0x1ac8f5)
PSK = bytes.fromhex("027617984f6227539a630b897c017d69")
NONCE_HARD = bytes.fromhex("809f82a7addf548d3ea9dd067ff9bb91")
# セッション固有 nonce
SERVER_NONCE = bytes.fromhex("e73104a8f4a9ed430d90a330d7978432")
CLIENT_NONCE = bytes.fromhex("a97e47477522ab39e39b322bdf818031")
# 既知セッション鍵 (全ターゲット)
ENC_KEY_0 = bytes.fromhex("0817065e29e6d1c8668473af9e13b3c2")
ENC_KEY_1 = bytes.fromhex("97b99f4e88e8e73779aa20ac11877c5d")
KNOWN_ENC_KEYS: list[bytes] = [
ENC_KEY_0,
ENC_KEY_1,
bytes.fromhex("f4b5e0519e8022b2801768cdc88816d6"), # enc_key_2
bytes.fromhex("834327638d92f129c9da8a5ab72bca3b"),
]
KNOWN_HMAC_KEYS: list[bytes] = [
bytes.fromhex(
"91f752f76d7ab4c2dc6e5b3ec1c0e5a16864421fe449be5457459602e298ebc1"
), # sign_key_0
bytes.fromhex("38b2030dd55e3367290213ca0d16ee079524ccd24fb7221a52145fb6de016fd8"),
bytes.fromhex("05ffd2d7407a6da255dfd89cde00504d1803ed81a8e5c17ea196c4498d01d825"),
bytes.fromhex(
"d45443fa11efec622c83b27c55f7a73143bdfa0d51820ac597b9e3fb5c28dbb0"
), # sign_key_1
bytes.fromhex("b733c6b8bd3c2d02098c6c679daa9b138f9e0d9d76f95d2c85240937d53c66c9"),
bytes.fromhex("a4333e99a34eef3663f8e38e217e696949cd3bf57598c5c260fedb8997afa82b"),
bytes.fromhex("b319e54dce7117932439aad5e1dfed1e8d18fe033aabe00ab786dc4239e8dd85"),
]
# appboot レスポンス key 33.6 (96 bytes)
KEY336_HEX = (
"bb73317f907f7a5a3f924bece878d6a6"
"8db3b8f354d2207a224a323297523f58"
"2d72dfb28ea593b584d096c861561be8"
"b7d72ef4dc404e076943130aa0303200"
"af5d720284876f9b1c076aacc2ad7fc8"
"6b5a242b0cf9beb28b2a87ad00f3db38"
)
KEY336 = bytes.fromhex(KEY336_HEX)
print(f"key 33.6 ({len(KEY336)} bytes):")
print(f" IV (bytes 0-15): {KEY336[:16].hex()}")
print(f" CT (bytes 16-63): {KEY336[16:64].hex()}")
print(f" HMAC(bytes 64-95): {KEY336[64:96].hex()}")
print()
# ===================================================================
# ユーティリティ
# ===================================================================
found: list[str] = []
def aes_cbc_decrypt(key: bytes, iv: bytes, ct: bytes) -> bytes | None:
"""AES-128-CBC 復号。失敗時は None を返す。"""
if len(key) not in (16, 24, 32):
return None
if len(iv) != 16:
return None
if len(ct) % 16 != 0:
return None
try:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
dec = cipher.decryptor()
return dec.update(ct) + dec.finalize()
except Exception:
return None
def pkcs7_unpad(data: bytes) -> bytes | None:
"""PKCS7 アンパディング。不正なパディングは None を返す。"""
if not data:
return None
pad_len = data[-1]
if pad_len == 0 or pad_len > 16:
return None
if data[-pad_len:] != bytes([pad_len]) * pad_len:
return None
return data[:-pad_len]
def hmac256(key: bytes, msg: bytes) -> bytes:
return hmac_mod.new(key, msg, hashlib.sha256).digest()
def verify_hmac(hmac_key: bytes, data: bytes, expected_hmac: bytes) -> bool:
"""HMAC-SHA256(hmac_key, data) == expected_hmac"""
computed = hmac256(hmac_key, data)
return computed == expected_hmac
def check_plaintext(label: str, pt: bytes) -> bool:
"""平文が既知のセッション鍵 (enc_key または sign_key) を含むか検査"""
matched = False
for enc_key in KNOWN_ENC_KEYS:
if len(pt) >= 16 and pt[:16] == enc_key:
hmac_part = pt[16:]
hmac_matched = any(
hmac_part == sk or hmac_part[:32] == sk for sk in KNOWN_HMAC_KEYS
)
if hmac_matched:
msg = (
f"[FULL MATCH] {label} enc={enc_key.hex()[:8]}... hmac_match=True"
)
print(msg)
found.append(msg)
else:
msg = f"[ENC_ONLY] {label} enc={enc_key.hex()[:8]}... rest={hmac_part[:16].hex()}"
print(msg)
found.append(msg)
matched = True
return matched
# ===================================================================
# 候補ラッピング鍵の生成
# ===================================================================
ss_int = int(DH_SHARED.hex(), 16)
sha256_ss = hashlib.sha256(DH_SHARED).digest()
sha384_ss = hashlib.sha384(DH_SHARED).digest()
sha512_ss = hashlib.sha512(DH_SHARED).digest()
WRAP_KEY_CANDIDATES: list[tuple[str, bytes]] = [
# --- ハッシュ切り出し ---
("sha256(DH)[:16]", sha256_ss[:16]),
("sha256(DH)[16:32]", sha256_ss[16:32]),
("sha384(DH)[:16]", sha384_ss[:16]),
("sha384(DH)[16:32]", sha384_ss[16:32]),
("sha384(DH)[32:48]", sha384_ss[32:48]),
("sha512(DH)[:16]", sha512_ss[:16]),
("sha512(DH)[16:32]", sha512_ss[16:32]),
("sha512(DH)[32:48]", sha512_ss[32:48]),
("sha512(DH)[48:64]", sha512_ss[48:64]),
# --- DH 直接切り出し ---
("DH[:16]", DH_SHARED[:16]),
("DH[16:32]", DH_SHARED[16:32]),
("DH[-16:]", DH_SHARED[-16:]),
# --- PSK / nonce 由来 ---
("PSK", PSK),
("NONCE_HARD", NONCE_HARD),
("SERVER_NONCE", SERVER_NONCE),
("CLIENT_NONCE", CLIENT_NONCE),
# --- 既知セッション鍵 (Phase 4 では enc_key_1 が wrap_key として使われた) ---
("enc_key_0", ENC_KEY_0),
("enc_key_1", ENC_KEY_1),
# --- HMAC チェーン由来 ---
("HMAC(PSK,DH)[:16]", hmac256(PSK, DH_SHARED)[:16]),
("HMAC(DH,PSK)[:16]", hmac256(DH_SHARED, PSK)[:16]),
("HMAC(PSK,DH)[16:32]", hmac256(PSK, DH_SHARED)[16:32]),
("HMAC(NONCE,DH)[:16]", hmac256(NONCE_HARD, DH_SHARED)[:16]),
("HMAC(SERVER_NONCE,DH)[:16]", hmac256(SERVER_NONCE, DH_SHARED)[:16]),
# --- 2 段 HMAC ---
(
"HMAC(HMAC(PSK,DH),NONCE)[:16]",
hmac256(hmac256(PSK, DH_SHARED), NONCE_HARD)[:16],
),
(
"HMAC(HMAC(PSK,DH),SRV_NONCE)[:16]",
hmac256(hmac256(PSK, DH_SHARED), SERVER_NONCE)[:16],
),
(
"HMAC(HMAC(DH,PSK),NONCE)[:16]",
hmac256(hmac256(DH_SHARED, PSK), NONCE_HARD)[:16],
),
# --- HKDF 由来 (サーバー nonce を salt/info に) ---
]
# HKDF 候補を動的生成
for hash_alg, hash_name in [
(hashes.SHA256(), "SHA256"),
(hashes.SHA384(), "SHA384"),
]:
for salt_val, salt_name in [
(None, "None"),
(SERVER_NONCE, "SRV_NONCE"),
(CLIENT_NONCE, "CLI_NONCE"),
(PSK, "PSK"),
]:
for info_val, info_name in [
(b"", "empty"),
(SERVER_NONCE, "SRV_NONCE"),
(PSK, "PSK"),
(b"wrap", "wrap"),
(b"session", "session"),
]:
try:
hkdf = HKDF(
algorithm=hash_alg,
length=16,
salt=salt_val,
info=info_val,
)
k = hkdf.derive(DH_SHARED)
label = f"HKDF({hash_name},s={salt_name},i={info_name})"
WRAP_KEY_CANDIDATES.append((label, k))
except Exception:
pass
print(f"Total wrap_key candidates: {len(WRAP_KEY_CANDIDATES)}")
print()
# ===================================================================
# HMAC 署名鍵の候補
# ===================================================================
SIGN_KEY_CANDIDATES: list[tuple[str, bytes]] = [
("PSK", PSK),
("NONCE_HARD", NONCE_HARD),
("SERVER_NONCE", SERVER_NONCE),
("DH_SHARED", DH_SHARED),
("sha256(DH)", sha256_ss),
("sha384(DH)", sha384_ss),
("enc_key_0", ENC_KEY_0),
("enc_key_1", ENC_KEY_1),
("HMAC(PSK,DH)", hmac256(PSK, DH_SHARED)),
("HMAC(DH,PSK)", hmac256(DH_SHARED, PSK)),
("HMAC(PSK,DH+NONCE)", hmac256(PSK, DH_SHARED + NONCE_HARD)),
]
# 全 known HMAC keys も候補として追加
for i, sk in enumerate(KNOWN_HMAC_KEYS):
SIGN_KEY_CANDIDATES.append((f"known_hmac_{i}_{sk.hex()[:8]}", sk))
# ===================================================================
# Section 1: 仮説 B — [IV:16B][CT:48B][HMAC:32B]
# ===================================================================
print("=" * 70)
print("仮説 B: [IV:16B][CT:48B][HMAC:32B]")
print(" IV = key336[0:16]")
print(" CT = key336[16:64]")
print(" HMAC = key336[64:96]")
print("=" * 70)
IV_B = KEY336[:16]
CT_B = KEY336[16:64]
HMAC_B = KEY336[64:96]
print(f" IV: {IV_B.hex()}")
print(f" CT: {CT_B.hex()}")
print(f" HMAC: {HMAC_B.hex()}")
print()
for wrap_name, wrap_key in WRAP_KEY_CANDIDATES:
pt = aes_cbc_decrypt(wrap_key, IV_B, CT_B)
if pt is None:
continue
pt_unpad = pkcs7_unpad(pt)
if pt_unpad is None:
continue
label = f"HypB(wrap={wrap_name})"
check_plaintext(label, pt_unpad)
# HMAC 検証: 候補 sign_key で HMAC(IV||CT) を計算して HMAC_B と比較
if any(pt_unpad[:16] == enc for enc in KNOWN_ENC_KEYS):
for sign_name, sign_key in SIGN_KEY_CANDIDATES:
if verify_hmac(sign_key, IV_B + CT_B, HMAC_B):
msg = f"[HMAC_VERIFIED] HypB wrap={wrap_name} sign={sign_name}"
print(msg)
found.append(msg)
# ===================================================================
# Section 2: 仮説 A — [CT:64B][HMAC:32B] (IV=zeros)
# ===================================================================
print()
print("=" * 70)
print("仮説 A: [CT:64B][HMAC:32B] — IV=zeros_16")
print(" CT = key336[0:64]")
print(" HMAC = key336[64:96]")
print("=" * 70)
CT_A = KEY336[:64]
HMAC_A = KEY336[64:96]
for wrap_name, wrap_key in WRAP_KEY_CANDIDATES:
pt = aes_cbc_decrypt(wrap_key, b"\x00" * 16, CT_A)
if pt is None:
continue
pt_unpad = pkcs7_unpad(pt)
if pt_unpad is None:
continue
label = f"HypA(wrap={wrap_name})"
check_plaintext(label, pt_unpad)
# ===================================================================
# Section 3: 仮説 A2 — [CT:64B][HMAC:32B] (IV=CT[:16])
# ===================================================================
print()
print("=" * 70)
print("仮説 A2: [CT:64B][HMAC:32B] — IV=CT[:16] (先頭 16B を IV として再利用)")
print("=" * 70)
for wrap_name, wrap_key in WRAP_KEY_CANDIDATES:
pt = aes_cbc_decrypt(wrap_key, CT_A[:16], CT_A[16:])
if pt is None:
continue
pt_unpad = pkcs7_unpad(pt)
if pt_unpad is None:
continue
label = f"HypA2(wrap={wrap_name})"
check_plaintext(label, pt_unpad)
# ===================================================================
# Section 4: 仮説 C — [IV:16B][CT:64B][HMAC:16B]
# key 33.6 が 96 bytes では収まらないため、代わりに CT を 48B として残り 16B が HMAC[:16]
# ===================================================================
print()
print("=" * 70)
print("仮説 C: [IV:16B][CT:48B][HMAC:32B] — wrap_key に enc_key_1 を使用")
print(" (Phase 4 では enc_key_1 が wrap_key として使われた)")
print("=" * 70)
# Phase 4 で確認済み: enc_key_1 が AES-CBC 復号鍵として機能
# 同じパターンが Phase 2 にも当てはまるか確認
for iv_val, iv_name in [
(IV_B, "key336[:16]"),
(b"\x00" * 16, "zeros"),
(SERVER_NONCE, "SERVER_NONCE"),
(CLIENT_NONCE, "CLIENT_NONCE"),
]:
pt = aes_cbc_decrypt(ENC_KEY_1, iv_val, CT_B)
if pt is None:
continue
pt_unpad = pkcs7_unpad(pt)
if pt_unpad is None:
continue
label = f"HypC(wrap=enc_key_1,iv={iv_name})"
check_plaintext(label, pt_unpad)
print(f" enc_key_1 wrap: iv={iv_name} pt(raw)={pt.hex()}")
# ===================================================================
# Section 5: 全 appboot レスポンスファイルへの適用
# ===================================================================
print()
print("=" * 70)
print("全 appboot レスポンスファイルへの適用")
print("=" * 70)
import gzip
try:
import cbor2
RAWS_DIR = Path("/home/vscode/app/raws/ios/20260408/raw")
res_files = sorted(RAWS_DIR.glob("res_*appboot*.bin"))
print(f"Found {len(res_files)} appboot response files")
unique_key336: dict[str, str] = {} # hex -> filename
for res_file in res_files:
raw = res_file.read_bytes()
if raw[:2] == b"\x1f\x8b":
try:
raw = gzip.decompress(raw)
except Exception:
continue
try:
obj = cbor2.loads(raw)
except Exception:
continue
k33 = obj.get(33)
if k33 is None:
continue
try:
if isinstance(k33, bytes):
inner = cbor2.loads(k33)
elif isinstance(k33, dict):
inner = k33
else:
continue
except Exception:
continue
k6 = inner.get(6)
if not isinstance(k6, bytes) or len(k6) != 96:
continue
hex_val = k6.hex()
if hex_val not in unique_key336:
unique_key336[hex_val] = res_file.name
print(f"Unique key 33.6 values (96B): {len(unique_key336)}")
for hex_val, fname in unique_key336.items():
blob = bytes.fromhex(hex_val)
iv = blob[:16]
ct48 = blob[16:64]
hmac32 = blob[64:96]
print(f"\n File: {fname}")
print(f" IV: {iv.hex()}")
print(f" CT: {ct48.hex()}")
for wrap_name, wrap_key in WRAP_KEY_CANDIDATES:
pt = aes_cbc_decrypt(wrap_key, iv, ct48)
if pt is None:
continue
pt_unpad = pkcs7_unpad(pt)
if pt_unpad is None:
continue
label = f"File[{fname}] HypB(wrap={wrap_name})"
check_plaintext(label, pt_unpad)
except ImportError:
print("[skip] cbor2 not available")
# ===================================================================
# Section 6: HMAC 署名の検証 (独立)
# ===================================================================
print()
print("=" * 70)
print("HMAC 署名の検証 — どの鍵で HMAC(IV||CT) == key336[64:96] になるか")
print("=" * 70)
for sign_name, sign_key in SIGN_KEY_CANDIDATES:
# 仮説 B: HMAC over IV||CT
if verify_hmac(sign_key, IV_B + CT_B, HMAC_B):
msg = f"[HMAC_MATCH HypB IV||CT] sign={sign_name}"
print(msg)
found.append(msg)
# HMAC over CT only
if verify_hmac(sign_key, CT_B, HMAC_B):
msg = f"[HMAC_MATCH CT_only] sign={sign_name}"
print(msg)
found.append(msg)
# HMAC over CT_A (full 64B)
if verify_hmac(sign_key, CT_A, HMAC_A):
msg = f"[HMAC_MATCH HypA CT64] sign={sign_name}"
print(msg)
found.append(msg)
# HMAC over entire 96 bytes
if len(KEY336) >= 64 + 32:
if verify_hmac(sign_key, KEY336[:64], KEY336[64:96]):
msg = f"[HMAC_MATCH full96[:64]] sign={sign_name}"
print(msg)
found.append(msg)
# ===================================================================
# Section 7: 平文構造の仮説追加 — enc_key || sign_key ではなく別の構造
# ===================================================================
print()
print("=" * 70)
print("平文構造の代替仮説テスト")
print(" 平文が enc_key(16B) + sign_key(32B) ではなく別の構造の可能性")
print("=" * 70)
for wrap_name, wrap_key in WRAP_KEY_CANDIDATES:
# 仮説 B の復号
pt_b = aes_cbc_decrypt(wrap_key, IV_B, CT_B)
if pt_b is None:
continue
# パディングなしで直接確認
if len(pt_b) == 48:
# enc_key はどこかにある?
for offset in range(0, 33):
for enc in KNOWN_ENC_KEYS:
if pt_b[offset : offset + 16] == enc:
msg = (
f"[ENC_AT_OFFSET_{offset}] wrap={wrap_name} enc={enc.hex()[:8]}"
)
print(msg)
found.append(msg)
# ===================================================================
# 結果サマリー
# ===================================================================
print()
print("=" * 70)
print("結果サマリー")
print("=" * 70)
if found:
print(f"一致 ({len(found)} 件):")
for item in found:
print(f" {item}")
else:
print("一致なし")
print()
print("考察:")
print()
print(" 1. enc_key_0 (0817065e) と enc_key_1 (97b99f4e) のいずれも")
print(" key 33.6 の復号で得られなかった。")
print()
print(" 2. 可能性 A: DH 共有秘密とセッション鍵のペアリングが誤っている")
print(" (別セッションのデータが混在している)")
print()
print(" 3. 可能性 B: wrap_key が DH 共有秘密から標準 KDF では導出されない")
print(" NFWebCrypto 内部の TFIT ホワイトボックス変換が関与")
print()
print(" 4. 可能性 C: key 33.6 の構造が [IV:16][CT:48][HMAC:32] ではなく")
print(" 全く異なる (例: プロトコル独自の AES Key Wrap)")
print()
print("推奨アクション:")
print(" - Frida で AES_set_decrypt_key の直前の caller を特定する")
print(
" - AES_set_decrypt_key に渡される鍵と key 33.6 の IV/CT を同時キャプチャする"
)
print(" - キャプチャした wrap_key を此処の WRAP_KEY_CANDIDATES に追加する")