利用 Xposed 快速实现一个简易微信机器人

目标

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

快速实现

项目框架的搭建

WechatSpellbook - 站在"巨人"的肩膀上

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

获得好友发来的消息

实现机器人功能的首要步骤就是获得好友发来的消息,获得消息之后才能回复吧,才能叫“机器人”吧。
使用了 WechatSpellbook,获取消息是很容易的,参见api,当新消息存入数据库后回调,具体代码:

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
    这步到此就完成了,下一步是机器人怎么将消息回复给好友。

机器人回复消息

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

利用 Monitor 的 Method Profiling 功能分析

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

  1. 先搜索 click,点击发送按钮,肯定是触发了点击事件的嘛,先找找看


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

反编译查看源码分析

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

  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类的方法,看下源码:

    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的方法,看下这个类的源码:

    // 截取了几个方法
    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 这个方法,然后打印出调用堆栈看看到底是哪里回调了。

        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>,看下这个方法的源码:

    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 这个类的构造方法发送消息啊。

找到的 hook 关键点

  1. com.tencent.mm.ui.chatting.o.FZ(String) 方法,参数是消息文本,调用该方法可以发消息,但是无法设置接收者
  2. com.tencent.mm.modelmulti.i()构造方法,第0个参数是接收者 id,第1个参数是消息文本

机器人回复消息思路:调用第一个 API 发送消息文本,hook 第二个 API 修改接收者 id,然后就可以愉快的发消息了

关键点存在的问题

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

问题解决方法

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

代码实现

源码在这里,关键地方都有注释,有兴趣可以 star

效果图

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,411评论 25 707
  • 去年有段时间得空,就把谷歌GAE的API权威指南看了一遍,收获颇丰,特别是在自己几乎独立开发了公司的云数据中心之后...
    骑单车的勋爵阅读 20,431评论 0 41
  • 文/云端一梦今早,一段记录生活的文字读一遍又一遍,没有诗意里面满是平淡字里行间,写满你的笑颜最美的一天 中午,在厨...
    云端一梦l阅读 1,068评论 32 60
  • ‌曾经,我是一个默默无闻的人,没有方向!没有目标!真的很迷茫!总以为这样平凡的过一生也好!可是,现在现社会的压力补...
    555106a62337阅读 244评论 0 0