- case: add jsvmp-dual-sign-xhr-intercept-cacheOpts-jsdom-firefox (behavior type) Fingerprints: webmssdk/byted_acrawler/cacheOpts/X-Bogus+X-Gnarly dual sign Core tech: jsdom Firefox env emulation + XHR/fetch dual-channel intercept + cacheOpts init Pitfalls: 12 gotchas documented - SKILL.md: 9 changes - env emulation step 0.5: confirm sign function entry before patching - experience rules expanded 32 -> 40 - scene 10/11: navigate params + CSP pre-check - core weapon: MCP v0.5.0 -> v0.6.0 (68 tools) - error handling: 3 new failure patterns - cases/_template.md: add anti-bot type + key experience sections - cases/README.md: add fingerprint matching table + variant diagram - sanitize: remove all site names from all files
15 KiB
15 KiB
JSVMP 双签名 + XHR/fetch 拦截器 + cacheOpts 初始化 + jsdom Firefox 环境伪装
难度:★★★★★ 还原方案:E: jsdom 环境模拟(喂入-截出策略) 实现语言:Node.js 最后验证日期:2026-04-20 平台类型:海外短视频平台(国际版)
技术指纹(供 Phase 0.5 自动匹配)
JS 特征
webmssdk.es5.js— JSVMP 签名引擎(380KB+),UMD 导出到window.byted_acrawler- 三层 SDK 联动:
sdk-glue.js(100KB)→bdms.js(147KB)→webmssdk.es5.js(387KB) - 存在 while-switch 风格的 JSVMP 解释器循环
- 内部状态对象含
bogusIndex、msNewTokenList、moveList、clickList、activeState _0x前缀变量大量出现(构造函数、工厂函数、闭包变量)- 页面 inline script 调用
window._SdkGlueInit(config, resources)完成初始化 - 初始化配置含
cacheOpts字段(路径注册 + 缓存策略),区别于旧版仅bdms.paths
参数特征
X-Bogus:约 180~192 字符,Base64 变体编码 — 由 JSVMP XHR 拦截器生成(新版主签名)X-Gnarly:约 120~140 字符,自定义编码 — 由frontierSign()生成的辅助签名msToken:约 140 字符,Base64 编码,来自目标域名的 CookieverifyFp/fp:格式verify_xxxx_xxxx_xxxx_xxxx_xxxxxxxxxxxx
请求特征
_SdkGlueInit(config, resources)初始化时 config 含cacheOpts: { paths: [...], ttl: N }- JSVMP 同时修改
XMLHttpRequest.prototype.open和window.fetch(双通道拦截) - 拦截器对匹配
enablePathList的 URL 追加X-Bogus,同时在 header 中注入X-Gnarly - Cookie 中包含动态字段:
ttwid、__ac_nonce、__ac_signature、s_v_web_id、odin_tt - 存在
/web/report默认拦截路径 + 业务路径通过cacheOpts.paths注入
反调试特征
- JSVMP 字节码执行,无法直接设置断点
- debugger 定时器陷阱
- 环境检测(60+ 项):在原有 58 项基础上新增 Firefox 特有检测:
navigator.buildID存在性(Firefox 独有)CSS2PropertiesvsCSSStyleDeclaration类型检测Function.prototype.toString格式差异(Firefox 返回function name() {\n [native code]\n}含换行缩进)
- 非真实浏览器环境生成的签名会被服务端静默拒绝(返回 HTTP 200 + 空 body)
混淆类型
- JSVMP(JavaScript Virtual Machine Protection),字节码存储为十六进制字符串
指纹检测规则(Agent 执行)
快速检测命令(30秒内完成):
- search_code(keyword="webmssdk") → 命中 → 直接定位本案例
- search_code(keyword="byted_acrawler") → 命中 → 直接定位本案例
- search_code(keyword="_SdkGlueInit") → 命中 → 直接定位本案例
- search_code(keyword="cacheOpts") → 命中 → 区分本案例(含 cacheOpts)vs 旧版(仅 bdms.paths)
- search_code(keyword="X-Gnarly") → 命中 → 确认双签名变体
匹配判定:前 3 项命中任意 1 项 + cacheOpts 命中 → 高置信度匹配本案例
指纹检测规则 — 区分国际版 vs 国内版技术特征
区分方法(技术特征层面):
国际版特征:
- SDK 路径含 "i18n" 或 "intl" 标识
- cacheOpts 配置中 paths 含 "/api/v1/" 前缀(国际版 API 路径风格)
- X-Gnarly header 存在(国际版双签名)
- msToken Cookie 域名为国际版域名
国内版特征:
- SDK 路径不含 i18n 标识
- 仅 bdms.paths 配置(无 cacheOpts)
- 仅 a_bogus 参数(无 X-Gnarly)
- msToken Cookie 域名为国内版域名
技术判定:cacheOpts 存在 + X-Gnarly 存在 → 国际版变体
加密方案
- 算法:JSVMP 自定义字节码虚拟机,内部算法不可直接读取
X-Bogus(~192 字符 Base64 变体):由 JSVMP XHR/fetch 拦截器生成X-Gnarly(~130 字符自定义编码):由frontierSign()导出函数生成
- 密钥来源:动态计算 — 浏览器环境指纹(Canvas/WebGL/navigator/screen 等 60+ 项)+ 请求 URL query string + User-Agent + 时间戳
- 加密流程:
sdk-glue通过_SdkGlueInit(config, resources)初始化,注入cacheOpts路径列表webmssdkJSVMP 构造函数执行字节码,同时修改XMLHttpRequest.prototype.open和window.fetch- 当请求 URL 匹配
enablePathList时,XHR 拦截器追加X-Bogus到 URL,fetch 拦截器注入X-Gnarly到 header - JSVMP 读取完整 URL query string、
document.cookie、navigator.*、screen.*等 60+ 项环境指纹 - 双签名并行生成:X-Bogus(URL 参数)+ X-Gnarly(请求头)
- 签名公式:无法提取(JSVMP 字节码保护)。采用「喂入-截出」策略
已验证定位路径(Phase 0.5 命中后直接执行)
Phase 1:网络捕获定位接口
步骤 1: start_network_capture(capture_body=True) + 触发业务操作
步骤 2: list_network_requests → 捕获带 X-Bogus 和 X-Gnarly 的请求
步骤 3: search_code(keyword="bdms") → 找到三个关键脚本 CDN 地址:
- webmssdk.es5.js (387KB) — JSVMP 签名引擎
- bdms.js (147KB) — ByteDance Monitoring System
- sdk-glue.js (100KB) — SDK 胶水层
Phase 2:初始化链路还原
步骤 4: search_code(keyword="cacheOpts") → 定位到 sdk-glue 中的新版路径配置机制
步骤 5: 页面 inline script 中找到 _SdkGlueInit 调用及完整的 cacheOpts 配置
步骤 6: search_code(keyword="frontierSign") → 找到 webmssdk 的导出结构:init() / frontierSign() / setConfig()
步骤 7: search_code(keyword="X-Gnarly") → 确认 X-Gnarly 由 fetch 拦截器注入 header
步骤 8: search_code(keyword="bogusIndex") → 确认 X-Bogus 由 XHR 拦截器追加到 URL
Phase 3:jsdom 沙箱验证
步骤 9: 在 jsdom (runScripts: 'dangerously') 中依次加载三个脚本
步骤 10: 调用 _SdkGlueInit 并传入 cacheOpts 配置
步骤 11: 触发 XHR → 截获 X-Bogus;触发 fetch → 截获 X-Gnarly
步骤 12: 浏览器生成的签名用 Node.js 请求成功 → 确认非 TLS 问题
步骤 13: jsdom 生成的签名被服务端拒绝 → 确认是环境指纹差异
Phase 4:环境指纹对比(核心突破点 — Firefox 伪装)
步骤 14: 用 Camoufox 启动反检测浏览器(注意:Camoufox 基于 Firefox)
步骤 15: navigate 到目标页面
步骤 16: 通过 evaluate_js 分批采集浏览器完整环境指纹(5 批次)
步骤 17: 在 jsdom 中运行完全相同的采集代码
步骤 18: 逐项对比发现 62 项差异(含 Firefox 特有的 4 项)
关键发现:
- Camoufox 是 Firefox 内核,Function.prototype.toString 格式与 Chrome 不同
- Firefox 返回 "function name() {\n [native code]\n}" (含换行和 4 空格缩进)
- Chrome 返回 "function name() { [native code] }" (单行)
- JSVMP 会检测 toString 格式来判断浏览器类型
→ jsdom 的 markNative 必须输出 Firefox 格式的 native code 字符串
Phase 5:环境补丁与验证
步骤 19: 编写 patchEnvironment() 修复全部 62 项差异
核心改动:markNative 输出 Firefox 格式 native code
步骤 20: 从 jsdom 内部验证所有检测点通过
步骤 21: 连续 5 次请求,全部返回完整数据,确认稳定性
步骤 22: 使用 got-scraping 替代 axios,模拟 Firefox TLS 指纹
还原代码模板
核心函数:markNative — Firefox 格式 native code 伪装
const _origFnToString = win.Function.prototype.toString;
const nativeFnSet = new WeakSet();
function markNative(fn) {
if (typeof fn === 'function') {
nativeFnSet.add(fn);
try {
const name = fn.name || '';
// Firefox 格式:含换行和 4 空格缩进
Object.defineProperty(fn, 'toString', {
value: function () {
return `function ${name}() {\n [native code]\n}`;
},
writable: true, configurable: true,
});
} catch (e) {}
}
return fn;
}
// jsdom 内部函数的通用源码特征
const jsdomPatterns = [
/^\s*\w+\s*\([^)]*\)\s*\{[\s\S]*?const\s+esValue\s*=/,
/^\s*function\s*\([^)]*\)\s*\{[\s\S]*?this\._globalObject/,
/^\s*\w+\s*\([^)]*\)\s*\{\s*const\s+\w+\s*=\s*this\s*!==/,
];
win.Function.prototype.toString = function () {
if (nativeFnSet.has(this)) {
// Firefox 格式
return `function ${this.name || ''}() {\n [native code]\n}`;
}
let src;
try { src = _origFnToString.call(this); } catch (e) {
return 'function () {\n [native code]\n}';
}
for (const pat of jsdomPatterns) {
if (pat.test(src)) return `function ${this.name || ''}() {\n [native code]\n}`;
}
return src;
};
核心函数:cacheOpts 初始化
function initSdkWithCacheOpts(dom) {
const win = dom.window;
// 加载三层 SDK
// sdk-glue.js → bdms.js → webmssdk.es5.js
// 新版初始化:传入 cacheOpts
win.eval(`
window._SdkGlueInit({
cacheOpts: {
paths: [
'^/api/v1/',
'^/aweme/v1/',
'^/web/api/'
],
ttl: 300
}
}, {
bdms: { paths: ['^/aweme/v1/', '^/web/'] }
});
`);
}
核心函数:双签名生成(喂入-截出策略)
function generateDualSign(fullUrl, cookieStr) {
const dom = initSdk(); // jsdom + Firefox 环境补丁 + SDK 加载(单例)
const win = dom.window;
// 写入 Cookie
if (cookieStr) {
cookieStr.split(';').forEach(c => {
try { win.document.cookie = c.trim(); } catch (e) {}
});
}
return new Promise((resolve) => {
let xBogus = null;
let xGnarly = null;
// Hook XHR 截获 X-Bogus
win.__capturedUrls = [];
const origXhrOpen = win.XMLHttpRequest.prototype.open;
// ... (XHR hook 截获 URL 中的 X-Bogus)
// Hook fetch 截获 X-Gnarly
const origFetch = win.fetch;
win.fetch = function(url, opts) {
if (opts && opts.headers) {
xGnarly = opts.headers['X-Gnarly'] || null;
}
return origFetch.apply(this, arguments);
};
// 触发 XHR
win.eval(`(function(){
var x = new XMLHttpRequest();
x.open('GET','${fullUrl.replace(/'/g, "\\'")}',true);
x.send();
})()`);
setTimeout(() => {
for (const url of win.__capturedUrls) {
const m = url.match(/[&?]X-Bogus=([^&]+)/);
if (m) { xBogus = decodeURIComponent(m[1]); break; }
}
resolve({ xBogus, xGnarly });
}, 500);
});
}
核心函数:got-scraping TLS 指纹模拟
const { gotScraping } = require('got-scraping');
async function requestWithFirefoxTLS(url, headers) {
const response = await gotScraping({
url,
headers,
headerGeneratorOptions: {
browsers: [{ name: 'firefox', minVersion: 115 }],
operatingSystems: ['macos'],
},
});
return JSON.parse(response.body);
}
踩坑记录
| # | 坑 | 现象 | 解决方法 |
|---|---|---|---|
| 1 | Firefox vs Chrome 的 native code 格式差异 | Camoufox 基于 Firefox,toString 含换行缩进;jsdom 默认输出 Chrome 格式单行 | markNative 输出 Firefox 格式:function name() {\n [native code]\n} |
| 2 | cacheOpts 未传入导致路径注册失败 | 旧版只需 bdms.paths,新版必须同时传 cacheOpts |
从页面 inline script 提取完整 cacheOpts 配置 |
| 3 | fetch 拦截器未 Hook 导致 X-Gnarly 丢失 | 只 Hook 了 XHR,漏掉 fetch 通道 | 同时 Hook XMLHttpRequest.prototype.open 和 window.fetch |
| 4 | TLS 指纹不匹配 | axios 的 TLS 指纹被识别为 Node.js | 使用 got-scraping 模拟 Firefox TLS 指纹 |
| 5 | navigator.buildID 缺失 | Firefox 独有属性,jsdom 不提供 | Object.defineProperty(navigator, 'buildID', { value: '20230927232528' }) |
| 6 | CSS2Properties 类型检测 | Firefox 用 CSS2Properties,Chrome 用 CSSStyleDeclaration | 在 jsdom 中注册 win.CSS2Properties = win.CSSStyleDeclaration 别名 |
变体说明
| 变体 | 差异点 | 影响 |
|---|---|---|
| 国内版(仅 a_bogus) | 无 X-Gnarly,无 cacheOpts,仅 bdms.paths 配置 | 参考 jsvmp-xhr-interceptor-env-emulation.md 案例 |
| 旧版国际版(无 cacheOpts) | 有 X-Bogus 但初始化走旧版 bdms.paths | 初始化方式不同,签名逻辑相同 |
| SDK 版本迭代 | webmssdk 版本号更新,字节码变化 | 环境检测项可能增加,需重新 compare_env |
| Chrome 环境伪装 | 如果不用 Camoufox 而用 Chrome 采集基准 | markNative 改回 Chrome 单行格式 |
浏览器指纹采集方法
使用 camoufox-reverse MCP 在 Firefox 内核浏览器中采集指纹:
步骤 1: launch_browser({headless: false, os_type: "macos", locale: "zh-CN"})
步骤 2: navigate({url: "目标页面", wait_until: "domcontentloaded"})
步骤 3: 分批采集环境指纹(5 批次)
批次 A-D:同 jsvmp-xhr-interceptor-env-emulation.md
批次 E(新增):Firefox 特有属性
JSON.stringify({
nav_buildID: navigator.buildID,
css2props_type: typeof window.CSS2Properties,
fn_toString_format: document.createElement.toString(),
// Firefox: "function createElement() {\n [native code]\n}"
// Chrome: "function createElement() { [native code] }"
mozInnerScreenX: window.mozInnerScreenX,
mozInnerScreenY: window.mozInnerScreenY,
InstallTrigger: typeof window.InstallTrigger
})
环境检测差异全表(62 项)
继承自基础案例的 58 项
参考 jsvmp-xhr-interceptor-env-emulation.md 的完整 58 项差异表。
新增 Firefox 特有检测(4 项)
| 检测点 | Firefox 真实值 | jsdom 原始值 | 修复方式 |
|---|---|---|---|
Function.prototype.toString 格式 |
function name() {\n [native code]\n} |
暴露实现代码 | markNative 输出 Firefox 格式(含 \n 和 4 空格缩进) |
navigator.buildID |
"20230927232528" |
undefined |
Object.defineProperty 设置 |
window.CSS2Properties |
function CSS2Properties() |
undefined |
别名指向 CSSStyleDeclaration |
window.InstallTrigger |
undefined(已废弃但 typeof 仍返回 "undefined" 而非报错) |
不存在 | 无需修复(行为一致) |
关键经验总结
1. Firefox 格式 native code 是本案例的核心突破点
Camoufox 基于 Firefox 内核,其 Function.prototype.toString 对原生函数返回的格式与 Chrome 不同。JSVMP 会检测这个格式来判断浏览器类型。如果 jsdom 的 markNative 输出 Chrome 格式,会导致浏览器类型判断不一致,签名被拒。
2. 双签名 = 双通道拦截
X-Bogus 通过 XHR 拦截器追加到 URL,X-Gnarly 通过 fetch 拦截器注入到 header。必须同时 Hook 两个通道才能截获完整签名。
3. cacheOpts 是新版初始化的关键
旧版只需 bdms.paths,新版必须传入 cacheOpts 才能正确注册业务路径。缺少 cacheOpts 会导致拦截器不触发。
4. got-scraping 解决 TLS 指纹问题
Node.js 原生 HTTP 客户端的 TLS 指纹会被识别。使用 got-scraping 可以模拟 Firefox 的 TLS Client Hello 指纹,与 Camoufox 采集的环境保持一致。