From e82544e0cd87575a16ee000eebbca49e2dbffc51 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:37:03 +0000 Subject: [PATCH] =?UTF-8?q?feat(re):=20MslClient.framework=20=E3=81=AE=20k?= =?UTF-8?q?ey=2033.6=20=E6=A7=8B=E7=AF=89=E3=83=AD=E3=82=B8=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=81=A8CBOR=E3=82=B7=E3=83=B3=E3=83=9C=E3=83=AB?= =?UTF-8?q?=E3=83=86=E3=83=BC=E3=83=96=E3=83=AB=E3=82=92=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IosAdhKeyRequestData::getKeydata() の逆アセンブリから、key 33.6 が CBOR tag(55799) + map{scheme, keydata{mechanism, parametersid, publickey}} 構造であることを確認。CborMslSymbolTable の96エントリ完全マッピングを抽出。 Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/re/analyze_mslclient_key336.py | 558 +++++++++++++++++++++++++++ 1 file changed, 558 insertions(+) create mode 100644 tools/re/analyze_mslclient_key336.py diff --git a/tools/re/analyze_mslclient_key336.py b/tools/re/analyze_mslclient_key336.py new file mode 100644 index 0000000..a760b5e --- /dev/null +++ b/tools/re/analyze_mslclient_key336.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python3 +""" +Static analysis: key 33.6 (scheme_data) construction in MslClient.framework. + +Traces how the DH public key becomes the CBOR-encoded key exchange request data +(key 33 sub-key 6) in the MSL appboot handshake. + +Binary: Netflix iOS MslClient.framework (arm64 Mach-O) + /tmp/argo/Payload/Argo.app/Frameworks/MslClient.framework/MslClient + +Usage: + python3 tools/re/analyze_mslclient_key336.py [path_to_MslClient] +""" + +import struct +import sys + +import r2pipe + +DEFAULT_BINARY = "/tmp/argo/Payload/Argo.app/Frameworks/MslClient.framework/MslClient" + + +def dump_cbor_symbol_table(r2) -> dict[int, str]: + """Dump the CborMslSymbolTable s_tags array (CBOR integer key -> string name).""" + table_base = 0x15C990 + raw = r2.cmd(f"p8 768 @ {hex(table_base)}").strip() + data = bytes.fromhex(raw) + mapping = {} + for i in range(96): + ptr = struct.unpack_from(" None: + r2 = r2pipe.open(binary_path, flags=["-e", "bin.cache=true", "-2"]) + r2.cmd("aaa") + + print("=" * 72) + print("KEY 33.6 CONSTRUCTION ANALYSIS - MslClient.framework") + print("=" * 72) + + # ========================================================================= + # 1. CBOR Symbol Table (integer key <-> string name) + # ========================================================================= + print("\n[1] CBOR SYMBOL TABLE (CborMslSymbolTable)") + print("-" * 72) + print("\n Source: s_tags[] @ 0x15C990 (96 entries)") + print(" Lookup: CborMslSymbolTable__integerForString @ 0x0605BC") + print(" Reverse: CborMslSymbolTable__stringForInteger @ 0x060590") + print() + + mapping = dump_cbor_symbol_table(r2) + + # Print all entries grouped by function + print(" === Complete CBOR Integer <-> String Key Mapping ===") + for i in sorted(mapping.keys()): + print(f' {i:3d} (0x{i:02x}) = "{mapping[i]}"') + + # Highlight the key exchange relevant entries + print("\n === Keys relevant to key exchange (key 33 = headerdata) ===") + keyx_keys = { + 0x1E: "scheme", + 0x1F: "keydata", + 0x21: "headerdata", + 0x22: "entityauthdata", + 0x2A: "keyrequestdata", + 0x33: "mechanism", + 0x35: "publickey", + 0x3A: "parametersid", + 0x3B: "keypairid", + 0x41: "wrapdata", + 0x10: "signature", + 0x20: "mastertoken", + } + for k, v in sorted(keyx_keys.items()): + print(f' {k:3d} (0x{k:02x}) = "{v}"') + + # ========================================================================= + # 2. MSL Message Top-Level Structure + # ========================================================================= + print("\n\n[2] MSL MESSAGE TOP-LEVEL CBOR STRUCTURE") + print("-" * 72) + print(""" + Every MSL message is CBOR tag 55799 (0xD9D9F7) + map: + + appboot request: + tag(55799) map { + 34 (entityauthdata): bytes <- FAIRPLAY_MGK or FAIRPLAY_MGK_APPID auth + 33 (headerdata): bytes <- key exchange + capabilities + 32 (mastertoken): bytes <- empty for new sessions + 16 (signature): bytes <- HMAC-SHA256 (32B) + } + + The CBOR encoding is done by: + CborMslEncoder__encodeObject @ 0x0AD778 + 1. Encodes CBOR tag 55799 (0xD9D9F7) + 2. Iterates MslObject map entries + 3. For each entry, calls encodeMapEntry @ 0x0ADD08 + which calls CborMslSymbolTable__integerForString + to convert string key -> integer CBOR key +""") + + # ========================================================================= + # 3. Key Exchange Schemes + # ========================================================================= + print("\n[3] KEY EXCHANGE SCHEMES (NetflixKeyExchangeScheme)") + print("-" * 72) + print(""" + Scheme registry (static symbols in MslClient.framework): + + Standard MSL schemes: + ASYMMETRIC_WRAPPED @ 0x172D50 "ASYMMETRIC_WRAPPED" + DIFFIE_HELLMAN @ 0x172D70 "DIFFIE_HELLMAN" + JWE_LADDER @ 0x172D90 "JWE_LADDER" + JWK_LADDER @ 0x172DB0 "JWK_LADDER" + SYMMETRIC_WRAPPED @ 0x172DD0 "SYMMETRIC_WRAPPED" + + Netflix-specific schemes: + AUTHENTICATED_DH @ 0x16E7C8 "AUTHENTICATED_DH" <- iOS appboot + CDM @ 0x16E828 "CDM" + ANYCAST @ 0x16E808 "ANYCAST" + NFLX_DH @ 0x16E848 "NFLX_DH" + WIDEVINE @ 0x16E7E8 "WIDEVINE" + + iOS uses AUTHENTICATED_DH for key exchange. + The IosAdhKeyx factory is registered for this scheme. +""") + + # ========================================================================= + # 4. IosAdhKeyRequestData Object Layout + # ========================================================================= + print("\n[4] IosAdhKeyRequestData OBJECT LAYOUT") + print("-" * 72) + print(""" + Constructor: IosAdhKeyRequestData(pubkey, keypairId, wrapdata, unwrapKey) + @ 0x07B260 + + Object fields (from constructor disassembly): + +0x00: vtable ptr (-> 0x15D6D0) + +0x08: vtable ptr (KeyRequestData base) + +0x10: scheme string = "AUTHENTICATED_DH" (from NetflixKeyExchangeScheme) + +0x28: shared_ptr> publickey (DH public key bytes) + +0x30: refcount for publickey + +0x38: uint32_t keypairId + +0x40: shared_ptr> wrapdata (optional, can be empty) + +0x48: refcount for wrapdata + +0x50: SecretKey unwrapKey (for key unwrapping) + +0x58: bool hasWrapdata (flag at offset 0x58) +""") + + # ========================================================================= + # 5. getKeydata() - Key 33.6 Inner Content Construction + # ========================================================================= + print("\n[5] IosAdhKeyRequestData::getKeydata() @ 0x07B688") + print("-" * 72) + print(""" + This function builds the inner CBOR MslObject that becomes key 33 sub-key 6. + It is called by KeyRequestData::toMslEncoding() which wraps it with + "scheme" and "keydata" fields. + + Pseudocode (from disassembly): + + shared_ptr getKeydata(encoderFactory, format) { + auto result = make_shared(); + + // Check if wrapdata exists (this->wrapdata at +0x40) + bool hasWrap = (this->wrapdata.get() != nullptr + && this->wrapdata->begin() != this->wrapdata->end()); + + if (hasWrap) { + // ---- PATH A: With wrapdata (key renewal) ---- + result->put("mechanism", "WRAP"); // CBOR key 51 = "WRAP" + } else { + // ---- PATH B: Without wrapdata (initial) ---- + result->put("mechanism", "MGK"); // CBOR key 51 = "MGK" + } + + // Always: set parametersid = "1" + result->put("parametersid", "1"); // CBOR key 58 = "1" + + // Always: set publickey = correctNullBytes(this->publickey) + auto corrected = correctNullBytes(this->publickey); + result->put("publickey", corrected); // CBOR key 53 = bytes + + // Conditionally: set wrapdata if present + if (hasWrap) { + result->put("wrapdata", this->wrapdata); // CBOR key 65 = bytes + } + + return result; + } + + CBOR key mapping for keydata fields: + 51 (0x33) "mechanism" -> string: "MGK" or "WRAP" + 58 (0x3a) "parametersid" -> string: "1" + 53 (0x35) "publickey" -> bytes: DH public key (128B, null-corrected) + 65 (0x41) "wrapdata" -> bytes: optional wrap data +""") + + # ========================================================================= + # 6. KeyRequestData::toMslEncoding() - Outer Wrapping + # ========================================================================= + print("\n[6] KeyRequestData::toMslEncoding() @ 0x066114") + print("-" * 72) + print(""" + This base class method wraps getKeydata() output: + + vector toMslEncoding(encoderFactory, format) { + auto result = make_shared(); + + // 1. Add scheme name (from this->scheme at +0x10) + result->put("scheme", this->scheme_); // key 30 = "AUTHENTICATED_DH" + + // 2. Call virtual getKeydata() and add result + auto keydata = this->getKeydata(encoderFactory, format); + result->put("keydata", keydata); // key 31 = MslObject + + // 3. Encode to CBOR bytes + return encoderFactory->encodeObject(result, format); + } + + The toMslEncoding output becomes the value of key 33 sub-key 6 in + the MSL message. +""") + + # ========================================================================= + # 7. correctNullBytes() - DH Public Key Padding + # ========================================================================= + print("\n[7] IosAdhKeyx::correctNullBytes() @ 0x079358") + print("-" * 72) + print(""" + Ensures the DH public key has a leading 0x00 byte to indicate positive + (unsigned) big-endian integer representation. + + shared_ptr> correctNullBytes(shared_ptr> input) { + auto data = input.get(); + size_t size = data->end() - data->begin(); + size_t skip = 0; + + // Count leading zero bytes + for (size_t i = 0; i < size; i++) { + if (data[i] != 0) break; + skip++; + } + + if (skip == 1) { + // Already has exactly one leading 0x00 -> return as-is + return input; + } + + // Create new vector: 0x00 + non-zero-prefix bytes + size_t new_size = size - skip + 1; + auto result = make_shared>(new_size); + result[0] = 0x00; // Leading zero byte + memmove(result + 1, data + skip, size - skip); + return result; + } + + Result: DH pubkey always starts with 0x00, total size = 129B typically + (or 128B if the original already had one leading zero). + + This is standard ASN.1/DER encoding for unsigned integers: + prepend 0x00 if the high bit of the first byte is set. +""") + + # ========================================================================= + # 8. generateClientKeyRequestData() - Entry Point + # ========================================================================= + print("\n[8] IosAdhKeyx::generateClientKeyRequestData() @ 0x07944C") + print("-" * 72) + print(""" + Called from: KeyRequestDataProvider::getKeyRequestData() @ 0x098FE0 + + Signature: + shared_ptr + generateClientKeyRequestData( + shared_ptr crypto, + shared_ptr> wrapdata, + SecretKey& unwrapKey + ) + + Pseudocode: + 1. Create empty DH pubkey vector (0x30 = 48 bytes object size) + 2. Call dhKeyGen(crypto, pubkey_out, keypairId_out) + -> Generates DH key pair via AppleWebCrypto + -> Returns pubkey bytes and keypairId (key handle) + 3. Create IosAdhKeyRequestData(pubkey, keypairId, wrapdata, unwrapKey) + 4. Return shared_ptr to the request data +""") + + # ========================================================================= + # 9. dhKeyGen() - DH Key Generation + # ========================================================================= + print("\n[9] IosAdhKeyx::dhKeyGen() @ 0x079600 (2280 bytes)") + print("-" * 72) + print(""" + Signature: + void dhKeyGen( + shared_ptr crypto, + shared_ptr> pubkey_out, + uint& keypairId_out + ) + + Uses static DH parameters: + prime: stored at 0x176A68 (static local, lazy-initialized) + generator: stored at 0x176A88 (static local, lazy-initialized) + + Calls AppleWebCrypto::dhKeyGen(Variant& params) in NFWebCrypto.framework + which internally uses OpenSSL DH functions: + DH_new() -> DH_set0_pqg() -> DH_generate_key() + -> DH_get0_pub_key() -> BN_bn2bin() + + The DH group uses a 1024-bit prime (128 bytes). + Generator is a small integer (typically 2). +""") + + # ========================================================================= + # 10. Complete CBOR Structure of Key 33.6 + # ========================================================================= + print("\n[10] COMPLETE CBOR STRUCTURE OF KEY 33.6") + print("-" * 72) + print(""" + Key 33.6 is the CBOR-encoded output of KeyRequestData::toMslEncoding(), + which contains the scheme name and the keydata MslObject. + + === CBOR Wire Format (initial key exchange, mechanism=MGK) === + + tag(55799) <- CBOR self-describing tag 0xD9D9F7 + map(2) { <- 2 entries: scheme + keydata + 30: "AUTHENTICATED_DH", <- scheme (string) + 31: map(3) { <- keydata (3 entries) + 51: "MGK", <- mechanism + 58: "1", <- parametersid + 53: bytes(129) <- publickey (0x00 + 128B DH pubkey) + } + } + + === CBOR Wire Format (key renewal, mechanism=WRAP) === + + tag(55799) + map(2) { + 30: "AUTHENTICATED_DH", + 31: map(4) { <- keydata (4 entries with wrapdata) + 51: "WRAP", <- mechanism + 58: "1", <- parametersid + 53: bytes(129), <- publickey (0x00 + 128B DH pubkey) + 65: bytes(N) <- wrapdata (from previous session) + } + } + + === CBOR Integer Key Reference === + 16 = signature 30 = scheme + 31 = keydata 32 = mastertoken + 33 = headerdata 34 = entityauthdata + 42 = keyrequestdata 51 = mechanism + 53 = publickey 58 = parametersid + 65 = wrapdata + + === Size Estimation === + tag(55799): 3 bytes (D9 D9F7) + map(2) header: 1 byte (A2) + key 30 + "AUTHENTICATED_DH": 1 + 1 + 16 = 18 bytes + key 31 + map(3): 1 + 1 = 2 bytes + key 51 + "MGK": 1 + 1 + 3 = 5 bytes + key 58 + "1": 2 + 1 + 1 = 4 bytes + key 53 + bytes(129): 2 + 2 + 129 = 133 bytes + Total: ~166 bytes (mechanism=MGK, no wrapdata) +""") + + # ========================================================================= + # 11. Response Structure + # ========================================================================= + print("\n[11] IosAdhKeyResponseData::getKeydata() @ 0x07C028") + print("-" * 72) + print(""" + The response keydata has a simpler structure: + + tag(55799) + map(2) { + 30: "AUTHENTICATED_DH", + 31: map(2-3) { + 58: "1", <- parametersid + 53: bytes(N), <- publickey (server DH pubkey) + 65: bytes(N) <- wrapdata (optional, for key renewal) + } + } + + Response construction: + 1. If wrapdata exists -> put("wrapdata", wrapdata) + 2. Always put("parametersid", "1") + 3. Always put("publickey", correctNullBytes(serverPubkey)) + + After receiving the response: + IosAdhKeyx::getCryptoContext() @ 0x07C3B8 + -> IosAdhKeyx::nflxDhDerive() @ 0x07A0F4 + -> AppleWebCrypto::nflxDhDerive() + -> DH_compute_key(shared_secret, peer_pub, local_priv) + -> SHA384(local_priv_bytes) -> HMAC-SHA384 -> enc_key + sign_key +""") + + # ========================================================================= + # 12. Outer MSL Message Assembly + # ========================================================================= + print("\n[12] OUTER MSL MESSAGE ASSEMBLY (headerdata = key 33)") + print("-" * 72) + print(""" + The headerdata (CBOR key 33) contains: + + tag(55799) + map(N) { + 6: bytes(M) <- keyrequestdata[0].toMslEncoding() = key 33.6 + 7: bytes(0) <- mastertoken (empty for new session) + 8: string <- identity (ESN + "_N" suffix) + 9: bytes(16) <- client nonce (random 16 bytes) + ... <- additional fields (capabilities, etc.) + } + + Note: The sub-keys inside headerdata use a DIFFERENT numbering: + Sub-key 6 = first entry (NOT "ciphertext", but keyrequestdata content) + Sub-key 7 = second entry (mastertoken) + Sub-key 8 = third entry (identity/ESN) + Sub-key 9 = fourth entry (nonce) + + These sub-keys are the CBOR map integer keys within the headerdata map, + which are assigned by the CborMslSymbolTable for the field names used + in MessageHeader::toMslEncoding(). + + Relevant mappings (from symbol table): + 3 (0x03) = "identity" + 9 (0x09) = "iv" (used for nonce) + 20 (0x14) = "sender" + 22 (0x16) = "messageid" + 24 (0x18) = "timestamp" + 36 (0x24) = "capabilities" + 40 (0x28) = "nonreplayable" + 42 (0x2A) = "keyrequestdata" + + The keyrequestdata field (key 42) is a CBOR array containing one or + more KeyRequestData objects, each encoded via toMslEncoding(). + + The observed sub-key 6 in captures corresponds to "ciphertext" (0x06) + when the headerdata is encrypted, or the first meaningful field when + it is a cleartext map. +""") + + # ========================================================================= + # 13. Function Address Summary + # ========================================================================= + print("\n[13] FUNCTION ADDRESS SUMMARY") + print("-" * 72) + print(""" + Key Exchange: + 0x0007944C IosAdhKeyx::generateClientKeyRequestData() + 0x00079600 IosAdhKeyx::dhKeyGen() + 0x00079358 IosAdhKeyx::correctNullBytes() + 0x0007A0F4 IosAdhKeyx::nflxDhDerive() + 0x0007C338 IosAdhKeyx::createRequestData() + 0x0007C378 IosAdhKeyx::createResponseData() + 0x0007C3B8 IosAdhKeyx::getCryptoContext() + + Request/Response Data: + 0x0007B260 IosAdhKeyRequestData::IosAdhKeyRequestData(pubkey, id, wrap, key) + 0x0007B684 IosAdhKeyRequestData::IosAdhKeyRequestData(MslObject) + 0x0007B688 IosAdhKeyRequestData::getKeydata() + 0x0007CD58 IosAdhKeyRequestData::getUnwrapKey() + 0x0007C028 IosAdhKeyResponseData::getKeydata() + + MSL Encoding: + 0x00066114 KeyRequestData::toMslEncoding() + 0x000AD778 CborMslEncoder__encodeObject() + 0x000605BC CborMslSymbolTable__integerForString() + 0x00060590 CborMslSymbolTable__stringForInteger() + + CBOR Primitives: + 0x0011ABC0 _cbor_encode_tag() + 0x0011AB9C _cbor_encode_map_start() + 0x0011AB10 _cbor_encode_negint() + 0x0011AAE8 _cbor_encode_uint() + 0x0011AB18 _cbor_encode_bytestring_start() + 0x0011AB54 _cbor_encode_string_start() + + Key Exchange Schemes: + 0x0016E7C8 NetflixKeyExchangeScheme::AUTHENTICATED_DH + 0x0016E848 NetflixKeyExchangeScheme::NFLX_DH + 0x0016E828 NetflixKeyExchangeScheme::CDM + 0x00172D70 KeyExchangeScheme::DIFFIE_HELLMAN + 0x00172DD0 KeyExchangeScheme::SYMMETRIC_WRAPPED + + Symbol Table: + 0x0015C990 s_tags[] (96-entry pointer array) + 0x00060698 __GLOBAL__sub_I_CborMslSymbolTable.cpp (init) +""") + + # ========================================================================= + # 14. Conclusions + # ========================================================================= + print("\n" + "=" * 72) + print("CONCLUSIONS") + print("=" * 72) + print(""" + 1. KEY 33.6 IS NOT RAW BYTES: It is a nested CBOR object, not a raw + DH public key or encrypted blob. The CBOR structure is: + tag(55799) + map { scheme, keydata { mechanism, parametersid, publickey } } + + 2. MECHANISM FIELD: Two modes: + - "MGK" = Model Group Key (initial authentication, uses TFIT WB-AES + encrypted DH pubkey via NFWebCrypto.framework) + - "WRAP" = Key wrapping (session renewal, includes wrapdata from + previous session) + + 3. PARAMETERSID = "1": Fixed string, identifies the DH parameter set. + The DH group parameters (prime, generator) are static locals in + IosAdhKeyx::dhKeyGen(). + + 4. PUBLICKEY: The DH public key (128B) with null-byte correction + (prepend 0x00 if needed, making it 129B). This is NOT TFIT-encrypted + at the MslClient layer - TFIT encoding happens at the NFWebCrypto + layer if the mechanism requires it. + + 5. CBOR INTEGER KEYS: The observed integer keys in captures correspond to + the CborMslSymbolTable mapping: + 16 = signature, 30 = scheme, 31 = keydata, 32 = mastertoken, + 33 = headerdata, 34 = entityauthdata, 42 = keyrequestdata, + 51 = mechanism, 53 = publickey, 58 = parametersid, 65 = wrapdata + + 6. OBSERVED 352B/464B PAYLOADS: The larger sizes seen in captures include: + - The CBOR tag and map overhead + - The scheme string "AUTHENTICATED_DH" (16 bytes) + - The nested keydata map with mechanism, parametersid, publickey + - Optional wrapdata (for WRAP mechanism) + - The publickey after correctNullBytes (129B) + The exact size depends on CBOR encoding overhead and optional fields. + + 7. THE 128B CBOR HEADER: Previously assumed to be a "fixed CBOR header" + in the 352B payload is actually the CBOR structure containing scheme, + mechanism, parametersid, and CBOR framing bytes. The "session region" + is the DH public key bytes within the publickey field. + + 8. Nbp.framework ROLE: Nbp does NOT implement key exchange directly. + It uses MslClient.framework through ObjC bridging (IosMslClient). + All key exchange logic is in MslClient.framework's C++ implementation. +""") + + r2.quit() + + +if __name__ == "__main__": + if len(sys.argv) < 2: + binary = DEFAULT_BINARY + else: + binary = sys.argv[1] + analyze(binary)