目标

当前微信网页版限制越来越多,考虑尝试在手机上实现类似机器人的功能。本文目的是利用 Xposed
快速实现简易机器人功能,包括获取好友发来的消息,以及回复消息。后续可以增加智能回复,比如接入图灵机器人,或者自己自定义实现一些功能。


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0>
快速实现


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E9%A1%B9%E7%9B%AE%E6%A1%86%E6%9E%B6%E7%9A%84%E6%90%AD%E5%BB%BA>
项目框架的搭建


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#WechatSpellbook-%E7%AB%99%E5%9C%A8%E2%80%9D%E5%B7%A8%E4%BA%BA%E2%80%9D%E7%9A%84%E8%82%A9%E8%86%80%E4%B8%8A>
WechatSpellbook - 站在”巨人”的肩膀上

WechatSpellbook <https://github.com/Gh0u1L5/WechatSpellbook/>
 是微信巫师作者在微信巫师的基础提取出来的通用微信 Xposed 插件框架。它提供了友好的的
API,提供自动分析微信内部结构特征的API(忽略微信版本差异),对 hook 微信出现的常见问题都做了优化,总之就是使用它会更容易对微信
hook,感谢作者的贡献,项目的集成和详细介绍参见wiki
<https://github.com/Gh0u1L5/WechatSpellbook/wiki/%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B>
,以下步骤的实现都是基于这个框架的。
以下源码均基于微信 6.6.6 版本,由于使用了 WechatSpellbook 框架动态匹配的原理,大部分微信版本均可自动适配。


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E8%8E%B7%E5%BE%97%E5%A5%BD%E5%8F%8B%E5%8F%91%E6%9D%A5%E7%9A%84%E6%B6%88%E6%81%AF>
获得好友发来的消息

