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

作者 : 开心源码 本文共4948个字,预计阅读时间需要13分钟 发布时间: 2022-05-12 共199人阅读

众所周知的,微信每个版本更新后,变量名都会有少量变化,引起过去的 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 = 1while (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 正式版发布后,理论上也可以不经过修改直接使用。

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

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 实现可以跨版本使用的微信 Xposed 板块

发表回复