Files
revkit/packages/frida/hook_entityauth_capture.js
T
tkgstratorandClaude Sonnet 4.6 0d73df8249 feat(frida): add entity_auth_data / NFWebCrypto key capture hooks
- hook_entityauth_capture.js: runtime capture of devicetoken (216B),
  apphmac (32B HMAC-SHA256), and device_key_data (~6576B) via Nbp
  symbol enumeration, ObjC method scanning, NSData file hooks,
  NSUserDefaults, and sqlite3_step BLOB filter
- hook_nfwebcrypto_keys.js: dump kAppBootKey (RSA-4096 SPKI) and
  kAppBootEccKey (ECDSA P-256 SPKI) via NFWebCrypto memory scan +
  d2i_PUBKEY / EVP_DigestVerifyInit hooks; also scans for ABKP /
  ABECCKP handle strings
- hook_appboot_dh.js: extend DH prime p log from first 16B to full
  128B; extend BN_mod_exp base/exp/mod and result dumps to full bytes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 08:33:44 +00:00

619 lines
26 KiB
JavaScript

/**
* hook_entityauth_capture.js — entity_auth_data 未知値の実行時キャプチャ
*
* 目的: MSL entity_auth_data に含まれる以下の3値を実行時にキャプチャする
* 1. devicetoken (216B protobuf) — Nbp.framework 生成
* 2. apphmac (32B HMAC-SHA256) — NFWebCrypto.framework HMAC
* 3. device_key_data (~6576B) — ファイル / NSUserDefaults / SQLite から読み込み
*
* 使い方:
* frida -H host.docker.internal:27042 -n Argo -l hook_entityauth_capture.js
*
* 注意:
* - Netflix は standalone spawn で JB 検知して落ちるため、必ず attach で使うこと
* - appboot 発生後に entity_auth_data が組み立てられるタイミングでキャプチャされる
*/
"use strict";
// ===== Utility =====
function ts() {
return new Date().toISOString();
}
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:" + e.message + ")";
}
}
function hexShort(ptr, len) {
if (len <= 64) return hex(ptr, len);
return hex(ptr, 32) + "..." + hex(ptr.add(len - 16), 16) + " (" + len + "B)";
}
function hexArr(arr) {
return Array.from(arr)
.map(function (b) { return ("0" + b.toString(16)).slice(-2); })
.join("");
}
function banner(title) {
var line = "=".repeat(60);
console.log("\n" + line);
console.log(" " + title);
console.log(" " + ts());
console.log(line);
}
// ===== Module wait helper =====
function waitForModule(name, cb) {
var m = Process.findModuleByName(name);
if (m) { cb(m); return; }
var iv = setInterval(function () {
var m2 = Process.findModuleByName(name);
if (m2) { clearInterval(iv); cb(m2); }
}, 200);
}
// ===== Captured state (accessible via RPC) =====
var captured = {
devicetoken: null, // hex string (216B protobuf)
apphmac: null, // hex string (32B HMAC-SHA256)
deviceKeyData: null, // hex string (~6576B)
hmacCandidates: [], // all 32B HMAC outputs seen near entity_auth assembly
fileReads: [], // large file reads (>= 1KB)
};
// ===== 1. devicetoken capture — Nbp.framework =====
//
// Strategy:
// a. Enumerate all exports/symbols in Nbp that match token/credential/Device
// b. Hook promising candidates and log 216B-sized return buffers
// c. Hook ObjC methods on NflxPinnedCertEvaluator / IosMslClient that return NSData
waitForModule("Nbp", function (nbp) {
banner("Nbp.framework loaded — scanning for devicetoken symbols");
console.log("[*] Nbp base=" + nbp.base + " size=" + nbp.size);
// ---- Enumerate exports matching token/credential/device patterns ----
var tokenPatterns = ["token", "Token", "credential", "Credential", "DeviceToken",
"device", "Device", "attestation", "Attestation", "regist", "Register"];
var candidateExports = [];
var candidateSymbols = [];
try {
nbp.enumerateExports().forEach(function (exp) {
for (var i = 0; i < tokenPatterns.length; i++) {
if (exp.name.indexOf(tokenPatterns[i]) !== -1) {
candidateExports.push(exp);
console.log("[NBP-EXP] " + exp.type + " " + exp.name + " @ " + exp.address);
break;
}
}
});
} catch (e) {
console.log("[-] enumerateExports error: " + e.message);
}
try {
nbp.enumerateSymbols().forEach(function (sym) {
if (sym.type !== "function") return;
for (var i = 0; i < tokenPatterns.length; i++) {
if (sym.name.indexOf(tokenPatterns[i]) !== -1) {
candidateSymbols.push(sym);
console.log("[NBP-SYM] " + sym.type + " " + sym.name + " @ " + sym.address);
break;
}
}
});
} catch (e) {
console.log("[-] enumerateSymbols error: " + e.message);
}
// ---- Hook all candidates: watch for 216B buffer returns ----
var TARGET_TOKEN_LEN = 216;
function tryHookTokenCandidate(name, addr) {
try {
Interceptor.attach(addr, {
onEnter: function (args) {
// Common pattern: output buffer pointer as first or second arg
this.arg0 = args[0];
this.arg1 = args[1];
this.arg2 = args[2];
},
onLeave: function (retval) {
// Check if retval looks like a pointer to NSData or raw buffer
if (!retval || retval.isNull()) return;
// Try reading as length-prefixed buffer (NSData: [ptr isa][ptr bytes][size_t length])
try {
var lenOffset = 2 * Process.pointerSize;
var dataLen = retval.add(lenOffset).readUSize();
if (dataLen === TARGET_TOKEN_LEN) {
var bytesPtr = retval.add(Process.pointerSize).readPointer();
if (!bytesPtr.isNull()) {
var h = hex(bytesPtr, TARGET_TOKEN_LEN);
banner("devicetoken CANDIDATE (NSData path) — " + name);
console.log("[+] length = " + dataLen);
console.log("[+] bytes = " + h);
captured.devicetoken = h;
}
}
} catch (e2) {}
// Try reading retval directly as 216B buffer
try {
var h2 = hex(retval, TARGET_TOKEN_LEN);
// Sanity: first byte should be 0x0a (protobuf field 1 type 2)
var first = retval.readU8();
if (first === 0x0a || first === 0x08 || first === 0x12) {
banner("devicetoken CANDIDATE (raw ptr path) — " + name);
console.log("[+] bytes = " + h2);
if (!captured.devicetoken) captured.devicetoken = h2;
}
} catch (e3) {}
}
});
console.log("[+] Hooked NBP: " + name);
} catch (e) {
console.log("[-] Failed to hook NBP " + name + ": " + e.message);
}
}
candidateExports.forEach(function (exp) {
if (exp.type === "function") tryHookTokenCandidate(exp.name, exp.address);
});
candidateSymbols.forEach(function (sym) {
tryHookTokenCandidate(sym.name, sym.address);
});
// Also dump ALL exports so we can review them offline
console.log("\n[NBP] Full export list:");
try {
nbp.enumerateExports().forEach(function (exp) {
console.log(" [NBP-ALL] " + exp.type + " " + exp.name);
});
} catch (e) {
console.log("[-] " + e.message);
}
});
// ===== ObjC hooks for devicetoken via IosMslClient / NflxPinnedCertEvaluator =====
setTimeout(function () {
if (!ObjC.available) {
console.log("[-] ObjC runtime not available");
return;
}
banner("ObjC scan for devicetoken / device_key_data methods");
var TARGET_TOKEN_LEN = 216;
var TARGET_DKD_LEN_MIN = 6000;
var TARGET_DKD_LEN_MAX = 7200;
// Scan all loaded ObjC classes for methods that return NSData and match our patterns
var classPatterns = ["Nflx", "Netflix", "Msl", "MSL", "Device", "Token",
"Credential", "Auth", "Attestation", "Boot", "Key"];
var methodPatterns = ["token", "Token", "credential", "Credential", "deviceKey",
"DeviceKey", "keyData", "KeyData", "attestation", "deviceData"];
ObjC.enumerateLoadedClasses(function (name) {
var matchClass = false;
for (var i = 0; i < classPatterns.length; i++) {
if (name.indexOf(classPatterns[i]) !== -1) { matchClass = true; break; }
}
if (!matchClass) return;
try {
var cls = ObjC.classes[name];
if (!cls) return;
cls.$ownMethods.forEach(function (method) {
var matchMethod = false;
for (var j = 0; j < methodPatterns.length; j++) {
if (method.indexOf(methodPatterns[j]) !== -1) { matchMethod = true; break; }
}
if (!matchMethod) return;
console.log("[OBJC-CAND] " + name + " " + method);
try {
var impl = cls[method];
if (!impl) return;
Interceptor.attach(impl.implementation, {
onLeave: function (retval) {
if (!retval || retval.isNull()) return;
try {
// Check if it's NSData
var isData = retval.isKindOfClass_
? retval.isKindOfClass_(ObjC.classes.NSData)
: false;
// Try ObjC API
var obj = new ObjC.Object(retval);
var className = obj.$className;
if (className === "NSData" || className === "__NSCFData" ||
className === "NSMutableData" || className === "_NSInlineData") {
var dataLen = obj.length();
if (dataLen === TARGET_TOKEN_LEN) {
var bytesPtr = obj.bytes();
var h = hex(bytesPtr, TARGET_TOKEN_LEN);
banner("devicetoken via ObjC: " + name + " " + method);
console.log("[+] length = " + dataLen);
console.log("[+] bytes = " + h);
captured.devicetoken = h;
} else if (dataLen >= TARGET_DKD_LEN_MIN && dataLen <= TARGET_DKD_LEN_MAX) {
var bytesPtr2 = obj.bytes();
var h2 = hex(bytesPtr2, dataLen);
banner("device_key_data via ObjC: " + name + " " + method);
console.log("[+] length = " + dataLen);
console.log("[+] bytes = " + h2);
captured.deviceKeyData = h2;
}
}
} catch (e) {}
}
});
console.log("[+] ObjC hooked: " + name + " " + method);
} catch (e) {
console.log("[-] ObjC hook failed: " + name + " " + method + ": " + e.message);
}
});
} catch (e) {}
});
}, 500);
// ===== 2. apphmac capture — NFWebCrypto.framework HMAC =====
//
// Hook NFWebCrypto HMAC function. Filter for 32B outputs that occur in the
// timeframe when entity_auth_data is being assembled (near MslClient calls).
// Log: key bytes, data bytes, output hash for all candidates.
//
// Known candidates:
// HMAC(PSK, devicetoken)
// HMAC(enc_key_0, ESN)
// HMAC(sign_key, payload)
waitForModule("NFWebCrypto", function (nfwc) {
banner("NFWebCrypto.framework loaded — hooking HMAC for apphmac capture");
console.log("[*] NFWebCrypto base=" + nfwc.base + " size=" + nfwc.size);
var pHMAC = nfwc.findExportByName("HMAC");
if (!pHMAC) {
console.log("[-] HMAC not found in NFWebCrypto");
return;
}
console.log("[+] HMAC @ " + pHMAC);
// HMAC(const EVP_MD *evp_md,
// const void *key, int key_len,
// const unsigned char *data, size_t data_len,
// unsigned char *md, unsigned int *md_len)
Interceptor.attach(pHMAC, {
onEnter: function (args) {
this.evp_md = args[0];
this.key = args[1];
this.keyLen = args[2].toInt32();
this.data = args[3];
this.dataLen = args[4].toUInt32();
this.md = args[5];
this.mdLenPtr = args[6];
},
onLeave: function (retval) {
if (retval.isNull()) return;
var outLen = (this.mdLenPtr && !this.mdLenPtr.isNull())
? this.mdLenPtr.readU32() : 32;
// Only care about HMAC-SHA256 (32B output) for apphmac
if (outLen !== 32) return;
var keyHex = hex(this.key, Math.min(this.keyLen, 64));
var dataHex = hexShort(this.data, Math.min(this.dataLen, 256));
var outHex = hex(retval, 32);
var entry = {
ts: ts(),
keyLen: this.keyLen,
key: keyHex,
dataLen: this.dataLen,
data: dataHex,
out: outHex,
};
captured.hmacCandidates.push(entry);
// Check if output matches 32B and data size matches devicetoken (216B)
var isTokenHmac = (this.dataLen === 216);
// Check if data size matches ESN-like (variable, usually 20-40B)
var isEsnHmac = (this.dataLen >= 20 && this.dataLen <= 50);
var label = "HMAC-SHA256";
if (isTokenHmac) label += " [DEVICETOKEN-SIZE DATA]";
if (isEsnHmac) label += " [ESN-SIZE DATA]";
console.log("\n[" + ts() + "] " + label);
console.log(" key (" + this.keyLen + "B) = " + keyHex);
console.log(" data (" + this.dataLen + "B) = " + dataHex);
console.log(" output (32B) = " + outHex);
// Heuristic: if dataLen == 216 this is likely HMAC(PSK, devicetoken) = apphmac
if (isTokenHmac) {
banner("apphmac CANDIDATE — HMAC(key, 216B_data)");
console.log(" [CANDIDATE] key = " + keyHex);
console.log(" [CANDIDATE] data = " + dataHex);
console.log(" [CANDIDATE] hmac = " + outHex);
captured.apphmac = outHex;
}
// Backtrace to identify caller context
var bt = Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0, 5);
console.log(" backtrace:");
bt.forEach(function (addr) {
var sym = DebugSymbol.fromAddress(addr);
console.log(" " + addr + " " + sym);
});
}
});
console.log("[+] HMAC hooked in NFWebCrypto");
});
// ===== 3. device_key_data capture =====
//
// Strategy A: NSData dataWithContentsOfFile: — watch all file reads >= 1KB
// Strategy B: NSUserDefaults objectForKey: — watch large NSData values
// Strategy C: sqlite3_step — watch for large BLOB column reads
setTimeout(function () {
if (!ObjC.available) return;
banner("device_key_data capture — file / NSUserDefaults / SQLite hooks");
var TARGET_DKD_LEN_MIN = 4000; // be generous — might be base64 or padded
var TARGET_DKD_LEN_MAX = 10000;
// ---- Strategy A: NSData dataWithContentsOfFile: ----
try {
var NSData = ObjC.classes.NSData;
if (NSData) {
var dataWithContents = NSData["+ dataWithContentsOfFile:"];
if (dataWithContents) {
Interceptor.attach(dataWithContents.implementation, {
onEnter: function (args) {
// args[2] is the NSString path (index 0=self, 1=sel, 2=path)
try {
this.path = new ObjC.Object(args[2]).toString();
} catch (e) {
this.path = "(unknown)";
}
},
onLeave: function (retval) {
if (!retval || retval.isNull()) return;
try {
var obj = new ObjC.Object(retval);
var dataLen = obj.length();
var entry = { ts: ts(), path: this.path, size: dataLen };
captured.fileReads.push(entry);
console.log("[FILE] " + ts() + " dataWithContentsOfFile: " + this.path + " → " + dataLen + "B");
if (dataLen >= TARGET_DKD_LEN_MIN && dataLen <= TARGET_DKD_LEN_MAX) {
var bytesPtr = obj.bytes();
var h = hex(bytesPtr, dataLen);
banner("device_key_data CANDIDATE — file read");
console.log(" path = " + this.path);
console.log(" length = " + dataLen);
console.log(" bytes = " + h.slice(0, 128) + "...");
captured.deviceKeyData = h;
}
} catch (e) {}
}
});
console.log("[+] NSData dataWithContentsOfFile: hooked");
}
// Also hook dataWithContentsOfFile:options:error:
var dataWithContentsOpts = NSData["+ dataWithContentsOfFile:options:error:"];
if (dataWithContentsOpts) {
Interceptor.attach(dataWithContentsOpts.implementation, {
onEnter: function (args) {
try {
this.path = new ObjC.Object(args[2]).toString();
} catch (e) {
this.path = "(unknown)";
}
},
onLeave: function (retval) {
if (!retval || retval.isNull()) return;
try {
var obj = new ObjC.Object(retval);
var dataLen = obj.length();
console.log("[FILE] " + ts() + " dataWithContentsOfFile:options:error: " + this.path + " → " + dataLen + "B");
if (dataLen >= TARGET_DKD_LEN_MIN && dataLen <= TARGET_DKD_LEN_MAX) {
var bytesPtr = obj.bytes();
var h = hex(bytesPtr, dataLen);
banner("device_key_data CANDIDATE — file read (options:error:)");
console.log(" path = " + this.path);
console.log(" length = " + dataLen);
console.log(" bytes = " + h.slice(0, 128) + "...");
captured.deviceKeyData = h;
}
} catch (e) {}
}
});
console.log("[+] NSData dataWithContentsOfFile:options:error: hooked");
}
}
} catch (e) {
console.log("[-] NSData file hook error: " + e.message);
}
// ---- Strategy B: NSUserDefaults objectForKey: ----
try {
var NSUserDefaults = ObjC.classes.NSUserDefaults;
if (NSUserDefaults) {
var objectForKey = NSUserDefaults["- objectForKey:"];
if (objectForKey) {
Interceptor.attach(objectForKey.implementation, {
onEnter: function (args) {
try {
this.key = new ObjC.Object(args[2]).toString();
} catch (e) {
this.key = "(unknown)";
}
},
onLeave: function (retval) {
if (!retval || retval.isNull()) return;
try {
var obj = new ObjC.Object(retval);
var className = obj.$className;
if (className === "NSData" || className === "__NSCFData" ||
className === "NSMutableData" || className === "_NSInlineData") {
var dataLen = obj.length();
console.log("[DEFAULTS] " + ts() + " objectForKey: " + this.key + " → NSData " + dataLen + "B");
if (dataLen >= TARGET_DKD_LEN_MIN && dataLen <= TARGET_DKD_LEN_MAX) {
var bytesPtr = obj.bytes();
var h = hex(bytesPtr, dataLen);
banner("device_key_data CANDIDATE — NSUserDefaults");
console.log(" key = " + this.key);
console.log(" length = " + dataLen);
console.log(" bytes = " + h.slice(0, 128) + "...");
captured.deviceKeyData = h;
}
}
} catch (e) {}
}
});
console.log("[+] NSUserDefaults objectForKey: hooked");
}
}
} catch (e) {
console.log("[-] NSUserDefaults hook error: " + e.message);
}
// ---- Strategy C: sqlite3_step — watch for large BLOB column reads ----
// We hook sqlite3_column_blob + sqlite3_column_bytes after sqlite3_step
var sqlite3Mods = ["libsqlite3.dylib", "libsqlite3.0.dylib"];
var sqlite3 = null;
Process.enumerateModules().forEach(function (m) {
if (m.name.indexOf("sqlite3") !== -1) sqlite3 = m;
});
if (sqlite3) {
var pStep = sqlite3.findExportByName("sqlite3_step");
var pColBlob = sqlite3.findExportByName("sqlite3_column_blob");
var pColBytes = sqlite3.findExportByName("sqlite3_column_bytes");
if (pStep && pColBlob && pColBytes) {
var colBlob = new NativeFunction(pColBlob, 'pointer', ['pointer', 'int']);
var colBytes = new NativeFunction(pColBytes, 'int', ['pointer', 'int']);
Interceptor.attach(pStep, {
onLeave: function (retval) {
// SQLITE_ROW = 100
if (retval.toInt32() !== 100) return;
var stmt = this.context.x0; // arm64: first arg stays in x0
// Scan first 10 columns
for (var col = 0; col < 10; col++) {
try {
var blobLen = colBytes(stmt, col);
if (blobLen >= TARGET_DKD_LEN_MIN && blobLen <= TARGET_DKD_LEN_MAX) {
var blobPtr = colBlob(stmt, col);
if (blobPtr && !blobPtr.isNull()) {
var h = hex(blobPtr, blobLen);
banner("device_key_data CANDIDATE — SQLite BLOB");
console.log(" column = " + col);
console.log(" length = " + blobLen);
console.log(" bytes = " + h.slice(0, 128) + "...");
captured.deviceKeyData = h;
}
}
} catch (e) {}
}
}
});
console.log("[+] sqlite3_step hooked (BLOB size filter " +
TARGET_DKD_LEN_MIN + "-" + TARGET_DKD_LEN_MAX + "B)");
} else {
console.log("[-] sqlite3 step/column functions not found");
}
} else {
console.log("[-] libsqlite3 not found");
}
}, 300);
// ===== RPC exports =====
rpc.exports = {
getCaptured: function () {
return JSON.stringify({
devicetoken: captured.devicetoken,
apphmac: captured.apphmac,
deviceKeyData: captured.deviceKeyData
? captured.deviceKeyData.slice(0, 64) + "... (" + (captured.deviceKeyData.length / 2) + "B)"
: null,
hmacCandidates: captured.hmacCandidates.length,
fileReads: captured.fileReads.length,
}, null, 2);
},
getHmacCandidates: function () {
return JSON.stringify(captured.hmacCandidates, null, 2);
},
getDeviceKeyData: function () {
return captured.deviceKeyData || null;
},
getDeviceToken: function () {
return captured.devicetoken || null;
},
getAppHmac: function () {
return captured.apphmac || null;
},
getFileReads: function () {
return JSON.stringify(captured.fileReads, null, 2);
},
};
// Make callable from REPL
var global = this;
global.getCaptured = function () {
console.log(rpc.exports.getCaptured());
};
global.getHmacCandidates = function () {
console.log(rpc.exports.getHmacCandidates());
};
global.getDeviceToken = function () {
console.log(rpc.exports.getDeviceToken());
};
global.getDeviceKeyData = function () {
var d = rpc.exports.getDeviceKeyData();
if (d) console.log(d.slice(0, 256) + "... (" + (d.length / 2) + "B)");
else console.log("(not captured yet)");
};
console.log("\n=== hook_entityauth_capture.js loaded ===");
console.log("Targets:");
console.log(" 1. devicetoken (216B) — Nbp.framework token symbols");
console.log(" 2. apphmac (32B) — NFWebCrypto HMAC (dataLen==216 filter)");
console.log(" 3. device_key_data — file / NSUserDefaults / SQLite (4-10KB filter)");
console.log("RPC commands:");
console.log(" getCaptured() — summary of all captured values");
console.log(" getHmacCandidates() — all 32B HMAC calls seen");
console.log(" getDeviceToken() — devicetoken hex");
console.log(" getDeviceKeyData() — device_key_data hex");
console.log("");
console.log("[*] Navigate to Netflix homescreen to trigger entity_auth_data assembly");