问题的本质:anti_content 不是签名,是"答卷"
很多人把拼多多的 anti_content 当成普通接口签名来逆,这是方向性错误。普通签名回答的是"这个请求有没有被篡改",而 anti_content 回答的是"你这个客户端是不是一个真实的、可信的浏览器"——它是一份环境答卷,里面塞满了设备指纹、行为特征、运行时自检结果。
抓包能看到它长这样:0an 开头的一长串编码,每次请求都不同,长度随环境信息浮动。web 端由风控 SDK(riskControl 相关 chunk)生成,App 端则是 native 层(安全 SDK 的 SO)产出,两端算法同宗但密钥和采集项不同。本文讲 web 端。
第一步:别搜字符串,hook 请求
直接在 Sources 里全局搜 anti_content 会淹没在几千个结果里——它作为参数名出现在每个请求封装层。正确的定位姿势是从请求出口往回追:
// hook fetch,拼多多 H5 主要走 fetch
const _fetch = window.fetch;
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : input.url;
if (url.includes('anti_content=')) {
// 断在这里,然后看右侧 Call Stack
debugger;
}
return _fetch.apply(this, arguments);
};断下后沿着调用栈往上翻,会经过业务层 → 请求封装层 → 风控层,最终落在一个特征很明显的函数上:入参是 url 和参数对象,内部大量访问 navigator、screen、document.createElement('canvas'),返回一个 Promise。这就是生成器本体。
第二步:看懂它在采集什么
跟进去之后别急着读混淆代码,先开 Console 观察它访问了哪些属性。实测 web 端采集清单(不同版本有增减):
- 基础环境:ua、platform、language、时区、屏幕分辨率、色彩深度、devicePixelRatio
- 指纹三件套:canvas 指纹(绘制特定文本+渐变后 toDataURL 取 hash)、webgl 指纹(vendor/renderer/扩展列表)、音频指纹(AudioContext 振荡器渲染结果的特征值)
- 运行时自检:
navigator.webdriver、是否有 PhantomJS/Selenium 特征函数、Function.prototype.toString是否被 hook(它会调用几个原生函数的 toString 验证没被改写) - 行为数据:页面停留时长、鼠标移动事件的采样统计(所以纯协议模拟没有行为数据,这一项是空的,本身就是特征)
最后把这些揉进一个结构体,加上时间戳和请求参数摘要,走"自定义加密 + 换表 base64"输出。
第三步:识别那个"换表 base64"
解码 anti_content 时标准 base64 解出来是乱码,因为索引表被置换过。识别和还原方法:
import base64
STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
# 从 SDK 里抠出来的置换表(示例结构,实际表以你抓的版本为准)
CUSTOM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
def custom_b64decode(s, table):
trans = str.maketrans(table, STD)
return base64.b64decode(s.translate(trans))置换表怎么拿?在 SDK 里搜长度为 64 的字符串字面量,或者更简单:在生成函数出口下断点,喂已知明文(比如全零环境),对比输入输出反推表。加密层在 base64 之前,是一个 TEA 变种——特征是多轮 <<4/>>5 位移和 0x9E3779B9 黄金分割常量的加减,看到这个常量基本就能确认是 TEA 家族。
第四步:工程落地的两条路
路线 A:整段抠出 + 补环境。把风控 chunk 完整保存(不要格式化,格式化会改变某些字符串解密的执行时序),在 Node 里补 window/navigator/document/screen/canvas。最大的坑是 canvas 和 webgl:Node 里没有真实现,要么用 node-canvas + headless-gl,要么从真实浏览器采集固定值拦截返回。后者更稳——风控校验的是"指纹与 ua 的一致性",不是指纹本身的多样性,所以一组真实采集的固定指纹配对应 ua,长期可用。
路线 B:浏览器 RPC。用 undetected-chromedriver 或 Camoufox 起真实浏览器,页面里注入脚本把生成函数挂到 window,再通过 CDP 的 Runtime.evaluate 对外提供签名服务。成本高但通过率天花板也最高,适合对稳定性要求极高的场景。
// 注入后暴露给外部的签名服务
window.__sign = function(url, params) {
return window.RiskControl.getAntiContent(url, params); // 函数名以实际为准
};精髓:三个容易踩死的坑
- webdriver 检测不是改一个属性就完事。它会在不同时机多次读取
navigator.webdriver,用Object.defineProperty改成 false 之后,还要防它通过Object.getOwnPropertyDescriptor验证属性描述符——补环境时 enumerable/configurable 都要和真机一致。 - 时间戳一致性。anti_content 内部的时间戳、请求参数里的时间戳、服务器收到请求的时间,三者偏差有容忍窗口。代理链路延迟大时,宁可提前生成也不要用过期的。
- 指纹别乱随机。canvas 指纹是显卡渲染结果,NVIDIA 和 Apple Silicon 渲染同一串文本结果不同。随机指纹 + 固定 ua = 自相矛盾的环境答卷,比不填还糟。
总结
- anti_content 是环境答卷不是签名,逆向重点是"采集了什么"而非"怎么加密"
- 定位用 hook fetch/XHR 回溯调用栈,别搜字符串
- 换表 base64 + TEA 变种是外壳,
0x9E3779B9常量是识别路标 - 落地优先"整段抠出 + 固定真实指纹",指纹与 ua 必须成套