0x01 前言
资源混淆
对于apk反编译有很多直接的工具可以使用,但是微信使用AndResGuard对资源文件进行混淆,导致直接使用apktool反编译后无法获取资源文件,即使使用apktool -r d
保持资源文件,重打包的微信也不能直接运行,因为微信启动后会对dex文件进行校验
解决方案:ShakaApktool对资源混淆进行Bypass,针对腾讯的apk可以加上-xn
参数
文件校验
解决方案:找到校验的的位置将代码patch掉
在微信6.6.1版本中,校验代码位于smali/com/tencent/mm/f/a.smali
.line 532
const v2, 0x19000
:try_start_2
invoke-static {v3, v2}, Lcom/tencent/mm/a/g;->a(Ljava/io/InputStream;I)Ljava/lang/String;
move-result-object v2
.line 533
if-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_3
invoke-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_1
invoke-static {v3}, Lcom/tencent/mm/f/a;->b(Ljava/io/Closeable;)V
goto :goto_0
0x02 准备
就目前而言,学习的主要目的是如何跟踪代码逻辑,找到需要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上可能因线程过多导致崩溃,推荐直接使用单线程
> 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一下,发现在打开表情界面时触发了以下两个,在点击骰子后没有反应
[>]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); };
[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:08f223fa83f1ca34e143d1e580252c7c field_svrid: field_catalog:18 field_type:1 field_size:0 field_start:0 field_state:0 field_name:dice.png field_content:50 field_reserved1: field_reserved2: field_reserved3:0 field_reserved4:0 field_app_id: field_groupId:18 field_lastUseTime:0 field_framesInfo: field_idx:0 field_temp:0 field_source:0 field_needupload:0 field_designerID:null field_thumbUrl:null field_captureStatus:0 field_captureUploadErrCode0 field_captureUploadCounter0
分析下代码,从判断逻辑上看,最后有
.show()
的Toast分支肯定不用看了,基本可以确定是最中央的else
static /* 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 HERE if (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 1 final 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 2 smileyGrid.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
下的getEmojiMgr
public 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.gq
import 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; };
发现此函数也用来生成猜拳结果,且生成规则如下
# 输入 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;
};
})
});