Files
hello_js_reverse_skill/cases/universal-vmp-source-instrumentation.md
T

214 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 案例:通用 VMP 源码级插桩(骨架模板)
> **v2.5.0 骨架案例**。本文是一个**方法论模板**,不是某个具体站点的案例。适用于:RS 5/6、Akamai sensor_data v2/v3、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(RS)或 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]")
# 注意:本案例针对的是"通用"场景(含签名型和行为型)。如果目标是签名型反爬
# (RS/Akamai),不要这样用 hook_jsvmp_interpreter,改为:
# - instrument_jsvmp_source(mode="ast") (首选)
# - hook_jsvmp_interpreter(mode="transparent") (备选)
# 参考 SKILL.md "反爬类型识别与工具选择" 章节
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 模式从 MCP v0.5.0 起在 MCP 侧使用 esprima,不依赖页面 CDN**——若原始源码 parse 失败或改写产物未执行,再降级到 `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 / 页面跳转)** → MCP 会按 URL 缓存改写结果;当前接口不需要传缓存参数
- **坑 6:VMP 通过 eval / new Function 动态生成子 VMP** → 源码级插桩看不到动态生成的部分,需要额外 `hook_function(Function, ...)` 截获
- **坑 7:Proxy(jsvmp_hook)与源码级插桩叠加时页面崩溃** → 尝试 `hook_jsvmp_interpreter(track_props=False)` 关闭 Proxy,只留 apply/Reflect.*
---
## 变体说明
| 变体 | 差异点 | 调整策略 |
|------|-------|---------|
| RS 5 | 固定 `sdenv-*.js` 命名 | url_pattern 用 `**/sdenv-*.js` |
| RS 6 | 脚本名每次不同 + 多层 VMP | 先 `list_network_requests` 找所有 100KB+ JS,逐个 instrument(tag 区分) |
| Akamai sensor_data | `_abck` / `bm_sz` cookie | 重点看 `hot_keys` 中 touch/mouse 事件相关属性 |
| webmssdk(短视频平台) | 配合 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 等已知模式字段