实现可以跨版本使用的微信 Xposed 模块

众所周知的,微信每个版本升级后,变量名都会有一些变化,引起过去的 xposed 模块失效,所以针对微信的 xposed 模块都有版本判断,以便告知用户该模块适应哪个版本。而一旦用户不小心把版本升级了,那么再想回去就很麻烦了。

为了解决这一问题,能够跨版本的 xposed 模块就是必要的了,而且还要尽可能的可以适应后续的版本更新。

通过反编译微信的代码可以知道,微信有一些固定的代码是不变的,比如说几个主要类的类名,但是类里面的成员变量,方法等,命名都会变。所以在下勾子时,尽可能的找不变的就行了。

下面具体以一个简单的案例来讲述如何进行,实现一个 防止发送微信语音 的功能。

以微信 6.7.3 版本为例,有以下代码(仅实现禁用聊天界面的长按发送语音)

XposedHelpers.findAndHookConstructor(
    "com.tencent.mm.pluginsdk.ui.chat.ChatFooter", 
    loadPackageParam.classLoader, 
    Context::class.java, AttributeSet::class.java, Integer.TYPE, 
    object : XC_MethodHook() {
        @Throws(Throwable::class)
        override fun afterHookedMethod(param: MethodHookParam) {
            val objChatFooter = param.thisObject as LinearLayout
            val clzChatFooter = objChatFooter.javaClass
            val fRvz = clzChatFooter.getDeclaredField("seT")
            fRvz.isAccessible = true
            val btn = fRvz.get(objChatFooter) as Button
            btn.setOnKeyListener(null)
            btn.setOnTouchListener(null)
            btn.setOnClickListener {
                Toast.makeText(objChatFooter.context, "语音消息已被禁用", Toast.LENGTH_SHORT).show()
            }
        }
    }
)

恩,seT 是什么鬼?其实这就是一个按钮的变量名,此处还有一个大坑,即使用 uiautomator 找到的 id 和代码中的并非一致,如此处的 seT,在代码中的变量名是 rvz这样的差异是由微信的加固方式导致的,你会发现在反编译的代码中,和运行时实际反射得到的变量名是完全不一样的,它使得我们更难以找到真正的变量名。

为此,我还特地开发了一个可以找到代码与运行时变量名关系的工具(出于一些考虑,暂时不打算开源)

反正不论如何,最终我们能找到真实的变量名,并且写出代码来。

好了,这个时候微信升级了,我们可以尝试着安装一个 6.7.4 内测版,会马上发现,上面的这段代码完全失效了,会报一个 seT 找不到的异常。

我们当然可以重新进行一次变量的搜索,并且进行版本号相关的兼容,但是这样一来,每次微信升级,都必须要改代码了,有没有一劳永逸的办法呢?

来看看下面这段代码:

XposedHelpers.findAndHookConstructor(
    "com.tencent.mm.pluginsdk.ui.chat.ChatFooter", 
    loadPackageParam.classLoader, 
    Context::class.java, AttributeSet::class.java, Integer.TYPE, 
    object : XC_MethodHook() {
        @Throws(Throwable::class)
        override fun afterHookedMethod(param: MethodHookParam) {
            val objChatFooter = param.thisObject as LinearLayout
            val clzChatFooter = objChatFooter.javaClass
            val flist = clzChatFooter.declaredFields
            for (f in flist) {
                f.isAccessible = true
                val fobj = f.get(objChatFooter)
                if (fobj is Button) {
                    if (fobj.text in arrayOf("按住 说话", "Hold to Talk")) {
                        fobj.setOnKeyListener(null)
                        fobj.setOnTouchListener(null)
                        fobj.setOnClickListener {
                            Toast.makeText(objChatFooter.context, "语音消息已被禁用", Toast.LENGTH_SHORT).show()
                        }
                    }
                }
            }
        }
    }
)

这里采用遍历并判断界面文字的办法,来确定一个具体的按钮,并对它作出 hook 操作,这样就不再需要知道具体的变量名,当下一次更新后,只要这部分代码结构整体不变,代码就永远是有效的(当然真要变了也没办法,只能重新搜索)。


可能有人会说了,这个案例太简单了,要是一个基本不变的类里面,存在着会变化的内容,怎么办呢,比如说有很多类都被混淆成了 xxx.xxx.a.b.c 这种形式,而且这种混淆也会随着版本变化,要怎么处理?下面再给一个案例,同样是上面的 防止发送微信语音 的功能,只不过这次要 hook 的,是在聊天窗口内按右下角加号出现的菜单里的按钮。

原始代码是这样的:

