Learning Man's Blog

Frida —— WeChat(一) 🎲

字数统计: 1.9k阅读时长: 9 min
2019/01/19

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上可能因线程过多导致崩溃,推荐直接使用单线程

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

0x03 新手酷爱的骰子

探索

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

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

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

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

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

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


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

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

  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);
     };
     [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. PluginEmojigetProvider中,会创建一个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下同名类?)

  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.bogq函数中看到了让人激动的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;
        };

    发现此函数也用来生成猜拳结果,且生成规则如下

    # 输入 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;
        };
    })
});

参考资料

  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
CATALOG
  1. 1. 0x01 前言
  2. 2. 0x02 准备
  3. 3. 0x03 新手酷爱的骰子
  4. 4. 参考资料