debug_wechat_send.js 14.5 KB
/**
 * 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");
});