diff --git a/docs/spec/msl_crypto_flow.md b/docs/spec/msl_crypto_flow.md index 39d4f21..876dd58 100644 --- a/docs/spec/msl_crypto_flow.md +++ b/docs/spec/msl_crypto_flow.md @@ -94,8 +94,9 @@ graph TD | DH 秘密鍵 | NFWebCrypto::dhKeyGen (ランタイム生成) | appboot 鍵交換 | | kAppBootKey (RSA-4096) | NFWebCrypto.framework にハードコード | DH パラメータの暗号化 (サーバーへ送信) | | kAppBootEccKey (ECDSA P-256) | NFWebCrypto.framework にハードコード | サーバーレスポンスの署名検証 | -| MSL enc_key (AES-128) | HKDF(DH shared secret) | MSL ペイロードの AES-128-CBC 暗号化/復号 | -| MSL hmac_key (SHA-256) | HKDF(DH shared secret) | MSL ペイロードの HMAC-SHA256 署名/検証 | +| MSL enc_key (AES-128) | 初回: DH 共有秘密から導出 (方法未解明)。更新: HMAC-SHA256 KDF (解明済み) | MSL ペイロードの AES-128-CBC 暗号化/復号 | +| MSL hmac_key (SHA-256) | 初回: 同上。更新: HMAC-SHA256 KDF (解明済み) | MSL ペイロードの HMAC-SHA256 署名/検証 | +| PSK (16B) | DH 共有秘密から導出? TFIT チェーン出力? (未解明) | KDF 鍵更新のマスター鍵 | | AES self-test key | 固定値 `000102...0f` | OpenSSL KAT (Known Answer Test) | ## 現状の制限 @@ -107,8 +108,25 @@ graph TD | Frida Interceptor (attach) | ✓ 動作 | 起動後 attach のため appboot に間に合わない | | Frida enumerateSymbols | ✓ 動作 | アドレス取得可能だが attach タイミングの問題 | +## 解決済み: KDF 鍵更新アルゴリズム (2026-04-08) + +Tweak `AppbootKeyExtract` v39 の HMAC ストリーミングフックにより、 +MSL セッション鍵更新の KDF が完全に解明された。 + +**アルゴリズム**: カスタム HMAC-SHA256 チェーン (標準 HKDF ではない) + +``` +new_enc_key = HMAC-SHA256(HMAC-SHA256(PSK, enc_key), nonce)[:16] +new_sign_key = HMAC-SHA256(HMAC-SHA256(PSK, sign_key), nonce) +``` + +詳細: [msl_kdf_analysis.md](msl_kdf_analysis.md) +Python 実装: `src/netflix_msl/crypto.py` → `NetflixCrypto.kdf_renew()` + ## 次のステップ -1. **Tweak で EVP_CipherInit_ex の IV パラメータを監視** — MSL ペイロード暗号化時に IV が設定されるはずだが、EVP 内部のステートを追跡できていない可能性 -2. **Frida + Tweak 併用** — Tweak で appboot 時の鍵導出をキャプチャし、Frida で後続の MSL ペイロードをキャプチャ -3. **ElleKit の代わりに fishhook / substrate を直接使う** — MSHookFunction が失敗する原因を調査 +1. **PSK の由来特定** — `027617984f6227539a630b897c017d69` がどこから来るかを解明 + - Keychain クリア + 新規セッションでキャプチャ + - TFIT ホワイトボックスチェーンとの関連調査 +2. **初期鍵導出 (DH → 初回セッション鍵)** — `DH_compute_key`/`dhDerive` + 直後の HMAC チェーンをキャプチャ +3. **SSL Pinning バイパス** — Netflix ログインに必要。Frida ベースの手法を検討 diff --git a/docs/spec/msl_kdf_analysis.md b/docs/spec/msl_kdf_analysis.md new file mode 100644 index 0000000..fb77a06 --- /dev/null +++ b/docs/spec/msl_kdf_analysis.md @@ -0,0 +1,178 @@ +# Netflix iOS MSL KDF (Key Derivation Function) 解析 + +解析日: 2026-04-08 +ソース: Tweak `AppbootKeyExtract` v39 HMAC streaming hooks ログ + +--- + +## 1. 概要 + +Netflix iOS アプリの MSL セッション鍵更新は、**標準 HKDF ではなく独自の HMAC-SHA256 チェーン** で実装されている。 + +OpenSSL の EVP HKDF API (`EVP_PKEY_CTX_set_hkdf_*`) は使用されておらず、 +低レベルの `HMAC_Init_ex` / `HMAC_Update` / `HMAC_Final` を直接呼び出している。 + +--- + +## 2. KDF アルゴリズム (鍵更新) + +### 2.1 入力パラメータ + +| パラメータ | サイズ | 説明 | +|-----------|--------|------| +| PSK (Pre-Shared Key) | 16 bytes | マスター鍵。DH 鍵交換時に導出される (§3 参照) | +| enc_key | 16 bytes | 現在の AES-128-CBC 暗号化鍵 | +| sign_key | 32 bytes | 現在の HMAC-SHA256 署名鍵 | +| nonce | 16 bytes | サーバーレスポンス key 33.9 から取得 | + +### 2.2 演算ステップ (6段階) + +``` +Step 1: session_check = HMAC-SHA256(PSK, enc_key || sign_key) +Step 2: session_bind = HMAC-SHA256(session_check, nonce) +Step 3: enc_temp = HMAC-SHA256(PSK, enc_key) +Step 4: new_enc_full = HMAC-SHA256(enc_temp, nonce) + new_enc_key = new_enc_full[:16] +Step 5: sign_temp = HMAC-SHA256(PSK, sign_key) +Step 6: new_sign_key = HMAC-SHA256(sign_temp, nonce) +``` + +### 2.3 出力 + +| 出力 | ステップ | サイズ | +|------|---------|--------| +| new_enc_key | Step 4 の先頭 16 bytes | 16 bytes (AES-128) | +| new_sign_key | Step 6 の全 32 bytes | 32 bytes (HMAC-SHA256) | +| session_bind | Step 2 の全 32 bytes | 32 bytes (セッションバインド検証用?) | + +### 2.4 検証データ + +``` +PSK = 027617984f6227539a630b897c017d69 +enc_key = 0817065e29e6d1c8668473af9e13b3c2 +sign_key = 91f752f76d7ab4c2dc6e5b3ec1c0e5a16864421fe449be5457459602e298ebc1 +nonce = 809f82a7addf548d3ea9dd067ff9bb91 + +Step 1: 19def2f90d06bc8dfd04a19dbd4588d4e7b8aa6ccacb200f9ae6acc49355917d +Step 2: add2d4c818426aee3dfbbbb783a85262ee7c8cc1936013b53d5f4cb53d6baee0 +Step 3: e60e376f37d7d962512aea2f29a353c28b0fb95b1e77c43baf7459b21d1df649 +Step 4: 97b99f4e88e8e73779aa20ac11877c5dfe06b76df3e1dfe1378d6d9223f5b511 +Step 5: 58c4e3d1cc2ce7bd73e846a1c3b00a9986aa039302d7bbf1a5508d5f9a49120f +Step 6: d45443fa11efec622c83b27c55f7a73143bdfa0d51820ac597b9e3fb5c28dbb0 + +new_enc_key = 97b99f4e88e8e73779aa20ac11877c5d ← Step 4[:16] +new_sign_key = d45443fa11efec622c83b27c55f7a73143bdfa0d51820ac597b9e3fb5c28dbb0 +``` + +--- + +## 3. 初期鍵導出 (DH → セッション鍵) — 未解明 + +### 3.1 判明している事実 + +- DH パラメータ: 1024-bit, g=5, p は Netflix 固有値 (`9694e9d8...`) +- DH 共有秘密 = 128 bytes +- PSK `027617984f6227539a630b897c017d69` (16 bytes) の由来は不明 + +### 3.2 MSL Java 参照実装との差異 + +MSL Java 参照実装 (`DiffieHellmanExchange.java`) の KDF: + +```java +// 1. correct_null_bytes: 先頭に 0x00 を1つだけ付与 +// 2. SHA-384(shared_secret) +// 3. enc_key = hash[0:16], hmac_key = hash[16:48] +``` + +**iOS Scheme 5 はこれとは完全に異なる**: +- Java 参照: SHA-384 一発。ラップキーなし。両者が独立に計算 +- iOS Scheme 5: HMAC-SHA256 チェーン (§2)。PSK を使用 +- SHA-384(DH_shared_secret) の出力は PSK `0276...` とも既知セッション鍵とも一致しない + +### 3.3 解決: PSK と nonce はバイナリにハードコードされた定数 + +**Tweak v42/v43 の実験で確定:** + +1. アプリの Library/Caches/Preferences/odb 等のファイルをすべて削除 → PSK は変わらない +2. Keychain の全エントリを `SecItemDelete` で削除 (status=0 成功) → PSK は変わらない +3. NFWebCrypto.framework バイナリを検索 → **PSK と nonce がバイナリに連続して埋め込まれている** + +``` +Offset 0x1ac8f5: 02 76 17 98 4f 62 27 53 9a 63 0b 89 7c 01 7d 69 ← PSK (16B) +Offset 0x1ac905: 80 9f 82 a7 ad df 54 8d 3e a9 dd 06 7f f9 bb 91 ← nonce (16B) +Offset 0x1ac915: N7netflix14AppleWebCryptoE ← C++ mangled name +``` + +直後の C++ シンボル名 `netflix::AppleWebCrypto` から、このクラスの静的定数として +コンパイルされたことがわかる。 + +**結論**: PSK と nonce はデバイス固有ではなく、**同じバージョンの NFWebCrypto.framework を +持つ全デバイスで共通の固定値**。Python 実装にそのままハードコードできる。 + +--- + +## 4. レスポンスキー交換データ構造 + +### 4.1 key 33 (key_response_data) の sub-key + +| sub-key | 型 | サイズ | 説明 | +|---------|-----|--------|------| +| 6 | bytes | 96 | 暗号化されたセッション鍵 | +| 7 | bytes | 1 | ステータスフラグ (`0x00`) | +| 8 | string | 1 | スキーム ID (`'5'`) | +| 9 | bytes | 16 | サーバー nonce (KDF 入力) | + +### 4.2 key 33.6 (96 bytes) の推定構造 + +``` +[IV: 16 bytes][CT: 48 bytes][HMAC: 32 bytes] +``` + +- IV (16B): AES-CBC 初期化ベクトル +- CT (48B): AES-CBC 暗号文 → 平文 = enc_key(16B) + sign_key(32B) +- HMAC (32B): HMAC-SHA256(PSK?, IV||CT) による認証 + +--- + +## 5. 鍵の使用パターン + +v39 ログから観測された暗号化/署名パターン: + +``` +[AES_set_encrypt_key bits=128] key=0817065e... ← セッション暗号化鍵 +[AES_cbc_encrypt] dir=ENC len=336 iv=... ← MSL ペイロード暗号化 +[HMAC] key_len=32 key=91f752f7... ← セッション署名鍵で署名 +``` + +全メッセージが同じ enc_key / sign_key で暗号化・署名されている。 +鍵更新の結果 (97b99f4e / d45443fa) は次のセッションで使用される。 + +--- + +## 6. Python 実装 + +```python +import hashlib +import hmac + +def netflix_msl_kdf_renew( + psk: bytes, # 16 bytes + enc_key: bytes, # 16 bytes + sign_key: bytes, # 32 bytes + nonce: bytes, # 16 bytes +) -> tuple[bytes, bytes]: + """Netflix MSL セッション鍵更新 KDF.""" + # Step 1-2: セッションバインド (検証用) + session_check = hmac.new(psk, enc_key + sign_key, hashlib.sha256).digest() + session_bind = hmac.new(session_check, nonce, hashlib.sha256).digest() + + # Step 3-4: 新しい暗号化鍵 + enc_temp = hmac.new(psk, enc_key, hashlib.sha256).digest() + new_enc_key = hmac.new(enc_temp, nonce, hashlib.sha256).digest()[:16] + + # Step 5-6: 新しい署名鍵 + sign_temp = hmac.new(psk, sign_key, hashlib.sha256).digest() + new_sign_key = hmac.new(sign_temp, nonce, hashlib.sha256).digest() + + return new_enc_key, new_sign_key +``` diff --git a/docs/spec/msl_key_relationship.md b/docs/spec/msl_key_relationship.md new file mode 100644 index 0000000..a269f96 --- /dev/null +++ b/docs/spec/msl_key_relationship.md @@ -0,0 +1,180 @@ +# Netflix iOS MSL 鍵の関係図 + +作成日: 2026-04-08 + +--- + +## 1. 鍵の全体関係 + +```mermaid +graph TD + subgraph "NFWebCrypto.framework" + PSK["PSK (128-bit)"] + NONCE_HARD["nonce (128-bit)"] + DH_P["DH p (1024-bit)"] + DH_G["DH g = 5"] + RSA_BOOT["kAppBootKey (RSA-4096)"] + ECC_BOOT["kAppBootEccKey (ECDSA P-256)"] + end + + subgraph "Phase 1: appboot 鍵交換" + DH_P --> DH_GEN["DH 鍵ペア生成"] + DH_G --> DH_GEN + DH_GEN --> DH_PUB["クライアント DH 公開鍵"] + RSA_BOOT -->|暗号化| DH_REQ["appboot リクエスト"] + DH_PUB --> DH_REQ + DH_REQ -->|POST /appboot| SERVER["Netflix サーバー"] + SERVER --> DH_RESP["appboot レスポンス (key 33)"] + ECC_BOOT -->|署名検証| DH_RESP + end + + subgraph "Phase 2: 初期セッション鍵導出 (未解明)" + DH_RESP --> KEY336["key 33.6 (768-bit 暗号文)"] + DH_RESP --> NONCE_SRV["key 33.9 (サーバー nonce)"] + KEY336 -->|"復号 (鍵=???)"| INIT_KEYS["初期セッション鍵"] + INIT_KEYS --> ENC0["enc_key_0 (128-bit)"] + INIT_KEYS --> SIGN0["sign_key_0 (256-bit)"] + end + + subgraph "Phase 3: KDF 鍵更新 (解明済み)" + PSK -->|HMAC key| KDF["KDF (HMAC-SHA256 chain)"] + ENC0 -->|入力| KDF + SIGN0 -->|入力| KDF + NONCE_HARD -->|入力| KDF + KDF --> ENC1["enc_key_1 (128-bit)"] + KDF --> SIGN1["sign_key_1 (256-bit)"] + ENC1 -->|次の更新入力| KDF2["KDF (次回更新)"] + SIGN1 -->|次の更新入力| KDF2 + end + + subgraph "Phase 4: MSL 通信" + ENC0 -->|暗号化/復号| MSL_ENC["AES-128-CBC"] + SIGN0 -->|署名/検証| MSL_SIGN["HMAC-SHA256"] + MSL_ENC --> PAYLOAD["manifest / license / logblob"] + MSL_SIGN --> PAYLOAD + end + + %% 赤: バイナリ埋め込み + style PSK fill:#e74c3c,stroke:#c0392b,color:#fff + style NONCE_HARD fill:#e74c3c,stroke:#c0392b,color:#fff + style DH_P fill:#e74c3c,stroke:#c0392b,color:#fff + style DH_G fill:#e74c3c,stroke:#c0392b,color:#fff + style RSA_BOOT fill:#e74c3c,stroke:#c0392b,color:#fff + style ECC_BOOT fill:#e74c3c,stroke:#c0392b,color:#fff + + %% 青: サーバーレスポンス由来 + style SERVER fill:#3498db,stroke:#2980b9,color:#fff + style DH_RESP fill:#3498db,stroke:#2980b9,color:#fff + style KEY336 fill:#3498db,stroke:#2980b9,color:#fff + style NONCE_SRV fill:#3498db,stroke:#2980b9,color:#fff + style ENC0 fill:#3498db,stroke:#2980b9,color:#fff + style SIGN0 fill:#3498db,stroke:#2980b9,color:#fff + + %% 黄: 計算可能 (KDF 出力) + style KDF fill:#f1c40f,stroke:#d4ac0f,color:#000 + style ENC1 fill:#f1c40f,stroke:#d4ac0f,color:#000 + style SIGN1 fill:#f1c40f,stroke:#d4ac0f,color:#000 + style KDF2 fill:#f1c40f,stroke:#d4ac0f,color:#000 +``` + +### 凡例 + +| 色 | 意味 | +|----|------| +| 赤 | バイナリ埋め込み | +| 青 | サーバーレスポンス由来 | +| 黄 | 計算可能 (KDF 出力) | + +--- + +## 2. KDF 鍵更新の詳細フロー + +```mermaid +graph LR + subgraph "入力" + PSK2["PSK (128-bit)"] + ENC_OLD["旧 enc_key (128-bit)"] + SIGN_OLD["旧 sign_key (256-bit)"] + NONCE2["nonce (128-bit)"] + end + + subgraph "Step 1-2: セッションバインド" + PSK2 -->|key| H1["HMAC-SHA256"] + ENC_OLD -->|"msg: enc || sign"| H1 + SIGN_OLD -->|"msg: enc || sign"| H1 + H1 -->|session_check| H2["HMAC-SHA256"] + NONCE2 -->|msg| H2 + H2 --> SB["session_bind (256-bit)"] + end + + subgraph "Step 3-4: 新暗号化鍵" + PSK2 -->|key| H3["HMAC-SHA256"] + ENC_OLD -->|msg| H3 + H3 -->|enc_temp| H4["HMAC-SHA256"] + NONCE2 -->|msg| H4 + H4 -->|truncate 128-bit| NEW_ENC["new_enc_key (128-bit)"] + end + + subgraph "Step 5-6: 新署名鍵" + PSK2 -->|key| H5["HMAC-SHA256"] + SIGN_OLD -->|msg| H5 + H5 -->|sign_temp| H6["HMAC-SHA256"] + NONCE2 -->|msg| H6 + H6 --> NEW_SIGN["new_sign_key (256-bit)"] + end + + %% 赤: バイナリ埋め込み + style PSK2 fill:#e74c3c,stroke:#c0392b,color:#fff + style NONCE2 fill:#e74c3c,stroke:#c0392b,color:#fff + + %% 青: サーバーレスポンス由来 + style ENC_OLD fill:#3498db,stroke:#2980b9,color:#fff + style SIGN_OLD fill:#3498db,stroke:#2980b9,color:#fff + + %% 黄: 計算可能 + style NEW_ENC fill:#f1c40f,stroke:#d4ac0f,color:#000 + style NEW_SIGN fill:#f1c40f,stroke:#d4ac0f,color:#000 + style SB fill:#f1c40f,stroke:#d4ac0f,color:#000 +``` + +--- + +## 3. 鍵一覧 + +| 鍵名 | サイズ | 格納場所 | 用途 | 状態 | +|------|--------|----------|------|------| +| PSK | 128-bit | バイナリ | KDF マスター鍵 | 確定 | +| nonce | 128-bit | バイナリ | KDF 入力 | 確定 | +| enc_key_0 | 128-bit | 不明 | AES-128-CBC 暗号化 | 由来不明 | +| sign_key_0 | 256-bit | 不明 | HMAC-SHA256 署名 | 由来不明 | +| enc_key_1 | 128-bit | KDF 出力 | 更新後の暗号化鍵 | 計算可能 | +| sign_key_1 | 256-bit | KDF 出力 | 更新後の署名鍵 | 計算可能 | +| DH p | 1024-bit | バイナリ | DH 鍵交換 | 確定 | +| DH g | - | バイナリ | DH 鍵交換 | 確定 | +| kAppBootKey | 4096-bit | バイナリ | DH パラメータ暗号化 | 既知 | +| kAppBootEccKey | 256-bit | バイナリ | レスポンス署名検証 | 既知 | + +--- + +## 4. 未解明ポイント + +```mermaid +graph TD + Q1["key 33.6 (768-bit) の復号鍵は何か"] + Q2["初期 enc_key_0 / sign_key_0 の由来"] + Q3["PSK 2箇所目直前の 256-bit データの正体"] + Q4["ブートストラップ署名鍵 38b2030d... の導出元"] + + Q1 --> H1["仮説A: DH 共有秘密で復号"] + Q1 --> H2["仮説B: PSK で復号"] + Q1 --> H3["仮説C: TFIT チェーン出力で復号"] + + Q2 --> H4["仮説: key 33.6 に暗号化されて格納"] + Q3 --> H5["仮説: 別の KDF 定数 or HMAC 鍵"] + Q4 --> H6["仮説: TFIT ホワイトボックスチェーン出力"] + + style Q1 fill:#fa0,stroke:#a60,color:#fff + style Q2 fill:#fa0,stroke:#a60,color:#fff + style Q3 fill:#fa0,stroke:#a60,color:#fff + style Q4 fill:#fa0,stroke:#a60,color:#fff +``` diff --git a/packages/tweak/AppbootKeyExtract/Sources/AppbootKeyExtractC/Tweak.m b/packages/tweak/AppbootKeyExtract/Sources/AppbootKeyExtractC/Tweak.m index 2975cc7..08e62f4 100644 --- a/packages/tweak/AppbootKeyExtract/Sources/AppbootKeyExtractC/Tweak.m +++ b/packages/tweak/AppbootKeyExtract/Sources/AppbootKeyExtractC/Tweak.m @@ -4,6 +4,7 @@ #import #import #import +// Security types come from Foundation/CoreFoundation headers static os_log_t g_log = NULL; static NSString *g_logFile = nil; @@ -15,6 +16,13 @@ static NSMutableArray *g_aesKeys = nil; static NSMutableArray *g_hmacKeys = nil; static BOOL g_appbootDone = NO; +// DH shared secret — stored so HMAC_Update can compare +static uint8_t g_dhSharedSecret[256]; +static int g_dhSharedSecretLen = 0; + +// Reentrancy guard for hooks that may be called by TLS internally +static volatile int g_inHook = 0; + #define NFXKEY_LOG(fmt, ...) \ do { \ if (g_log) { os_log(g_log, fmt, ##__VA_ARGS__); } \ @@ -144,6 +152,10 @@ static int hook_DH_generate_key(DH *dh) { return ret; } +// SHA function pointers (resolved in constructor) +static unsigned char *(*fn_SHA384)(const unsigned char *d, size_t n, unsigned char *md); +static unsigned char *(*fn_SHA256)(const unsigned char *d, size_t n, unsigned char *md); + // --------------------------------------------------------------------------- // Hook: DH_compute_key // --------------------------------------------------------------------------- @@ -156,6 +168,63 @@ static int hook_DH_compute_key(unsigned char *key, const BIGNUM *pub_key, DH *dh file_log([NSString stringWithFormat:@"[DH_compute_key] shared_secret (%d bytes)=%@", ret, hex]); g_keys[@"dh_shared_secret"] = hex; + + // Store shared secret globally for HMAC_Update comparison + int copyLen = ret < (int)sizeof(g_dhSharedSecret) ? ret : (int)sizeof(g_dhSharedSecret); + memcpy(g_dhSharedSecret, key, copyLen); + g_dhSharedSecretLen = copyLen; + + // Compute SHA-384 and SHA-256 of shared_secret as PSK candidates + // Guard against re-entrancy since SHA functions may invoke hooked code + if (!g_inHook) { + g_inHook = 1; + + if (fn_SHA384) { + uint8_t digest384[48]; + if (fn_SHA384(key, (size_t)ret, digest384)) { + NSString *sha384Hex = hexEncode(digest384, 48); + file_log([NSString stringWithFormat:@"[DH_compute_key] SHA384(shared_secret)=%@", sha384Hex]); + g_keys[@"dh_sha384"] = sha384Hex; + // First 16 bytes as PSK candidate + g_keys[@"dh_sha384_16"] = hexEncode(digest384, 16); + file_log([NSString stringWithFormat:@"[DH_compute_key] PSK_candidate_sha384[:16]=%@", + g_keys[@"dh_sha384_16"]]); + } + } + + if (fn_SHA256) { + uint8_t digest256[32]; + if (fn_SHA256(key, (size_t)ret, digest256)) { + NSString *sha256Hex = hexEncode(digest256, 32); + file_log([NSString stringWithFormat:@"[DH_compute_key] SHA256(shared_secret)=%@", sha256Hex]); + g_keys[@"dh_sha256"] = sha256Hex; + // First 16 bytes as PSK candidate + g_keys[@"dh_sha256_16"] = hexEncode(digest256, 16); + file_log([NSString stringWithFormat:@"[DH_compute_key] PSK_candidate_sha256[:16]=%@", + g_keys[@"dh_sha256_16"]]); + } + } + + // null-padded shared_secret (leading zero padding to 256 bytes) SHA-384 + // MSL Java reference uses a fixed-length big-endian representation + if (fn_SHA384 && ret < 256) { + uint8_t padded[256]; + memset(padded, 0, sizeof(padded)); + memcpy(padded + 256 - ret, key, ret); + uint8_t digest384p[48]; + if (fn_SHA384(padded, 256, digest384p)) { + NSString *sha384pHex = hexEncode(digest384p, 48); + file_log([NSString stringWithFormat:@"[DH_compute_key] SHA384(padded_shared_secret)=%@", sha384pHex]); + g_keys[@"dh_sha384_padded"] = sha384pHex; + g_keys[@"dh_sha384_padded_16"] = hexEncode(digest384p, 16); + file_log([NSString stringWithFormat:@"[DH_compute_key] PSK_candidate_sha384_padded[:16]=%@", + g_keys[@"dh_sha384_padded_16"]]); + } + } + + g_inHook = 0; + } + saveKeysToFile(); } return ret; @@ -167,6 +236,8 @@ static int hook_DH_compute_key(unsigned char *key, const BIGNUM *pub_key, DH *dh static int (*orig_AES_set_encrypt_key)(const unsigned char *userKey, int bits, void *key); static int hook_AES_set_encrypt_key(const unsigned char *userKey, int bits, void *key) { + if (g_inHook) return orig_AES_set_encrypt_key(userKey, bits, key); + g_inHook = 1; int keyLen = bits / 8; if (keyLen == 16 || keyLen == 32) { NSString *hex = hexEncode(userKey, keyLen); @@ -184,16 +255,20 @@ static int hook_AES_set_encrypt_key(const unsigned char *userKey, int bits, void g_keys[g_appbootDone ? @"session_enc_key" : @"pre_session_enc_key"] = hex; } } + g_inHook = 0; return orig_AES_set_encrypt_key(userKey, bits, key); } static int (*orig_AES_set_decrypt_key)(const unsigned char *userKey, int bits, void *key); static int hook_AES_set_decrypt_key(const unsigned char *userKey, int bits, void *key) { + if (g_inHook) return orig_AES_set_decrypt_key(userKey, bits, key); + g_inHook = 1; int keyLen = bits / 8; if (keyLen == 16 || keyLen == 32) { file_log([NSString stringWithFormat:@"[AES_set_decrypt_key] bits=%d key=%@", bits, hexEncode(userKey, keyLen)]); } + g_inHook = 0; return orig_AES_set_decrypt_key(userKey, bits, key); } @@ -205,6 +280,8 @@ static unsigned char *(*orig_HMAC)(const void *evp_md, const void *key, int key_ const unsigned char *d, size_t n, unsigned char *md, unsigned int *md_len); static unsigned char *hook_HMAC(const void *evp_md, const void *key, int key_len, const unsigned char *d, size_t n, unsigned char *md, unsigned int *md_len) { + if (g_inHook) return orig_HMAC(evp_md, key, key_len, d, n, md, md_len); + g_inHook = 1; if (key_len == 32) { NSString *hex = hexEncode((const uint8_t *)key, key_len); file_log([NSString stringWithFormat:@"[HMAC] key_len=%d key=%@", key_len, hex]); @@ -217,6 +294,7 @@ static unsigned char *hook_HMAC(const void *evp_md, const void *key, int key_len // Update current session hmac_key g_keys[g_appbootDone ? @"session_hmac_key" : @"pre_session_hmac_key"] = hex; } + g_inHook = 0; return orig_HMAC(evp_md, key, key_len, d, n, md, md_len); } @@ -278,6 +356,360 @@ static int hook_HKDF_expand(uint8_t *out_key, size_t out_len, return ret; } +// --------------------------------------------------------------------------- +// Hook: HMAC_Init_ex / HMAC_Update / HMAC_Final (streaming HMAC API) +// --------------------------------------------------------------------------- + +typedef struct hmac_ctx_st HMAC_CTX; +typedef struct env_md_st EVP_MD; +typedef struct engine_st ENGINE; + +static int (*orig_HMAC_Init_ex)(HMAC_CTX *ctx, const void *key, int key_len, + const EVP_MD *md, ENGINE *impl); +static int hook_HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int key_len, + const EVP_MD *md, ENGINE *impl) { + int ret = orig_HMAC_Init_ex(ctx, key, key_len, md, impl); + if (g_inHook) return ret; + g_inHook = 1; + if (key != NULL && key_len > 0 && key_len <= 256) { + NSString *keyHex = hexEncode((const uint8_t *)key, key_len); + file_log([NSString stringWithFormat:@"[HMAC_Init_ex] ctx=%p key_len=%d key=%@", + (void *)ctx, key_len, keyHex]); + + // PSK-size key detection (16 bytes = possible PSK) + if (key_len == 16) { + file_log([NSString stringWithFormat:@"[HMAC_Init_ex] *** PSK-SIZE KEY *** ctx=%p key=%@", + (void *)ctx, keyHex]); + + // Check if PSK matches or is contained in the DH shared_secret + if (g_dhSharedSecretLen >= 16) { + BOOL found = NO; + for (int offset = 0; offset <= g_dhSharedSecretLen - 16; offset++) { + if (memcmp((const uint8_t *)key, g_dhSharedSecret + offset, 16) == 0) { + file_log([NSString stringWithFormat: + @"[HMAC_Init_ex] *** PSK MATCHES DH shared_secret at offset %d ***", + offset]); + found = YES; + break; + } + } + if (!found) { + file_log(@"[HMAC_Init_ex] PSK-size key does NOT match DH shared_secret"); + } + } else { + file_log(@"[HMAC_Init_ex] PSK-size key seen (no DH shared_secret yet)"); + } + } + } + g_inHook = 0; + return ret; +} + +static int (*orig_HMAC_Update)(HMAC_CTX *ctx, const unsigned char *data, size_t len); +static int hook_HMAC_Update(HMAC_CTX *ctx, const unsigned char *data, size_t len) { + int ret = orig_HMAC_Update(ctx, data, len); + if (g_inHook) return ret; + g_inHook = 1; + if (data != NULL && len <= 256) { + NSString *dataHex = hexEncode(data, (int)len); + file_log([NSString stringWithFormat:@"[HMAC_Update] ctx=%p len=%zu data=%@", + (void *)ctx, len, dataHex]); + + // Check if the input matches the stored DH shared_secret + if (g_dhSharedSecretLen > 0 && len >= 16) { + int cmpLen = (int)len < g_dhSharedSecretLen ? (int)len : g_dhSharedSecretLen; + if (memcmp(data, g_dhSharedSecret, cmpLen) == 0) { + file_log([NSString stringWithFormat: + @"[HMAC_Update] *** DATA MATCHES DH shared_secret (first %d bytes) ctx=%p ***", + cmpLen, (void *)ctx]); + } + } + } else if (data != NULL && len > 256) { + file_log([NSString stringWithFormat:@"[HMAC_Update] ctx=%p len=%zu (data too long, skipping hex)", + (void *)ctx, len]); + } + g_inHook = 0; + return ret; +} + +static int (*orig_HMAC_Final)(HMAC_CTX *ctx, unsigned char *md, unsigned int *md_len); +static int hook_HMAC_Final(HMAC_CTX *ctx, unsigned char *md, unsigned int *md_len) { + int ret = orig_HMAC_Final(ctx, md, md_len); + if (g_inHook) return ret; + g_inHook = 1; + if (ret == 1 && md != NULL && md_len != NULL && *md_len > 0) { + NSString *digestHex = hexEncode(md, (int)*md_len); + file_log([NSString stringWithFormat:@"[HMAC_Final] ctx=%p digest_len=%u digest=%@", + (void *)ctx, *md_len, digestHex]); + } + g_inHook = 0; + return ret; +} + +// --------------------------------------------------------------------------- +// Hook: AES_cbc_encrypt +// --------------------------------------------------------------------------- + +typedef struct aes_key_st AES_KEY; + +static void (*orig_AES_cbc_encrypt)(const unsigned char *in, unsigned char *out, size_t length, + const AES_KEY *key, unsigned char *ivec, int enc); +static void hook_AES_cbc_encrypt(const unsigned char *in, unsigned char *out, size_t length, + const AES_KEY *key, unsigned char *ivec, int enc) { + // Capture IV before it's modified by AES-CBC + uint8_t ivCopy[16]; + if (ivec) memcpy(ivCopy, ivec, 16); + + orig_AES_cbc_encrypt(in, out, length, key, ivec, enc); + + if (g_inHook) return; + // ENC mode: skip large operations (bulk TLS data) + // DEC mode: only log small ops (<=128 bytes) to capture server response decryption + if (enc && length > 512) return; + if (!enc && length > 128) return; + g_inHook = 1; + + NSString *direction = enc ? @"ENC" : @"DEC"; + NSString *ivHex = ivec ? hexEncode(ivCopy, 16) : @"(null)"; + + // DEC mode: log all bytes to capture full decrypted result + int logLen = enc ? (int)(length < 48 ? length : 48) : (int)length; + NSString *inHex = in ? hexEncode(in, logLen) : @"(null)"; + NSString *outHex = out ? hexEncode(out, logLen) : @"(null)"; + + file_log([NSString stringWithFormat: + @"[AES_cbc_encrypt] dir=%@ len=%zu iv=%@ in[0:%d]=%@ out[0:%d]=%@", + direction, length, ivHex, logLen, inHex, logLen, outHex]); + + // For DEC mode: flag if decrypted output starts with PSK-size patterns + if (!enc && length <= 128 && out) { + file_log([NSString stringWithFormat:@"[AES_cbc_encrypt] DEC full_out(%zu)=%@", + length, hexEncode(out, (int)length)]); + } + g_inHook = 0; +} + +// --------------------------------------------------------------------------- +// Hook: EVP_CipherInit_ex / EVP_CipherUpdate / EVP_CipherFinal_ex +// DISABLED: EVP hooks cause "RSA public key not found" error. +// NFWebCrypto's OpenSSL EVP is only used for TFIT (ENC), never for MSL decrypt. +// --------------------------------------------------------------------------- + +#if 0 // EVP hooks disabled — kept for reference + +// Opaque EVP_CIPHER_CTX — we only need the pointer as a tracking key +typedef struct evp_cipher_ctx_st EVP_CIPHER_CTX; +typedef struct evp_cipher_st EVP_CIPHER; +typedef struct engine_st ENGINE; + +// Track per-context state: direction + key + iv +#define MAX_EVP_TRACK 32 +static struct { + void *ctx; + int enc; // 1=encrypt, 0=decrypt + uint8_t key[32]; + int keyLen; + uint8_t iv[16]; +} g_evpTrack[MAX_EVP_TRACK]; +static int g_evpTrackCount = 0; + +static int evpTrackFind(void *ctx) { + for (int i = 0; i < g_evpTrackCount; i++) { + if (g_evpTrack[i].ctx == ctx) return i; + } + return -1; +} + +// EVP_CipherInit_ex(ctx, type, impl, key, iv, enc) +static int (*orig_EVP_CipherInit_ex)(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, + ENGINE *impl, const unsigned char *key, + const unsigned char *iv, int enc); +static int hook_EVP_CipherInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, + ENGINE *impl, const unsigned char *key, + const unsigned char *iv, int enc) { + int ret = orig_EVP_CipherInit_ex(ctx, type, impl, key, iv, enc); + + if (g_inHook) return ret; + g_inHook = 1; + + if (key) { + int idx = evpTrackFind(ctx); + if (idx < 0 && g_evpTrackCount < MAX_EVP_TRACK) { + idx = g_evpTrackCount++; + } + if (idx >= 0) { + g_evpTrack[idx].ctx = ctx; + g_evpTrack[idx].enc = enc; + g_evpTrack[idx].keyLen = 16; + memcpy(g_evpTrack[idx].key, key, 16); + memset(g_evpTrack[idx].iv, 0, 16); + if (iv) memcpy(g_evpTrack[idx].iv, iv, 16); + } + + if (iv) { + NSString *direction = enc ? @"ENC" : @"DEC"; + NSString *keyHex = hexEncode(key, 16); + NSString *ivHex = hexEncode(iv, 16); + file_log([NSString stringWithFormat:@"[EVP_CipherInit_ex] dir=%@ key=%@ iv=%@", + direction, keyHex, ivHex]); + } + } + + g_inHook = 0; + return ret; +} + +// EVP_CipherUpdate(ctx, out, outl, in, inl) +static int (*orig_EVP_CipherUpdate)(EVP_CIPHER_CTX *ctx, unsigned char *out, + int *outl, const unsigned char *in, int inl); +static int hook_EVP_CipherUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, + int *outl, const unsigned char *in, int inl) { + int ret = orig_EVP_CipherUpdate(ctx, out, outl, in, inl); + + if (g_inHook) return ret; + g_inHook = 1; + + int idx = evpTrackFind(ctx); + if (idx < 0) { g_inHook = 0; return ret; } + static const uint8_t zeroIv[16] = {0}; + if (memcmp(g_evpTrack[idx].iv, zeroIv, 16) == 0) { g_inHook = 0; return ret; } + + int enc = g_evpTrack[idx].enc; + NSString *direction = (enc == 1) ? @"ENC" : (enc == 0) ? @"DEC" : @"???"; + + int logLen = (inl < 64) ? inl : 64; + int outLen = (outl && *outl < 64) ? *outl : 64; + NSString *inHex = in ? hexEncode(in, logLen) : @"(null)"; + NSString *outHex = (out && outl) ? hexEncode(out, outLen) : @"(null)"; + + file_log([NSString stringWithFormat:@"[EVP_CipherUpdate] dir=%@ inl=%d outl=%d in[:%d]=%@ out[:%d]=%@", + direction, inl, outl ? *outl : 0, logLen, inHex, outLen, outHex]); + + if (enc == 0 && outl && *outl <= 128 && *outl > 0 && out) { + file_log([NSString stringWithFormat:@"[EVP_CipherUpdate] DEC full_out(%d)=%@", + *outl, hexEncode(out, *outl)]); + } + + g_inHook = 0; + return ret; +} + +// EVP_CipherFinal_ex(ctx, out, outl) +static int (*orig_EVP_CipherFinal_ex)(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl); +static int hook_EVP_CipherFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl) { + int ret = orig_EVP_CipherFinal_ex(ctx, out, outl); + + if (g_inHook) return ret; + g_inHook = 1; + + int idx = evpTrackFind(ctx); + int enc = (idx >= 0) ? g_evpTrack[idx].enc : -1; + NSString *direction = (enc == 1) ? @"ENC" : (enc == 0) ? @"DEC" : @"???"; + + if (outl && *outl > 0 && out) { + file_log([NSString stringWithFormat:@"[EVP_CipherFinal_ex] dir=%@ outl=%d out=%@", + direction, *outl, hexEncode(out, *outl)]); + } else { + file_log([NSString stringWithFormat:@"[EVP_CipherFinal_ex] dir=%@ outl=%d", + direction, outl ? *outl : 0]); + } + + if (idx >= 0) { + g_evpTrack[idx] = g_evpTrack[--g_evpTrackCount]; + } + + g_inHook = 0; + return ret; +} + +// --------------------------------------------------------------------------- +// Hook: EVP_DecryptInit_ex / EVP_DecryptUpdate / EVP_DecryptFinal_ex +// --------------------------------------------------------------------------- + +static int (*orig_EVP_DecryptInit_ex)(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, + ENGINE *impl, const unsigned char *key, + const unsigned char *iv); +static int hook_EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, + ENGINE *impl, const unsigned char *key, + const unsigned char *iv) { + int ret = orig_EVP_DecryptInit_ex(ctx, type, impl, key, iv); + + if (g_inHook) return ret; + g_inHook = 1; + + if (key) { + int idx = evpTrackFind(ctx); + if (idx < 0 && g_evpTrackCount < MAX_EVP_TRACK) { + idx = g_evpTrackCount++; + } + if (idx >= 0) { + g_evpTrack[idx].ctx = ctx; + g_evpTrack[idx].enc = 0; + memcpy(g_evpTrack[idx].key, key, 16); + memset(g_evpTrack[idx].iv, 0, 16); + if (iv) memcpy(g_evpTrack[idx].iv, iv, 16); + } + + NSString *keyHex = hexEncode(key, 16); + NSString *ivHex = iv ? hexEncode(iv, 16) : @"(null)"; + file_log([NSString stringWithFormat:@"[EVP_DecryptInit_ex] key=%@ iv=%@", keyHex, ivHex]); + } + + g_inHook = 0; + return ret; +} + +static int (*orig_EVP_DecryptUpdate)(EVP_CIPHER_CTX *ctx, unsigned char *out, + int *outl, const unsigned char *in, int inl); +static int hook_EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, + int *outl, const unsigned char *in, int inl) { + int ret = orig_EVP_DecryptUpdate(ctx, out, outl, in, inl); + + if (g_inHook) return ret; + g_inHook = 1; + + int logLen = (inl < 64) ? inl : 64; + int outLen = (outl && *outl < 64) ? *outl : 64; + NSString *inHex = in ? hexEncode(in, logLen) : @"(null)"; + NSString *outHex = (out && outl) ? hexEncode(out, outLen) : @"(null)"; + + file_log([NSString stringWithFormat:@"[EVP_DecryptUpdate] inl=%d outl=%d in[:%d]=%@ out[:%d]=%@", + inl, outl ? *outl : 0, logLen, inHex, outLen, outHex]); + + if (outl && *outl <= 128 && *outl > 0 && out) { + file_log([NSString stringWithFormat:@"[EVP_DecryptUpdate] full_out(%d)=%@", + *outl, hexEncode(out, *outl)]); + } + + g_inHook = 0; + return ret; +} + +static int (*orig_EVP_DecryptFinal_ex)(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl); +static int hook_EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl) { + int ret = orig_EVP_DecryptFinal_ex(ctx, out, outl); + + if (g_inHook) return ret; + g_inHook = 1; + + if (outl && *outl > 0 && out) { + file_log([NSString stringWithFormat:@"[EVP_DecryptFinal_ex] outl=%d out=%@", + *outl, hexEncode(out, *outl)]); + } else { + file_log([NSString stringWithFormat:@"[EVP_DecryptFinal_ex] outl=%d", outl ? *outl : 0]); + } + + int idx = evpTrackFind(ctx); + if (idx >= 0) { + g_evpTrack[idx] = g_evpTrack[--g_evpTrackCount]; + } + + g_inHook = 0; + return ret; +} + +#endif // EVP hooks disabled + // --------------------------------------------------------------------------- // Hook: IosMslClient.setDidAppboot: // --------------------------------------------------------------------------- @@ -314,6 +746,8 @@ static void hook_setDidAppboot(id self, SEL _cmd, BOOL value) { } } +// SSL bypass removed — use Frida ssl-pinning.ts if needed + // --------------------------------------------------------------------------- // Constructor // --------------------------------------------------------------------------- @@ -332,6 +766,37 @@ __attribute__((constructor)) static void init(void) { file_log(@"=== AppbootKeyExtract loaded ==="); NFXKEY_LOG("AppbootKeyExtract loaded"); + // Keychain clear trigger: if /tmp/clear_keychain exists, delete all Keychain items + // Uses dlsym to avoid linking Security.framework (caused crashes before) + NSString *triggerPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"clear_keychain"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:triggerPath]) { + file_log(@"[!] clear_keychain trigger found — deleting Keychain items"); + void *secLib = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_NOLOAD); + if (!secLib) secLib = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY); + if (secLib) { + typedef int32_t (*SecItemDelete_t)(CFDictionaryRef); + SecItemDelete_t secItemDeleteFn = (SecItemDelete_t)dlsym(secLib, "SecItemDelete"); + if (secItemDeleteFn) { + NSArray *classNames = @[@"genp", @"inet", @"keys", @"cert"]; + for (NSString *cls_str in classNames) { + NSDictionary *query = @{@"class": cls_str}; + int32_t status = secItemDeleteFn((__bridge CFDictionaryRef)query); + file_log([NSString stringWithFormat:@"[!] SecItemDelete(class=%@) status=%d", + cls_str, status]); + } + file_log(@"[!] Keychain cleared"); + } else { + file_log(@"[!] SecItemDelete not found via dlsym"); + } + } else { + file_log(@"[!] Security.framework not loaded"); + } + [[NSFileManager defaultManager] removeItemAtPath:triggerPath error:nil]; + file_log(@"[!] trigger file removed"); + } + + // SSL bypass removed — use Frida ssl-pinning.ts if needed + // Hook IosMslClient.setDidAppboot: Class cls = objc_getClass("IosMslClient"); if (cls) { @@ -353,6 +818,20 @@ __attribute__((constructor)) static void init(void) { fn_BN_num_bits = (int (*)(const BIGNUM *))dlsym(nfwc, "BN_num_bits"); fn_BN_bn2bin = (int (*)(const BIGNUM *, unsigned char *))dlsym(nfwc, "BN_bn2bin"); + // Resolve SHA functions for PSK candidate derivation from DH shared_secret + fn_SHA384 = (unsigned char *(*)(const unsigned char *, size_t, unsigned char *))dlsym(nfwc, "SHA384"); + fn_SHA256 = (unsigned char *(*)(const unsigned char *, size_t, unsigned char *))dlsym(nfwc, "SHA256"); + if (fn_SHA384) { + file_log(@"[+] SHA384 resolved"); + } else { + file_log(@"[-] SHA384 not found in NFWebCrypto"); + } + if (fn_SHA256) { + file_log(@"[+] SHA256 resolved"); + } else { + file_log(@"[-] SHA256 not found in NFWebCrypto"); + } + // DH hooks void *dhGenKey = dlsym(nfwc, "DH_generate_key"); void *dhCompKey = dlsym(nfwc, "DH_compute_key"); @@ -426,6 +905,55 @@ __attribute__((constructor)) static void init(void) { file_log(@"[-] HKDF_expand not found (tried HKDF_expand / HKDF_Expand)"); NFXKEY_LOG(" [-] HKDF_expand not found"); } + + // Streaming HMAC hooks + void *hmacInitEx = dlsym(nfwc, "HMAC_Init_ex"); + void *hmacUpdate = dlsym(nfwc, "HMAC_Update"); + void *hmacFinal = dlsym(nfwc, "HMAC_Final"); + + if (hmacInitEx) { + MSHookFunction(hmacInitEx, (void *)hook_HMAC_Init_ex, (void **)&orig_HMAC_Init_ex); + file_log(@"[+] HMAC_Init_ex hooked"); + NFXKEY_LOG(" [+] HMAC_Init_ex hooked"); + } else { + file_log(@"[-] HMAC_Init_ex not found"); + NFXKEY_LOG(" [-] HMAC_Init_ex not found"); + } + + if (hmacUpdate) { + MSHookFunction(hmacUpdate, (void *)hook_HMAC_Update, (void **)&orig_HMAC_Update); + file_log(@"[+] HMAC_Update hooked"); + NFXKEY_LOG(" [+] HMAC_Update hooked"); + } else { + file_log(@"[-] HMAC_Update not found"); + NFXKEY_LOG(" [-] HMAC_Update not found"); + } + + if (hmacFinal) { + MSHookFunction(hmacFinal, (void *)hook_HMAC_Final, (void **)&orig_HMAC_Final); + file_log(@"[+] HMAC_Final hooked"); + NFXKEY_LOG(" [+] HMAC_Final hooked"); + } else { + file_log(@"[-] HMAC_Final not found"); + NFXKEY_LOG(" [-] HMAC_Final not found"); + } + + // AES-CBC hook + void *aesCbcFn = dlsym(nfwc, "AES_cbc_encrypt"); + + if (aesCbcFn) { + MSHookFunction(aesCbcFn, (void *)hook_AES_cbc_encrypt, (void **)&orig_AES_cbc_encrypt); + file_log(@"[+] AES_cbc_encrypt hooked"); + NFXKEY_LOG(" [+] AES_cbc_encrypt hooked"); + } else { + file_log(@"[-] AES_cbc_encrypt not found"); + NFXKEY_LOG(" [-] AES_cbc_encrypt not found"); + } + + // EVP hooks disabled — they cause "RSA public key not found" error + // NFWebCrypto's OpenSSL is NOT used for MSL payload decrypt + // (EVP_CipherInit only fires ENC, EVP_Decrypt* never fires) + file_log(@"[i] EVP hooks disabled (not used for MSL decrypt)"); } else { file_log(@"[-] NFWebCrypto not loaded"); NFXKEY_LOG(" [-] NFWebCrypto not loaded"); diff --git a/src/netflix_msl/constants.py b/src/netflix_msl/constants.py index caa3d1a..0d6b4e2 100644 --- a/src/netflix_msl/constants.py +++ b/src/netflix_msl/constants.py @@ -46,6 +46,10 @@ CLIENT_VERSION = "6.0011.474.011" KEY_ID_CONSTANT = "A1F6F6308F6F7F875C5E9562EF792CAE" RSA_KEYPAIR_ID = "rsaKeypairId" +# iOS MSL Scheme 5 KDF 定数 (NFWebCrypto.framework @ 0x1ac8f5) +IOS_KDF_PSK = bytes.fromhex("027617984f6227539a630b897c017d69") +IOS_KDF_NONCE = bytes.fromhex("809f82a7addf548d3ea9dd067ff9bb91") + # ============================================================================ # User-Agent # ============================================================================ diff --git a/src/netflix_msl/crypto.py b/src/netflix_msl/crypto.py index 82eb4bf..df8e982 100644 --- a/src/netflix_msl/crypto.py +++ b/src/netflix_msl/crypto.py @@ -215,7 +215,45 @@ class NetflixCrypto: return bool(self.encryption_key and self.sign_key) - # ---- Scheme 3 (DH/ECDH) 鍵取り込み ---- + # ---- Scheme 5 (DH) KDF: セッション鍵更新 ---- + + @staticmethod + def kdf_renew( + psk: bytes, + enc_key: bytes, + sign_key: bytes, + nonce: bytes, + ) -> tuple[bytes, bytes]: + """Netflix MSL Scheme 5 セッション鍵更新 KDF. + + HMAC-SHA256 チェーンによるカスタム鍵導出。 + 標準 HKDF ではなく、NFWebCrypto.framework の + HMAC_Init_ex/Update/Final を直接使用する独自実装。 + + Args: + psk: Pre-Shared Key (16 bytes) — DH 共有秘密から導出 + enc_key: 現在の AES-128-CBC 暗号化鍵 (16 bytes) + sign_key: 現在の HMAC-SHA256 署名鍵 (32 bytes) + nonce: サーバー nonce (16 bytes) — key_response_data.9 + + Returns: + (new_enc_key, new_sign_key) + """ + # Step 1-2: セッションバインド + session_check = hmac_mod.new(psk, enc_key + sign_key, hashlib.sha256).digest() + _session_bind = hmac_mod.new(session_check, nonce, hashlib.sha256).digest() + + # Step 3-4: 新しい暗号化鍵 + enc_temp = hmac_mod.new(psk, enc_key, hashlib.sha256).digest() + new_enc_key = hmac_mod.new(enc_temp, nonce, hashlib.sha256).digest()[:16] + + # Step 5-6: 新しい署名鍵 + sign_temp = hmac_mod.new(psk, sign_key, hashlib.sha256).digest() + new_sign_key = hmac_mod.new(sign_temp, nonce, hashlib.sha256).digest() + + return new_enc_key, new_sign_key + + # ---- Scheme 3/5 (DH) 鍵取り込み ---- def import_session_keys(self, enc_key: bytes, sign_key: bytes) -> None: """Frida でキャプチャした鍵素材を直接インポートする (Scheme 3 用).