Mythsman


乐极生悲,苦尽甘来。


Frida爬虫分析流程——以微信视频号下载为例

前言

微信的通信协议没有使用传统的https,而是采用 mmtls 和 quic 协议结合的方案(可能),导致常用的抓包方案完全无效。因此我们考虑使用逆向 hook 的方式,对微信视频号的数据进行获取。

Frida 是目前几乎最好跨平台hook工具,深受广大牢友的喜爱。因此我们考虑用这个工具来进行 hook 。

准备

  1. 准备一个解锁了 bootloader 、刷了 TWRP 并安装了 Magisk 的手机。(当然,也有无需root权限的方法,但是用起来会不方便,还是建议 root )
  2. 准备好 adb 环境。
  3. 参考 FRIDA 安装 frida 用于 hook。并最好把官网的 Tutorials 看下。
  4. 准备 Pycharm 作为开发环境。
  5. 准备好 wechat.apk 。
  6. 参考 JADX 安装好 jadx 用于代码静态分析。

思路

Frida 爬虫的思路如下:

  1. 利用 adb 的 dumpsys 工具定位到我们关心的 Activity 页。
  2. 利用 jadx 的静态分析工具在代码中定位到解密后的后的数据对象。
  3. 利用 frida 的 hook 能力重写数据对象的构造、拷贝等关键方法,提取出入参出参等。
  4. 将提取到的数据序列化成json,并持久化。

流程

启动 frida-server

首先需要在 Magisk 商店中找到 MagiskFrida 插件。这个插件会在手机启动时以高权限启动 frida-server 服务器用于后续注入 hook 代码。

如果一不小心 frida-server 跪了,只需要重启手机即可。

定位Activity

首先我们需要大概了解我们关注的页面的一些信息,方便我们后续定位代码。因此我们先打开感兴趣的页面(我这里是微信视频号的 tab),并执行 dumpsys 命令。

这样我们知道了,这个页面对应的是 FinderHomeUI 这个 activity。

定位数据对象

打开 jadx-gui ,定位到 FinderHomeUI 这个类。(可能loading一段时间,如果报OOM,则需要 通过 $ mdfind jadx-gui 找到启动脚本,并修改 JVM 参数)。

简单的四下观望,就可以找一个叫 FeedData 的类,也找到类这个类的一个类似 Builder 模式的静态内部类。

看起来这个 i 方法大概就是构造 FeedData 这个类的方法了,因此我们可以考虑下 hook 这个 i 方法。

提取重要参数

找到了上面的 com.tencent.mm.plugin.finder.storage.FeedData$a  对象,我们就可以简单编写一个hook脚本看看。

# -*- coding: UTF-8 -*-
import frida
import sys


def on_message(message, data):
    print(message)


jscode = """
Java.perform(function () {

  var a = Java.use('com.tencent.mm.plugin.finder.storage.FeedData$a');

  var i = a.i;
  i.implementation = function (finderItem) {

    var res = i.call(this, finderItem);

    try{
        send(JSON.stringify(res))
    }catch (e){
        console.log('Error: ' + e);//自己的逻辑要做好catch防止脚本有问题导致app崩溃。
    }
    return res;//注意保证函数输出不变。
  };

});
"""

process = frida.get_usb_device().attach('com.tencent.mm')
script = process.create_script(jscode)
script.on('message', on_message)
print('[*] Running Hook')
script.load()
sys.stdin.read()

执行这个脚本,并滑动一下可以看到如下输入:

显然,这里的 payload 并没有正确的被序列化,因此我们需要再做一个用序列化工具。

数据序列化输出

由于安卓自带的 org.json.JSONObject 不支持json序列化,而 Javascript 的方法也无法序列化 java 对象。因此我们需要自己引入一个java包用于序列化,这里我选用无脑的fastjson。

  1. 首先需要下载fastjson的jar包,我在本地的maven仓库中找到了: /Users/myths/.gradle/caches/modules-2/files-2.1/com.alibaba/fastjson/1.2.69/6cb063f1d527ff65bdbb9ea74888a5ffc3f92197/fastjson-1.2.69.jar
  2. 然后利用 adb 的 build-tools 中的 dx 工具将 jar 包重新打包成 dex 包:$ /Users/myths/Library/Android/sdk/build-tools/26.0.2/dx --dex --output=fastjson.dex fastjson.jar
  3. 将上述生成的 dex 包 push 到手机中,例如 /data/local/tmp/fastjson.dex  下。
  4. 更改下脚本,再滑动下页面:
