/* * WeChat Android SQLCipher Key Hunter (Frida 17.x) * Targets WeChat 8.0.72 on Android with WCDB/SQLCipher * * Strategy: * 1. Hook Java layer: com.tencent.wcdb.database.SQLiteDatabase.openDatabase variants * 2. Hook native layer: sqlite3_key / sqlite3_key_v2 * 3. Hook SQLiteConnection / SQLiteCipherSpec if needed */ var TAG = "[WeChatKeyHunter]"; var seenKeys = {}; var targetDbNames = ["EnMicroMsg.db", "EnMicroMsg"]; function isTargetDb(path) { if (!path) return false; var s = path.toString(); for (var i = 0; i < targetDbNames.length; i++) { if (s.indexOf(targetDbNames[i]) !== -1) return true; } return false; } function bytesToHex(bytes, len) { len = len || bytes.length; var hex = ""; for (var i = 0; i < len; i++) { var b = bytes[i] & 0xFF; hex += (b < 16 ? "0" : "") + b.toString(16); } return hex; } function isPrintableKey(keyPtr, keyLen) { // SQLCipher keys are usually 7-32 bytes (passwords) or raw 32-byte AES keys if (keyLen < 4 || keyLen > 64) return false; try { var raw = keyPtr.readByteArray(keyLen); if (!raw) return false; var bytes = new Uint8Array(raw); // Skip trivial keys var map = {}, unique = 0; for (var i = 0; i < bytes.length; i++) { if (!map[bytes[i]]) { map[bytes[i]] = true; unique++; } } return unique >= 3; } catch (e) { return false; } } function reportKey(keyHex, keyLen, via, path, extra) { var cacheKey = keyHex + "@" + (path || ""); if (seenKeys[cacheKey]) return; seenKeys[cacheKey] = true; send({ type: "key", hex: keyHex, len: keyLen, via: via, path: path || "", extra: extra || {} }); } function reportInfo(msg) { send({ type: "info", msg: msg }); } function reportError(msg) { send({ type: "error", msg: msg }); } // ── Native layer: sqlite3_key / sqlite3_key_v2 ─────────────────────────────── function hookNativeSqlite3Key() { var modules = Process.enumerateModules(); var hooked = []; for (var i = 0; i < modules.length; i++) { var mod = modules[i]; var name = mod.name.toLowerCase(); // WCDB / SQLCipher likely lives in libwcdb.so, libsqlcipher.so, // or bundled inside libwechatcommon.so / libwechatmm.so if (name.indexOf("wcdb") === -1 && name.indexOf("sqlcipher") === -1 && name.indexOf("wechat") === -1 && name.indexOf("sqlite") === -1) { continue; } try { var modObj = Process.findModuleByName(mod.name); if (!modObj) continue; // Try direct findExportByName first (works even when enumerateExports misses symbols) var keyAddr = modObj.findExportByName("sqlite3_key"); var keyV2Addr = modObj.findExportByName("sqlite3_key_v2"); if (keyAddr && !keyAddr.isNull()) { var label = mod.name + "!sqlite3_key"; (function (lbl, addr) { Interceptor.attach(addr, { onEnter: function (args) { var dbPtr = args[0]; var keyPtr = args[1]; var keyLen = args[2].toInt32(); if (!isPrintableKey(keyPtr, keyLen)) return; var hex = bytesToHex(new Uint8Array(keyPtr.readByteArray(keyLen))); reportKey(hex, keyLen, lbl, null, { native: true, v2: false, db: dbPtr.toString() }); } }); })(label, keyAddr); hooked.push(label); } if (keyV2Addr && !keyV2Addr.isNull()) { var label2 = mod.name + "!sqlite3_key_v2"; (function (lbl, addr) { Interceptor.attach(addr, { onEnter: function (args) { var dbPtr = args[0]; var keyPtr = args[2]; var keyLen = args[3].toInt32(); if (!isPrintableKey(keyPtr, keyLen)) return; var hex = bytesToHex(new Uint8Array(keyPtr.readByteArray(keyLen))); reportKey(hex, keyLen, lbl, null, { native: true, v2: true, db: dbPtr.toString() }); } }); })(label2, keyV2Addr); hooked.push(label2); } // Also hook sqlite3_open_v2 to capture db paths var openV2Addr = modObj.findExportByName("sqlite3_open_v2"); if (openV2Addr && !openV2Addr.isNull()) { (function (addr) { Interceptor.attach(addr, { onEnter: function (args) { var pathPtr = args[0]; var flags = args[1].toInt32(); try { var path = pathPtr.readUtf8String(); if (path && path.indexOf("EnMicroMsg") !== -1) { send({ type: "info", msg: "sqlite3_open_v2: " + path + " flags=" + flags }); } } catch (e) {} } }); })(openV2Addr); } // Hook sqlite3_exec to catch PRAGMA key var execAddr = modObj.findExportByName("sqlite3_exec"); if (execAddr && !execAddr.isNull()) { (function (addr) { Interceptor.attach(addr, { onEnter: function (args) { var sqlPtr = args[1]; try { var sql = sqlPtr.readUtf8String(); if (sql && sql.toUpperCase().indexOf("PRAGMA KEY") !== -1) { send({ type: "info", msg: "sqlite3_exec PRAGMA: " + sql }); } } catch (e) {} } }); })(execAddr); } } catch (e) { // ignore } } return hooked; } // ── Java layer: WCDB SQLiteDatabase ─────────────────────────────────────────── function hookWcdbJava() { var SQLiteDatabase; try { SQLiteDatabase = Java.use("com.tencent.wcdb.database.SQLiteDatabase"); } catch (e) { reportInfo("WCDB SQLiteDatabase class not found, trying androidx/sqlite..."); try { SQLiteDatabase = Java.use("android.database.sqlite.SQLiteDatabase"); } catch (e2) { reportError("Neither WCDB nor standard SQLiteDatabase found: " + e2); return []; } } var hooked = []; // Overload 1: openDatabase(String path, CursorFactory factory, int flags) try { var open1 = SQLiteDatabase.openDatabase.overload( "java.lang.String", "com.tencent.wcdb.database.SQLiteDatabase$CursorFactory", "int" ); open1.implementation = function (path, factory, flags) { if (isTargetDb(path)) { reportInfo("openDatabase(path, factory, flags) -> " + path); } return open1.call(this, path, factory, flags); }; hooked.push("openDatabase(String,CursorFactory,int)"); } catch (e) {} // Overload 2: openDatabase(String path, CursorFactory factory, int flags, String password) try { var open2 = SQLiteDatabase.openDatabase.overload( "java.lang.String", "com.tencent.wcdb.database.SQLiteDatabase$CursorFactory", "int", "java.lang.String" ); open2.implementation = function (path, factory, flags, password) { if (isTargetDb(path) && password) { var pw = password.toString(); reportKey(pw, pw.length, "WCDB.openDatabase(password)", path, { flags: flags, type: "string_password" }); } return open2.call(this, path, factory, flags, password); }; hooked.push("openDatabase(String,CursorFactory,int,String)"); } catch (e) {} // Overload 3: openDatabase(String path, CursorFactory factory, int flags, SQLiteDatabaseErrorHandler errorHandler, String password) try { var open3 = SQLiteDatabase.openDatabase.overload( "java.lang.String", "com.tencent.wcdb.database.SQLiteDatabase$CursorFactory", "int", "com.tencent.wcdb.database.SQLiteDatabaseErrorHandler", "java.lang.String" ); open3.implementation = function (path, factory, flags, errorHandler, password) { if (isTargetDb(path) && password) { var pw = password.toString(); reportKey(pw, pw.length, "WCDB.openDatabase(errHandler,password)", path, { flags: flags, type: "string_password" }); } return open3.call(this, path, factory, flags, errorHandler, password); }; hooked.push("openDatabase(String,CursorFactory,int,ErrorHandler,String)"); } catch (e) {} // Overload 4: openDatabase(String path, byte[] password, ...) try { var open4 = SQLiteDatabase.openDatabase.overload( "java.lang.String", "[B", "com.tencent.wcdb.database.SQLiteDatabase$CursorFactory", "int" ); open4.implementation = function (path, password, factory, flags) { if (isTargetDb(path) && password) { var bytes = Java.array("byte", password); var hex = bytesToHex(bytes); reportKey(hex, bytes.length, "WCDB.openDatabase(byte[])", path, { flags: flags, type: "byte_array_password" }); } return open4.call(this, path, password, factory, flags); }; hooked.push("openDatabase(String,byte[],CursorFactory,int)"); } catch (e) {} // Hook constructor that takes password try { var ctor1 = SQLiteDatabase.$init.overload( "java.lang.String", "int", "com.tencent.wcdb.database.SQLiteDatabase$CursorFactory", "com.tencent.wcdb.support.CancellationSignal" ); ctor1.implementation = function (path, flags, factory, cancel) { if (isTargetDb(path)) { reportInfo("SQLiteDatabase constructor -> " + path); } ctor1.call(this, path, flags, factory, cancel); }; hooked.push("constructor(String,int,CursorFactory,CancellationSignal)"); } catch (e) {} return hooked; } // ── Java layer: SQLiteCipherSpec (cipher configuration) ────────────────────── function hookCipherSpec() { var specs = []; try { var SQLiteCipherSpec = Java.use("com.tencent.wcdb.database.SQLiteCipherSpec"); specs.push("SQLiteCipherSpec"); // Hook setters to capture cipher config try { SQLiteCipherSpec.setPageSize.implementation = function (size) { send({ type: "cipher_spec", field: "pageSize", value: size }); return this.setPageSize(size); }; } catch (e) {} try { SQLiteCipherSpec.setKDFIteration.implementation = function (iter) { send({ type: "cipher_spec", field: "kdfIteration", value: iter }); return this.setKDFIteration(iter); }; } catch (e) {} try { SQLiteCipherSpec.setHMACUse.implementation = function (use) { send({ type: "cipher_spec", field: "hmacUse", value: use }); return this.setHMACUse(use); }; } catch (e) {} } catch (e) {} return specs; } // ── Java layer: SQLiteOpenHelper (higher-level wrapper) ────────────────────── function hookOpenHelper() { var helpers = []; try { var SQLiteOpenHelper = Java.use("com.tencent.wcdb.database.SQLiteOpenHelper"); helpers.push("SQLiteOpenHelper"); try { var getDB = SQLiteOpenHelper.getWritableDatabase.overload("java.lang.String"); getDB.implementation = function (password) { if (password) { var pw = password.toString(); send({ type: "info", msg: "SQLiteOpenHelper.getWritableDatabase(password=" + pw + ")" }); } return getDB.call(this, password); }; } catch (e) {} try { var getDB2 = SQLiteOpenHelper.getWritableDatabase.overload("[B"); getDB2.implementation = function (password) { if (password) { var bytes = Java.array("byte", password); var hex = bytesToHex(bytes); send({ type: "key", hex: hex, len: bytes.length, via: "SQLiteOpenHelper.getWritableDatabase(byte[])", path: "" }); } return getDB2.call(this, password); }; } catch (e) {} } catch (e) {} return helpers; } // ── Auto-detect database path by scanning /data/data/com.tencent.mm ────────── function scanWechatDbPath() { Java.perform(function () { var File = Java.use("java.io.File"); var basePath = "/data/data/com.tencent.mm/MicroMsg"; try { var base = File.$new(basePath); if (!base.exists()) { reportInfo("MicroMsg base path not found"); return; } var files = base.listFiles(); if (!files) { reportInfo("No files in MicroMsg"); return; } for (var i = 0; i < files.length; i++) { var f = files[i]; if (f.isDirectory()) { var name = f.getName(); var dbFile = File.$new(f.getAbsolutePath() + "/EnMicroMsg.db"); if (dbFile.exists()) { reportInfo("FOUND_DB: " + dbFile.getAbsolutePath()); } } } } catch (e) { reportError("Scan error: " + e); } }); } // ── Main ───────────────────────────────────────────────────────────────────── function main() { reportInfo("=== WeChat Android Key Hunter started ==="); reportInfo("Process: " + Process.id + " | Arch: " + Process.arch); reportInfo("Java.available: " + (typeof Java !== 'undefined' ? Java.available : false)); // 1. Native hooks (work immediately, no Java required) var nativeHooks = hookNativeSqlite3Key(); reportInfo("Native sqlite3_key hooks: " + nativeHooks.length + (nativeHooks.length ? " -> " + nativeHooks.join(", ") : " (none found yet, modules may load later)")); // 2. Java hooks (require Java VM) if (typeof Java !== 'undefined' && Java.available) { Java.perform(function () { var javaHooks = hookWcdbJava(); reportInfo("WCDB Java hooks: " + javaHooks.length + (javaHooks.length ? " -> " + javaHooks.join(", ") : " (none found)")); var cipherSpecs = hookCipherSpec(); reportInfo("CipherSpec hooks: " + cipherSpecs.length); var helpers = hookOpenHelper(); reportInfo("OpenHelper hooks: " + helpers.length); // Scan for DB path scanWechatDbPath(); }); } else { reportInfo("Java VM not available in this process, relying on native hooks only."); } // 3. Monitor for new modules being loaded (WCDB/SQLCipher may load dynamically) var alreadyHookedModules = {}; function tryHookModuleSqlite3Key(modName) { try { var modObj = Process.findModuleByName(modName); if (!modObj) return false; var hookedAny = false; var keyAddr = modObj.findExportByName("sqlite3_key"); if (keyAddr && !keyAddr.isNull() && !alreadyHookedModules[modName + "!sqlite3_key"]) { alreadyHookedModules[modName + "!sqlite3_key"] = true; var label = modName + "!sqlite3_key"; (function (lbl, addr) { Interceptor.attach(addr, { onEnter: function (args) { var keyPtr = args[1]; var keyLen = args[2].toInt32(); if (!isPrintableKey(keyPtr, keyLen)) return; var hex = bytesToHex(new Uint8Array(keyPtr.readByteArray(keyLen))); reportKey(hex, keyLen, lbl, null, { native: true, v2: false, late: true }); } }); })(label, keyAddr); hookedAny = true; reportInfo("Late-hook " + label + " at " + keyAddr); } var keyV2Addr = modObj.findExportByName("sqlite3_key_v2"); if (keyV2Addr && !keyV2Addr.isNull() && !alreadyHookedModules[modName + "!sqlite3_key_v2"]) { alreadyHookedModules[modName + "!sqlite3_key_v2"] = true; var label2 = modName + "!sqlite3_key_v2"; (function (lbl, addr) { Interceptor.attach(addr, { onEnter: function (args) { var keyPtr = args[2]; var keyLen = args[3].toInt32(); if (!isPrintableKey(keyPtr, keyLen)) return; var hex = bytesToHex(new Uint8Array(keyPtr.readByteArray(keyLen))); reportKey(hex, keyLen, lbl, null, { native: true, v2: true, late: true }); } }); })(label2, keyV2Addr); hookedAny = true; reportInfo("Late-hook " + label2 + " at " + keyV2Addr); } return hookedAny; } catch (e) { return false; } } setInterval(function () { var modules = Process.enumerateModules(); for (var i = 0; i < modules.length; i++) { var name = modules[i].name; if (alreadyHookedModules[name]) continue; alreadyHookedModules[name] = true; var lname = name.toLowerCase(); if (lname.indexOf("wcdb") !== -1 || lname.indexOf("sqlcipher") !== -1 || lname.indexOf("sqlite") !== -1) { reportInfo("New module loaded: " + name); tryHookModuleSqlite3Key(name); } } }, 2000); reportInfo("=== All hooks installed. Waiting for EnMicroMsg.db access... ==="); } main();