Frida —— WeChat(一) 🎲
对于apk反编译有很多直接的工具可以使用,但是微信使用AndResGuard对资源文件进行混淆,导致直接使用apktool反编译后无法获取资源文件,即使使用apktool -r d保持资源文件,重打包的微信也不能直接运行,因为微信启动后会对dex文件进行校验
0x01 前言
资源混淆
对于apk反编译有很多直接的工具可以使用,但是微信使用AndResGuard对资源文件进行混淆,导致直接使用apktool反编译后无法获取资源文件,即使使用apktool -r d保持资源文件,重打包的微信也不能直接运行,因为微信启动后会对dex文件进行校验
解决方案:ShakaApktool对资源混淆进行Bypass,针对腾讯的apk可以加上-xn参数
文件校验
解决方案:找到校验的的位置将代码patch掉
在微信6.6.1版本中,校验代码位于smali/com/tencent/mm/f/a.smali
.line 532const v2, 0x19000
:try_start_2invoke-static {v3, v2}, Lcom/tencent/mm/a/g;->a(Ljava/io/InputStream;I)Ljava/lang/String;
move-result-object v2
.line 533if-eqz v2, :cond_2
iget-object v4, p0, Lcom/tencent/mm/f/a$a;->eHC:Ljava/lang/String;
invoke-virtual {v2, v4}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z:try_end_2.catch Ljava/io/IOException; {:try_start_2 .. :try_end_2} :catch_6.catchall {:try_start_2 .. :try_end_2} :catchall_2
move-result v2
if-eqz v2, :cond_2
.line 534:try_start_3invoke-virtual {v5}, Lcom/tencent/tinker/loader/shareutil/ShareFileLockHelper;->close()V:try_end_3.catch Ljava/lang/Exception; {:try_start_3 .. :try_end_3} :catch_0
.line 551:goto_1invoke-static {v3}, Lcom/tencent/mm/f/a;->b(Ljava/io/Closeable;)V
goto :goto_00x02 准备
就目前而言,学习的主要目的是如何跟踪代码逻辑,找到需要hook的点,等技能日渐成熟可直接上xposed进行持续维持,毕竟frida太容易导致进程崩溃
Apktool和ShakaApktool反编译出来的是smali,可以配合ideasmali动态调试,但是就是很怪的装不上,于是选用jadx直接反编译出java代码进行静态分析
反编译
- Android studio
- jadx
- 微信 v7.0.0
常见报错
-
如果报 OOM 错误,需要根据物理内存大小修改JVM内存使用限制,推荐至少为5G
set DEFAULT_JVM_OPTS="-Xms5g" "-Xmx8g" -
在Mac上可能因线程过多导致崩溃,推荐直接使用单线程
Terminal window > jadx -e -d out -j 1 weixin700android1380.apk
0x03 新手酷爱的骰子
探索
-
先使用 Android SDK 下的
uiautomatorviewer工具获取🎲的资源ID
-
如果没有反编译资源文件就在resources.arsc中查找资源类型id下名为x5的资源对应的ID

如果反编译了资源文件就在res/values/public.xml下查找对应ID

