背景
之前的文章《优酷生成淘宝绑定二维码协议》里提到过,绑定业务的核心是调通两个 mtop 接口:
mtop.alibaba.ucc.taobao.apply.usertoken:用优酷 sid 换 userTokenmtop.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 的思路:
- Frida 注入优酷 App 进程
- 在 Java 层找到 mtop 的签名入口
- 用
rpc.exports把签名函数暴露出来 - 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-wuabuildRiskControlInfo(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 True2. 时间戳必须一致
x-t 请求头、签名入参里的 t、以及 data 里的时间戳要保持在同一秒,差太多会被网关直接拒绝。
3. 混淆类名随版本变化
s.f.c 这种混淆名只对应特定版本(本文基于 11.1.10),App 升级后需要重新定位。
总结
Frida RPC 方案的优点是不用还原算法、不怕 VMP,缺点也很明显:依赖一台常驻的 Root 设备或模拟器,App 不能升级,并发受限于单进程。
如果要做大规模并发,就得把 SO 库从 App 里抠出来,用 unidbg 在服务器上直接跑——这是下一篇的内容。