众所周知的,微信每个版本升级后,变量名都会有一些变化,引起过去的 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 使用了。
作者:何晓杰Dev
链接:https://www.jianshu.com/p/5f91411afa73