feat(frida): appboot DH共有秘密の手動計算と初期鍵復号試行スクリプト

DH_generate_keyフックでDHハンドルを保存し、サーバー公開鍵を手動設定後
NativeFunctionでDH_compute_keyを呼び出して共有秘密を計算する。
SHA-384/SHA-256/HMAC等の複数候補鍵でkey 33.6の復号を試行し、
復号結果にKDFを適用して既知のenc_key_1と照合する。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
tkgstrator
2026-04-08 13:48:48 +00:00
co-authored by Claude Opus 4.6
parent f0b16a257f
commit 9fc124f112
+517
View File
@@ -0,0 +1,517 @@
/**
* hook_appboot_dh.js — appboot DH 共有秘密の手動計算と初期鍵復号試行
*
* 目的: DH_generate_key で得た秘密鍵と appboot レスポンスのサーバー公開鍵から
* DH_compute_key を手動呼び出しして共有秘密を取得し、
* key 33.6 (96B 暗号文) の復号を試行する
*
* 使い方:
* frida -U -n Netflix -l hook_appboot_dh.js
*
* 1. アプリデータを削除して Netflix を起動 (appboot を発生させる)
* 2. DH_generate_key が発火したら DH ハンドルが自動保存される
* 3. appboot レスポンスから key 33 のサーバー公開鍵・暗号文を手動で設定:
* setServerPubKey("hex...")
* setKey336("hex...") // 96 bytes
* setKey339("hex...") // 16 bytes nonce
* 4. computeAndDecrypt() で共有秘密の計算 → 復号試行を実行
*/
"use strict";
// ===== Utility =====
function hex(ptr, len) {
if (!ptr || ptr.isNull() || len <= 0) return "(null)";
try {
return Array.from(new Uint8Array(ptr.readByteArray(len)))
.map(function (b) { return ("0" + b.toString(16)).slice(-2); })
.join("");
} catch (e) {
return "(unreadable)";
}
}
function hexToBytes(hexStr) {
var bytes = [];
for (var i = 0; i < hexStr.length; i += 2) {
bytes.push(parseInt(hexStr.substr(i, 2), 16));
}
return new Uint8Array(bytes);
}
function bytesToHex(arr) {
return Array.from(arr).map(function (b) { return ("0" + b.toString(16)).slice(-2); }).join("");
}
// ===== NFWebCrypto module & function pointers =====
var nfwc = Process.findModuleByName("NFWebCrypto");
if (!nfwc) {
console.log("[-] NFWebCrypto not found. Is Netflix running?");
} else {
console.log("[*] NFWebCrypto base=" + nfwc.base + " size=" + nfwc.size);
}
// OpenSSL function pointers from NFWebCrypto
var fn = {};
var fnNames = [
"DH_generate_key", "DH_compute_key", "DH_new", "DH_free",
"DH_get0_key", "DH_get0_pqg", "DH_set0_pqg", "DH_set0_key",
"BN_new", "BN_free", "BN_bin2bn", "BN_bn2bin", "BN_num_bits", "BN_dup",
"SHA256", "SHA384",
"AES_set_decrypt_key", "AES_cbc_encrypt",
"HMAC",
"EVP_sha256",
];
fnNames.forEach(function (name) {
var addr = nfwc ? nfwc.findExportByName(name) : null;
if (addr) {
fn[name] = addr;
console.log("[+] " + name + " @ " + addr);
} else {
console.log("[-] " + name + " not found");
}
});
// ===== NativeFunction wrappers =====
var DH_compute_key = fn.DH_compute_key ? new NativeFunction(fn.DH_compute_key,
'int', ['pointer', 'pointer', 'pointer']) : null;
var BN_bin2bn = fn.BN_bin2bn ? new NativeFunction(fn.BN_bin2bn,
'pointer', ['pointer', 'int', 'pointer']) : null;
var BN_bn2bin = fn.BN_bn2bin ? new NativeFunction(fn.BN_bn2bin,
'int', ['pointer', 'pointer']) : null;
var BN_num_bits = fn.BN_num_bits ? new NativeFunction(fn.BN_num_bits,
'int', ['pointer']) : null;
var BN_free = fn.BN_free ? new NativeFunction(fn.BN_free,
'void', ['pointer']) : null;
var SHA256 = fn.SHA256 ? new NativeFunction(fn.SHA256,
'pointer', ['pointer', 'size_t', 'pointer']) : null;
var SHA384 = fn.SHA384 ? new NativeFunction(fn.SHA384,
'pointer', ['pointer', 'size_t', 'pointer']) : null;
var AES_set_decrypt_key = fn.AES_set_decrypt_key ? new NativeFunction(fn.AES_set_decrypt_key,
'int', ['pointer', 'int', 'pointer']) : null;
var AES_cbc_encrypt = fn.AES_cbc_encrypt ? new NativeFunction(fn.AES_cbc_encrypt,
'void', ['pointer', 'pointer', 'size_t', 'pointer', 'pointer', 'int']) : null;
var HMAC_fn = fn.HMAC ? new NativeFunction(fn.HMAC,
'pointer', ['pointer', 'pointer', 'int', 'pointer', 'size_t', 'pointer', 'pointer']) : null;
var EVP_sha256 = fn.EVP_sha256 ? new NativeFunction(fn.EVP_sha256,
'pointer', []) : null;
// ===== State =====
var g_dhHandle = null; // DH* from DH_generate_key
var g_dhPubKey = null; // client public key (hex)
var g_dhPrivKey = null; // client private key (hex)
var g_dhP = null; // DH p parameter (hex)
var g_dhG = null; // DH g parameter (hex)
var g_serverPubKey = null; // server DH public key (hex) — set manually
var g_key336 = null; // key 33.6 ciphertext 96 bytes (hex)
var g_key339 = null; // key 33.9 nonce 16 bytes (hex)
// Known PSK and nonce from binary
var PSK_HEX = "027617984f6227539a630b897c017d69";
var NONCE_HEX = "809f82a7addf548d3ea9dd067ff9bb91";
// ===== Hook DH_generate_key to capture DH handle =====
if (fn.DH_generate_key) {
var DH_get0_key = fn.DH_get0_key ? new NativeFunction(fn.DH_get0_key,
'void', ['pointer', 'pointer', 'pointer']) : null;
var DH_get0_pqg = fn.DH_get0_pqg ? new NativeFunction(fn.DH_get0_pqg,
'void', ['pointer', 'pointer', 'pointer', 'pointer']) : null;
Interceptor.attach(fn.DH_generate_key, {
onEnter: function (args) {
this.dh = args[0];
},
onLeave: function (retval) {
if (retval.toInt32() !== 1) {
console.log("[-] DH_generate_key failed");
return;
}
g_dhHandle = this.dh;
console.log("\n[DH_generate_key] DH handle saved: " + this.dh);
// Extract pub/priv keys
if (DH_get0_key) {
var pubPtr = Memory.alloc(Process.pointerSize);
var privPtr = Memory.alloc(Process.pointerSize);
DH_get0_key(this.dh, pubPtr, privPtr);
var pub = pubPtr.readPointer();
var priv = privPtr.readPointer();
if (!pub.isNull() && BN_num_bits && BN_bn2bin) {
var pubBits = BN_num_bits(pub);
var pubBytes = (pubBits + 7) >> 3;
var pubBuf = Memory.alloc(pubBytes);
BN_bn2bin(pub, pubBuf);
g_dhPubKey = hex(pubBuf, pubBytes);
console.log("[DH] pub_key (" + pubBytes + "B) = " + g_dhPubKey.substring(0, 32) + "...");
}
if (!priv.isNull() && BN_num_bits && BN_bn2bin) {
var privBits = BN_num_bits(priv);
var privBytes = (privBits + 7) >> 3;
var privBuf = Memory.alloc(privBytes);
BN_bn2bin(priv, privBuf);
g_dhPrivKey = hex(privBuf, privBytes);
console.log("[DH] priv_key (" + privBytes + "B) = " + g_dhPrivKey.substring(0, 32) + "...");
}
}
// Extract DH parameters (p, g)
if (DH_get0_pqg) {
var pPtr = Memory.alloc(Process.pointerSize);
var qPtr = Memory.alloc(Process.pointerSize);
var gPtr = Memory.alloc(Process.pointerSize);
DH_get0_pqg(this.dh, pPtr, qPtr, gPtr);
var p = pPtr.readPointer();
var g = gPtr.readPointer();
if (!p.isNull() && BN_num_bits && BN_bn2bin) {
var pBits = BN_num_bits(p);
var pBytes = (pBits + 7) >> 3;
var pBuf = Memory.alloc(pBytes);
BN_bn2bin(p, pBuf);
g_dhP = hex(pBuf, pBytes);
console.log("[DH] p (" + pBytes + "B) = " + g_dhP.substring(0, 16) + "...");
}
if (!g.isNull() && BN_num_bits && BN_bn2bin) {
var gBits = BN_num_bits(g);
var gBytes = (gBits + 7) >> 3;
var gBuf = Memory.alloc(gBytes);
BN_bn2bin(g, gBuf);
g_dhG = hex(gBuf, gBytes);
console.log("[DH] g = " + g_dhG);
}
}
console.log("\n[*] DH handle captured. Now set server public key with:");
console.log(' setServerPubKey("hex...")');
}
});
console.log("[+] DH_generate_key hooked");
}
// ===== Also hook DH_compute_key to see if Netflix calls it =====
if (fn.DH_compute_key) {
Interceptor.attach(fn.DH_compute_key, {
onEnter: function (args) {
this.outBuf = args[0];
console.log("[!] DH_compute_key CALLED by Netflix! (not expected)");
},
onLeave: function (retval) {
var len = retval.toInt32();
if (len > 0) {
console.log("[DH_compute_key] shared_secret (" + len + "B) = " + hex(this.outBuf, Math.min(len, 64)) + "...");
}
}
});
console.log("[+] DH_compute_key hooked (observer)");
}
// ===== RPC functions callable from Frida console =====
// Set server's DH public key (from appboot response)
rpc.exports.setServerPubKey = function (hexStr) {
g_serverPubKey = hexStr;
console.log("[*] Server pub key set (" + (hexStr.length / 2) + " bytes)");
};
// Set key 33.6 (96 bytes ciphertext)
rpc.exports.setKey336 = function (hexStr) {
g_key336 = hexStr;
console.log("[*] key 33.6 set (" + (hexStr.length / 2) + " bytes)");
};
// Set key 33.9 (16 bytes nonce)
rpc.exports.setKey339 = function (hexStr) {
g_key339 = hexStr;
console.log("[*] key 33.9 set (" + (hexStr.length / 2) + " bytes)");
};
// Helper: AES-128-CBC decrypt
function aesCbcDecrypt(keyBytes, ivBytes, ctBytes) {
if (!AES_set_decrypt_key || !AES_cbc_encrypt) {
console.log("[-] AES functions not available");
return null;
}
var keyBuf = Memory.alloc(keyBytes.length);
keyBuf.writeByteArray(keyBytes.buffer);
// AES_KEY struct is ~256 bytes
var aesKey = Memory.alloc(256);
var ret = AES_set_decrypt_key(keyBuf, keyBytes.length * 8, aesKey);
if (ret !== 0) {
console.log("[-] AES_set_decrypt_key failed: " + ret);
return null;
}
var ivBuf = Memory.alloc(16);
ivBuf.writeByteArray(ivBytes.buffer);
var ctBuf = Memory.alloc(ctBytes.length);
ctBuf.writeByteArray(ctBytes.buffer);
var ptBuf = Memory.alloc(ctBytes.length);
// enc=0 means decrypt
AES_cbc_encrypt(ctBuf, ptBuf, ctBytes.length, aesKey, ivBuf, 0);
return new Uint8Array(ptBuf.readByteArray(ctBytes.length));
}
// Helper: HMAC-SHA256
function hmacSha256(keyBytes, dataBytes) {
if (!HMAC_fn || !EVP_sha256) {
console.log("[-] HMAC functions not available");
return null;
}
var md = EVP_sha256();
var keyBuf = Memory.alloc(keyBytes.length);
keyBuf.writeByteArray(keyBytes.buffer);
var dataBuf = Memory.alloc(dataBytes.length);
dataBuf.writeByteArray(dataBytes.buffer);
var outBuf = Memory.alloc(32);
var outLen = Memory.alloc(4);
outLen.writeU32(32);
var result = HMAC_fn(md, keyBuf, keyBytes.length, dataBuf, dataBytes.length, outBuf, outLen);
if (result.isNull()) {
console.log("[-] HMAC failed");
return null;
}
return new Uint8Array(outBuf.readByteArray(32));
}
// Main: compute shared secret and try to decrypt key 33.6
rpc.exports.computeAndDecrypt = function () {
return computeAndDecryptImpl();
};
function computeAndDecryptImpl() {
console.log("\n========== computeAndDecrypt ==========");
if (!g_dhHandle) {
console.log("[-] No DH handle. Wait for DH_generate_key to fire.");
return;
}
if (!g_serverPubKey) {
console.log("[-] No server pub key. Call setServerPubKey(hex)");
return;
}
// --- Step 1: Convert server pub key hex to BIGNUM ---
var serverPubBytes = hexToBytes(g_serverPubKey);
var serverPubBuf = Memory.alloc(serverPubBytes.length);
serverPubBuf.writeByteArray(serverPubBytes.buffer);
var serverPubBN = BN_bin2bn(serverPubBuf, serverPubBytes.length, ptr(0));
if (serverPubBN.isNull()) {
console.log("[-] BN_bin2bn failed for server pub key");
return;
}
console.log("[+] Server pub key BIGNUM created (" + serverPubBytes.length + " bytes)");
// --- Step 2: Call DH_compute_key ---
var outBuf = Memory.alloc(256);
var sharedLen = DH_compute_key(outBuf, serverPubBN, g_dhHandle);
BN_free(serverPubBN);
if (sharedLen <= 0) {
console.log("[-] DH_compute_key failed: " + sharedLen);
return;
}
var sharedSecret = new Uint8Array(outBuf.readByteArray(sharedLen));
var sharedHex = bytesToHex(sharedSecret);
console.log("[+] DH shared_secret (" + sharedLen + "B) = " + sharedHex);
// --- Step 3: Derive candidate keys from shared_secret ---
console.log("\n--- Candidate key derivation ---");
// 3a. SHA-384(shared_secret)
var sha384Out = Memory.alloc(48);
var sha384In = Memory.alloc(sharedLen);
sha384In.writeByteArray(sharedSecret.buffer);
SHA384(sha384In, sharedLen, sha384Out);
var sha384 = new Uint8Array(sha384Out.readByteArray(48));
console.log("[SHA384] " + bytesToHex(sha384));
console.log(" enc_candidate = " + bytesToHex(sha384.slice(0, 16)));
console.log(" sign_candidate = " + bytesToHex(sha384.slice(16, 48)));
// 3b. SHA-384(0x00 || shared_secret) — MSL Java reference style
var padded1 = new Uint8Array(1 + sharedLen);
padded1[0] = 0x00;
padded1.set(sharedSecret, 1);
var padded1Buf = Memory.alloc(padded1.length);
padded1Buf.writeByteArray(padded1.buffer);
SHA384(padded1Buf, padded1.length, sha384Out);
var sha384null = new Uint8Array(sha384Out.readByteArray(48));
console.log("[SHA384(0x00||ss)] " + bytesToHex(sha384null));
console.log(" enc_candidate = " + bytesToHex(sha384null.slice(0, 16)));
console.log(" sign_candidate = " + bytesToHex(sha384null.slice(16, 48)));
// 3c. SHA-256(shared_secret)
var sha256Out = Memory.alloc(32);
SHA256(sha384In, sharedLen, sha256Out);
var sha256 = new Uint8Array(sha256Out.readByteArray(32));
console.log("[SHA256] " + bytesToHex(sha256));
console.log(" enc_candidate = " + bytesToHex(sha256.slice(0, 16)));
// 3d. Shared secret first 16 bytes as raw key
console.log("[RAW] first 16B = " + bytesToHex(sharedSecret.slice(0, 16)));
// --- Step 4: Try to decrypt key 33.6 if available ---
if (g_key336) {
console.log("\n--- key 33.6 decryption attempts ---");
var key336Bytes = hexToBytes(g_key336);
if (key336Bytes.length !== 96) {
console.log("[!] key 33.6 is " + key336Bytes.length + " bytes (expected 96)");
}
// Structure: IV(16) + CT(48) + HMAC(32)
var iv = key336Bytes.slice(0, 16);
var ct = key336Bytes.slice(16, 64);
var hmacTag = key336Bytes.slice(64, 96);
console.log(" IV = " + bytesToHex(iv));
console.log(" CT = " + bytesToHex(ct));
console.log(" HMAC = " + bytesToHex(hmacTag));
// Try each candidate key
var candidates = [
{ name: "SHA384[:16]", key: sha384.slice(0, 16) },
{ name: "SHA384(0x00||ss)[:16]", key: sha384null.slice(0, 16) },
{ name: "SHA256[:16]", key: sha256.slice(0, 16) },
{ name: "raw_ss[:16]", key: sharedSecret.slice(0, 16) },
{ name: "PSK", key: hexToBytes(PSK_HEX) },
];
// Also try HMAC-based derivations
var pskBytes = hexToBytes(PSK_HEX);
// HMAC-SHA256(PSK, shared_secret)[:16]
var hmacPskSs = hmacSha256(pskBytes, sharedSecret);
if (hmacPskSs) {
candidates.push({ name: "HMAC(PSK,ss)[:16]", key: hmacPskSs.slice(0, 16) });
}
// HMAC-SHA256(shared_secret[:16], PSK)
var hmacSsPsk = hmacSha256(sharedSecret.slice(0, 16), pskBytes);
if (hmacSsPsk) {
candidates.push({ name: "HMAC(ss[:16],PSK)[:16]", key: hmacSsPsk.slice(0, 16) });
}
// HMAC-SHA256(shared_secret, PSK)[:16]
var hmacFullSsPsk = hmacSha256(sharedSecret, pskBytes);
if (hmacFullSsPsk) {
candidates.push({ name: "HMAC(ss,PSK)[:16]", key: hmacFullSsPsk.slice(0, 16) });
}
candidates.forEach(function (c) {
var pt = aesCbcDecrypt(c.key, iv, ct);
if (pt) {
console.log("\n [" + c.name + "] decrypt key = " + bytesToHex(c.key));
console.log(" plaintext (48B) = " + bytesToHex(pt));
console.log(" enc_key_0? = " + bytesToHex(pt.slice(0, 16)));
console.log(" sign_key_0? = " + bytesToHex(pt.slice(16, 48)));
// Verify: run KDF with this candidate enc_key_0/sign_key_0
// and check if it produces known enc_key_1
var nonceBytes = g_key339 ? hexToBytes(g_key339) : hexToBytes(NONCE_HEX);
var encKey0 = pt.slice(0, 16);
var signKey0 = pt.slice(16, 48);
// KDF Step 3: enc_temp = HMAC(PSK, enc_key_0)
var encTemp = hmacSha256(pskBytes, encKey0);
if (encTemp) {
// KDF Step 4: new_enc = HMAC(enc_temp, nonce)[:16]
var newEnc = hmacSha256(encTemp, nonceBytes);
if (newEnc) {
console.log(" KDF → enc_key_1 = " + bytesToHex(newEnc.slice(0, 16)));
}
}
// KDF Step 5: sign_temp = HMAC(PSK, sign_key_0)
var signTemp = hmacSha256(pskBytes, signKey0);
if (signTemp) {
// KDF Step 6: new_sign = HMAC(sign_temp, nonce)
var newSign = hmacSha256(signTemp, nonceBytes);
if (newSign) {
console.log(" KDF → sign_key_1 = " + bytesToHex(newSign));
}
}
}
});
} else {
console.log("\n[*] key 33.6 not set. Call setKey336(hex) to try decryption.");
}
console.log("\n========== done ==========");
}
// ===== Convenience: also hook AES_set_encrypt_key / AES_set_decrypt_key to capture session keys =====
if (fn.AES_set_decrypt_key) {
Interceptor.attach(fn.AES_set_decrypt_key, {
onEnter: function (args) {
var bits = args[1].toInt32();
var keyLen = bits / 8;
if (keyLen === 16) {
console.log("[AES_set_decrypt_key] bits=" + bits + " key=" + hex(args[0], keyLen));
}
}
});
}
// Make functions callable from Frida REPL
// Usage: setServerPubKey("abcd..."), computeAndDecrypt()
var global = this;
global.setServerPubKey = function (h) { g_serverPubKey = h; console.log("[*] Server pub key set (" + (h.length / 2) + "B)"); };
global.setKey336 = function (h) { g_key336 = h; console.log("[*] key 33.6 set (" + (h.length / 2) + "B)"); };
global.setKey339 = function (h) { g_key339 = h; console.log("[*] key 33.9 set (" + (h.length / 2) + "B)"); };
global.computeAndDecrypt = computeAndDecryptImpl;
global.getState = function () {
console.log("DH handle: " + (g_dhHandle ? g_dhHandle : "null"));
console.log("Client pub: " + (g_dhPubKey ? g_dhPubKey.substring(0, 32) + "..." : "null"));
console.log("Client priv: " + (g_dhPrivKey ? g_dhPrivKey.substring(0, 32) + "..." : "null"));
console.log("Server pub: " + (g_serverPubKey ? g_serverPubKey.substring(0, 32) + "..." : "null"));
console.log("key 33.6: " + (g_key336 ? g_key336.substring(0, 32) + "... (" + (g_key336.length / 2) + "B)" : "null"));
console.log("key 33.9: " + (g_key339 || "null"));
};
console.log("\n=== appboot DH hook v1 ===");
console.log("Commands:");
console.log(" getState() — show current state");
console.log(" setServerPubKey('hex...') — set server DH public key");
console.log(" setKey336('hex...') — set key 33.6 ciphertext (96B)");
console.log(" setKey339('hex...') — set key 33.9 nonce (16B)");
console.log(" computeAndDecrypt() — compute shared secret & try decrypt");
console.log("");
console.log("[*] Waiting for DH_generate_key... (clear app data & restart Netflix)");