抱歉,Xposed真的可以为所欲为——6.你的表白撤不回了

12,026 阅读12分钟

一句话概括本文

一步步Hook微信,实现微信消息防撤回。


引言

上周六下班,与往常一样,和公司同事去吃个饭,然后打个球,在支付的时候突然却意外发现我的微信号 被封了,出现了一个这样的页面(别人的截图,当时慌了没截图,只想着解封):

卧槽,想想在深圳从不带钱包出门的我,要是没同事在,而我只用微信支付这个工具的话...

细思极恐,解封的话需要发短信,然后让另一个朋友帮你解封,要输身份证,手机号,银行卡后八位帮你解封。 有点繁琐,我开始回想我什么时候使用了微信外挂,是之前hook了微信步数,几天98800步,然后捐步,被检测出来了? 但是改步数那个是挺久之前的事了,隔那么久才检测出来?第二天才发现并不是我一个人被封号了,在逼乎上看到这篇:

《如何看待微信6月15号大面积封号问题(装xposed插件)?》

逼乎上讨论没那么激烈,但是在酷安却炸锅了:

貌似此举的目标是打击大部分的微商插件,但是感觉误伤很多普通用户啊,原理貌似是检测 是否安装了xposed,装了的话直接封...上节hook下厨房检测xposed的套路,看看能否也用在 微信上,反编译一波dex文件,然后全局搜xposed:

果然,判断异常堆栈里是否有XposedBridge,em... 后面这个 com.zte.heartyservice.SCC.FrameworkBridge 又是?百度搜了一波包名,发现是中兴的手机管家

呃,一个管家类的APP都拦截?是无需root就可以冻结应用的原因? 综上所以说只要你装了xposed,不管你有没有用微信相关的插件都会被封!哇,吃瓜群众表示很受伤...

好吧,关于封号风波的事就讲到这里,本节的话想折腾个微信消息防撤回的东东, 原因是这样的,某天和UI小姐姐聊天到深夜:

万一小姐姐是向我表白,然后害羞又撤回了,我不就亏了是吧~

好吧,开个玩笑,不过能看到别人撤回的东西内心会莫名的偷(愉)税(悦),而且有些老司机喜欢 发车然后撤回,图还没加载完,就撤回了,裤子他么都脱了你TM撤回?

所以就有了这篇文章,Hook微信实现信息防撤回!


1.胡乱分析第一波

图1

微信消息从发送到撤回的几步

先不具体去看源码,如果是让你来做这个消息撤回,流程会是怎样? 首先,信息肯定是持久化到本地的,一般会保存在数据库里,不然怎么每次打开。

撤回操作 -> 根据某一个标记检索数据库里的记录 -> 删除记录 -> 更新页面 -> 发送信息到服务器更新其他用户的撤回状态。

Maybe是这样,接着就开始跟代码了,依旧是捞比的逆向套路:方法跟踪 + 反编译源码 + 日志定位。 从点下撤销开始跟踪,直到页面显示撤销成功~,入手关键词SQLite,然后依次涉及到这几个类:

com.tencent.wcdb.database.SQLiteCursor
com.tencent.wcdb.database.SQLiteQuery
com.tencent.wcdb.database.SQLiteSession
com.tencent.wcdb.database.SQLiteConnection
com.tencent.wcdb.database.SQLiteDatabase
com.tencent.wcdb.database.SQLiteProgram
com.tencent.wcdb.database.SQLiteStatement
com.tencent.wcdb.database.SQLiteQueryBuilder

接下来就要一个个看这个类大概是干嘛的了,用AS打开反编译的项目,然后打开这些类,左侧选择

一个个结构看,限于篇幅这里就不一一列举了,最终定位类应该是SQLiteDatabase,它的结构里看到了这些方法:

接着就细化关键词再搜索,进行方法跟踪。

进去代码看下这个update方法~

好吧,update调用的是**updateWithOnConflict()**方法,把参数打印出来看看?

运行结果发现每接受到一条信息都会触发这个方法,连公众号信息也会调用。 (PS:Hook的时候参数的个数和顺序要对应,我一开始漏了一个string,log一直没打印,还以为 垃圾魅族的问题,没把我恶心死)

可以,那么分别来看看自己发信息撤回别人发信息撤回 的两种情况:

不难发现,第一个参数应该是一个代表信息类型的东西,普通信息是rconversation,而撤回信息类型是message,然后是第二个参数,类型是ContentValue,这个类用于存取键值对的数据,每个键值对表示一列的列名和该列的数据,撤回类型的信息除了上面的content外还有后面一块:

我们先要明确防止撤回的是别人发的信息而不是自己的,所以先要过滤掉自己的,对比两种情况得出这样的结论:

-> 判断第一个参数是否为message -> 获取第二个参数中的type判断是否等于10000(自己撤回是10002)

修改下程序,只处理别人发的信息的情况,打Log验证下,

看Log却意外发现了一个问题:

是的,多了个你撤回了一条信息,出现这条信息的原因是微信整了个比较贴心的功能:

用户撤回信息的原因可能是编辑内容错误,在五分钟以内,用户点击可以通过点击重新编辑, 获得撤回的文本信息,然后进行重新编辑,五分钟过后就会变成你撤回了一条消息。

所以判断条件还要加上这个过滤条件,所以代码就变成:

继续看Log:

行吧,是个聪明人都知道msgId这个字段里面有东西。

图2


2.胡乱分析第二波

图3

先从第一波跳出来,想想怎么我们到底要干嘛?信息处理简单流程是这样的:

收到信息会插入到数据库里,然后撤回就是移除数据库里对应的这条信息!

那么我们可以:

  • 1.找个东西把所有接收到的信息存起来;
  • 2.当触发了撤回操作,然后通过一个标识找到我们存起来的信息;
  • 3.把这条信息当做一个新的信息插入到数据库里;
  • 4.更改撤回的提示