实现机器人功能的首要步骤就是获得好友发来的消息,获得消息之后才能回复吧,才能叫“机器人”吧。
使用了 WechatSpellbook,获取消息是很容易的,参见api
<https://github.com/Gh0u1L5/WechatSpellbook/blob/master/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IMessageStorageHook.kt#L27>
,当新消息存入数据库后回调,具体代码:


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 object WechatMessageHook :
IMessageStorageHook { override fun onMessageStorageInserted(msgId: Long,
msgObject:Any) { XposedBridge.log("onMessageStorageInserted msgId=$msgId
,msgObject=$msgObject") // 这些都是消息的属性,内容,发送人,类型等 val field_content =
XposedHelpers.getObjectField(msgObject,"field_content") as String? val
field_talker = XposedHelpers.getObjectField(msgObject,"field_talker") as String?
val field_type = (XposedHelpers.getObjectField(msgObject, "field_type") as Int
).toInt() val field_isSend = (XposedHelpers.getObjectField(msgObject,
"field_isSend") as Int).toInt() XposedBridge.log("field_content=$field_content
,field_talker=$field_talker," + "field_type=$field_type,field_isSend=
$field_isSend") if (field_isSend == 1) {// 代表自己发出的,不处理 return } // 做其他事情 } }


其中字段名含义如下:

* field_content: 消息内容
* field_talker: 发送者
* field_type: 消息类型
* field_isSend: 是谁发出的,我自己发出为1
这步到此就完成了,下一步是机器人怎么将消息回复给好友。

<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%9B%9E%E5%A4%8D%E6%B6%88%E6%81%AF>
机器人回复消息

机器人回复消息需要找到发送消息出去这个 API,然后 hook 它,在我们的代码里调用就行了。


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E5%88%A9%E7%94%A8-Monitor-%E7%9A%84-Method-Profiling-%E5%8A%9F%E8%83%BD%E5%88%86%E6%9E%90>
利用 Monitor 的 Method Profiling 功能分析

首先在模拟器中打开微信聊天窗口,打开 Monitor,选中微信进程,点击Start Method Profiling
,然后在聊天窗口随便发送一条消息,然后回来点击Stop Method Profiling,会生成分析文件。分析步骤如下:

* 先搜索 click,点击发送按钮,肯定是触发了点击事件的嘛,先找找看image.png
* 发现调用了 ChatFooter$3.onClick() 方法,单从名字上来看,应该就是这里了,点进去,看这个函数调用了哪里image.png
* 它调用了 chatting.o.FZ 方法,注意参数是 String,返回值是
Boolean,大胆猜测一下,这个字符串就是消息文本,返回值应该是发送是否成功。验证一下,直接 Hook
这个函数,运行发现猜测是真的,这里比较简单就不贴代码了。
* 分析到这里,已经知道了chatting.o.FZ 方法就是发送消息的,参数就是消息文本,但是有个很重要的地方忽略了,为什么没有接收者参数?
,微信内部联系人 ID 一般是以 wx_idxxx 开头的,接收者 id 设置在哪,怎么设置 hook,现在就差这个问题了。
到这里已经知道了发送消息的 API,hook 掉就可以搞事情了,但是缺少接收者这个重要参数的设置,分析下源码吧。

<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E5%8F%8D%E7%BC%96%E8%AF%91%E6%9F%A5%E7%9C%8B%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90>
反编译查看源码分析

反编译之后分析 chatting.o.FZ 方法源码:


1 2 3 4 5 public final boolean FZ(String str) { mS(false); ctQ(); return this
.yOg.yRO.dt(str,0); }


然后分析yOg.yRO.dt方法,它是com.tencent.mm.ui.chatting.b类的方法,看下源码:


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public final boolean dt(String str,
int i) { int i2 = 0; String Xf = bh.Xf(str); if (Xf == null || Xf.length() == 0
) { w.e("MicroMsg.ChattingUI.TextImp", "doSendMessage null"); return false; }
x xVar =this.yXC; if (!ah.oB(Xf)) { az azVar = new az();
azVar.setContent(Xf); azVar.eW(1); xVar.aB(azVar); } bt btVar = new bt();
// 省略 }


可以看到在azVar.setContent(Xf);这里将发送的消息文本放在放在了az这个类中,setContent() 是 az 的父类
com.tencent.mm.g.c.cg的方法,看下这个类的源码:


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 截取了几个方法 public
final void av(long j) { this.field_createTime = j; this.eRw = true; } public
final long wQ() { return this.field_createTime; } public final void ed(String
str) { this.field_talker = str; this.feh = true; } public final String wR() {
return this.field_talker; } public final void setContent(String str) { this
.field_content = str; this.eRE = true; }


只截取了几个方法,可以看到这个类不仅仅包含消息文本,还包含了接受者field_talker,发送时间field_createTime
等,大胆猜想,这个类就是消息的包装类,包含消息所有的属性,这里关注的字段是接收者 field_talker,只要知道在哪里调用了ed方法 hook
掉就可以为所欲为了。
但是,通过 AS 查找调用这个的地方有很多,根本无法判断具体发消息是哪里调用了,怎么办。
借助 Xposed 分析com.tencent.mm.g.c.cg.ed()方法,也就是设置接收者 field_talker 的方法,只要 hook
这个方法,然后打印出调用堆栈看看到底是哪里回调了。


1 2 3 4 5 6 7 8 val clz = XposedHelpers.findClass("com.tencent.mm.g.c.cg",
WechatGlobal.wxLoader) XposedHelpers.findAndHookMethod(clz, "ed", String::class.
java, object : XC_MethodHook() { override fun beforeHookedMethod(param:
MethodHookParam?) { log("set field_talker start") LogUtil.logStackTraces() //
打印调用堆栈 log("set field_talker end") } })


打印结果:

image.png
可以看到函数调用链,关键点在com.tencent.mm.modelmulti.i.<init>,看下这个方法的源码:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public i(String str, String str2, int i,
int i2, Object obj) { w.d("MicroMsg.NetSceneSendMsg", "dktext :%s", new
Object[]{bh.cjG()}); if (!bh.oB(str)) { cg azVar = new az(); azVar.eV(1);
azVar.ed(str); azVar.av(bd.in(str)); azVar.eW(1); azVar.setContent(str2);
azVar.setType(i); String a = a(((o) g.l(o.class)).s(azVar), obj, i2); if
(!bh.oB(a)) { azVar.ej(a); w.d("MicroMsg.NetSceneSendMsg",
"NetSceneSendMsg:MsgSource:%s", new Object[]{azVar.fnF}); // 省略很多代码 }


可以看到这个类的构造方法实例化了cg azVar = new az();,并调用了ed()方法。分析下这个构造函数,很有意思的是:参数 str 就是微信
id,str2是文本内容,后几个不知道,大胆猜测下这个类就是去发送消息的,从源码很难分析,hook 掉看看。
hook com.tencent.mm.modelmulti.i的构造方法打印参数,看下是否和发送消息有关。这里就不贴代码和截图了,结论是有关。那可以
hook 这个类的构造方法发送消息啊。


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E6%89%BE%E5%88%B0%E7%9A%84-hook-%E5%85%B3%E9%94%AE%E7%82%B9>
找到的 hook 关键点

* com.tencent.mm.ui.chatting.o.FZ(String) 方法,参数是消息文本,调用该方法可以发消息,但是无法设置接收者
* com.tencent.mm.modelmulti.i()构造方法,第0个参数是接收者 id,第1个参数是消息文本
机器人回复消息思路:调用第一个 API 发送消息文本,hook 第二个 API 修改接收者 id,然后就可以愉快的发消息了


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E5%85%B3%E9%94%AE%E7%82%B9%E5%AD%98%E5%9C%A8%E7%9A%84%E9%97%AE%E9%A2%98>
关键点存在的问题

上述 hook 思路存在的问题:当 hook 第二个API 时,不知道该条消息的接收者是谁,不太好设置。


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95>
问题解决方法

既然我能 hook 这两个 API,那么我可不可以直接在调用第一个 API 的时候,将接收者 id 放在文本消息前面,然后在 hook 第二个 API
时将文本消息中的接收者 id 解析出来赋值给第0个参数。
新消息文本 = 接收者ID + 分隔符号 + 真实消息文本
分割符号可以采用特殊字符,用户不会输入的字符,比如 \t 等


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0>
代码实现

源码 <https://github.com/Blankeer/WechatBotXposed>在这里,关键地方都有注释,有兴趣可以 star


<https://blankeer.github.io/2018/05/09/%E5%88%A9%E7%94%A8-Xposed-%E5%BF%AB%E9%80%9F%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/#%E6%95%88%E6%9E%9C%E5%9B%BE>
效果图