Files
revkit/tools/find_wrap_key.py
T
tkgstrator 03e4754243 feat(tweak): add HKDF hook and remove frida/proxyman skills
Hooks HKDF_extract and HKDF_expand in the tweak for improved key derivation
logging and analysis. Removes Frida and Proxyman skills, references, and
documentation for a more focused Agent Team workflow.

Adds several Python tools for systematic HKDF and session key derivation
analysis, supporting ongoing reverse engineering and debugging efforts.

Switches all agent workflow documentation and prompts to English, clarifies
inter-agent communication constraints, and updates the workflow to reflect
current team structure and supported features.

Relates to the need for more accurate key extraction and simplified agent
usage.
2026-04-08 10:32:32 +00:00

248 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""
appboot response の key33.6 (96 bytes) から enc_key を逆算
19時台のセッションの key33.6 を AES-CBC で復号して enc_key を得るための wrap_key を特定する
ログから判明した値:
DH pub_key (new session): 9349f3...
DH shared_secret: 052a8b...
enc_key (target): 97b99f4e...
hmac_key (target): d45443fa...
19時台のキャプチャがないため、07時台のどのレスポンスが対応するか不明
→ 全 key33.6 を全候補鍵で試す
また、DH pub_key から req を特定する試み:
req.key33.6 (464 bytes) に DH pub_key が含まれているはず
ただし RSA-4096 暗号化されているので見えない
→ req の client nonce (16 bytes) から server nonce 付きで hash して wrap_key を導出?
"""
import gzip
import hashlib
import hmac as hmac_mod
from pathlib import Path
import cbor2
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, HKDFExpand
RAWS_DIR = Path("/home/vscode/app/raws/ios/20260408/raw")
# 19時セッションの情報
DH_PRIV_NEW = bytes.fromhex(
"4c61046cb05493fcea0b316122849fe0"
"21cac93bc3858c8c6b43a1fc8329b9be"
"4ac7d73da8253e491c1b0c8cabf1f614"
"1144b8cd448821636ceec04105d88b2e"
"d6a2180994ee41a7be1a7eb0c4f8b866"
"bc2796e08dd9d2c4b393727e915fa680"
"dfef7b8115f2a7b3416418048c0a9c9b"
"4c0ccc1a7a79d38e3cdd6e9f0539020f"
)
DH_PUB_NEW = bytes.fromhex(
"9349f329e100d6fb1a9eb2e81666af56"
"32c408bb395f56191d1a125a0d1675b1"
"a83d623f00dd7e06b4dd296083fd29ee"
"c44bb7805f6bb0b7ff90e6417eb63ece"
"b05840dd9e5f1d5991a59997ca866ef2"
"b6d767418fe147656d4a32b7c2e24b95"
"1952c423291b14a00d245915460585d5"
"f93f98ef8755873e9792ea0534be15e5"
)
DH_SHARED_NEW = bytes.fromhex(
"052a8bfe9f1a1a9becdd67672338191b"
"d7b5aff7fffe1f4cfbd97a0b14f8d59a"
"f54697a0bc1cf96ad6f6e84af98ffd1c"
"ebbc0fb5b04360878710f215f1261129"
"652808fd11f5164d84a501b40e63ba91"
"fcdcc932b6dbd61017099673f552db6e"
"94ff19934bfdef21b9701c4c9d312b78"
"f2cb8911e446313c2cb0567f7de865b3"
)
TARGET_ENC = bytes.fromhex("97b99f4e88e8e73779aa20ac11877c5d")
TARGET_HMAC = bytes.fromhex("d45443fa11efec622c83b27c55f7a73143bdfa0d51820ac597b9e3fb5c28dbb0")
# ログで記録された全ての 32-byte HMAC 鍵 (DH後)
LOGGED_HMAC_KEYS_AFTER_DH = [
"a4333e99a34eef3663f8e38e217e696949cd3bf57598c5c260fedb8997afa82b",
"d45443fa11efec622c83b27c55f7a73143bdfa0d51820ac597b9e3fb5c28dbb0",
"b319e54dce7117932439aad5e1dfed1e8d18fe033aabe00ab786dc4239e8dd85",
]
print(f"DH pub new: {DH_PUB_NEW.hex()[:20]}...")
print(f"DH shared new: {DH_SHARED_NEW.hex()[:20]}...")
print(f"Target enc_key: {TARGET_ENC.hex()}")
print(f"Target hmac_key: {TARGET_HMAC.hex()}")
print()
# 07時台の appboot req から DH pub_key が 9349f3... に一致するものを探す
# (RSA 暗号化されているため直接は見えないが、同一 DH グループなら p と g が一致するはず)
# 全 req の key33 sub-key を確認
print("=" * 70)
print("07時台の appboot req から DH 鍵の手がかりを探す")
print("=" * 70)
def try_decode_cbor(data: bytes) -> dict | None:
if data[:2] == b"\x1f\x8b":
try:
data = gzip.decompress(data)
except Exception:
return None
try:
return cbor2.loads(data)
except Exception:
return None
def aes_cbc_decrypt_try(key: bytes, iv: bytes, ct: bytes) -> bytes | 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:
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]
# 全 wrap_key 候補 (128-bit AES 用)
sha256_new = hashlib.sha256(DH_SHARED_NEW).digest()
sha384_new = hashlib.sha384(DH_SHARED_NEW).digest()
sha512_new = hashlib.sha512(DH_SHARED_NEW).digest()
WRAP_KEY_CANDIDATES_16: list[tuple[str, bytes]] = [
("sha256[:16]", sha256_new[:16]),
("sha256[16:32]", sha256_new[16:32]),
("sha384[:16]", sha384_new[:16]),
("sha384[16:32]", sha384_new[16:32]),
("sha384[32:48]", sha384_new[32:48]),
("sha512[:16]", sha512_new[:16]),
("sha512[16:32]", sha512_new[16:32]),
("sha512[32:48]", sha512_new[32:48]),
("sha512[48:64]", sha512_new[48:64]),
("shared[:16]", DH_SHARED_NEW[:16]),
("shared[16:32]", DH_SHARED_NEW[16:32]),
]
# HKDF 候補
HASH_ALGS = {
"SHA256": hashes.SHA256(),
"SHA384": hashes.SHA384(),
}
for hash_name, hash_alg in HASH_ALGS.items():
for salt in [None, b"", b"\x00" * 32]:
for info in [b"", b"wrap", b"enc", b"session", b"MSL"]:
try:
hkdf = HKDF(algorithm=hash_alg, length=16, salt=salt, info=info)
k = hkdf.derive(DH_SHARED_NEW)
label = f"HKDF({hash_name},s={repr(salt)[:10]},i={repr(info)})"
WRAP_KEY_CANDIDATES_16.append((label, k))
except Exception:
pass
# logged HMAC keys の先頭 16B も候補として追加
for hex_key in LOGGED_HMAC_KEYS_AFTER_DH:
kb = bytes.fromhex(hex_key)
WRAP_KEY_CANDIDATES_16.append((f"logged_hmac_{hex_key[:8]}[:16]", kb[:16]))
WRAP_KEY_CANDIDATES_16.append((f"logged_hmac_{hex_key[:8]}[16:32]", kb[16:32]))
print(f"Total wrap_key candidates: {len(WRAP_KEY_CANDIDATES_16)}")
print()
# 全 appboot レスポンスの key33.6 で試す
matches_found = []
for res_file in sorted(RAWS_DIR.glob("res_*appboot*.bin")):
raw = res_file.read_bytes()
if raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
try:
obj = try_decode_cbor(raw)
if obj is None:
continue
k33 = obj.get(33)
if isinstance(k33, bytes):
inner = cbor2.loads(k33)
elif isinstance(k33, dict):
inner = k33
else:
continue
k6 = inner.get(6)
if not isinstance(k6, bytes) or len(k6) != 96:
continue
# 仮説 C: IV(16) + CT(48) + HMAC(32)
iv = k6[:16]
ct_48 = k6[16:64]
for wrap_name, wrap_key in WRAP_KEY_CANDIDATES_16:
pt = aes_cbc_decrypt_try(wrap_key, iv, ct_48)
if pt is None:
continue
pt_unpad = pkcs7_unpad(pt)
if pt_unpad is None:
continue
if len(pt_unpad) >= 16 and pt_unpad[:16] == TARGET_ENC:
msg = f"[MATCH] {res_file.name}: wrap_key={wrap_name} pt={pt_unpad.hex()}"
print(msg)
matches_found.append(msg)
# 仮説 B: CT(64) + HMAC(32), IV=zeros or IV=first block
ct_64 = k6[:64]
for iv_candidate, iv_name in [(b"\x00" * 16, "zeros"), (k6[:16], "k6[:16]")]:
for wrap_name, wrap_key in WRAP_KEY_CANDIDATES_16:
pt = aes_cbc_decrypt_try(wrap_key, iv_candidate, ct_64)
if pt is None:
continue
pt_unpad = pkcs7_unpad(pt)
if pt_unpad is None:
continue
if len(pt_unpad) >= 16 and pt_unpad[:16] == TARGET_ENC:
msg = f"[MATCH-B] {res_file.name}: iv={iv_name} wrap_key={wrap_name}"
print(msg)
matches_found.append(msg)
except Exception:
pass
print()
print("=" * 70)
print("結果")
print("=" * 70)
if matches_found:
for m in matches_found:
print(f" {m}")
else:
print("一致なし")
print()
print("【最終仮説】")
print("NFWebCrypto は DH shared_secret から MSL セッション鍵を")
print("HKDF 以外の方法 (独自実装または OpenSSL 独自 API) で導出している")
print()
print("可能性1: PKCS#3 DH Agreed Value として共有秘密を使い,")
print(" その後 AES Key Wrap (RFC 3394) で appboot レスポンスから")
print(" セッション鍵を取り出す")
print()
print("可能性2: ECDH (楕円曲線) が DH の代わりに使われていて,")
print(" shared_secret の計算方法が異なる")
print()
print("可能性3: Netflix MSL の scheme 5 固有の DH 鍵導出関数が")
print(" OpenSSL の HMAC を使わない内部実装を持つ")
print()
print("→ NFWebCrypto の HKDF エクスポートシンボルを直接フックする")
print(" または Frida で AES_set_encrypt_key のコールスタックを取得する")
print(" ことで、鍵導出の入力パラメータを特定できる可能性がある")