mirror of
https://github.com/qtmleap/revkit.git
synced 2026-09-30 06:51:50 +02:00
feat(crypto): implement build_session_region() with TFIT-WB-AES DH key encryption
Add session_region builder for key 33.6 scheme_data (plaintext[128:300] = 172B). Emits 7B constant CBOR prefix + 128B TFIT-WB-AES-ECB(DH_pub_key) + 37B zero-filled MGK tail (CBOR encoding TBD). Falls back to bytes(172) if NFWebCrypto binary absent. Update test_appboot_e2e.py to call build_session_region() instead of hardcoded zeros. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9be376736d
commit
455de1ebee
@@ -119,6 +119,11 @@ IOS_SHARKBOOT_KEY_DER = bytes.fromhex(
|
||||
"7591de8897f6764ff4ad1fb552"
|
||||
)
|
||||
|
||||
# key 33.6 session_region の先頭 7B CBOR プレフィックス (iPhone デバイス共通)
|
||||
# 実測: 全 352B appboot キャプチャで session_region[0:7] が全セッションで同一。
|
||||
# CBOR データストリームの継続部分 — ヘッダーと TFIT データの間のフレーム。
|
||||
IOS_KEY336_SESSION_REGION_PREFIX: bytes = bytes.fromhex("6260c8a117cf31")
|
||||
|
||||
# key 33.6 scheme_data の固定デバイスヘッダー (128B)
|
||||
# 180 個の 352B appboot サンプルのうち 165 個 (標準 iPhone) で共通の定数。
|
||||
# plaintext[0:128] の値 (XOR 復号後)。
|
||||
|
||||
@@ -25,6 +25,7 @@ from netflix_msl.constants import (
|
||||
IOS_KDF_NONCE,
|
||||
IOS_KDF_PSK,
|
||||
IOS_KEY336_DEVICE_HEADER,
|
||||
IOS_KEY336_SESSION_REGION_PREFIX,
|
||||
RSA_KEYPAIR_ID,
|
||||
)
|
||||
|
||||
@@ -478,6 +479,113 @@ class NetflixCrypto:
|
||||
prk = hmac_mod.new(mgk, IOS_KDF_PSK, hashlib.sha256).digest()
|
||||
return hmac_mod.new(prk, IOS_KDF_NONCE, hashlib.sha256).digest()
|
||||
|
||||
# ---- key 33.6 session_region 構築 (TFIT-WB-AES-128-ECB) ----
|
||||
|
||||
@staticmethod
|
||||
def build_session_region(
|
||||
dh_pub_key: bytes,
|
||||
enc_key_0: bytes,
|
||||
sign_key_0: bytes,
|
||||
) -> bytes:
|
||||
"""key 33.6 の session_region (172B) を TFIT エミュレーションで構築する.
|
||||
|
||||
session_region (172B) の構成:
|
||||
[0:7] 7B CBOR プレフィックス (iPhone デバイス共通定数)
|
||||
[7:135] 128B TFIT-WB-AES-128-ECB(DH_pub_key) — 8 ブロック × 16B
|
||||
[135:172] 37B MGK テール — TFIT 暗号化 enc_key_0/sign_key_0 と CBOR フレーム
|
||||
※ 詳細な CBOR エンコーディングは未解明のためゼロ埋め
|
||||
|
||||
TFIT エミュレーションの前提:
|
||||
- NFWebCrypto.framework バイナリが
|
||||
/tmp/nfwc/Payload/Argo.app/Frameworks/NFWebCrypto.framework/NFWebCrypto
|
||||
に存在する必要がある。
|
||||
- バイナリが存在しない場合は 172B ゼロ埋めにフォールバック (警告表示)。
|
||||
|
||||
Args:
|
||||
dh_pub_key: DH 公開鍵 (128 bytes, big-endian)
|
||||
enc_key_0: Phase 0 MGK 暗号化鍵 (16 bytes)
|
||||
sign_key_0: Phase 0 MGK 署名鍵 (32 bytes)
|
||||
|
||||
Returns:
|
||||
172 bytes の session_region
|
||||
|
||||
Raises:
|
||||
ValueError: dh_pub_key が 128B でない場合
|
||||
ValueError: enc_key_0 が 16B でない場合
|
||||
ValueError: sign_key_0 が 32B でない場合
|
||||
"""
|
||||
if len(dh_pub_key) != 128:
|
||||
raise ValueError(f"dh_pub_key must be 128 bytes, got {len(dh_pub_key)}")
|
||||
if len(enc_key_0) != 16:
|
||||
raise ValueError(f"enc_key_0 must be 16 bytes, got {len(enc_key_0)}")
|
||||
if len(sign_key_0) != 32:
|
||||
raise ValueError(f"sign_key_0 must be 32 bytes, got {len(sign_key_0)}")
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BINARY_PATH = Path(
|
||||
"/tmp/nfwc/Payload/Argo.app/Frameworks/NFWebCrypto.framework/NFWebCrypto"
|
||||
)
|
||||
|
||||
if not BINARY_PATH.exists():
|
||||
print(
|
||||
" [WARN] build_session_region: NFWebCrypto binary not found at "
|
||||
f"{BINARY_PATH}. session_region はゼロ埋めにフォールバック。"
|
||||
)
|
||||
return bytes(172)
|
||||
|
||||
try:
|
||||
# tools/emulate_tfit.py をモジュールとしてインポート
|
||||
tools_dir = str(Path(__file__).resolve().parent.parent.parent / "tools")
|
||||
if tools_dir not in sys.path:
|
||||
sys.path.insert(0, tools_dir)
|
||||
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"emulate_tfit", Path(tools_dir) / "emulate_tfit.py"
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError("emulate_tfit.py のロードに失敗")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod) # type: ignore[arg-type]
|
||||
|
||||
import lief
|
||||
|
||||
binary_data = BINARY_PATH.read_bytes()
|
||||
binary = lief.MachO.parse(str(BINARY_PATH)).at(0)
|
||||
|
||||
emu = mod.TFITEmulator(binary_data, binary)
|
||||
ks = mod.load_key_schedule(mod.MGK_TYPE_IPHONE, binary_data)
|
||||
|
||||
# TFIT-WB-AES-128-ECB: 8 ブロック × 16B = 128B
|
||||
tfit_dh_pub = bytearray()
|
||||
for i in range(8):
|
||||
block = dh_pub_key[i * 16 : (i + 1) * 16]
|
||||
tfit_dh_pub.extend(emu.encrypt_block(ks, block))
|
||||
|
||||
except Exception as e:
|
||||
print(
|
||||
f" [WARN] build_session_region: TFIT エミュレーション失敗 ({e}). "
|
||||
"session_region はゼロ埋めにフォールバック。"
|
||||
)
|
||||
return bytes(172)
|
||||
|
||||
# session_region[135:172] = 37B MGK テール
|
||||
# 構成: CBOR フレーム + TFIT(enc_key_0) + TFIT(sign_key_0[:16]) の一部
|
||||
# ※ 正確な CBOR エンコーディングは未解明のためゼロ埋め
|
||||
# TODO: MGK テールの正確な CBOR エンコーディングを解明して実装する
|
||||
mgk_tail = bytes(37)
|
||||
|
||||
result = (
|
||||
IOS_KEY336_SESSION_REGION_PREFIX # 7B: 定数 CBOR プレフィックス
|
||||
+ bytes(tfit_dh_pub) # 128B: TFIT(DH_pub_key)
|
||||
+ mgk_tail # 37B: MGK テール (未解明)
|
||||
)
|
||||
assert len(result) == 172, f"session_region length {len(result)} != 172"
|
||||
return result
|
||||
|
||||
# ---- key 33.6 scheme_data 構築 (Scheme 3 / appboot) ----
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -460,15 +460,32 @@ def run_e2e_test(
|
||||
print(f" DH pub_key: {dh_pub_key[:16].hex()}... ({len(dh_pub_key)}B)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# session_region を DH 公開鍵からゼロパディングで仮構築
|
||||
# session_region を TFIT-WB-AES で構築 (NFWebCrypto.framework が必要)
|
||||
# ------------------------------------------------------------------
|
||||
# TODO: 本来は TFIT-WB-AES (tools/emulate_tfit.py の session_region 導出) が必要。
|
||||
# 現在は 172B のゼロ埋めプレースホルダーを使用。
|
||||
# このため key 33.6 の内容は正しくなく、サーバーは鍵交換を拒否する。
|
||||
# Phase 2 以降の DH 鍵合意は成立しない。
|
||||
# 正式フローでは emulate_tfit.py で DH 公開鍵を TFIT-WB-AES 暗号化し
|
||||
# session_region (172B) に格納する。
|
||||
session_region_placeholder = b"\x00" * 172
|
||||
# TFIT エミュレーションで DH 公開鍵を WB-AES-128-ECB 暗号化して session_region を構築。
|
||||
# NFWebCrypto バイナリが存在しない場合は 172B ゼロ埋めにフォールバック。
|
||||
# NOTE: session_region[135:172] (37B MGK テール) は CBOR エンコーディングが未解明のため
|
||||
# 現状はゼロ埋め。サーバーが鍵交換を拒否する可能性がある。
|
||||
print()
|
||||
print("[Phase 1a'] session_region を TFIT エミュレーションで構築中...")
|
||||
session_region = NetflixCrypto.build_session_region(
|
||||
dh_pub_key=dh_pub_key,
|
||||
enc_key_0=enc_key_0,
|
||||
sign_key_0=sign_key_0,
|
||||
)
|
||||
is_zero_filled = session_region == bytes(172)
|
||||
if is_zero_filled:
|
||||
print(
|
||||
" session_region: ゼロ埋め (TFIT バイナリ未検出またはエミュレーション失敗)"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f" session_region: TFIT 暗号化済み {session_region[:7].hex()}..."
|
||||
f" ({len(session_region)}B)"
|
||||
)
|
||||
print(f" prefix (7B): {session_region[:7].hex()}")
|
||||
print(f" TFIT[0] (16B): {session_region[7:23].hex()}")
|
||||
print(f" TFIT[-1] (16B): {session_region[119:135].hex()}")
|
||||
|
||||
# セパレータは実測キャプチャから取得した既知の値を使用
|
||||
# (セッション固有値のため、実際の接続では Frida キャプチャが必要)
|
||||
@@ -489,7 +506,7 @@ def run_e2e_test(
|
||||
)
|
||||
|
||||
key_request_bytes, k9_xor_nonce, nonce_7b = build_key_request_data(
|
||||
session_region=session_region_placeholder,
|
||||
session_region=session_region,
|
||||
s1=s1,
|
||||
s2=s2,
|
||||
s3=s3,
|
||||
|
||||
Reference in New Issue
Block a user