Files
hello_js_reverse_skill/cases/jsvmp-dual-sign-xhr-intercept-cacheOpts-jsdom-firefox.md
T
白夜 2ae0c35a6e v2.7.0: dual-sign case + experience library upgrade + MCP v0.6.0 alignment
- 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
2026-04-18 04:00:05 +08:00

15 KiB
Raw Blame History

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 编码,来自目标域名的 Cookie
  • verifyFp / 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 独有)
    • CSS2Properties vs CSSStyleDeclaration 类型检测
    • 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 + 时间戳
  • 加密流程:
    1. sdk-glue 通过 _SdkGlueInit(config, resources) 初始化,注入 cacheOpts 路径列表
    2. webmssdk JSVMP 构造函数执行字节码,同时修改 XMLHttpRequest.prototype.open 和 window.fetch
    3. 当请求 URL 匹配 enablePathList 时,XHR 拦截器追加 X-Bogus 到 URL,fetch 拦截器注入 X-Gnarly 到 header
    4. JSVMP 读取完整 URL query string、document.cookie、navigator.*、screen.* 等 60+ 项环境指纹
    5. 双签名并行生成: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 采集的环境保持一致。