# -*- coding: UTF-8 -*-
import frida
import sys


def on_message(message, data):
    print(message['payload'])


jscode = """
Java.perform(function () {

  var a = Java.use('com.tencent.mm.plugin.finder.storage.FeedData$a');
  Java.openClassFile('/data/local/tmp/fastjson.dex').load();
  var JSONObject = Java.use('com.alibaba.fastjson.JSONObject')
  
  var i = a.i;
  i.implementation = function (finderItem) {

    var res = i.call(this, finderItem);

    try{
        send(JSONObject.toJSONString(res));
    }catch (e){
        console.log('Error: ' + e);
    }
    return res;
  };

});
"""

process = frida.get_usb_device().attach('com.tencent.mm')
script = process.create_script(jscode)
script.on('message', on_message)
print('[*] Running Hook')
script.load()
sys.stdin.read()

得到的 json 如下:

{
    "commentCount": 62,
    "description": "人心换人心,谁都有底线!更多情感内容点击关注@疗伤情感 #晏子情感",
    "expectId": -4877419130498574272,
    "feedId": -4877419130498574272,
    "hasBgmInfo": false,
    "id": -4877419130498574272,
    "likeCount": 2951,
    "liveId": 0,
    "liveStatus": 0,
    "localId": 0,
    "longVideo": false,
    "mediaList": [
        {
            "NMD": false,
            "NMq": 0,
            "NMt": false,
            "NMv": 28000,
            "NMw": "http://wxapp.tc.qq.com/251/20350/stodownload?encfilekey=RBfjicXSHKCOONJnTbRmmlD8cOQPXE48ibNoPibrzxbICjN0mdDNRj71nM2TJfVYGhIxX0WCMf74biaftFqLW2zGtdmXTJ8wibeesB7n8Ntyu5uOic8136mUibM44fnge8amjDu6BLmlSE59icicdjIrI7KtpP8FxXibG6LwzicQMlmaVaAicD0&adaptivelytrans=0&bizid=1023&dotrans=0&hy=SH&idx=1&m=5190cada35800678ed0b1f917a2a32b5",
            "NMx": "&token=x5Y29zUxcibDjE9JYkmdS0shbZ7djRmsC99U0UibtNT1u9hlAxSyM01icQZXHkptzicv",
            "bitrate": 0,
            "coverUrl": "http://wxapp.tc.qq.com/251/20304/stodownload?filekey=30350201010421301f020200fb040253480410d8869ba74f63ba490ac4069f994280f202030180d8040d00000004627466730000000131&storeid=323032313034303531303235343030303064316634353761356264386134653730353566363430303030303066623030303034663530&adaptivelytrans=0&bizid=1023&dotrans=0&hy=SH&m=d8869ba74f63ba490ac4069f994280f2",
            "decodeKey": "2065249527",
            "fileSize": 18284035,
            "full_bitrate": 0,
            "full_file_size": 0,
            "full_height": 0,
            "full_width": 0,
            "height": 1264,
            "hot_flag": 0,
            "includeUnKnownField": false,
            "md5sum": "f9f9eceb-6982-4e4d-bfa6-8db01fa9b522",
            "mediaId": "a6b5fac7d510c532a4376934a31015a4",
            "mediaType": 4,
            "spec": [
                {
                    "MZJ": 3141665,
                    "Nbp": 637,
                    "efu": "xV0",
                    "includeUnKnownField": true,
                    "vKg": "h264"
                },
                {
                    "MZJ": 1646534,
                    "Nbp": 345,
                    "efu": "xV2",
                    "includeUnKnownField": true,
                    "vKg": "h265"
                },
                {
                    "MZJ": 1037647,
                    "Nbp": 226,
                    "efu": "xV4",
                    "includeUnKnownField": true,
                    "vKg": "h265"
                },
                {
                    "MZJ": 472808,
                    "Nbp": 109,
                    "efu": "xV8",
                    "includeUnKnownField": true,
                    "vKg": "h265"
                },
                {
                    "MZJ": 418280,
                    "Nbp": 97,
                    "efu": "xV9",
                    "includeUnKnownField": true,
                    "vKg": "h264"
                },
                {
                    "MZJ": 295747,
                    "Nbp": 66,
                    "efu": "xV10",
                    "includeUnKnownField": true,
                    "vKg": "h265"
                }
            ],
            "thumbUrl": "http://wxapp.tc.qq.com/251/20304/stodownload?encfilekey=RBfjicXSHKCOONJnTbRmmlD8cOQPXE48ib0TrgC9GMRrlchGCNdXCyD2Pu6YbIWudBh6BngIDXS3M8Y18doicwuaXmAiblJJG5s2ib1XR3KMredUlbax6ZQvhQ77Ntoekw0O4VfpbFuHrhjIyEDd78AKe5GGybePkVA1jP75HyKHEyNc&adaptivelytrans=0&bizid=1023&dotrans=0&hy=SH&idx=1&m=d8869ba74f63ba490ac4069f994280f2",
            "thumb_url_token": "&token=x5Y29zUxcibCadRELU5qibEtIicbNZqQqzxGicvUUexAbqribwZDAdDicOa5koiawnrKUtV",
            "url": "http://wxapp.tc.qq.com/251/20302/stodownload?encfilekey=G83YYE2iciaib491UK8yGibLXOdhNpDoPpG748uNIa5DNuyuonSofEYDt1yf8eDoibNty4U8UXvSG2micv4HaEUcErdibfTOiaKKalN8FrUibibrfVqnPOh8sZFWl5oDALZajdFJsTg7Sqd4bPIyWib5CehDW4NbxzzdLUpoDvYBVjDkfp9C7Q&adaptivelytrans=0&bizid=1023&dotrans=906&hy=SH&idx=1&m=6b3f33e50bfc1c5c5f6f6c14e06b7d03&scene=0",
            "url_token": "&token=AxricY7RBHdWhPYjkduXw4angAXxhu8UMIxGebhCliaYtTT7dCtwIxRibXyGodLxZxcPJYzq9CN5dU",
            "videoDuration": 28,
            "width": 1080
        }
    ],
    "mediaType": 4,
    "nickName": "疗伤情感",
    "onlineNum": 0,
    "rvFeedList": [],
    "sessionBuffer": "eyJzZXNzaW9uX2lkIjoic2lkXzIzNjY4ODU5NDVfMTYxNzc4MDIwOTk3NzYzNl8xNDk3MjAwODg4IiwicmVjb21tZW5kX3R5cGUiOjMsInJlY29tbWVuZF9zeXN0ZW0iOjIsInJlY29tbWVuZF93b3JkaW5nIjoiIiwiY3VyX2xpa2VfY291bnQiOjI5NTEsImN1cl9jb21tZW50X2NvdW50Ijo2MiwicmVjYWxsX3R5cGVzIjpbMTAxMTM1XSwiZGVsaXZlcnlfc2NlbmUiOjEzLCJkZWxpdmVyeV90aW1lIjoxNjE3NzgwMjEwLCJzZXRfY29uZGl0aW9uX2ZsYWciOjIsInRvdGFsX2ZyaWVuZF9saWtlX2NvdW50IjowLCJuZXdfZnJpZW5kX2xpa2VfY291bnQiOjAsInJlY2FsbF9pbmRleCI6WzBdLCJ0YWdfaWQiOiIwOyIsInJlcXVlc3RfaWQiOjE2MTc3ODAyMDg4MTc5MTMsIm1lZGlhX3R5cGUiOjQsInZpZF9sZW4iOjI4LCJjcmVhdGVfdGltZSI6MTYxNzU4OTU4NiwidGFiX3R5cGUiOjQsInJlY2FsbF9pbmZvIjpbeyJyZWNhbGxfdHlwZSI6MTAxMTM1LCJyZWNhbGxfc2NvcmUiOjAuNzI2MjMyOTQ1OTE5LCJyZWNhbGxfaW5kZXgiOjAsInJlcG9ydF9pbmZvIjoiNDA2XzEwMzVfMF8wXzEifV0sInJhbmtfc2NvcmUiOjEwNC40NzExNDU2Mywic2VjcmV0ZV9kYXRhIjoiQmdBQUFmMmliZTJFMXIrRFRHc2gwbzB6RGJVbnpvTkUwZDZncjliOVdKdyttakM2NkhnM3VXY0phOTg9IiwidGFiX3Nlc3Npb25faWQiOjE2MTc3ODAyMDg5MjE3ODQsImZyaWVuZF9saWtlZF9saXN0IjoiIiwiZGV2aWNlX3R5cGVfaWQiOjIsImRldmljZV9wbGF0Zm9ybSI6IlJlZG1pIDYiLCJkb3dubG9hZF9zcGVlZF9rYnBzIjoxMzE1OTUsIm5ldF90eXBlIjoxLCJ2aWRlb19pZCI6MCwiaXNfY2hpbGQiOnRydWUsInBhcmVudF9tZWRpYV90eXBlIjowLCJwYXJlbnRfaWQiOjAsImZlZWRfcG9zIjoyLCJwdWxsX3R5cGUiOjEsInBhZ2VfbnVtIjowLCJjbGllbnRfcmVwb3J0X2J1ZmYiOiJ7XCJzZXNzaW9uSWRcIjpcIjE0M18xNjE3NzEwNzYyNTY4IyQyXzE2MTc3MTA3NjEyNjgjXCJ9IiwiaXNfbGl2ZV9mZWVkIjowLCJpc19saXZlX2ZpbmRlcnVzZXIiOjAsImV4dF9mbGFnIjowLCJjb21tZW50X3NjZW5lIjoyMCwib2JqZWN0X2lkIjoxMzU2OTMyNDk0MzIxMDk3NzM0NCwiZmluZGVyX3VpbiI6MTMxMDQ4MDgwNjQ2MDA0ODYsInBvaW5hbWUiOiIiLCJjaXR5IjoiIiwiZ2VvaGFzaCI6MzM3NzY5OTcyMDUyNzg3Mn0=",
    "timestamps": 1617780210295,
    "urlValidDuration": 172800,
    "userName": "v2_060000231003b20faec8cae38f1dc5d5ce00e432b077b11d4f3ce9c011535e0fff9bba95dcc6@finder"
}