val clzF = XposedHelpers.findClass("com.tencent.mm.pluginsdk.model.app.f", loadPackageParam.classLoader)
XposedHelpers.findAndHookMethod(
    "com.tencent.mm.pluginsdk.ui.chat.AppPanel\$3", 
    loadPackageParam.classLoader, 
    "a", Integer.TYPE, clzF, 
    object : XC_MethodHook() {
        override fun beforeHookedMethod(param: MethodHookParam) {
            val idx = param.args[0] as Int
            if (idx == 2 || idx == 10) {
                val clzThis = param.thisObject.javaClass
                val fAppPanel= clzThis.getDeclaredField("sen")
                fAppPanel.isAccessible = true
                val objAppPanel = fAppPanel.get(param.thisObject) as LinearLayout
                Toast.makeText(objAppPanel.context, "语音消息已被禁用", Toast.LENGTH_SHORT).show()
                param.args[0] = Int.MAX_VALUE - 1
             }
        }
    }
)

可以很明显的看到,最上方的 com.tencent.mm.pluginsdk.model.app.f 就是一个可能会变化的变量,而下面的函数名称 a,也可能会变化,所以整个函数有很大的可能性会随着版本而变动。

所以再对这个函数做一点处理吧,让它可以动态的去完成搜索和 hook:

var idx = 1
while (true) {
    val hookName = "com.tencent.mm.pluginsdk.ui.chat.AppPanel\$$idx"
    val clzPanel = try {
        XposedHelpers.findClass(hookName, loadPackageParam.classLoader)
    } catch (e: Throwable) {
        null
    }
    if (clzPanel != null) {
        val mlist = clzPanel.declaredMethods
        if (mlist != null && mlist.size == 3) {
            if (getHookInAppPanelClassName(loadPackageParam.classLoader, mlist) { paramClz, methodName ->
                XposedHelpers.findAndHookMethod(hookName, loadPackageParam.classLoader, methodName, Integer.TYPE, paramClz, object : XC_MethodHook() {
                    @Throws(Throwable::class)
                    override fun beforeHookedMethod(param: MethodHookParam) {
                        val itemIdx = param.args[0] as Int
                        if (itemIdx == 2 || itemIdx == 10) {
                            val clzThis = param.thisObject.javaClass
                            val flist = clzThis.declaredFields
                            var objPanel: LinearLayout? = null
                            for (f in flist) {
                                f.isAccessible = true
                                val obj = f.get(param.thisObject)
                                if (obj is LinearLayout) {
                                    objPanel = obj
                                    break
                                }
                            }
                            if (objPanel != null) {
                                Toast.makeText(objPanel.context, "语音消息已被禁用", Toast.LENGTH_SHORT).show()
                            }
                            param.args[0] = Int.MAX_VALUE - 1
                        }
                    }
                })
            }) {
                break
            }
        }
    } else {
        break
    }
    idx++
}

是不是一样也很简单,无非就是用反射去遍历类以及类内的成员,然后看是否符合一些特定的要求,以判断反射到的类是否自己要找的。其中的 getHookInAppPanelClassName 方法具体的实现如下:

private fun getHookInAppPanelClassName(loader: ClassLoader, mlist: Array<Method>, callback: (paramClz: Class<*>, methodName: String) -> Unit): Boolean {
    var ret = false
    var hitI = 0
    var hitA = 0
    var pclzName = ""
    var mname = ""
    mlist.forEach {
        val ps = it.parameterTypes
        if (ps != null && ps.isNotEmpty()) {
            if (ps.size == 1 && ps[0].name == "int") {
                hitI++
            }
            if (ps.size == 2 && ps[0].name == "int") {
                if (ps[1].name.startsWith("com.tencent.mm")) {
                    mname = it.name
                    pclzName = ps[1].name
                    hitA++
                }
            }
        }
    }
    if (hitI == 2 && hitA == 1) {
        ret = true
    }
    if (ret) {
        val clz = XposedHelpers.findClass(pclzName, loader)
        callback(clz, mname)
    }
    return ret
}

这个函数的目的,是找到一个类,该类是 AppPanel 的匿名内部类,包含三个方法,并且这三个方法分别符合 (I)V(I)V(ILcom/tencent/mm/pluginsdk/model/app/f;)V 的函数签名。

当然了,此处 f 类我们并没有必要知道,但是需要知道它位于 com.tencent.mm 下。由此把它找出来即可。当找到这个的类时,即可以返回类和函数名,供 hook 使用了。


到这里,基本上已经讲完了本篇想讲的内容了,经过实测,证实这些代码均可以同时工作于微信 6.7.3 和 6.7.4 内测版,等微信 7.0 正式版发布后,理论上也可以不经过修改直接使用。

虽然相比之前的做法,跨版本的实现代码较多较繁琐,但是至少我们绕开了微信版本的问题,以后真要再改,也不会有太大的麻烦了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容