百趣云 百趣云的博客

Frida RPC 实战:还原优酷 mtop x-sign 签名体系

背景

之前的文章《优酷生成淘宝绑定二维码协议》里提到过,绑定业务的核心是调通两个 mtop 接口:

  • mtop.alibaba.ucc.taobao.apply.usertoken:用优酷 sid 换 userToken
  • mtop.alibaba.ucc.getlocalsiteauthurl:用 userToken 换 request_token,生成绑定二维码

这两个接口挂在阿里 mtop 网关(acs.youku.com / acs.m.taobao.com)下面,请求头里必须带齐签名四件套:

  • x-sign:请求签名,对 data、时间戳、appKey 等计算得出
  • x-sgext:签名扩展信息
  • x-mini-wua:简化版 wua 风控参数
  • x-umt:设备指纹 token

这四个值全部由 App 内置的阿里 SecurityGuard 安全库(libsgmain.so)在 Native 层生成,还带 VMP 保护,纯算法还原的成本极高。

思路:不逆算法,直接 RPC

既然签名函数就在 App 里,最省事的办法不是还原它,而是直接调用它——这就是 Frida RPC 的思路:

  1. Frida 注入优酷 App 进程
  2. 在 Java 层找到 mtop 的签名入口
  3. rpc.exports 把签名函数暴露出来
  4. Python 端通过 RPC 调用拿到签名,再组装请求发出去

相当于"借鸡生蛋":签名让 App 自己算,我们只负责喂参数、收结果。

定位签名入口

优酷 Android 端经过混淆,mtop 签名相关的类都被混淆成了短名。通过抓包和反编译对比,定位到这几个关键类:

  • s.d.e.a:MtopConfig,承载 mtop 配置
  • s.f.c:签名核心类,它的 c() 方法接收参数 Map,返回包含 x-sign 等字段的签名结果
  • com.ali.user.open.core.util.RiskControlInfoContext:阿里百川的风控上下文,生成 umidToken 等风控信息

hook.js:注入与导出

function main(data, timestime, api, sid1) {
    var result = null;
    var result1 = null;
    Java.perform(function () {
        var MtopConfig = Java.use("s.d.e.a").$new("INNER");
        var RYW = Java.use('s.f.c').$new();
        var ryw1 = Java.use("com.ali.user.open.core.util.RiskControlInfoContext").$new();
        RYW.q(MtopConfig);

        var HashMap1 = Java.use('java.util.HashMap').$new();
        HashMap1.put("data", data);
        HashMap1.put("sid", sid1);
        HashMap1.put("x-features", "27");
        HashMap1.put("appKey", "23570660");
        HashMap1.put("api", api);
        HashMap1.put("t", timestime);
        HashMap1.put("v", "1.0");

        var HashMap2 = Java.use('java.util.HashMap').$new();
        HashMap2.put("pageId", "");
        HashMap2.put("pageName", "");

        result = RYW["c"](HashMap1, HashMap2, "23570660", "null", false);
        result1 = ryw1['buildRiskControlInfo'](null);
    });
    return result + "===" + result1;
}

rpc.exports = { main };

几个关键点:

  • Java.use(...).$new() 直接在 App 进程里实例化混淆类,跑的就是 App 自己的代码
  • 第一个 HashMap 装的是签名原料:请求体 data、时间戳 t、appKey、接口名 api、会话 sid
  • RYW.c(...) 返回的字符串里就包含 x-sign、x-sgext、x-mini-wua
  • buildRiskControlInfo(null) 生成风控信息
  • 最后通过 rpc.exports 把函数暴露给 Python 调用

Python 端:RPC 调用 + Flask 封装

Python 端负责三件事:attach 进程、调用 RPC、发 HTTP 请求。

import frida

session = frida.get_remote_device().attach('优酷视频')
with open("hook.js", encoding='utf-8') as f:
    script = session.create_script(f.read())
script.on('message', lambda msg: print(msg))
script.load()

# 调用 JS 侧的 main(),拿到签名串
res = script.exports_sync.main(
    data, statime,
    'mtop.alibaba.ucc.taobao.apply.usertoken',
    sid1
)

返回的字符串用正则把四个签名字段抠出来:

reslist = res.split('===')
x_sgext = re.findall(r'x-sgext=(.*?),', reslist[0])
x_umt = re.findall(r'x-umt=(.*?),', reslist[0])
x_min_wua = re.findall(r'x-mini-wua=(.*?),', reslist[0])
x_sign = re.findall(r'x-sign=(.*?)}', reslist[0])

headers["x-sgext"] = urllib.parse.quote_plus(x_sgext[0])
headers["x-sign"] = urllib.parse.quote_plus(x_sign[0])
headers["x-umt"] = urllib.parse.quote_plus(x_umt[0])
headers["x-mini-wua"] = urllib.parse.quote_plus(x_min_wua[0])

注意:这四个值必须 URL 编码后再放进 header,否则里面的 +/ 会被错误解析,网关直接报签名错误。

最后套一层 Flask,把整套流程封装成 HTTP 接口:

@app.route('/api/getYoukuToken', methods=['POST'])
def api_my_function():
    data = json.loads(request.data)
    sid = urllib.parse.quote(data['sid'])
    sid1 = urllib.parse.unquote(sid)
    result = get_token(sid, sid1)
    return jsonify({'result': result})

这样任何语言、任何机器都能通过 HTTP 调用签名服务了。

踩坑记录

1. script has been destroyed

Frida 脚本在 App 崩溃或 frida-server 重连后会被销毁,必须做好检测和自动重载。我的做法是每次调用前先 ping 一下脚本,挂了就 detach、重新 attach、重新 load:

def ensure_script_active():
    global session, script
    try:
        if script is not None:
            script.exports_sync.ping()
            return True
    except Exception:
        pass
    if session is not None:
        try:
            session.detach()
        except Exception:
            pass
    session = frida.get_remote_device().attach('优酷视频')
    with open("hook.js", encoding='utf-8') as f:
        script = session.create_script(f.read())
    script.on('message', on_message)
    script.load()
    return True

2. 时间戳必须一致

x-t 请求头、签名入参里的 t、以及 data 里的时间戳要保持在同一秒,差太多会被网关直接拒绝。

3. 混淆类名随版本变化

s.f.c 这种混淆名只对应特定版本(本文基于 11.1.10),App 升级后需要重新定位。

总结

Frida RPC 方案的优点是不用还原算法、不怕 VMP,缺点也很明显:依赖一台常驻的 Root 设备或模拟器,App 不能升级,并发受限于单进程。

如果要做大规模并发,就得把 SO 库从 App 里抠出来,用 unidbg 在服务器上直接跑——这是下一篇的内容。

By 百趣云 阅读量:6 On