/** * Frida debug script for WeChat 8.0.72 send message API discovery * Usage: frida -U -f com.tencent.mm --no-pause -l debug_wechat_send.js * * Commands in Frida REPL: * sendTestMsg("wxid_xxx", "hello") // try to send a test message * searchSendMsg() // list all send-msg related classes * hookWCDB() // monitor message table inserts * hookFactories() // hook u01/v01/w01 factory patterns */ Java.perform(function () { console.log("[HermesDebug] Script loaded, waiting for classes..."); // ── 1. Hook WCDB insert to verify if outgoing messages reach DB ── function hookWCDB() { try { var SQLiteDatabase = Java.use("com.tencent.wcdb.database.SQLiteDatabase"); SQLiteDatabase.insert.overload('java.lang.String', 'java.lang.String', 'android.content.ContentValues').implementation = function(table, nullColumnHack, values) { if (table && table.indexOf("message") !== -1) { var talker = values.getAsString("talker"); var content = values.getAsString("content"); var type = values.getAsInteger("type"); var isSend = values.getAsInteger("isSend"); console.log("[WCDB] INSERT message talker=" + talker + " type=" + type + " isSend=" + isSend + " content=" + (content ? content.substring(0, 60) : "null")); } return this.insert(table, nullColumnHack, values); }; SQLiteDatabase.insertWithOnConflict.overload('java.lang.String', 'java.lang.String', 'android.content.ContentValues', 'int').implementation = function(table, nullColumnHack, values, conflictAlgorithm) { if (table && table.indexOf("message") !== -1) { var talker = values.getAsString("talker"); var content = values.getAsString("content"); var type = values.getAsInteger("type"); var isSend = values.getAsInteger("isSend"); console.log("[WCDB] INSERT2 message talker=" + talker + " type=" + type + " isSend=" + isSend + " content=" + (content ? content.substring(0, 60) : "null")); } return this.insertWithOnConflict(table, nullColumnHack, values, conflictAlgorithm); }; console.log("[HermesDebug] WCDB hooks installed"); } catch (e) { console.log("[HermesDebug] WCDB hook failed: " + e); } } // ── 2. Search for send-msg related classes dynamically ── function searchSendMsg() { console.log("[HermesDebug] Searching for send-msg classes..."); var count = 0; Java.enumerateLoadedClasses({ onMatch: function(className) { var lower = className.toLowerCase(); if (lower.indexOf("sendmsg") !== -1 || lower.indexOf("sendmessage") !== -1 || lower.indexOf("scenesendmsg") !== -1 || lower.indexOf("netscenesendmsg") !== -1 || className.indexOf("u01.") === 0 || className.indexOf("v01.") === 0 || className.indexOf("w01.") === 0) { console.log(" [CLASS] " + className); count++; } }, onComplete: function() { console.log("[HermesDebug] Found " + count + " candidate classes"); } }); } // ── 3. Hook factory methods in u01/v01/w01 packages ── function hookFactories() { console.log("[HermesDebug] Hooking factory methods..."); var hookCount = 0; Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.indexOf("u01.") !== 0 && className.indexOf("v01.") !== 0 && className.indexOf("w01.") !== 0) { return; } try { var cls = Java.use(className); var methods = cls.class.getDeclaredMethods(); for (var i = 0; i < methods.length; i++) { var m = methods[i]; var name = m.getName(); var paramTypes = m.getParameterTypes(); var retType = m.getReturnType().getName(); // Hook a(String) that returns non-void (factory pattern) if (name === "a" && paramTypes.length === 1 && paramTypes[0].getName() === "java.lang.String" && retType !== "void") { try { cls[name].overload('java.lang.String').implementation = function(arg) { var result = this[name](arg); console.log("[FACTORY] " + className + ".a(\"" + arg + "\") => " + retType + " instance=" + result); return result; }; hookCount++; } catch (e) { // overload mismatch, skip } } // Also hook b(String) content setter if (name === "b" && paramTypes.length === 1 && paramTypes[0].getName() === "java.lang.String") { try { cls[name].overload('java.lang.String').implementation = function(arg) { console.log("[SETTER] " + className + ".b(\"" + arg.substring(0, 30) + "\")"); return this[name](arg); }; } catch (e) {} } // Hook h(int) if (name === "h" && paramTypes.length === 1 && paramTypes[0].getName() === "int") { try { cls[name].overload('int').implementation = function(arg) { console.log("[SETTER] " + className + ".h(" + arg + ")"); return this[name](arg); }; } catch (e) {} } // Hook parameter-less a() that returns something (build/submit) if (name === "a" && paramTypes.length === 0 && retType !== "void") { try { cls[name].overload().implementation = function() { var result = this[name](); console.log("[BUILD] " + className + ".a() => " + retType + " result=" + result); return result; }; } catch (e) {} } } } catch (e) { // class not found or other error } }, onComplete: function() { console.log("[HermesDebug] Hooked " + hookCount + " factory methods"); } }); } // ── 4. Try to send a test message via reflection ── function sendTestMsg(toUser, content) { console.log("[HermesDebug] Attempting to send test msg to=" + toUser); try { // Try u01.r1 first var factoryClass = Java.use("u01.r1"); console.log("[HermesDebug] Found u01.r1"); // List all methods var methods = factoryClass.class.getDeclaredMethods(); console.log("[HermesDebug] u01.r1 methods:"); for (var i = 0; i < methods.length; i++) { var m = methods[i]; if (m.getModifiers() & 0x0001) { // public var params = ""; var pts = m.getParameterTypes(); for (var j = 0; j < pts.length; j++) { params += pts[j].getName() + " "; } console.log(" " + m.getName() + "(" + params.trim() + ") => " + m.getReturnType().getName()); } } // Try static a(String) var builder = factoryClass.a("java.lang.String").call(factoryClass, toUser); console.log("[HermesDebug] builder class=" + builder.getClass().getName()); var builderClass = Java.use(builder.getClass().getName()); // Try common setter methods var setterMap = { "content": ["b", "c", "d", "e", "f", "g", "setContent", "setText"], "type": ["h", "i", "j", "k", "setType", "setMsgType"], "flag": ["l", "m", "n", "setFlag"] }; // Try content setter for (var si = 0; si < setterMap.content.length; si++) { var sn = setterMap.content[si]; try { builderClass[sn].overload('java.lang.String').call(builder, content); console.log("[HermesDebug] Set content via " + sn + "(String)"); break; } catch (e) {} } // Try type setter (0 = text) for (var si = 0; si < setterMap.type.length; si++) { var sn = setterMap.type[si]; try { builderClass[sn].overload('int').call(builder, 0); console.log("[HermesDebug] Set type=0 via " + sn + "(int)"); break; } catch (e) {} } // Try build var result = builderClass.a().call(builder); console.log("[HermesDebug] Build result class=" + (result ? result.getClass().getName() : "null")); // Try submit if (result) { var resultClass = Java.use(result.getClass().getName()); try { var submitResult = resultClass.a().call(result); console.log("[HermesDebug] Submit result=" + submitResult); } catch (e) { console.log("[HermesDebug] Submit failed: " + e); } } } catch (e) { console.log("[HermesDebug] u01.r1 failed: " + e); // Fallback: search for any class with a(String) -> factory pattern console.log("[HermesDebug] Searching for alternative factory classes..."); Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.indexOf("u01.") !== 0 && className.indexOf("v01.") !== 0) return; try { var cls = Java.use(className); var methods = cls.class.getDeclaredMethods(); for (var i = 0; i < methods.length; i++) { var m = methods[i]; if (m.getName() === "a" && m.getParameterTypes().length === 1 && m.getParameterTypes()[0].getName() === "java.lang.String" && m.getModifiers() & 0x0008) { // static console.log("[ALT] " + className + ".a(String) is static, ret=" + m.getReturnType().getName()); } } } catch (e) {} }, onComplete: function() {} }); } } // ── 5. Hook ContentProvider to see if LSPosed module's provider calls arrive ── function hookContentProvider() { try { var ContentResolver = Java.use("android.content.ContentResolver"); ContentResolver.query.overload('android.net.Uri', '[Ljava.lang.String;', 'java.lang.String', '[Ljava.lang.String;', 'java.lang.String').implementation = function(uri, projection, selection, selectionArgs, sortOrder) { if (uri.toString().indexOf("com.tencent.mm.plugin.ext") !== -1) { console.log("[CP] query uri=" + uri + " args=" + (selectionArgs ? selectionArgs.join(",") : "null")); } return this.query(uri, projection, selection, selectionArgs, sortOrder); }; ContentResolver.insert.overload('android.net.Uri', 'android.content.ContentValues').implementation = function(uri, values) { if (uri.toString().indexOf("com.tencent.mm.plugin.ext") !== -1) { console.log("[CP] insert uri=" + uri + " values=" + values); } return this.insert(uri, values); }; console.log("[HermesDebug] ContentProvider hooks installed"); } catch (e) { console.log("[HermesDebug] ContentProvider hook failed: " + e); } } // ── 6. Hook broadcast receiver registration ── function hookBroadcast() { try { var Context = Java.use("android.content.Context"); Context.registerReceiver.overload('android.content.BroadcastReceiver', 'android.content.IntentFilter', 'int').implementation = function(receiver, filter, flags) { var actions = []; var count = filter.countActions(); for (var i = 0; i < count; i++) { actions.push(filter.getAction(i)); } if (actions.join(",").indexOf("hermes") !== -1) { console.log("[BROADCAST] registerReceiver actions=" + actions.join(",") + " flags=" + flags); } return this.registerReceiver(receiver, filter, flags); }; console.log("[HermesDebug] Broadcast hook installed"); } catch (e) { console.log("[HermesDebug] Broadcast hook failed: " + e); } } // ── Install all hooks ── hookWCDB(); hookContentProvider(); hookBroadcast(); // Wait a bit for WeChat to load more classes, then hook factories setTimeout(function() { searchSendMsg(); hookFactories(); }, 5000); // ── Expose functions to Frida REPL ── rpc.exports = { sendTestMsg: sendTestMsg, searchSendMsg: searchSendMsg, hookFactories: hookFactories, hookWCDB: hookWCDB }; console.log("[HermesDebug] Setup complete. Available rpc exports: sendTestMsg, searchSendMsg, hookFactories, hookWCDB"); });