-
-
[原创]MyToken(mytokencap)code/timestamp 参数 JS 逆向实战(补环境 + execjs 还原 MD5 签名)
-
发表于: 2天前 294
-
MyToken(mytokencap)code/timestamp 参数 JS 逆向实战(补环境 + execjs 还原 MD5 签名)
本文是「JS 逆向实战 30 例」系列第 2 篇,项目源码:847K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6K6j5$3!0G2L8o6p5I4x3W2)9J5c8X3!0H3k6h3&6v1f1#2u0W2N6X3g2J5M7$3f1`. (
Project2)
一、目标与背景
MyToken(mytokencap.com)是一个加密货币行情/市值榜站,我要抓它的币种排行榜列表接口。
- 站点:
849K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2E0P5i4c8G2K9$3g2F1j5$3q4H3i4K6u0W2j5$3!0E0i4K6u0r3 - 目标接口:
405K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2E0P5i4c8G2K9$3g2F1i4K6u0W2K9h3&6X3L8#2)9J5c8Y4c8A6j5$3E0W2M7W2)9J5c8X3y4#2M7Y4u0W2L8X3y4&6M7X3q4F1K9$3I4A6M7%4b7`. - 需还原字段:
code、timestamp两个 query 参数 - 难度:⭐⭐⭐(三星。签名算法本身很清晰——MD5,但要先把被 webpack 打包 + 混淆的导出函数补环境跑起来)
这个项目的逆向点同样是请求签名,但比七麦那次温和得多:JS 是 webpack 产物,函数名被混淆成 Fk / xZ 这种两字母,但逻辑一旦理顺就只是一次 MD5。我的做法依旧是「补环境 + 执行导出函数」,只是这次我把签名公式也读出来验证了一遍。
二、抓包分析
打开 2deK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2E0P5i4c8G2K9$3g2F1j5$3q4H3i4K6u0W2j5$3!0E0i4K6u0r3,F12 → Network → 筛 Fetch/XHR,翻一下排行榜,就能看到对 api.mytoken.info/ticker/currencyranklist 的请求。
看 Payload(Query String):
pages=2,1
sizes=100,100
subject=market_cap
language=zh_CN
legal_currency=USD
code=xxxxxxxxxxxxxxxxxxxxxxxx
timestamp=1783931313912
platform=web_pc
v=0.1.0
mytoken=
除了 code 和 timestamp,其它参数(pages / sizes / subject …)都是明文业务参数。timestamp 一看就是毫秒时间戳,会随时间变化;code 是一串十六进制,跟着 timestamp 一起变,明显是由 timestamp 派生出来的签名。
【图1:Network 面板里 currencyranklist 请求的 Query String —— 建议截图】
结论:要还原的是 code = f(timestamp) 这个派生关系,请求其它部分照抄即可。
三、定位加密入口
在 Sources 面板全局搜参数名 code / timestamp,或者搜接口路径 currencyranklist,会落到站点的主 JS 上(webpack 打包)。因为被 webpack 分模块,我用的办法是:搜 timestamp: 这种对象字面量,直接定位到构造签名对象的地方。
在打包代码里我找到了关键几行:
// 取自仓库真实 js:Project2/mytoken/mytoken1.js
r.d(t,{rQ:function(){return b}, ..., xZ:function(){return k}, Fk:function(){return w}});
var e=r(47568), o=r(828), i=r(97582), u=r(2568), a=r.n(u), c=r(35487), f=r.n(c), l=r(59130),
s=r(26042), v=r(94786),
p=Date.now().toString(),
d={timestamp:p, code:v.V8.hashStr(p+"9527"+p.substring(0,6)), platform:"web_pc", v:"0.1.0"},
这一行信息量极大:
p = Date.now().toString()—— 当前毫秒时间戳,就是 query 里的timestamp;code = v.V8.hashStr(p + "9527" + p.substring(0,6))—— 签名算法一目了然:把 时间戳 + 固定盐 "9527" + 时间戳前 6 位 拼起来,做一次hashStr;platform: "web_pc"、v: "0.1.0"是写死的常量。
v = r(94786) 是 webpack 的模块编号,模块 94786 里导出的是 V8 这个类。Fk(即 w)是负责把参数对象拼成 query 字符串的函数。
【图2:Sources 面板里 code/timestamp 构造处 / hashStr 调用栈 —— 建议截图】
四、JS 还原与补环境
先确认 V8.hashStr 到底是什么哈希。扒到模块 94786:
// 取自仓库真实 js:Project2/mytoken/mytoken1.js(模块 94786)
94786: function(t, e, r) {
"use strict";
r.d(e, {
V8: function() {
return s
}
});
class s {
constructor() {
this._dataLength = 0,
this._bufferLength = 0,
this._state = new Int32Array(4),
this._buffer = new ArrayBuffer(68),
this._buffer8 = new Uint8Array(this._buffer,0,68),
this._buffer32 = new Uint32Array(this._buffer,0,17),
this.start()
}
static hashStr(t, e=!1) {
return this.onePassHasher.start().appendStr(t).end(e)
}
...
static _md5cycle(t, e) {
let r = t[0]
, s = t[1]
, n = t[2]
, i = t[3];
r += (s & n | ~s & i) + e[0] - 680876936 | 0,
r = (r << 7 | r >>> 25) + s | 0,
i += (r & s | ~r & n) + e[1] - 389564586 | 0,
...
看到 _md5cycle 和那串经典常数(-680876936、-389564586、606105819…)就能确认:V8 其实是一份增量式 MD5 实现(128 位、4 个 32 位状态字,_buffer 68 字节 = 64 字节分组 + 4 字节长度)。所以
code = MD5(timestamp + "9527" + timestamp.substring(0, 6))
理解了算法,但要落成一个 query,还得看 Fk(w)怎么把对象拼成串:
// 取自仓库真实 js:Project2/mytoken/mytoken1.js(Fk 即拼接函数,节选)
w = function(n){
var t = new URLSearchParams, r = !0, e = !1, i = void 0;
try {
for (var u, a = Object.entries(n)[Symbol.iterator](); !(r = (u = a.next...
它就是用 URLSearchParams 把对象按 key 拼成 key=value&...。最后是导出给外部调用的入口 get_code:
// 取自仓库真实 js:Project2/mytoken/mytoken1.js(文件尾部)
function get_code(page){
a = BB(26042)
s = BB(88077)
n = {
"pages": page + ",1", // 参数改变
"sizes": "100,100",
"subject": "market_cap",
"language": "zh_CN",
"legal_currency": "USD"
}
t = (0,
s.Fk)((0,
a.Z)({}, n, (0,
s.xZ)(null === n || void 0 === n ? void 0 : n.mytoken)))
var t_s = t.toString();
console.log(t_s)
var result = {}
var timestamp = t_s.split("timestamp=")[1].split("&")[0]
var code = t_s.split("code=")[1].split("×tamp")[0]
result["timestamp"] = timestamp
result["code"] = code
return result
}
console.log(get_code("3").timestamp)
console.log(get_code("3").code)
这里 BB(26042) / BB(88077) 是 webpack 的 __webpack_require__,分别取到工具模块和「含 Fk/xZ 的签名模块」。get_code 把业务参数和签名对象(d)合并后,交给 Fk 拼成完整 query,再用字符串切分把 timestamp 和 code 抠出来返回。
补环境成本几乎为零:这段代码不依赖 document / window 的任何东西(时间戳来自 Date.now()),所以只要 Node 能 require 到打包依赖即可,execjs 直接跑通。
五、Python 实现
仓库里的 py/mytoken1.py:读 JS → compile → 调 get_code(page) → 取 code / timestamp 填进 query:
# 取自仓库真实 py:Project2/mytoken/mytoken1.py
import requests
import execjs
def Spider_mytoken(page:int):
page_i = str(page)
with open("mytoken1.js",encoding="utf-8") as f:
js_code = f.read()
js = execjs.compile(js_code)
arg = js.call("get_code",page_i)
code = arg["code"]
timestamp = arg["timestamp"]
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"dnt": "1",
"origin": "a7cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2E0P5i4c8G2K9$3g2F1j5$3q4H3i4K6u0W2j5$3!0E0",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "cbfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2E0P5i4c8G2K9$3g2F1j5$3q4H3i4K6u0W2j5$3!0E0i4K6u0r3",
"sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Google Chrome\";v=\"138\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"sec-gpc": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
}
url = "da2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2E0P5i4c8G2K9$3g2F1i4K6u0W2K9h3&6X3L8#2)9J5c8Y4c8A6j5$3E0W2M7W2)9J5c8X3y4#2M7Y4u0W2L8X3y4&6M7X3q4F1K9$3I4A6M7%4b7`."
params = {
"pages": "2,1",
"sizes": "100,100",
"subject": "market_cap",
"language": "zh_CN",
"legal_currency": "USD",
"code": code,
"timestamp": timestamp,
"platform": "web_pc",
"v": "0.1.0",
"mytoken": ""
}
print(params)
response = requests.get(url, headers=headers, params=params)
print(response.text)
print(response)
Spider_mytoken(4)
要点:
get_code接收的是字符串页码(str(page)),因为它内部会做page + ",1"的字符串拼接;- 返回的
arg是个字典,直接取arg["code"]/arg["timestamp"]; - 其余参数(
subject/language/legal_currency…)和浏览器保持一致。
六、运行结果
执行 python mytoken1.py,终端先打印本次的 params,能看到 code / timestamp 每次都不同,随后是接口返回的币种列表:
{'pages': '2,1', 'sizes': '100,100', 'subject': 'market_cap', 'language': 'zh_CN', 'legal_currency': 'USD', 'code': '3f2a...e1', 'timestamp': '1783931313912', 'platform': 'web_pc', 'v': '0.1.0', 'mytoken': ''}
{"data":{"tickerList":[{"symbol":"BTC","price_usd":...,"market_cap_usd":...}, ...]},"code":0}
<Response [200]>
只要列表能正常返回、字段完整,签名链路就算打通。
【图3:终端打印出的币种排行榜数据 —— 建议截图】
七、踩坑与注意
页码必须传字符串。
get_code里是page + ",1"的字符串拼接,传数字(如4)在execjs里行为可能和预期不一致;仓库统一用str(page)。timestamp用「毫秒字符串」。Date.now().toString()是毫秒级、字符串形态,p.substring(0, 6)取的是毫秒时间戳的前 6 位数字。别写成秒级或数字类型。固定盐
"9527"别改。 这是写死在打包代码里的常量,改了签名就错。hashStr不是普通md5(...)的十六进制小写。 它是增量式 MD5,输出可能是大写/带 seed 的变体(hashStr(t, e=!1)的第二个参数)。最稳的仍是直接调原函数,不要自作主张用别的 MD5 库顶替。webpack 模块编号会变。 站点重新打包后
BB(88077)这类编号可能失效,届时重新用搜索定位Fk/hashStr即可。低频请求。 行情接口数据更新快,但仍要控制频率,遵守站点条款。
八、总结
code = MD5(timestamp + "9527" + timestamp.substring(0,6))这类「固定盐 + 时间戳」是最常见的签名形态,先搜timestamp字面量往往一击命中。- 判断 JS 哈希库看常数即可:出现
_md5cycle/-680876936就是 MD5,别的库同理(SHA 系则是1779033703一类初值)。 - 能跑原函数就别复现算法:签名由时间戳驱动,
execjs每次现算,比自己用 Python 复现哈希更稳。
免责声明:本文仅供网络安全技术学习交流,请勿用于任何商业或非法用途。
赞赏
- [原创]山西省政府采购电子卖场 nsssjss/sign 请求头签名逆向实战(JS 逆向 / RSA / 嵌套哈希 / execjs) 0
- [原创]36Kr 登录接口 sign 签名 + 密码 RSA 加密逆向实战(JS 逆向 / MD5 签名 / JSEncrypt / execjs) 0
- [原创]五矿电商 ec.minmetals 请求 param 参数 RSA 长加密逆向实战(JS 逆向 / JSEncrypt / 补环境 / subprocess node) 251
- [原创]中国观鸟记录中心 birdreport 请求 RSA 加密 + 响应 AES 解密逆向实战(JS 逆向 / JSEncrypt / 补环境 / execjs) 277
- [原创][求助]数位观察 swguancha 响应 AES 解密逆向实战(JS 逆向 / 补环境 / execjs) 446