接口 sign 逆向:改了请求体就报签名错误,怎么按固定顺序还原算法
这类操作只适合自有/自研接口联调,或已获授权的安全评估。爬公开数据前先看站点协议和 robots。下面按一条固定路径走:抓包定锚点 → 断点/Hook 回溯 → 扣代码还原 → 复现验证 → 常见卡点。
抓包定锚点:先确认 sign 是否值得追
Chrome DevTools Network 按 XHR 过滤,找真正带签名的接口。例:某东 api.m.jd.com/client.action,body 里 sign 缺失直接 403;某招聘站 /api/v1/job/search 超时返回:
{"code":401,"msg":"invalid sign"}
判断锚点看三条:是否随每次请求变化;缺失或改动是否直接报错;是否与请求体强相关。不要只认字段名,淘宝可能是 _tb_token_,某夕是 __aes__,拼多多是 sign_v2。
32 位小写 hex 不等于 MD5。反复点同一商品,sign 每次都变,说明掺了 timestamp、nonce、Math.random()、performance.now()、navigator.plugins 哈希这类非确定性因子。遇到非对称或带盐哈希,先把参数固定,再逐步放开一个变量试。
弱凭据先试:base64、可直接解的 MD5,删除或置空 sign 看能不能绕过。不行再动反编译/扣代码。
断点/Hook 回溯:让请求自己停下
Sources 里对接口 URL 关键字加 XHR/fetch 断点(Break on XHR/fetch),请求发出时暂停,Call Stack 向上回溯到第一个自定义函数。找不到就顺栈上翻,直到出现第一个非原生函数。Ctrl+Shift+F 全局搜 "sign"、sign:、md5、sha256、hmac、encrypt、getSign。Initiator 也能回溯调用链。
Hook 比断点快,重写 XMLHttpRequest.prototype.send 或 fetch,打印入参并埋 debugger:
const _send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
console.log('xhr body', body);
debugger;
return _send.apply(this, arguments);
};
btoa/atob 同样可以 Hook。
站点用 fetch polyfill 时,Break on XHR 可能打不进。改到 Network → All,筛 js 按 Size 倒序,找约 1.2MB 的 main.xxxx.js 再下断点。有些 sign 只在用户点击时生成,比如“立即购买”的 __aes__,用 Elements → Event Listeners → click → Break on EventListener。
WASM 情况:签名在 .wasm 里。wasm 是 fetch 异步加载,类似 fetch('xxx.wasm').then(w=>WebAssembly.instantiate(w)),同步直接调用会取不到,需要放在异步回调或延时后调用。可以拦 .wasm 请求,用 wabt 反编译成 WAT,搜导出函数名。
扣代码还原:不读混淆,只截依赖
混淆常见 _0x1a2b['c3'] 数组索引。先在 Console 打印真实字符串:
var _0x1a2b=["sign","generateSign","getTimestamp","getRandomString"];
console.log(_0x1a2b['c3']); // "generateSign"
直接全文搜 sign 常无效,优先看按体积排序的 main.xxxx.js。
扣代码思路:不读混淆代码,只截取签名函数及其依赖,本地/node 跑,按报错补全 undefined 依赖,直到不报错。注意保留全局变量,例如汽车之家 login.js 的 hex_md5(s),扣 core_md5、binl2hex、str2binl 时保留 chrsz。Python execjs 可调 ctx.call('hex_md5','123456') 得 d6d7d5a8c4e6e7b8。
常见签名构造清单:加固定参数(timestamp/nonce/appVersion/platform) → 按键名字母排序 → 拼 key=value&... → 首尾或中间拼接固定 secret/salt → 做 MD5/HMAC-SHA256/SHA256 → 常见再 .toUpperCase()。
腾讯云案例里,body 的 sign 来自:
a = (0, d.generateSign)(i, "yd9349");
generateSign: function(e,t){
var a = Object.keys(e).sort().map(function(t){
return "".concat(t,"=").concat(e[t])
}).join("&");
return console.log("sortedParams",a), s.HmacSHA256(a,t).toString();
}
"yd9349" 就是密钥,(0, d.generateSign) 是逗号表达式,只为剥离 this。类似地,Object.keys(e).sort().map(k=>\${k}=${e[k]}`).join('&')后接 CryptoJSHmacSHA256(a, t).toString()` 是高频结构。
另一个微信小程序案例:
function n(e){var n=r1(e);return n.length>0?sha1(n):""}
删除 t.sign;收集所有 key;sort() 排序;拼接 "&"+key+"="+value,值为对象先 JSON.stringify;过滤 null/空串;最后 sha1(n)。
招聘站案例中,真实代码被拆成多层 IIFE,盐值动态生成(getRandomString(8)+getTimestamp().toString().slice(-4))。提取到:
generateSign = function(a,b){
var s = a + b + 'salt_2024_q2';
return CryptoJS['SHA256'](s)['toString'](CryptoJS['enc']['Hex']);
}
小程序 wxapkg:反编译前等功能点全,确保所有包都跑过;有的包加密,先 Unpacker 解密再 wxapkgconvertor。全局搜 MD5 找到 encryptSign,回溯 gatewayRequest → generateRequest。参数含 method/app_id/token_type/nonce_str/version/token/biz_content,特殊字符 ihos-xxx-8;按字母升序排列后 MD5 再转大写。
WASM 扣代码时,用 fs 读 wasm_code,定义 importObject,按报错补全 A/l/_ 等依赖。酷某音乐案例:signature 是 url 参数与 data 拼接,数组头尾各插入固定字符串,join 后 MD5;mid/params 用 AES-CBC,key 随机 16 字符,md5(key) 取前 32 作 key,iv 取后 16 字符,Pkcs7;pk 用 RSA(指数 10001,NoPadding)加密 key;sid/edt 需要 new wasm_bindgen.EData → t.get_sid()/get_edt()。某久批 x-sign 案例里,断点看 X-Sign 赋值位置 W,参数 y 是时间戳,g 是 POST + url + 时间戳 + ... 空格拼接。
复现验证:值一致才算还原
把扣出的 JS 交给 Node/Python 调用。Python 不必重写算法,用 node 调 js:
cmd = 'node -e "require(\\"%s\\").init(%s)"' % ('./test', params)
也可以用 execjs。抓包参数如 timestamp、sign=049da2ed71e3fa2f90b02d00b11f7f35e6190379,填入后输出一致才算还原成功。单次:
node -e 'require("./test").init(params)'
在线调试可用 jsrun。环境检测复杂时,用 Sekiro RPC 桥接浏览器执行;Chrome Overrides 可持久化改后的 JS。Burpy 用来打通 Burp 与 Python。另一个可参考的逆向 Skill 是 camoufox-reverse 逆向 Skill。
常见卡点
- 反调试
debugger:先条件断点或 Overrides 改掉循环 debugger。 - 字段名伪装:sign 可能叫
_tb_token_、__aes__、sign_v2,按“变化+报错+绑定 body”找。 - 非确定性因子:sign 每次变不一定是 MD5,先固定 timestamp/nonce 再放开。
- 异步 WASM:
WebAssembly.instantiate异步,同步调用取不到;放setTimeout/异步回调里。 - 全局变量缺失:扣代码后报
undefined,补chrsz、window、navigator、document等依赖,直到不报错。 - 请求库差异:fetch polyfill 会让 Break on XHR 失效,改按体积找大 JS。
- 点击才生成:对按钮 click 下 Event Listener 断点。
- 参数过滤:null/空串、对象
JSON.stringify、大小写、编码方式,都会让本地值对不上。
站点把签名搬进 WASM 或 JSVMP 后,纯扣代码成本上升,是否改走浏览器 RPC,让浏览器算 sign、Python 只做请求编排?