#!/usr/bin/env python3 """ appboot レスポンスの CBOR を解析して key 33.6 を取り出し、 DH shared_secret を使ってセッション鍵の復号を試みる msl_keys.json の DH 鍵ペアとサーバーレスポンスのペアリングも調べる """ import gzip import hashlib import json import struct 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") KEYS_FILE = Path("/home/vscode/app/raws/msl_keys.json") with open(KEYS_FILE) as f: keys = json.load(f) DH_SHARED = bytes.fromhex(keys["dh_shared_secret"]) DH_PUB = int(keys["dh_pub_key"], 16) DH_P = int(keys["dh_p"], 16) DH_G = int(keys["dh_g"], 16) DH_PRIV = int(keys["dh_priv_key"], 16) ALL_ENC_UNIQUE = list(dict.fromkeys(e["key"] for e in keys["aes_key_history"])) ALL_HMAC_UNIQUE = list(dict.fromkeys(e["key"] for e in keys["hmac_key_history"])) print(f"DH pub key: {keys['dh_pub_key'][:20]}...") print(f"DH shared: {keys['dh_shared_secret'][:20]}...") print() def try_decode_cbor(data: bytes) -> dict | None: """CBOR デコード試行 (gzip 展開も試みる)""" # gzip 展開 if data[:2] == b"\x1f\x8b": try: data = gzip.decompress(data) except Exception: pass try: return cbor2.loads(data) except Exception: return None def aes_cbc_decrypt_nopad(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] # appboot レスポンスファイルをすべて処理 appboot_res_files = sorted(RAWS_DIR.glob("res_*appboot*.bin")) print(f"appboot response files: {len(appboot_res_files)}") print() # key 33.6 の値を収集してユニークなものを調べる key_33_6_values: dict[str, str] = {} # hex -> filename for res_file in appboot_res_files: raw = res_file.read_bytes() obj = try_decode_cbor(raw) if obj is None: continue # key 33 を探す key33 = obj.get(33) if key33 is None: continue # key 33.6 (key_response_data) を取り出す inner = None try: if isinstance(key33, bytes): inner = cbor2.loads(key33) elif isinstance(key33, dict): inner = key33 except Exception: continue if inner is None: continue key33_6 = inner.get(6) if key33_6 is None: continue if isinstance(key33_6, bytes): hex_val = key33_6.hex() key_33_6_values[hex_val] = res_file.name print(f"Unique key 33.6 values: {len(key_33_6_values)}") for hex_val, fname in list(key_33_6_values.items())[:5]: print(f" {fname}: {hex_val[:32]}... ({len(bytes.fromhex(hex_val))} bytes)") print() # key 33.6 の各値を解析 print("=" * 70) print("key 33.6 の解析") print("=" * 70) # DH shared_secret から鍵候補を生成 sha256_ss = hashlib.sha256(DH_SHARED).digest() sha384_ss = hashlib.sha384(DH_SHARED).digest() sha512_ss = hashlib.sha512(DH_SHARED).digest() # HKDF 候補 HASH_ALGS = { "SHA-256": hashes.SHA256(), "SHA-384": hashes.SHA384(), "SHA-512": hashes.SHA512(), } hkdf_candidates: dict[str, bytes] = {} for hash_name, hash_alg in HASH_ALGS.items(): for salt in [None, b"", b"\x00" * 32]: for info in [b"", b"enc", b"session", b"MSL", b"Netflix"]: try: hkdf = HKDF( algorithm=hash_alg, length=16, salt=salt, info=info, ) k = hkdf.derive(DH_SHARED) label = f"HKDF({hash_name},salt={salt!r}[:4],info={info!r})" hkdf_candidates[label] = k except Exception: pass wrap_key_candidates: list[tuple[str, bytes]] = [ ("SHA256(shared)[:16]", sha256_ss[:16]), ("SHA256(shared)[16:32]", sha256_ss[16:32]), ("SHA384(shared)[:16]", sha384_ss[:16]), ("SHA384(shared)[16:32]", sha384_ss[16:32]), ("SHA512(shared)[:16]", sha512_ss[:16]), ("SHA512(shared)[16:32]", sha512_ss[16:32]), ("shared[:16]", DH_SHARED[:16]), ("shared[16:32]", DH_SHARED[16:32]), ("zeros_16", b"\x00" * 16), *[(label, k) for label, k in hkdf_candidates.items()], ] for hex_val, fname in key_33_6_values.items(): data = bytes.fromhex(hex_val) print(f"\nFile: {fname} ({len(data)} bytes)") print(f"Data: {data.hex()}") if len(data) == 96: print("→ 96 bytes: 試行 [IV(16)+CT(64)+HMAC(16)] or [CT(64)+HMAC(32)]") # 仮説 A: IV(16) + CT(64) + HMAC(16) - ただし HMAC は 32 bytes が標準 # 仮説 B: CT(64) + HMAC(32) # 仮説 C: IV(16) + CT(48) + HMAC(32) # 仮説 C: IV(16) + CT(48) + HMAC(32) iv = data[:16] ct_48 = data[16:64] hmac_32 = data[64:96] # ct_64 = data[0:64] # hmac_alt = data[64:96] for wrap_name, wrap_key in wrap_key_candidates: pt = aes_cbc_decrypt_nopad(wrap_key, iv, ct_48) if pt: pt_unpad = pkcs7_unpad(pt) if pt_unpad and len(pt_unpad) == 32: enc_candidate = pt_unpad[:16] hmac_candidate = pt_unpad[16:32] for enc_hex in ALL_ENC_UNIQUE: if bytes.fromhex(enc_hex) == enc_candidate: print(f" [MATCH enc] wrap_key={wrap_name}") print(f" enc={enc_candidate.hex()} hmac={hmac_candidate.hex()}") for h_hex in ALL_HMAC_UNIQUE: if bytes.fromhex(h_hex)[:16] == hmac_candidate: print(f" [MATCH hmac[:16]] wrap_key={wrap_name}") if pt_unpad and len(pt_unpad) == 48: enc_candidate = pt_unpad[:16] hmac_candidate = pt_unpad[16:48] for enc_hex in ALL_ENC_UNIQUE: if bytes.fromhex(enc_hex) == enc_candidate: print(f" [MATCH enc(48)] wrap_key={wrap_name}") print(f" enc={enc_candidate.hex()} hmac={hmac_candidate.hex()}") # 仮説 B: CT(64) + HMAC(32) ct_64 = data[:64] for wrap_name, wrap_key in wrap_key_candidates: for iv_candidate, iv_name in [ (b"\x00" * 16, "zeros_16"), (data[:16], "data[:16]"), ]: pt = aes_cbc_decrypt_nopad(wrap_key, iv_candidate, ct_64) if pt: pt_unpad = pkcs7_unpad(pt) if pt_unpad and 32 <= len(pt_unpad) <= 48: for enc_hex in ALL_ENC_UNIQUE: if len(pt_unpad) >= 16 and bytes.fromhex(enc_hex) == pt_unpad[:16]: print(f" [MATCH enc B] wrap_key={wrap_name} iv={iv_name}") print(f" pt={pt_unpad.hex()}") print() print("=" * 70) print("appboot req の client DH pub_key と msl_keys.json の dh_pub_key 比較") print("=" * 70) stored_pub = keys["dh_pub_key"] print(f"msl_keys dh_pub_key: {stored_pub[:20]}...") appboot_req_files = sorted(RAWS_DIR.glob("req_*appboot*.bin")) for req_file in appboot_req_files[:5]: raw = req_file.read_bytes() obj = try_decode_cbor(raw) if obj is None: continue key33 = obj.get(33) if key33 is None: continue inner = None try: if isinstance(key33, bytes): inner = cbor2.loads(key33) elif isinstance(key33, dict): inner = key33 except Exception: continue if inner is None: continue key33_6 = inner.get(6) print(f"\n{req_file.name}: key33.6 = {key33_6.hex()[:20] if isinstance(key33_6, bytes) else key33_6}... ({len(key33_6) if isinstance(key33_6, bytes) else '?'} bytes)") # key 33.8 = identity (ESN) key33_8 = inner.get(8) print(f" key33.8 (ESN) = {key33_8}") # key 33.9 = client nonce key33_9 = inner.get(9) if isinstance(key33_9, bytes): print(f" key33.9 (nonce) = {key33_9.hex()}") print() print("=" * 70) print("appboot res の key 33.6 を取り出して DH 由来の鍵でデコード試行 (req とペア)") print("=" * 70) # req_4/res_4 のペア (最初の appboot) for num in ["4", "33", "89", "98"]: req_file = RAWS_DIR / f"req_{num}_appboot_2026-04-08T07-52-22-999Z.bin" res_file = RAWS_DIR / f"res_{num}_appboot_2026-04-08T07-52-22-999Z.bin" if not req_file.exists(): # ファイル名が異なる可能性があるので glob で探す req_files = list(RAWS_DIR.glob(f"req_{num}_appboot*.bin")) res_files = list(RAWS_DIR.glob(f"res_{num}_appboot*.bin")) if req_files: req_file = req_files[0] if res_files: res_file = res_files[0] if not req_file.exists() or not res_file.exists(): continue print(f"\nPair: {req_file.name} / {res_file.name}") # req から client nonce と ESN を取得 req_raw = req_file.read_bytes() req_obj = try_decode_cbor(req_raw) if req_obj is None: print(" req: CBOR decode failed") continue req_inner = None k33 = req_obj.get(33) if isinstance(k33, bytes): try: req_inner = cbor2.loads(k33) except Exception: pass elif isinstance(k33, dict): req_inner = k33 if req_inner: client_nonce = req_inner.get(9) esn = req_inner.get(8) req_6 = req_inner.get(6) if isinstance(client_nonce, bytes): print(f" client nonce: {client_nonce.hex()}") print(f" ESN: {esn}") if isinstance(req_6, bytes): print(f" req key33.6: {req_6.hex()[:32]}... ({len(req_6)} bytes)") # res から server nonce と key33.6 を取得 res_raw = res_file.read_bytes() if res_raw[:2] == b"\x1f\x8b": res_raw = gzip.decompress(res_raw) res_obj = try_decode_cbor(res_raw) if res_obj is None: print(" res: CBOR decode failed") continue res_inner = None k33 = res_obj.get(33) if isinstance(k33, bytes): try: res_inner = cbor2.loads(k33) except Exception: pass elif isinstance(k33, dict): res_inner = k33 if res_inner: server_nonce = res_inner.get(9) scheme = res_inner.get(8) res_6 = res_inner.get(6) if isinstance(server_nonce, bytes): print(f" server nonce: {server_nonce.hex()}") print(f" scheme: {scheme}") if isinstance(res_6, bytes): print(f" res key33.6: {res_6.hex()} ({len(res_6)} bytes)") # msl_keys.json の DH shared secret + nonces で復号試行 if isinstance(client_nonce, bytes) and isinstance(server_nonce, bytes): for hash_name, hash_alg in {"SHA-256": hashes.SHA256(), "SHA-384": hashes.SHA384(), "SHA-512": hashes.SHA512()}.items(): for salt in [None, b"", client_nonce, server_nonce, client_nonce + server_nonce]: for info in [b"", b"enc", b"wrap", b"session", client_nonce, server_nonce]: try: hkdf = HKDF( algorithm=hash_alg, length=16, salt=salt, info=info, ) wrap_k = hkdf.derive(DH_SHARED) iv = res_6[:16] ct = res_6[16:64] pt = aes_cbc_decrypt_nopad(wrap_k, iv, ct) if pt: pt_unpad = pkcs7_unpad(pt) if pt_unpad: for enc_hex in ALL_ENC_UNIQUE: if bytes.fromhex(enc_hex) == pt_unpad[:16]: salt_repr = repr(salt)[:10] info_repr = repr(info) print(f" [MATCH] HKDF(hash={hash_name},salt={salt_repr},info={info_repr}) wrap_key={wrap_k.hex()}") print(f" pt={pt_unpad.hex()}") except Exception: pass