-
通过
Find in Path功能,全局搜索ID在R类中实际使用的变量名为art_emoji_icon_iv
-
通过搜索变量名
art_emoji_icon_iv,根据类所在的路径,基本可以定位相关代码应为以下其中之一
-
对上面出现的类都简单Hook一下,发现在打开表情界面时触发了以下两个,在点击骰子后没有反应
Terminal window [>]android.widget.FrameLayout{4b23dd3 V.E...... ......I. 0,0-0,0}[!]Hook com.tencent.mm.view.a.b[>]android.widget.RelativeLayout{de3011a V.E...... ......I. 0,0-0,0 #7f111a74 app:id/e82}[!]Hook com.tencent.mm.view.a.c -
发现在a.b下面并没有处理点击触发的相关函数(IOC——控制反转),查看a.b的引用,应该是跟入
com.tencent.mm.view.SmileyGrid

-
查看a.c的引用,没什么额外的东西

-
注意到
com.tencent.mm.view.SmileyGrid,yIT默认初始值为20,推测case 20是布局,case 23&25是监听点击,最后进入到SmileyGrid.a(SmileyGrid.this, emojiInfo);
-
尝试hook查看下传入值
sgClass = Java.use("com.tencent.mm.view.SmileyGrid");sgClass.a.overload('com.tencent.mm.view.SmileyGrid', 'com.tencent.mm.storage.emotion.EmojiInfo').implementation = function (arg1, arg2) {s = this.a(arg1, arg2);console.log(arg1, arg2);};Terminal window [LGE Nexus 5X::com.tencent.mm]-> com.tencent.mm.view.SmileySubGrid{8691b21 V.ED..C.. .......D 0,0-1080,644 #7f110382 app:id/x9} field_md5:08f223fa83f1ca34e143d1e580252c7cfield_svrid:field_catalog:18field_type:1field_size:0field_start:0field_state:0field_name:dice.pngfield_content:50field_reserved1:field_reserved2:field_reserved3:0field_reserved4:0field_app_id:field_groupId:18field_lastUseTime:0field_framesInfo:field_idx:0field_temp:0field_source:0field_needupload:0field_designerID:nullfield_thumbUrl:nullfield_captureStatus:0field_captureUploadErrCode0field_captureUploadCounter0 -
分析下代码,从判断逻辑上看,最后有
.show()的Toast分支肯定不用看了,基本可以确定是最中央的elsestatic /* synthetic */ void a(SmileyGrid smileyGrid, EmojiInfo emojiInfo) {if (smileyGrid.uga == null || emojiInfo == null) {ab.e("MicroMsg.emoji.SmileyGrid", "jacks npe dealCustomEmojiClick");} else if (yIS != 2) {if (!smileyGrid.uga.bEl()) {com.tencent.mm.ui.base.h.a(smileyGrid.getContext(), i.chatting_msg_type_not_support_send, 0, new OnClickListener() {public final void onClick(DialogInterface dialogInterface, int i) {}});} else if (emojiInfo == null) {} else {// HERE HERE HEREif (emojiInfo.field_type != EmojiInfo.AIJ && emojiInfo.field_type != EmojiInfo.AIU) {ab.i("MicroMsg.emoji.SmileyGrid", "cpan send dealcustom emoji click emoji:%s", emojiInfo.QX());// maybe 1final EmojiInfo c = ((com.tencent.mm.plugin.emoji.b.d) g.N(com.tencent.mm.plugin.emoji.b.d.class)).getProvider().c(emojiInfo);if (c != null) {...}ab.w("MicroMsg.emoji.SmileyGrid", "onSendCustomEmoji error, emoji is null");} else if (smileyGrid.uga.bEk()) {// maybe 2smileyGrid.uga.o(emojiInfo);ab.d("MicroMsg.emoji.SmileyGrid", "onSendAppMsgCustomEmoji emoji md5 is [%s]", emojiInfo.QX());} else {Toast.makeText(smileyGrid.getContext(), smileyGrid.getContext().getString(i.chatting_msg_type_not_support), 0).show();}}} else if (emojiInfo.field_catalog == EmojiGroupInfo.AIx) {Context context = smileyGrid.getContext();int i = i.chatting_can_not_del_sys_smiley;com.tencent.mm.ui.base.h.j(context, i, i).show();}} -
关键的两行代码
final EmojiInfo c = ((com.tencent.mm.plugin.emoji.b.d) g.N(com.tencent.mm.plugin.emoji.b.d.class)).getProvider().c(emojiInfo);// g 为 com.tencent.mm.kernel.g// com.tencent.mm.plugin.emoji.b.d 为一个接口类smileyGrid.uga.o(emojiInfo); -
通过
Hook g.N查看返回结果有以下两种class com.tencent.mm.plugin.emoji.PluginEmoji# 下面这个看名字应该就不是class com.tencent.mm.plugin.story.PluginStory -
在
PluginEmoji的getProvider中,会创建一个a类public e getProvider() {if (this.keM == null) {this.keM = new com.tencent.mm.ca.a();}return this.keM;} -
跟入到
com.tencent.mm.ca下类a中的c函数,又调用PluginEmoji下的getEmojiMgrpublic final EmojiInfo c(EmojiInfo emojiInfo) {if (((h) g.ME().Mg()).Nx()) {return ((d) g.N(d.class)).getEmojiMgr().c(emojiInfo);}...} -
PluginEmoji.getEmojiMgr代码如下public void setEmojiMgr() {if (this.keL == null) {this.keL = com.tencent.mm.plugin.emoji.b.b.a.kgD.getEmojiMgr();}}public com.tencent.mm.pluginsdk.a.d getEmojiMgr() {setEmojiMgr();return this.keL;} -
这里
hook直接获取返回值的类却显示[object Object],查询网上说可能是android版本问题,亦可能是返回类型问题,反正没能解决,只好尝试跟com.tencent.mm.plugin.emoji.b.b.a下值kgD的来源(话说model下的类是不是直接对应emoji下同名类?)
-
接着几次跟入
public final class b extends p {public b() {super(c.adY("emoji"));a.kgD = new com.tencent.mm.plugin.emoji.b.b() {public final d getEmojiMgr() {return j.bbs();}...跟入同路径下j.java
import com.tencent.mm.plugin.emoji.e.g;public class j implements as {public static g bbs() {com.tencent.mm.kernel.g.MF().LO();if (bbq().kjm == null) {bbq().kjm = new g();}return bbq().kjm;}...跟入后,发现关键信息来源于
bo.gqimport com.tencent.mm.sdk.platformtools.bo;public final class g implements d {...public final EmojiInfo c(EmojiInfo emojiInfo) {if (emojiInfo.field_catalog == EmojiGroupInfo.AIx && emojiInfo.field_type == EmojiInfo.AIG && emojiInfo.getContent().length() > 0 && EmojiInfo.SM(bo.getInt(emojiInfo.getContent(), 0))) {Cursor Ke = j.getEmojiStorageMgr().wKL.Ke(bo.getInt(emojiInfo.getContent(), 0));if (Ke != null && Ke.getCount() > 1) {int gq = bo.gq(Ke.getCount() - 1, 0);emojiInfo = new EmojiInfo();Ke.moveToPosition(gq);emojiInfo.d(Ke);}if (Ke != null) {Ke.close();}}return emojiInfo;}... -
在
com.tencent.mm.sdk.platformtools.bo下gq函数中看到了让人激动的Random,大致应该就是这里了public static int gq(int i, int i2) {Assert.assertTrue(i > i2);return new Random(System.currentTimeMillis()).nextInt((i - i2) + 1) + i2;} -
简单
hook下fClass = Java.use("com.tencent.mm.sdk.platformtools.bo");fClass.gq.implementation = function (arg1, arg2) {s = this.gq(arg1, arg2);console.log('------------------');console.log("[In]", arg1, arg2);console.log("[Out]", s);return s;};发现此函数也用来生成猜拳结果,且生成规则如下
Terminal window # 输入 5 0# 输出 0-5 对应骰子 1-6[In] 5 0[Out] 2# 输入 2 0# 输出 0-2 对应猜拳 剪刀-石头-布[In] 2 0[Out] 1
快速定位
根据网上资料可知最终还是通过Random函数产生随机结果,且代码位于com.tencent.mm.sdk.platformtools下,直接搜索即可定位

效果
可通过arg1区分猜拳、骰子,这里没处理
setImmediate(function () { Java.perform(function(){ cClass = Java.use("com.tencent.mm.sdk.platformtools.bo"); cClass.gq.implementation = function (arg1, arg2) { return 5; }; })});