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
This commit is contained in:
+38
-1
@@ -15,6 +15,32 @@
|
||||
| [jsvmp-xhr-interceptor-env-emulation.md](jsvmp-xhr-interceptor-env-emulation.md) | JSVMP 字节码虚拟机 + XHR 拦截器 + 多层 SDK 联动 + jsdom 全量环境伪装 | ★★★★★ | jsdom 沙箱 + 58 项环境补丁 + XHR Hook 截出 a_bogus | 行为型 |
|
||||
| [jsvmp-ruishu6-cookie-412-sdenv.md](jsvmp-ruishu6-cookie-412-sdenv.md) | JSVMP 瑞数6 + Cookie 生成 + 412 挑战 + sdenv 补环境 | ★★★★★ | sdenv(魔改 jsdom + C++ V8 Addon)让瑞数 JSVMP 真实执行生成 Cookie | 签名型 |
|
||||
| [universal-vmp-source-instrumentation.md](universal-vmp-source-instrumentation.md) | **[v2.5.0 新]** 通用 VMP(瑞数 5/6、Akamai sensor_data、webmssdk、obfuscator.io)+ 首屏挑战 + 混合 Cookie 模式 | ★★★★ | **源码级插桩** + hot_keys 指纹学习 + analyze_cookie_sources 归因(**骨架模板**,由使用者按实际站点填充) | 混合(以签名型为主) |
|
||||
| [jsvmp-dual-sign-xhr-intercept-cacheOpts-jsdom-firefox.md](jsvmp-dual-sign-xhr-intercept-cacheOpts-jsdom-firefox.md) | **[v2.7.0 新]** JSVMP 双签名(X-Bogus + X-Gnarly)+ XHR/fetch 双通道拦截 + cacheOpts 初始化 + jsdom Firefox 环境伪装 | ★★★★★ | jsdom 喂入-截出策略 + Firefox 格式 native code 伪装 + cacheOpts 路径注册 + got-scraping TLS 指纹模拟 | 行为型 |
|
||||
|
||||
## 指纹匹配快速参考
|
||||
|
||||
| 技术特征关键词 | 匹配案例 | 置信度 |
|
||||
|--------------|---------|--------|
|
||||
| `webmssdk` / `byted_acrawler` / `_SdkGlueInit` | jsvmp-xhr-interceptor-env-emulation 或 jsvmp-dual-sign-xhr-intercept-cacheOpts-jsdom-firefox | 高(需进一步区分) |
|
||||
| `cacheOpts` + `X-Gnarly` | jsvmp-dual-sign-xhr-intercept-cacheOpts-jsdom-firefox | 高(国际版双签名变体) |
|
||||
| `a_bogus` + 192字符 + 无 `cacheOpts` | jsvmp-xhr-interceptor-env-emulation | 高(国内版单签名) |
|
||||
| `sdenv` / `FuckCookie` / 412 挑战 | jsvmp-ruishu6-cookie-412-sdenv | 高(瑞数) |
|
||||
| `while-switch` 分发循环 + 200KB+ 文件 | universal-vmp-source-instrumentation | 中(通用骨架) |
|
||||
|
||||
## 变体关系图
|
||||
|
||||
```
|
||||
JSVMP 字节码虚拟机 + 多层 SDK 联动
|
||||
├── 国内短视频平台变体(单签名 a_bogus + bdms.paths)
|
||||
│ └── → jsvmp-xhr-interceptor-env-emulation.md
|
||||
├── 海外短视频平台(国际版)变体(双签名 X-Bogus + X-Gnarly + cacheOpts)
|
||||
│ └── → jsvmp-dual-sign-xhr-intercept-cacheOpts-jsdom-firefox.md
|
||||
└── 通用 VMP 骨架(瑞数/Akamai/webmssdk/obfuscator.io)
|
||||
└── → universal-vmp-source-instrumentation.md
|
||||
|
||||
瑞数 JSVMP + Cookie 签名
|
||||
└── → jsvmp-ruishu6-cookie-412-sdenv.md
|
||||
```
|
||||
|
||||
## 私有映射
|
||||
|
||||
@@ -26,6 +52,11 @@
|
||||
"pattern": "jsvmp-xhr-interceptor-env-emulation",
|
||||
"domain_hint": "短视频",
|
||||
"notes": "Cookie 字段 ttwid/__ac_nonce/__ac_signature"
|
||||
},
|
||||
"某海外短视频平台": {
|
||||
"pattern": "jsvmp-dual-sign-xhr-intercept-cacheOpts-jsdom-firefox",
|
||||
"domain_hint": "海外短视频",
|
||||
"notes": "双签名 X-Bogus + X-Gnarly,cacheOpts 初始化"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -33,7 +64,7 @@
|
||||
## 新增案例
|
||||
|
||||
1. 复制 `_template.md` 为新文件,以技术特征命名
|
||||
2. 按模板格式填写各段
|
||||
2. 按模板格式填写各段(v2.7.0 起模板新增「反爬类型判定」和「关键经验总结」段)
|
||||
3. 更新本文件的案例索引表
|
||||
4. 可选:在 `_private_mapping.json` 中添加私有域名映射
|
||||
|
||||
@@ -49,6 +80,9 @@
|
||||
## 案例名称
|
||||
(用技术特征命名,如"OB混淆+AES-CBC+动态密钥"、"JSVMP+Cookie生成")
|
||||
|
||||
## 反爬类型判定
|
||||
(签名型 / 行为型 / 纯混淆,附判定依据)
|
||||
|
||||
## 技术指纹
|
||||
(列出可用于自动检测的稳定特征,每条写成可搜索的模式)
|
||||
- JS特征: (如"_0x前缀变量大量出现"、"单文件200KB+"、"存在while-switch解释器循环")
|
||||
@@ -78,6 +112,9 @@
|
||||
## 变体说明
|
||||
(同类站点的已知变体差异)
|
||||
|
||||
## 关键经验总结
|
||||
(本次分析中最有价值的 2-3 条经验)
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
> 还原方案:[A: 纯算法还原 / B: 沙箱执行 / C: WASM 加载 / D: 浏览器自动化 / E: jsdom 环境模拟]
|
||||
> 实现语言:[Node.js / Python / 双语言]
|
||||
> 最后验证日期:YYYY-MM-DD
|
||||
> 平台类型:[如:短视频平台 / 电商平台 / 社交平台 / 通用]
|
||||
|
||||
---
|
||||
|
||||
@@ -40,6 +41,23 @@
|
||||
|
||||
---
|
||||
|
||||
## 反爬类型判定(v2.7.0 新增)
|
||||
|
||||
> 在进入加密方案分析之前,必须先判定反爬类型。参考 SKILL.md「反爬类型识别与工具选择」章节。
|
||||
|
||||
```
|
||||
判定步骤:
|
||||
1. navigate(url) 不加任何 hook → 观察 redirect_chain
|
||||
2. 按三分法判断:签名型 / 行为型 / 纯混淆
|
||||
3. 记录判定结果和依据
|
||||
|
||||
判定结果:[签名型 / 行为型 / 纯混淆]
|
||||
判定依据:[如 "redirect_chain 直接 200,页面后续 XHR 带 X-Bogus 签名参数 → 行为型"]
|
||||
工具路径:[如 "四板斧全开" / "仅源码级插桩" / "任意组合"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 加密方案
|
||||
|
||||
- **算法**:[具体算法名称和模式]
|
||||
@@ -118,3 +136,15 @@
|
||||
| 检测点 | 真实浏览器值 | 模拟环境原始值 | 修复方式 |
|
||||
|--------|-------------|---------------|---------|
|
||||
| | | | |
|
||||
|
||||
---
|
||||
|
||||
## 关键经验总结(v2.7.0 新增)
|
||||
|
||||
> 本次逆向分析中最有价值的经验,供后续类似场景参考
|
||||
|
||||
### 1. [经验标题]
|
||||
[经验描述]
|
||||
|
||||
### 2. [经验标题]
|
||||
[经验描述]
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
# JSVMP 双签名 + XHR/fetch 拦截器 + cacheOpts 初始化 + jsdom Firefox 环境伪装
|
||||
|
||||
> 难度:★★★★★
|
||||
> 还原方案:E: jsdom 环境模拟(喂入-截出策略)
|
||||
> 实现语言:Node.js
|
||||
> 最后验证日期:2026-04-20
|
||||
> 平台类型:海外短视频平台(国际版)
|
||||
|
||||
---
|
||||
|
||||
## 技术指纹(供 Phase 0.5 自动匹配)
|
||||
|
||||
### JS 特征
|
||||
- [x] `webmssdk.es5.js` — JSVMP 签名引擎(380KB+),UMD 导出到 `window.byted_acrawler`
|
||||
- [x] 三层 SDK 联动:`sdk-glue.js`(100KB)→ `bdms.js`(147KB)→ `webmssdk.es5.js`(387KB)
|
||||
- [x] 存在 while-switch 风格的 JSVMP 解释器循环
|
||||
- [x] 内部状态对象含 `bogusIndex`、`msNewTokenList`、`moveList`、`clickList`、`activeState`
|
||||
- [x] `_0x` 前缀变量大量出现(构造函数、工厂函数、闭包变量)
|
||||
- [x] 页面 inline script 调用 `window._SdkGlueInit(config, resources)` 完成初始化
|
||||
- [x] 初始化配置含 `cacheOpts` 字段(路径注册 + 缓存策略),区别于旧版仅 `bdms.paths`
|
||||
|
||||
### 参数特征
|
||||
- [x] `X-Bogus`:约 180~192 字符,Base64 变体编码 — 由 JSVMP XHR 拦截器生成(新版主签名)
|
||||
- [x] `X-Gnarly`:约 120~140 字符,自定义编码 — 由 `frontierSign()` 生成的辅助签名
|
||||
- [x] `msToken`:约 140 字符,Base64 编码,来自目标域名的 Cookie
|
||||
- [x] `verifyFp` / `fp`:格式 `verify_xxxx_xxxx_xxxx_xxxx_xxxxxxxxxxxx`
|
||||
|
||||
### 请求特征
|
||||
- [x] `_SdkGlueInit(config, resources)` 初始化时 config 含 `cacheOpts: { paths: [...], ttl: N }`
|
||||
- [x] JSVMP 同时修改 `XMLHttpRequest.prototype.open` 和 `window.fetch`(双通道拦截)
|
||||
- [x] 拦截器对匹配 `enablePathList` 的 URL 追加 `X-Bogus`,同时在 header 中注入 `X-Gnarly`
|
||||
- [x] Cookie 中包含动态字段:`ttwid`、`__ac_nonce`、`__ac_signature`、`s_v_web_id`、`odin_tt`
|
||||
- [x] 存在 `/web/report` 默认拦截路径 + 业务路径通过 `cacheOpts.paths` 注入
|
||||
|
||||
### 反调试特征
|
||||
- [x] JSVMP 字节码执行,无法直接设置断点
|
||||
- [x] debugger 定时器陷阱
|
||||
- [x] 环境检测(60+ 项):在原有 58 项基础上新增 Firefox 特有检测:
|
||||
- `navigator.buildID` 存在性(Firefox 独有)
|
||||
- `CSS2Properties` vs `CSSStyleDeclaration` 类型检测
|
||||
- `Function.prototype.toString` 格式差异(Firefox 返回 `function name() {\n [native code]\n}` 含换行缩进)
|
||||
- [x] 非真实浏览器环境生成的签名会被服务端**静默拒绝**(返回 HTTP 200 + 空 body)
|
||||
|
||||
### 混淆类型
|
||||
- [x] 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 伪装
|
||||
|
||||
```javascript
|
||||
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 初始化
|
||||
|
||||
```javascript
|
||||
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/'] }
|
||||
});
|
||||
`);
|
||||
}
|
||||
```
|
||||
|
||||
### 核心函数:双签名生成(喂入-截出策略)
|
||||
|
||||
```javascript
|
||||
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 指纹模拟
|
||||
|
||||
```javascript
|
||||
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 特有属性
|
||||
|
||||
```javascript
|
||||
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 采集的环境保持一致。
|
||||
@@ -102,7 +102,7 @@
|
||||
window 仅出现 3 次(环境访问全部通过 JSVMP 字节码间接进行)。
|
||||
|
||||
步骤 8: 补环境方案选型
|
||||
参考 ctl456/ruishu6-yaojianju(手动补环境)和 warterbili/ruishu-re(sdenv/纯算/JsRpc 多方案),
|
||||
参考开源社区的手动补环境方案和 sdenv/纯算/JsRpc 多方案,
|
||||
确定 sdenv(魔改 jsdom + C++ V8 Addon)是瑞数6的最优纯 Node.js 方案。
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 案例:通用 VMP 源码级插桩(骨架模板)
|
||||
|
||||
> **v2.5.0 骨架案例**。本文是一个**方法论模板**,不是某个具体站点的案例。适用于:瑞数 5/6、Akamai sensor_data v2/v3、TikTok webmssdk、obfuscator.io 等所有"算法封装在字节码 dispatch 循环里"的 VMP。
|
||||
> **v2.5.0 骨架案例**。本文是一个**方法论模板**,不是某个具体站点的案例。适用于:瑞数 5/6、Akamai sensor_data v2/v3、webmssdk、obfuscator.io 等所有"算法封装在字节码 dispatch 循环里"的 VMP。
|
||||
>
|
||||
> 使用方式:
|
||||
>
|
||||
@@ -199,7 +199,7 @@ function genSign(input) {
|
||||
| 瑞数 5 | 固定 `sdenv-*.js` 命名 | url_pattern 用 `**/sdenv-*.js` |
|
||||
| 瑞数 6 | 脚本名每次不同 + 多层 VMP | 先 `list_network_requests` 找所有 100KB+ JS,逐个 instrument(tag 区分) |
|
||||
| Akamai sensor_data | `_abck` / `bm_sz` cookie | 重点看 `hot_keys` 中 touch/mouse 事件相关属性 |
|
||||
| webmssdk(TikTok) | 配合 msToken 预热 | 需先让 `/v1/generate_token` 预热接口完成再触发业务 |
|
||||
| webmssdk(短视频平台) | 配合 msToken 预热 | 需先让 `/v1/generate_token` 预热接口完成再触发业务 |
|
||||
| obfuscator.io(开源) | case 数可能只有 20-30 | `find_dispatch_loops(min_case_count=15)` |
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user