微信插件之护妻宝(一)

2,196 阅读8分钟

前介

现如今最常用的社交工具是什么呢?必然是 微信 ,不得不说现在的微信不在简简单单是一个社交程序,它已经和我们生活紧密相关了。

做为一个程序员?最重要的是什么呢?

我认为做为一个程序员最重要的是

但是做为一个程序员没日没夜的敲代码,经常没有时间回复女朋友的微信或者尬聊

做为一个资深的程序员,怎么能被这种小挫折打败呢?我们要用程序的去维护我们的 娇妻因此我要开发一款自动回复女朋友消息的微信插件。

你的 娇妻 再也不能打断你 打游戏codeing风流快活 ,让你的 娇妻 和机器人聊吧(维护家庭和谐)!

确定目标

  1. 自动回复高情商话术
  2. 微信无感知回复,在后台也不能遗漏回复
  3. 开关设置,能指定自动回复的娇妻(我们的娇妻可不止一个)
  4. 热修复,兼容多版本微信
  5. 找到女朋友

方案定制

我个人习惯,再开发之前,先做准备。把思路屡清楚事半功倍。

  1. 自动回复高情商话术

我的第一想法是先去下载类似 恋爱话术App ,然后逆向其接口,接入我们的程序。

本人下载了将近 10 款这种类型的应用,最终都发现收费很贵

,并且话术都不是很全,最后放弃了。

然后找到了 图灵机器人 ,提供完整的聊天机器人 API,并且话术还是很精湛的,因此果断注册开发中账号。

  1. 微信无感知回复,后台也不能遗漏回复

这种没有很好的办法,因为要想在后台回复,只能想办法向微信注入我们的代码(也就是说我们的代码要在微信的进程中运行,在专业点就是我们要想办法拿到微信的 ClassLoader)。

我的方案还是使用 Xposed 框架 + 分身大师 实现免 Root 注入。

如何开发 Xposed 插件,请参看本人文章 参考链接

  1. 开关设置,能指定自动回复的娇妻(我们的娇妻可不止一个)

逆向分析微信的聊天界面,找一个合适的地方,通过 Xposed 注入一个开关按钮。这个难点是分析微信代码,并且寻找到的 Hook 代码尽量保证版本兼容(我不希望微信版本更新,就要重新发布或安装插件)。

这里补充下,为啥要 Hook 点尽量保证版本兼容呢?其实说白了就是寻找微信没有混淆的点做入口。为啥呢?因为一旦微信版本升级,肯定会再次混淆。若你 Hook 点是混淆的,那若微信版本升级,若要兼容新版本必定要从新寻找混淆后新的 Hook 点(我们可不得不到微信混淆后的 mapping 文件)。

  1. 热修复,兼容多版本微信

在第 3 点我讲过,尽量寻找没有混淆的点做 Hook,但是若死活寻找不到没有混淆的 Hook 点呢?我们只能想办法进行动态修复插件了,让用户无感知使用,我提供的方案是通过 DexClassLoader 去修复 Hook 点代码。
这套技术方案我在曾经在我的项目 微信语音助手 使用过(以停止维护),具体方案看下图。

  1. 找到女朋友

若有女程序员看到这里

开始敲代码

都准备好啦,接下来我们开始

对接图灵机器人

这个没啥好说的就是去注册账号,查看提供的API文档

搭建 Xposed 项目

首先在项目 build.gradle 加入依赖。

dependencies {
    ...
    compileOnly 'de.robv.android.xposed:api:82'
    compileOnly 'de.robv.android.xposed:api:82:sources'
    ...
}

注意一定要用 compileOnly 依赖。为啥呢?大家可以自行去看下 Xposed 的原理。我这里就叙述这个问题了,不再本文范畴。

接下来创建上图中所提到的入口类。

class XpCore : IXposedHookLoadPackage {

    override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
        MMCoreHook.hook(lpparam)
    }
}

寻找消息 Hook 点

其实寻找 Hook 点就是一个细心的活,我们要在微信的代码海中去寻找合适的 Hook 点,寻找尽量没有混淆的 Hook 点。

如何能尽量快速找到呢?由于文章篇幅问题,请 参考 鄙人文章。

object MsgHook {
    fun hook(actvity: Activity) {
        hookFun(
            {
                if (TextUtils.equals("message", it.args[0].toString())) {
                    try {
                        val contentValues = it.args[2] as ContentValues

                        contentValues.apply {

                            val msgSvrId = getAsString("msgSvrId")
                            if (TextUtils.isEmpty(msgSvrId)) {
                                return@hookFun
                            }
                            val talker = getAsString("talker")
                            val type = getAsString("type")
                            val content = contentValues.getAsString("content")
                            Core.msgGo(talker, type, content, actvity)

                        }
                    } catch (e: Exception) {
                        e.printStackTrace()
                        Core.toast("消息拦截失败!!")
                    }
                }
            },
            {},
            "com.tencent.wcdb.database.SQLiteDatabase",
            actvity.classLoader,
            "insert",
            String::class.java,
            String::class.java,
            ContentValues::class.java
        )
    }
}

寻找开关Hook点

前面说我们可能不光有一位娇妻