强调一点,搬运,一开始我还想着去处理找个信息,然后拿点什么,加点什么。 后面发现改动的成本非常大,而且不知道会引发什么样的问题,所以拿到信息直接 传就好,不要加特技进去!

然后是找个标识,猜测是第一波分析出来的msgId,等等还需要我们验证下! 继续跟方法,另一个账号发送一条信息,然后全局搜insert,定位到了一个数据库插入的方法!

不会直接跟这个bt.f类,因为参数一样,而且执行插入操作肯定是不止调用一个方法的,跟下该方法的父方法,

继续跟

可以,参数不一样,说明信息是从这里构造的,然后也调用了好几个方法,打开源码查看这个类: com.tencent.mm.storage.be 还有这个b方法,从左侧的类结构可以看到,b方法有好几个,但是第一个参数都是一样的(bd bdVa) 进去这个bd类看看 com.tencent.mm.storage.bd

好复杂的类,加上混淆,看死个人,上面讲了,搬运,而不是对信息做处理,我们要做的只是找到这个信息里和msgId一样的标记而已。全局搜msgId

恩,就是这个field_msgId了,我们通过Xposed拿到这个变量的值,然后做下对比,验证下我们的猜想! 然后写出这样的程序:

怀着忐忑的心情重启手机,发信息,撤回信息,看下日志

卧槽,屌,猜对了,接下来我们整个Map来存键值对,然后试试在触发撤回操作的时候,根据msgId取出 对应的信息然后调用插入数据的方法,所以上面的代码改一下,我们要拿到调用b方法的那个对象。 然后就有了这样的代码:

运行下看看效果~

正常的微信里撤回

Hook了的微信

可以,尽管提示XX撤回了一条消息,但是信息并没有真的被撤回,所以说Hook成功了~ 接着我们进行一些细节的优化,还有改下撤回的信息。把能打印的都打印出来吧,把Content内容修改下:

打印下:

呃,我的微信好像有点不对劲,卧槽,变成头像发信息了?:

感觉是type类型的问题,改改,改成撤回信息那个type?

嗯,信息是看到了,但是不应该是这样的啊,后面排查了一下是updateWithOnConflict方法返回值是0, 才这样,把返回值设置为1,就可以了(param.result = 1)

嗯,正常是正常了,不过这顺序有点不对劲啊,撤回的提示信息比我们插入的信息快了一些,那就加点吧。

运行看看:

想着大概差不多的时候,今早老同学发的一个链接,然后撤回,撤回信息提示变成这样了:

后来想想还是拼接成这样的提示信息就算了,不要再在提示信息里显示具体内容:

小猪拦截到 XXX 撤回的信息

最终效果如下,可以,很赞:


3.胡乱分析第三波

图4

在我以为一切大功告成,膨胀得沾沾自喜的时候,测试小哥发现了意外的发现了一个Bug, 发语音,图片,视频,虽然没撤回,但是打开失败,估计就是文件被删除了。

而Android里删除文件调用的是File类的delete方法,跟一波方法,直接搜File.delete看下是否真的有调用:

好吧,直接Hook掉File类的delete方法,看看微信删除对应类型文件的路径,观察一波这些路径的规律,然后如果delete方法被调用了,过滤一波,不删除文件,直接返回结果true,欺骗微信文件删除成功,以此瞒天过海~ 代码如下:

依次发送语音,图片,视频,然后撤回,对应的删除记录:

观察下路径规则,不难发现对应的文件夹是 voice2,image2,video,so过滤一波路径,如果路径包含这三个 直接过滤设置返回值为True,另外公众号,外链,都是属于文本信息,只是微信做了特别的解析,比如上面那个 百度贴吧的,所以和普通信息一样处理就好。

然后发现只有语音能够正常打开,图片和视频都无法正常打开,跟了下路径,

用ES浏览器来到这个路径下:

这就很奇怪了,明明图片存在,但是就是不能正确加载,而且每次点开都会执行一次删除的操作, 就是说数据库里还保存着这个记录,撤销信息无非 删数据库记录+删文件,删文件肯定是没问题的, 那么问题肯定就出在删数据库记录那里了。跟踪一下SQLiteDatabase里的delete()方法,把参数和返回值打印下:

依次做:发送语音,发送图片,发送视频然后撤回的日志跟踪。

结合delete方法一起看:

结合上面的日志和delete方法的代码,可以得这样的结论:

  • 1.音频相关放在voiceinfo表里,图片相关放在ImgInfo2表里,而视频相关则放在videoinfo2表里。
  • 2.还有个WxFileIndex2表,从删除语句可以知道就是根据msgId去删除对应的文件记录,但是返回值却有1,6,2,0?

跟下返回值的代码:executeUpdateDelete()方法,在SQLiteStatement类

改变的行的数目?一般来说删除一条数据,正确执行完,受影响的行数会是1。 而只有语音能正常播放,难道是这个原因?hook下这个方法,如果第一个参数是这四个表, 直接把返回值设置成1试试~

发下信息然后撤回下试试~

行吧,视频和图片都能打开了,到此总算完美解决的,真的是一波三折。 接着改下UI页面,补上上次下厨房Hook Xposed检测的开光和防撤回开关。 最后的界面如下:


小结

这就是我不喜欢折腾Xposed的原因,太多未知,总要去猜测,去验证自己的猜测,不断安装重启, 当你以为可以了,然后又出新的问题,如果不是 设计小姐姐的原因,真不想去碰这个东西。


附:因为代码比较长,而且改动了下厨房,还有王者荣耀相关的代码,所以就不贴代码了,
直接给出仓库链接自取 github.com/coder-pig/C…