这里要小心,过长的 long 在转 json 的时候可能会丢失精度,如果发现这种情况要特殊处理下,把 long 转成 string 。

数据处理

视频问题

检查了下数据,发现通过 url+url_token 拼接的视频流虽然能下载,但是下载下来是加密后的,无法直接播放。

后来发现 url+thumb_url_token 是可以播放的,高兴了一段时间。但是4月26号左右微信好像做了什么操作,导致这个渠道不能播放了。经过简单分析后发现视频解码的流程是放在native方法中,一时半会难以破解,那就只能想办法下载缓存了。

(当然,这些加密算法在安全组的同学面前都不算问题,三下五除二就发现原来是某个比较小众的流式加密算法🤭)

文件追踪

利用 frida-trace 可以追踪app的系统调用,因此我们可以尝试看下 app 写文件的 open 方法。

$ frida-trace -U -i open com.tencent.mm

滑动下页面,我们发现了下面的日志:

看起来非常像是视频的缓存文件。打开一看,果然是。。。

那么,照着这个格式搜索下代码,稍微拼接下就能搞定这个缓存路径。

其他

调试过程中还发现一个查看当前调用堆栈的方法,可以辅助分析:

console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()))

总结

头一次使用 frida 还是很有新鲜感的,用 Python 向 Android 中插入调用 Java api 的 Javascript 代码。。。

参考资料

基于Frida的全平台逆向分析

APP逆向神器之Frida

基于TLS1.3的微信安全通信协议mmtls介绍