feat(v2.5.0): upgrade to four-axe JSVMP methodology + MCP v0.4.0 alignment
- Add fourth axe: source-level instrumentation via instrument_jsvmp_source - Add new reference: references/jsvmp-source-instrumentation.md - Align MCP tool count 52 -> 65 across SKILL.md / README.md / mcp-cookbook.md - New scenario 11: universal JSVMP source instrumentation (Rui Shu / Akamai / webmssdk) - Enhance scenario 2 with analyze_cookie_sources for cookie attribution - Extend experience rules from 22 to 27 - New case skeleton: cases/universal-vmp-source-instrumentation.md - New case: cases/jsvmp-ruishu6-cookie-412-sdenv.md - Update jsvmp-analysis.md from three-axe to four-axe methodology - Document new presets 'cookie' and 'runtime_probe' - Document pre_inject_hooks / via_blank / reload_with_hooks for first-screen challenges - Add troubleshooting section for 412/403 first-screen challenges
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
| 案例文件 | 技术特征 | 难度 | 核心方案 |
|
||||
|---------|---------|------|---------|
|
||||
| [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 归因(**骨架模板**,由使用者按实际站点填充) |
|
||||
|
||||
## 私有映射
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# JSVMP-瑞数6+Cookie生成+412挑战+sdenv补环境
|
||||
|
||||
> 难度:★★★★★
|
||||
> 还原方案:E: jsdom 环境模拟(sdenv 魔改 jsdom + C++ V8 Addon)
|
||||
> 实现语言:Node.js
|
||||
> 最后验证日期:2026-04-17
|
||||
|
||||
---
|
||||
|
||||
## 技术指纹(供 Phase 0.5 自动匹配)
|
||||
|
||||
### JS 特征
|
||||
- [x] 单文件 230KB+,变量名为 `_$` 前缀加2-3位随机字母(如 `_$bV`, `_$ku`),存在 `if($_ts.cd){` 入口判断,函数名每次请求动态变化
|
||||
- [x] 页面底部存在 `<script>_$xx();</script>` 格式的入口函数调用,函数名每次不同
|
||||
- [x] HTML 中包含 `<meta id="固定ID" content="动态token" r='m'>` 标签,content 值约 100 字符,每次请求变化
|
||||
- [x] 内联脚本设置 `$_ts` 全局变量,包含 `nsd`(数字种子)和 `cd`(约 1800 字符的配置字符串),每次请求动态变化
|
||||
|
||||
### 参数特征
|
||||
- [x] Cookie 名称格式为 `XxxYyyZzz2Aaaa` + `S`/`T` 后缀(如 `NfBCSins2OywS` 和 `NfBCSins2OywT`),S 由服务端 Set-Cookie 下发,T 由客户端 JS 生成
|
||||
|
||||
### 请求特征
|
||||
- [x] 首次请求返回 HTTP 412(非标准状态码),响应体为精简 HTML(含 meta + 内联 $_ts 配置 + 外部 JS 引用 + 入口函数调用)
|
||||
- [x] 412 响应的 Set-Cookie 头同时下发 `acw_tc`(会话标识)和 `XxxS`(服务端标识,HttpOnly,过期时间 10 年)
|
||||
- [x] 成功请求需携带 3 个 Cookie:`acw_tc` + `XxxS`(服务端下发)+ `XxxT`(客户端生成)
|
||||
- [x] 瑞数 JS 文件 URL 路径包含随机目录名和文件名,但版本号后缀(如 `.e17ed02.js`)在一段时间内固定
|
||||
|
||||
### 反调试特征
|
||||
- [x] JSVMP 内部使用函数表+直接调用(非 `Function.prototype.apply/call`),导致标准 JSVMP Hook 工具无法拦截
|
||||
- [x] `$_ts` 全局变量在 JS 执行完毕后被清理,运行时无法通过 console 访问
|
||||
- [x] 检测 `typeof document.all` 必须为 `"undefined"`(浏览器特有行为,纯 JS 无法模拟)
|
||||
|
||||
### 混淆类型
|
||||
- [x] JSVMP(三层嵌套虚拟机),外层解析配置生成 eval 代码,中层执行 Cookie 生成和 XHR 劫持,内层执行 AES/CRC32/Huffman/Base64 加密
|
||||
|
||||
### 指纹检测规则(Agent 执行)
|
||||
|
||||
```
|
||||
快速检测命令(30秒内完成):
|
||||
- search_code(keyword="$_ts") → 命中 + 存在 nsd/cd 字段 → 高置信度
|
||||
- search_code(keyword="_$") → 大量 _$ 前缀变量 → 辅助确认
|
||||
- list_scripts → 存在 230KB+ 文件 → 辅助确认
|
||||
- list_network_requests → 首次请求返回 412 → 高置信度
|
||||
- 检查 Set-Cookie 头 → 存在 acw_tc + XxxS 格式 Cookie → 直接定位本案例
|
||||
匹配判定:412 响应 + $_ts 配置 + _$ 前缀变量 → 瑞数6 高置信度匹配
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 加密方案
|
||||
|
||||
- **算法**:Huffman 编码 → XOR → AES-128-CBC → CRC32 校验 → AES-128-CBC → Base64(URL-safe 变体)
|
||||
- **密钥来源**:从 `$_ts.cd` 配置字符串中通过 XOR offset 推导提取 45 组密钥,密钥随每次 412 响应动态变化
|
||||
- **加密流程**:
|
||||
1. 收集浏览器指纹(Canvas、WebGL、UA、屏幕尺寸、navigator 属性等)
|
||||
2. 组装 basearr(约 154 字节 TLV 结构,包含 8 种 type 的环境数据)
|
||||
3. basearr → Huffman 编码 → XOR 加密 → AES-128-CBC 加密 → 追加 CRC32 校验 → 再次 AES-128-CBC → Base64 编码
|
||||
4. Cookie T = `"0"` + Base64 结果
|
||||
5. 写入 `document.cookie`,然后通过 `location.replace` 或 XHR 重新请求页面
|
||||
- **签名公式**:Cookie T 不是简单的签名,而是完整的加密数据包,包含浏览器指纹、时间戳、随机数等信息,服务端解密后验证指纹一致性
|
||||
|
||||
---
|
||||
|
||||
## 已验证定位路径(Phase 0.5 命中后直接执行)
|
||||
|
||||
```
|
||||
步骤 1: Camoufox 反检测浏览器 + 网络捕获
|
||||
启动 Camoufox(C++ 引擎级指纹伪装),注入 XHR/Fetch/Crypto 持久化 Hook,导航到目标页面。
|
||||
通过 list_network_requests 发现请求链路:
|
||||
请求 #1 返回 412 + Set-Cookie + HTML body
|
||||
请求 #2 加载 230KB JS 文件
|
||||
请求 #3 携带 3 个 Cookie 返回 200
|
||||
|
||||
步骤 2: 412 响应体分析
|
||||
通过 get_network_request(id=1, include_body=true) 获取完整的 412 响应。
|
||||
发现 HTML body 结构为:
|
||||
<meta> 标签(动态 token)+ $_ts 内联配置(nsd + cd)+ 外部 JS 引用 + 入口函数调用 _$xx()
|
||||
同时从 Set-Cookie 头提取到 acw_tc 和 XxxS 两个服务端 Cookie。
|
||||
|
||||
步骤 3: 成功请求对比
|
||||
通过 get_network_request(id=3, include_headers=true) 对比成功请求的 Cookie 头,
|
||||
发现多了一个 XxxT Cookie — 这就是客户端 JS 生成的。
|
||||
同时发现请求头中 Referer 和 Sec-Fetch-Site: same-origin 是必需的。
|
||||
|
||||
步骤 4: Cookie setter Hook 验证
|
||||
注入 document.cookie setter Hook,发现 Cookie 日志为空 —
|
||||
说明瑞数 JS 不是通过标准 document.cookie setter 写入的
|
||||
(可能是通过 jsdom 内部机制或 location.replace 触发的重新请求时由浏览器自动携带)。
|
||||
|
||||
步骤 5: JSVMP 插桩尝试
|
||||
使用 hook_jsvmp_interpreter 对瑞数 JS 进行插桩,发现日志为空 —
|
||||
确认瑞数6使用内部函数表+直接调用,不经过 Function.prototype.apply/call,
|
||||
标准 JSVMP Hook 无效。
|
||||
|
||||
步骤 6: 环境对比
|
||||
使用 compare_env 采集 Camoufox 中的真实浏览器环境数据,
|
||||
发现 document.cookie 中包含 enable_XxxYyy=true 标记,
|
||||
说明瑞数 JS 执行成功后会设置一个启用标记。
|
||||
|
||||
步骤 7: 瑞数 JS 保存
|
||||
通过 save_script 将 230KB 的瑞数 JS 文件保存到本地,分析其结构:
|
||||
以 if($_ts.cd){ 开头,内部是 JSVMP 解释器,
|
||||
window 仅出现 3 次(环境访问全部通过 JSVMP 字节码间接进行)。
|
||||
|
||||
步骤 8: 补环境方案选型
|
||||
参考 ctl456/ruishu6-yaojianju(手动补环境)和 warterbili/ruishu-re(sdenv/纯算/JsRpc 多方案),
|
||||
确定 sdenv(魔改 jsdom + C++ V8 Addon)是瑞数6的最优纯 Node.js 方案。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 还原代码模板
|
||||
|
||||
### 核心函数:瑞数6 Cookie 生成 — 基于 sdenv
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 瑞数6 Cookie 生成 — 基于 sdenv (魔改 jsdom + C++ V8 Addon)
|
||||
* 依赖: npm install sdenv (需要 pnpm 安装 + node-gyp 编译原生模块)
|
||||
* 原理:
|
||||
* sdenv 的核心是 documentAll.node (51行C++),用 V8 的
|
||||
* ObjectTemplate::MarkAsUndetectable() 实现 document.all 的浏览器特有行为
|
||||
* (typeof === "undefined" 但可调用),加上完整的浏览器环境模拟,
|
||||
* 让瑞数 JSVMP 在 Node.js 中真实执行并生成有效 Cookie。
|
||||
*/
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
|
||||
const { jsdomFromUrl } = require('sdenv');
|
||||
const https = require('https');
|
||||
|
||||
class RuishuClient {
|
||||
constructor(config = {}) {
|
||||
this.host = config.host; // 目标主机名
|
||||
this.entryPath = config.entryPath; // 入口页面路径(返回 412 的页面)
|
||||
this.ua = config.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36';
|
||||
this.dom = null;
|
||||
this.cookies = '';
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const url = `https://${this.host}${this.entryPath}`;
|
||||
this.dom = await jsdomFromUrl(url, {
|
||||
userAgent: this.ua,
|
||||
consoleConfig: { error: () => {} },
|
||||
});
|
||||
|
||||
// 等待瑞数 JS 执行完毕
|
||||
await new Promise(resolve => {
|
||||
this.dom.window.addEventListener('sdenv:exit', () => resolve());
|
||||
setTimeout(resolve, 8000);
|
||||
});
|
||||
|
||||
this.cookies = this.dom.cookieJar.getCookieStringSync(`https://${this.host}`);
|
||||
this.ready = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
get(path) {
|
||||
if (!this.ready) throw new Error('请先调用 init()');
|
||||
return new Promise((resolve, reject) => {
|
||||
https.request({
|
||||
hostname: this.host, port: 443, path, method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': this.ua,
|
||||
'Host': this.host,
|
||||
'Cookie': this.cookies,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Referer': `https://${this.host}/`,
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
},
|
||||
}, res => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => resolve({ status: res.statusCode, body }));
|
||||
}).on('error', reject).end();
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.dom) {
|
||||
try { this.dom.window.close(); } catch(e) {}
|
||||
this.dom = null;
|
||||
this.ready = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 踩坑记录
|
||||
|
||||
| # | 坑 | 现象 | 解决方法 |
|
||||
|---|---|------|---------|
|
||||
| 1 | 误判反爬类型 | 通过 webFetch 直接请求返回 412,猜测是 JSL 加速乐(`__jsl_clearance_s` cookie),实际是瑞数信息(RS) | 不要猜,用 Camoufox 实际抓包看 412 响应体结构和 Set-Cookie 头 |
|
||||
| 2 | Cookie 有效但请求返回 400 | Camoufox 获取的 Cookie 有效(curl 测试返回 200),但 axios 请求返回 400 | 瑞数不仅检查 Cookie,还检查请求头指纹 — UA 必须与生成 Cookie 时一致,且必须包含 `Sec-Fetch-Site: same-origin` 等 Fetch Metadata 头 |
|
||||
| 3 | jsdom 执行瑞数 JS 卡死(超时) | 原生 jsdom + `runScripts: 'dangerously'` 加载 412 页面,瑞数 JS 执行后进入死循环 | `typeof document.all` 在 jsdom 中返回 `"object"` 而非 `"undefined"`,瑞数检测到非浏览器环境后故意卡死。必须用 sdenv(C++ 层面实现 `MarkAsUndetectable()`)或真实浏览器 |
|
||||
| 4 | V8 `%GetUndetectable()` 不够 | 用 V8 内部函数获取的 undetectable 对象通过了 `typeof === "undefined"` 检测,但它是空对象,没有 `HTMLAllCollection` 的方法 | 瑞数 JSVMP 后续尝试调用该对象的方法时报错 `_$xx[_$yy[49]] is not a function`。`document.all` 不仅要通过 typeof 检测,还要有完整的集合行为 |
|
||||
| 5 | 瑞数入口函数名每次不同 | 412 响应体底部的入口函数调用(如 `_$bV()`, `_$l1()`, `_$jH()`)每次请求都不同 | 正则匹配时需要用 `_\$[a-zA-Z0-9]+\(\)` 而非固定函数名 |
|
||||
| 6 | Node.js v24 的 Navigator 原型有只读属性 | `Navigator.prototype` 上的 `language` 等属性已经有了 getter,用 `Object.assign` 赋值报错 | 必须用 `Object.defineProperty` 逐个覆盖 |
|
||||
| 7 | sdenv 原生模块编译 | sdenv 的核心 `documentAll.node` 是 C++ 原生模块,需要 node-gyp 编译 | pnpm 安装时默认不执行 build scripts,需要手动 `pnpm approve-builds` 或 `npx node-gyp rebuild` |
|
||||
| 8 | JSVMP Hook 工具对瑞数6无效 | Camoufox 的 `hook_jsvmp_interpreter` 通过 Hook `Function.prototype.apply/call` 追踪,但瑞数6使用内部函数表+直接调用,日志为空 | 不是所有 JSVMP 都能用标准 Hook 工具分析,需要根据具体实现选择分析方法 |
|
||||
|
||||
---
|
||||
|
||||
## 变体说明
|
||||
|
||||
| 变体 | 差异点 | 影响 |
|
||||
|------|--------|------|
|
||||
| 瑞数 4/5 代 vs 6 代 | 瑞数6 JS 文件约 230KB,比 4/5 代(约 200KB)更大,环境检测项更多 | 4/5 代可通过手动补环境成功,6 代手动补环境极其困难,建议直接用 sdenv |
|
||||
| Cookie-only vs Cookie+URL后缀 | 大部分瑞数站点只需要 Cookie 即可访问。少数站点还需要 URL 后缀签名 | sdenv 只能生成 Cookie,不能生成 URL 后缀。需要后缀的站点建议用 JsRpc 方案 |
|
||||
| HTTP vs HTTPS | 部分瑞数站点使用 HTTP,其他使用 HTTPS | sdenv 的 `jsdomFromUrl` 两种协议都支持,但 HTTPS 站点需要设置 `NODE_TLS_REJECT_UNAUTHORIZED=0`(某些政府站点的证书链不完整) |
|
||||
| 静态页面 vs API 接口 | 列表页和详情页可能是纯静态 HTML(服务端渲染),用 cheerio 解析即可。其他站点可能有 JSON API 接口 | API 请求可能需要额外的签名参数(如 `sign = MD5(itemId + searchValue + timestamp)`) |
|
||||
| `$_ts` 配置差异 | 不同站点的 `$_ts` 配置结构可能不同,有的只有 `nsd` 和 `cd`,其他可能还有 `cp`、`aebi` 等字段 | sdenv 方案不需要关心这些差异,因为它让瑞数 JS 自己解析配置 |
|
||||
| 服务端动态更新 | 瑞数服务端可以随时更新 `basearr` 中的环境检测值 | 纯算方案会因此失效,sdenv 方案通常不受影响(除非瑞数新增了 sdenv 未模拟的检测点) |
|
||||
@@ -0,0 +1,208 @@
|
||||
# 案例:通用 VMP 源码级插桩(骨架模板)
|
||||
|
||||
> **v2.5.0 骨架案例**。本文是一个**方法论模板**,不是某个具体站点的案例。适用于:瑞数 5/6、Akamai sensor_data v2/v3、TikTok webmssdk、obfuscator.io 等所有"算法封装在字节码 dispatch 循环里"的 VMP。
|
||||
>
|
||||
> 使用方式:
|
||||
>
|
||||
> 1. 在 Phase 0.5 指纹匹配时,若检测到 VMP 特征(`find_dispatch_loops` 返回 case_count > 50),直接走本案例的「已验证定位路径」
|
||||
> 2. 完成一次具体站点逆向后,复制本文件重命名为 `vmp-<具体技术特征>.md`,按真实站点数据填充 `[填入...]` 占位符,加入 cases/README.md 索引
|
||||
|
||||
---
|
||||
|
||||
## 指纹检测规则
|
||||
|
||||
- **JS 特征**:
|
||||
- 单 JS 文件 100KB+,通常命名模式为 `sdenv-*.js` / `FuckCookie_*.js` / `webmssdk.es5.js` / `sensor_data.js` / `akam/xxx.js` / `a_bogus.js`
|
||||
- `find_dispatch_loops(script_url=<该文件>)` 返回 `case_count > 50` 的候选
|
||||
- 含超大字节码数组(`var X = [3,15,7,22,...]`,长度 1000+)
|
||||
- 含 `while(true){switch(...)}` 或 `while(!![]){switch(...)}` 解释器循环
|
||||
- **参数特征**:
|
||||
- 固定长度签名参数(128/192/256 字符) + Base64 变体(有 `-_` 替换 `+/`) 或 hex
|
||||
- 典型参数名:`a_bogus` / `_signature` / `X-Bogus` / `msToken` / `_m_h5_tk` / `acw_tc` / `FSSBBIl1UgzbN7N`
|
||||
- **请求特征**:
|
||||
- 首包返回 412(瑞数)或 403(Akamai)+ 动态 cookie 写入后通过
|
||||
- 存在预热接口(如 `/challenge` / `/akam/11/*` / `/v1/gen_token`)
|
||||
- Cookie 中有多个动态字段(JS 写的 + HTTP Set-Cookie 发的)
|
||||
- **反调试特征**:
|
||||
- debugger 陷阱(可 `bypass_debugger_trap` 绕过)
|
||||
- navigator.webdriver / Function.prototype.toString 检测
|
||||
- **混淆类型**:JSVMP(字节码虚拟机)
|
||||
|
||||
---
|
||||
|
||||
## 加密方案
|
||||
|
||||
- **算法**:[填入真实案例时填写,常见:HMAC-SHA256 / 自实现 MD5 / AES-CBC / 自定义哈希表 + 异或 / CRC32 变体]
|
||||
- **密钥来源**:[填入:硬编码字节码中 / 服务端预热下发 / 浏览器指纹哈希派生]
|
||||
- **加密流程**:
|
||||
1. VMP 收集浏览器环境指纹(`hot_keys` 暴露的属性集合)
|
||||
2. 拼接业务参数 + 时间戳 + 指纹
|
||||
3. 通过 dispatch 循环内的 handler 系列完成哈希/加密
|
||||
4. 输出 Base64/hex 编码的签名
|
||||
- **签名公式**:[填入真实公式]
|
||||
|
||||
---
|
||||
|
||||
## 已验证定位路径(源码级插桩黄金 8 步)
|
||||
|
||||
```
|
||||
Step 1 — launch + 抓包
|
||||
launch_browser(headless=False)
|
||||
start_network_capture(capture_body=True)
|
||||
|
||||
Step 2 — 首次导航
|
||||
若是首屏挑战站点:
|
||||
navigate(
|
||||
url="[填入目标URL]",
|
||||
pre_inject_hooks=["xhr", "fetch", "cookie", "jsvmp_probe", "runtime_probe"],
|
||||
via_blank=True,
|
||||
wait_until="networkidle"
|
||||
)
|
||||
否则:
|
||||
navigate(url="[填入]", wait_until="load")
|
||||
|
||||
Step 3 — 定位 VMP 脚本
|
||||
list_network_requests(resource_type="script")
|
||||
→ 找 size 最大的 JS,记 URL 为 <VMP_URL>
|
||||
find_dispatch_loops(script_url=<VMP_URL>, min_case_count=20)
|
||||
→ 确认 case_count > 50
|
||||
|
||||
Step 4 — 装源码级插桩
|
||||
instrument_jsvmp_source(
|
||||
url_pattern="[填入 glob,如 **/sdenv-*.js]",
|
||||
mode="ast",
|
||||
tag="vmp1",
|
||||
rewrite_member_access=True,
|
||||
rewrite_calls=True
|
||||
)
|
||||
|
||||
Step 5 — 装兜底 hook(若 Step 2 没走 pre_inject_hooks)
|
||||
inject_hook_preset("cookie", persistent=True)
|
||||
inject_hook_preset("xhr", persistent=True)
|
||||
inject_hook_preset("fetch", persistent=True)
|
||||
inject_hook_preset("crypto", persistent=True)
|
||||
hook_jsvmp_interpreter(script_url="[VMP basename]")
|
||||
bypass_debugger_trap()
|
||||
|
||||
Step 6 — reload_with_hooks 让探针先于 VMP 生效
|
||||
reload_with_hooks(clear_log=True)
|
||||
|
||||
Step 7 — 触发业务操作
|
||||
[填入具体操作]
|
||||
|
||||
Step 8 — 读日志
|
||||
get_instrumentation_log(tag_filter="vmp1", type_filter="tap_get", limit=200)
|
||||
→ 记录 hot_keys top 30 的环境属性
|
||||
get_instrumentation_log(tag_filter="vmp1", type_filter="tap_method", limit=200)
|
||||
→ 记录 hot_methods 是否含 CryptoJS.* / SubtleCrypto.*
|
||||
get_instrumentation_log(tag_filter="vmp1", type_filter="tap_call", limit=200)
|
||||
→ 记录 hot_functions 里的高频自定义函数名
|
||||
analyze_cookie_sources()
|
||||
→ 对每个目标 cookie 确认 sources
|
||||
|
||||
清理:
|
||||
stop_instrumentation(url_pattern="[glob]")
|
||||
remove_hooks()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 还原代码(按 hot_methods 分支)
|
||||
|
||||
### 分支 A:hot_methods 包含标准加密 API → 纯算法还原
|
||||
|
||||
```javascript
|
||||
// Node.js 示例
|
||||
const crypto = require('crypto');
|
||||
|
||||
function genSign(params, ts, fingerprint) {
|
||||
const payload = [
|
||||
// [填入实际字段顺序]
|
||||
params.url,
|
||||
params.body || '',
|
||||
ts,
|
||||
fingerprint,
|
||||
].join('|');
|
||||
|
||||
return crypto
|
||||
.createHmac('sha256', '[填入密钥]')
|
||||
.update(payload)
|
||||
.digest('base64')
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# Python 等价实现
|
||||
import hmac, hashlib, base64
|
||||
|
||||
def gen_sign(params, ts, fingerprint):
|
||||
payload = '|'.join([
|
||||
params['url'], params.get('body', ''),
|
||||
str(ts), fingerprint
|
||||
])
|
||||
raw = hmac.new(b'[填入密钥]', payload.encode(), hashlib.sha256).digest()
|
||||
return base64.urlsafe_b64encode(raw).rstrip(b'=').decode()
|
||||
```
|
||||
|
||||
### 分支 B:hot_methods 全自定义 + hot_keys 少 → vm/execjs 沙箱执行提取 JS
|
||||
|
||||
```javascript
|
||||
// Node.js + vm
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
|
||||
const sandboxCode = fs.readFileSync('./config/sign_extracted.js', 'utf8');
|
||||
|
||||
const sandbox = {
|
||||
// 最小环境:把 hot_keys 里出现的全部属性放进来
|
||||
navigator: { userAgent: '...', platform: '...', language: 'zh-CN' },
|
||||
// ... 其他 hot_keys 暴露的属性
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(sandboxCode, sandbox);
|
||||
|
||||
function genSign(input) {
|
||||
return sandbox.__export_sign(input);
|
||||
}
|
||||
```
|
||||
|
||||
### 分支 C:hot_keys 多(30+) + cookie 来自 HTTP Set-Cookie → 路径 B jsdom 环境伪装
|
||||
|
||||
见 `references/jsdom-env-patches.md` + `cases/jsvmp-xhr-interceptor-env-emulation.md`。
|
||||
|
||||
将本案例中采集到的 `hot_keys` 清单作为 "要对齐的环境属性集"——省去 58 项盲目对齐。
|
||||
|
||||
---
|
||||
|
||||
## 踩坑记录
|
||||
|
||||
- **坑 1:instrument_jsvmp_source 必须在 navigate/reload 之前调用**——route 注册晚了就抓不到 VMP 脚本加载
|
||||
- **坑 2:AST 模式需要 cdnjs 访问**——代理环境下要保证 `cdnjs.cloudflare.com` 可访问,否则降级到 `mode="regex"`
|
||||
- **坑 3:reload_with_hooks 只刷当前页,不改变 URL**——首屏挑战场景要用 `navigate(pre_inject_hooks=[...])` 而不是 reload_with_hooks
|
||||
- **坑 4:hot_keys 里 "[key of Symbol()]"**——这是 Symbol 键被 preview 化的表现,不是真实属性名,可以忽略
|
||||
- **坑 5:同一个 VMP 被多次加载(HMR / 页面跳转)** → 确保 `cache_rewritten=True`,否则每次重写开销巨大
|
||||
- **坑 6:VMP 通过 eval / new Function 动态生成子 VMP** → 源码级插桩看不到动态生成的部分,需要额外 `hook_function(Function, ...)` 截获
|
||||
- **坑 7:Proxy(jsvmp_hook)与源码级插桩叠加时页面崩溃** → 尝试 `hook_jsvmp_interpreter(track_props=False)` 关闭 Proxy,只留 apply/Reflect.*
|
||||
|
||||
---
|
||||
|
||||
## 变体说明
|
||||
|
||||
| 变体 | 差异点 | 调整策略 |
|
||||
|------|-------|---------|
|
||||
| 瑞数 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` 预热接口完成再触发业务 |
|
||||
| obfuscator.io(开源) | case 数可能只有 20-30 | `find_dispatch_loops(min_case_count=15)` |
|
||||
|
||||
---
|
||||
|
||||
## 指纹匹配时的权重建议
|
||||
|
||||
作为 Phase 0.5 指纹匹配的备选路径,本案例应命中以下任一条件即触发"尝试源码级插桩":
|
||||
|
||||
- **高权重(直接走)**:`find_dispatch_loops` 返回 case_count > 50
|
||||
- **中权重(优先试)**:单 JS > 200KB + 含 while-switch + 参数长度 128/192/256 + Base64 变体
|
||||
- **低权重(参考)**:Cookie 中有 acw_tc / FSSBBIl1 / _abck / ak_bmsc / ttwid / msToken 等已知模式字段
|
||||
Reference in New Issue
Block a user