本次分析针对米游社 app2.13.1 版本

前面主要记录下思路,算法附在最后。

上次分析米游社 app 还是在半年前,没想到半年来 mihoyo 保护还是增强了不少,这次用了几个小时弄出来未免还有些取巧的意思,有机会之后还可以回来再仔细逆一下 so 层的算法。

java 层代码比较容易定位,搜索一下getds就能定位到了 (笑),最后跟踪到 native 方法 com.mihoyo.hyperion.net.aaaaa.b5555(),打算先看下传参和返回值,但是不知道为啥,用 Frida hook 之后会无响应然后挂掉,感觉是触发了某种反调试。

于是写了个 xp 模块来 hook,核心代码如下

Class class_aaaaa = XposedHelpers.findClass("com.mihoyo.hyperion.net.aaaaa", cl);
XposedHelpers.findAndHookMethod(class_aaaaa, "b5555", String.class, String.class, new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam b5555Param) throws Throwable {
        super.beforeHookedMethod(param);
        XposedBridge.log("com.mihoyo.hyperion.net.aaaaa.b5555");
        XposedBridge.log("参数:");
        XposedBridge.log(b5555Param.args[0].toString());
        XposedBridge.log(b5555Param.args[1].toString());
        XposedBridge.log("返回值:");
        XposedBridge.log(b5555Param.getResult().toString());
}
});

获取到的日志大致如下

[LSPosed-Bridge] com.mihoyo.hyperion.net.aaaaa.b5555
[LSPosed-Bridge] 参数:
[LSPosed-Bridge]
[LSPosed-Bridge] role_id=157521044&server=cn_gf01
[LSPosed-Bridge] 返回值:
[LSPosed-Bridge] 1632975852,130697,dc8b42723b6ce4b5628005283b8d30b0

通过抓包我们能发现这就是我们的 ds。
那么关键来到 so 层,分析这个 native 函数到底干了什么。不过这次我失策了,mihoyo 也不是吃素的,这个版本的 so 代码花指令和控制流混淆拉满,以我的水平折腾半天也还没有把函数还原出来分析。
不过以我之前的经验,这个 ds 是用一个固定的 salt 加上时间戳等变量算出来的,所以算完内存里面肯定有 salt 的值,所以用了个取巧的方法:使用 Frida 的 objection 工具把内存全部 dump 出来分析。

objection -g com.mihoyo.hyperion explore
> memory dump all from_base

dump 出来的内存用 010 editor 搜索下salt=,果然能找到。

稍加验证可知,ds 的第三个值即为对此字符串进行 md5 得到的。根据我们的经验,salt 不变,t 为时间戳,r 为随机数。
奇怪的是这一次请求的参数貌似没有参与到 md5 之中去,不过我测试确实是正常使用的,暂且不表。
示例 python 代码 (未验证 header 哪些必须):

import requests
import time
import hashlib

url = "https://bbs-api.mihoyo.com/game_record/card/api/getGameRecordCard?uid=210749580"

def get_ds():
    salt = "6zT9berkIjLBimVKLeQiyYCN0tatGDpP"
    t = int(time.time())
    r = "233233" # 任意六位随机字符
    ds_str = f"salt={salt}&t={t}&r={r}"
    ds = hashlib.md5(ds_str.encode(encoding='UTF-8')).hexdigest()
    return f"{t},{r},{ds}"

headers = {
    "DS": get_ds(),
    "cookie": "", # put your own cookie here
    "x-rpc-client_type": "2",
    "x-rpc-app_version": "2.13.1",
    "x-rpc-sys_version": "11",
    "x-rpc-channel": "xiaomi",
    "x-rpc-device_id": "", # put your own device_id here
    "x-rpc-device_name": "Xiaomi M2007J3SC",
    "x-rpc-device_model": "M2007J3SC",
    "Referer": "https://app.mihoyo.com",
    "Host": "bbs-api.mihoyo.com",
    "User-Agent": "okhttp/4.8.0"
}
res = requests.get(url=url, headers=headers)
print(res.json())

成功截图 (该用户系从社区随机选择):

这个方法取巧是取巧,万一 mihoyo 换了算法就寄了,所以还是得研究下 so 层才行……