Frida —— WeChat(一) 🎲

对于apk反编译有很多直接的工具可以使用,但是微信使用AndResGuard对资源文件进行混淆,导致直接使用apktool反编译后无法获取资源文件,即使使用apktool -r d保持资源文件,重打包的微信也不能直接运行,因为微信启动后会对dex文件进行校验

本页目录
  1. 0x01 前言
  2. 资源混淆
  3. 文件校验
  4. 0x02 准备
  5. 反编译
  6. 0x03 新手酷爱的骰子
  7. 探索
  8. 快速定位
  9. 效果
  10. 参考资料

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

常见报错

  1. 如果报 OOM 错误,需要根据物理内存大小修改JVM内存使用限制,推荐至少为5G

    set DEFAULT_JVM_OPTS="-Xms5g" "-Xmx8g"
  2. 在Mac上可能因线程过多导致崩溃,推荐直接使用单线程

    Terminal window
    > jadx -e -d out -j 1 weixin700android1380.apk

0x03 新手酷爱的骰子

探索

  1. 先使用 Android SDK 下的uiautomatorviewer工具获取🎲的资源ID

    Frida —— WeChat(一) 🎲 · 图示

  2. 如果没有反编译资源文件就在resources.arsc中查找资源类型id下名为x5的资源对应的ID

    Frida —— WeChat(一) 🎲 · 图示

    如果反编译了资源文件就在res/values/public.xml下查找对应ID
    Frida —— WeChat(一) 🎲 · 图示

  3. 通过Find in Path功能,全局搜索ID在R类中实际使用的变量名为art_emoji_icon_iv

    Frida —— WeChat(一) 🎲 · 图示

  4. 通过搜索变量名art_emoji_icon_iv,根据类所在的路径,基本可以定位相关代码应为以下其中之一

    Frida —— WeChat(一) 🎲 · 图示

  5. 对上面出现的类都简单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
  6. 发现在a.b下面并没有处理点击触发的相关函数(IOC——控制反转),查看a.b的引用,应该是跟入com.tencent.mm.view.SmileyGrid

    Frida —— WeChat(一) 🎲 · 图示
    Frida —— WeChat(一) 🎲 · 图示

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

    Frida —— WeChat(一) 🎲 · 图示

  8. 注意到com.tencent.mm.view.SmileyGrid,yIT默认初始值为20,推测case 20是布局,case 23&25是监听点击,最后进入到SmileyGrid.a(SmileyGrid.this, emojiInfo);

    Frida —— WeChat(一) 🎲 · 图示

  9. 尝试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: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
  10. 分析下代码,从判断逻辑上看,最后有.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();
    }
    }
  11. 关键的两行代码

    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);
  12. 通过Hook g.N查看返回结果有以下两种

    class com.tencent.mm.plugin.emoji.PluginEmoji
    # 下面这个看名字应该就不是
    class com.tencent.mm.plugin.story.PluginStory
  13. 在PluginEmoji的getProvider中,会创建一个a类

    public e getProvider() {
    if (this.keM == null) {
    this.keM = new com.tencent.mm.ca.a();
    }
    return this.keM;
    }
  14. 跟入到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);
    }
    ...
    }
  15. 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;
    }
  16. 这里hook直接获取返回值的类却显示[object Object],查询网上说可能是android版本问题,亦可能是返回类型问题,反正没能解决,只好尝试跟com.tencent.mm.plugin.emoji.b.b.a下值kgD的来源(话说model下的类是不是直接对应emoji下同名类?)

    Frida —— WeChat(一) 🎲 · 图示

  17. 接着几次跟入

    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;
    }
    ...
  18. 在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;
    }
  19. 简单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下,直接搜索即可定位

Frida —— WeChat(一) 🎲 · 图示

效果

可通过arg1区分猜拳、骰子,这里没处理

setImmediate(function () {
Java.perform(function(){
cClass = Java.use("com.tencent.mm.sdk.platformtools.bo");
cClass.gq.implementation = function (arg1, arg2) {
return 5;
};
})
});

Frida —— WeChat(一) 🎲 · 图示

参考资料

  1. https://bbs.pediy.com/thread-225912.htm
  2. https://bbs.pediy.com/thread-202965.htm
  3. https://cloud.tencent.com/developer/article/1048423
  4. https://blog.csdn.net/jiangwei0910410003/article/details/52892330
  5. https://www.jianshu.com/p/3968ffabdf9d