我们需要一个开关,我可不想所有人都自动回复,更不想和一个男的暧昧,更重要的是 图灵机器人 收费啊。

最终通过不断的断点,分析,看源码,找到了一个非常好的 Hook 点。这个流程非常的长,我就不细讲了。具体如何逆向丶断点丶分析代码教程可以 参考 此文章。

部分核心代码,如下:

object ViewHook {
    fun hook(actvity: Activity) {
        log("view Hook开始")
        hookFun({}, {
            val viewGroup = it.thisObject as ViewGroup
            val wxId = it.args[0] as String
            Core.addClickView(viewGroup, wxId)

        }, "com.tencent.mm.pluginsdk.ui.chat.ChatFooter", actvity.classLoader,
            "setUserName",
            String::class.java
        )
    }
}

来看看效果:

是否打开守护状态的数据,我使用的是 GreenDao 来进行存储的。我挺喜欢这个数据库框架的哦。

回复消息

我们已经通过 Xposed 将我们的代码注入到微信当中了,并且获取到了微信的 ClassLoader。有 ClassLoader 我们当然能通过反射去调用微信的方法啦。

这又是一个很长很长的过程,我就不叙述了,反正就是逆向丶断点丶分析。

我的 张美丽丶李花丶杨柳絮又离我而去了。

在分析过程中我一直想寻找没有混淆的发消息 Hook 点,我找了很久,也想尽了所有的办法,包括构造上面的开关按钮 HookChatFooter 对象,通过调用 View 的方法。

我这里就跑题下,我当时想寻找到一个合适的微信发消息的 Hook 点(没有混淆的点),顺便给大家分享下我当时巧夺天工的想法

我们前面分析到了 ChatFooter 对象,他是一个 FrameLayout,提示这个布局就是我们聊天的布局,如下图:

思路很简单,我通过遍历 View 寻找到消息输入框,然后调用 setText 设置文本,然后在寻找 发送 按钮,在调用其 performClick 方法。

然后查看微信 ChatFooter 源码,在其构造方法中看到了。

接下来寻找发送消息按钮,我是通过搜索 setOnClickListener 方法,搜索到的。

接下来只需要递归遍历,寻找到消息输入框的 View 对象。

/**
   * 递归遍历寻找 微信输入框 EditText 对象
   */
  private fun findMsgEditText(view: View): EditText? {
      if (view is EditText) {
          return view
      } else {
          if (view is ViewGroup) {
              for (index in 0 until view.childCount) {
                  val editText = findMsgEditText(view.getChildAt(index))
                  if (editText != null) {
                      return editText
                  }
              }
          }
      }
      return null
  }

在开发过程中我发现个优化点,现在我们是通过递归查找到了,输入框 EditText 对象,本来还需要通过递归查找发送按钮的 View 对象,这样效率有点低,但是突然我想到了输入框,按回车也能发消息,因此从新去看了下微信代码,如下图:

啊哈哈,更能确定我的 View 对象是对的,然后就是通过反射获取到了 EditText 对象的 OnEditorActionListener 对象,然后调用了 onEditorAction 方法(由于无效,代码删除了,找不到了),这样如果能成功,我们的 护妻宝 就能兼容所有微信版本啦,也不用做什么热修复啦,好开心测试下。

妈的,既然无效,不应该啊。接下里我断点看了下,发送按钮的点击事件方法。

一直到我看到 ChatFooter 的这个方法。

死的心都有了,这个 xTi 对象又被混淆了,还是不行啊!回头我又想了想,腾讯微信这种大厂怎么可能将 View 层和逻辑层代码写在一起呢?

最终这种方案我还是放弃了,花费了这么长的篇幅讲这个,只是给大家提供一个思路,有时候我们可以通过这种手段去 Hook

最终我还是没找到,没混淆的发消息方法。

/**
 * hook 发消息方法
 */
private fun receiverMsg(activity: Activity, msg: String, talker: String) {
    val azClzz = XposedHelpers.findClass("com.tencent.mm.model.az", activity.classLoader)
    val obj = XposedHelpers.callStaticMethod(azClzz, "ZS")
    val msgClzz = XposedHelpers.findClass("com.tencent.mm.modelmulti.h", activity.classLoader)
    XposedHelpers.callMethod(obj, "b", XposedHelpers.newInstance(msgClzz, talker, msg, 1))
}

若有有能人,能找到没有混淆的发消息的 Hook(尽量兼容所有版本的),并且愿意无私提供给我,请留言哦。

热修复

其实我们大部分工作完成了,但是热修复还需要很多注意点。由于篇幅问题(代码还没写,哈哈)。

但是我们方案有了,其实大家也应该看我代码注意到,我在构建代码的时候,已经将核心的注入方法都抽取在了 Core 类,为了就是将来热修复方便。

总结

首先由于 图灵机器人 开发者账号,没审核下来(妈的,审核2天了)。我暂时没有集成,只是到收到 娇妻 信息后,自动回复 你好可爱哦

当前效果

补充

由于作者分析的微信版本是 7.0.7,等软件完成后在兼容最新版本微信,若想测试,可以安装微信 7.0.7 版本测试哦。

我是用我公司的 360分身大师X版 做的免 Root 使用 Xposed(微信版本是7.0.7哦)。

代码仓库