Learning Man's Blog

Frida —— WeChat(一) 🎲

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

一直在手机&PC上使用微信插件,正好学习Frida中尝试hook一下,记录下笔记。

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 前言
    1. 1.1. 资源混淆
    2. 1.2. 文件校验
  2. 2. 0x02 准备
    1. 2.1. 反编译
  3. 3. 0x03 新手酷爱的骰子
    1. 3.1. 探索
    2. 3.2. 快速定位
    3. 3.3. 效果
  4. 4. 参考资料