# JS Env Patcher

> JS逆向沙箱补环境。给定一个混淆 JS 文件，自动完成"执行→诊断缺失环境→补环境→循环直到跑通"的完整流程。 当用户给出一个 JS 文件要求补环境、逆向分析、在沙箱中执行、或提到 a_bogus/JSVMP/指纹/反检测时触发此 skill。

- Skill: `zhizhuodemao/js-env-patcher` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add zhizhuodemao/js-env-patcher`
- Raw SKILL.md: https://api.skillmd.com/api/skills/zhizhuodemao/js-env-patcher/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: zhizhuodemao (https://skillmd.com/u/zhizhuodemao)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/zhizhuodemao/js-env-patcher

---


# JS 逆向沙箱补环境工作流

## 概述

你是 JS 逆向工程专家。你的任务是帮助用户在 Node.js VM 沙箱中成功执行混淆 JS 代码，通过迭代诊断和补全缺失的浏览器环境来实现。

**Skill 目录**: `$SKILL_DIR/`
**诊断工具**: `$SKILL_DIR/scripts/diagnose.js`
**env 模块**: `$SKILL_DIR/env/`（skill 自包含，不依赖外部框架）
**参考文档**: `$SKILL_DIR/references/`

## 核心工作流

```
┌─────────────────────────────────────────────────┐
│  0. 检查 SDK init 参数（独立 SDK 必做）              │
│     → 判断目标 JS 是否为独立加载的 SDK               │
│     → 如果是，用 js-reverse-mcp 在浏览器中抓取       │
│       init/setup 函数的调用参数                      │
├─────────────────────────────────────────────────┤
│  1. 首次诊断（不加载任何 env 模块）                  │
│     → 获取 undefinedPaths 列表                     │
├─────────────────────────────────────────────────┤
│  2. 根据 undefinedPaths 选择 env 模块               │
│     → 查 references/env-modules.md 做前缀匹配       │
│     → 按 references/loading-order.md 排序           │
├─────────────────────────────────────────────────┤
│  3. 带模块重新诊断                                  │
│     → 检查 undefinedPaths 是否减少                  │
│     → 如果有新错误，分析原因                         │
├─────────────────────────────────────────────────┤
│  4. 处理剩余 undefinedPaths                         │
│     → 框架模块已覆盖 → 检查加载顺序问题              │
│     → 框架模块未覆盖 → 写 ai-generated 补丁         │
│     → 需要真实浏览器参数 → 用 js-reverse-mcp 采集    │
├─────────────────────────────────────────────────┤
│  5. 循环 3-4 直到成功或稳定                          │
└─────────────────────────────────────────────────┘
```

## 步骤详解

### 步骤 0：检查 SDK init 参数

**适用条件**：目标 JS 是独立加载的安全 SDK（从独立 CDN 加载，不和业务代码打包在一起）。

**判断标志**（满足任一即需要检查）：
- 调用链中有"胶水层/调度层"（如 sdk-glue.js、captcha/index.js）调用目标 JS 的 init/setup
- 加密不是直接调用 `sign()` 函数，而是通过 XHR/fetch hook 拦截机制工作
- 目标 JS 暴露了 `init`/`setup`/`config` 等初始化方法

**操作方法**：
1. 在浏览器中找到调用 `SDK.init(params)` 的位置（通常在胶水层/调度层）
2. 设断点抓取 init 的参数：
   ```
   set_breakpoint_on_text("SDK.init(", urlFilter: "sdk-glue")
   → 刷新页面 → evaluate_script 读取参数
   ```
3. 将 init 参数记录到项目文档 `docs/progress.md`

**init 参数的常见内容**：
- `aid` — 应用标识
- `paths` / `urls` / `patterns` — URL 匹配规则（决定哪些请求需要签名）
- `boe` / `debug` — 调试开关
- 版本号、功能开关等

**⚠️ 教训**：不传正确的 init 参数，SDK 可能加载成功、hook 生效，但签名逻辑被**静默跳过**（无报错），导致补环境看起来一切正常却始终拿不到签名值，极难排查。

### 步骤 1：首次诊断

```bash
node $SKILL_DIR/scripts/diagnose.js <目标脚本.js>
```

解读输出：
- `success: true` + 无 undefinedPaths → 脚本已可运行，可能不需要补环境
- `success: false` + error 包含 "is not defined" → 缺少全局对象
- `undefinedPaths` 列表 → 需要补全的属性

### 步骤 2：选择 env 模块

1. 读取 `$SKILL_DIR/references/env-modules.md`
2. 对每个 undefinedPath 提取前缀（第一个 `.` 前的部分）
3. 前缀匹配 → 找到对应模块
4. 按 `$SKILL_DIR/references/loading-order.md` 中的标准顺序排列
5. 自动补充依赖（如选了 `dom/elements.js` 必须加 `dom/document.js`）

### 步骤 3：带模块诊断

```bash
node $SKILL_DIR/scripts/diagnose.js \
  --env bom/navigator.js,bom/location.js,bom/crypto.js,dom/document.js \
  <目标脚本.js>
```

比较前后结果：
- undefinedPaths 减少 → 模块选择正确
- 出现新错误 → 可能是加载顺序问题或模块间依赖
- undefinedPaths 不变 → 可能需要真实值覆盖默认值

### 步骤 4：处理剩余问题

#### 4a. 写 ai-generated 补丁

当 undefinedPath 不在任何现有模块中时，创建补丁文件：

```javascript
// 文件: env/ai-generated/<属性名>_<timestamp>.js
(() => {
    'use strict';
    // 补全 window.someProperty
    Object.defineProperty(window, 'someProperty', {
        value: /* 合理的默认值 */,
        writable: false,
        configurable: true,
        enumerable: true
    });
})();
```

**补丁规范**：
- IIFE 包裹，`'use strict'`
- 用 `Object.defineProperty` 设置属性
- 提供合理的默认值（参考真实浏览器行为）
- 文件名格式: `<对象>_<属性>_<Date.now()>.js`
- 保存到框架的 `env/ai-generated/` 目录

#### 4b. 用 js-reverse-mcp 采集真实参数

当需要真实浏览器指纹值时（如特定网站的 navigator.userAgent、document.cookie 等），使用 js-reverse-mcp 的浏览器操作工具：

1. 用 `mcp__js-reverse__new_page` 打开目标网站
2. 用 `mcp__js-reverse__evaluate_script` 采集需要的属性值：
   ```javascript
   () => ({
       userAgent: navigator.userAgent,
       platform: navigator.platform,
       language: navigator.language,
       // ... 根据需要采集
   })
   ```
3. 将采集的值写入补丁文件

#### 4c. 应用 mock 预设

对于指纹对抗场景，参考框架的 mock 预设：
- **anti-detect**: navigator.webdriver、window.chrome 等
- **canvas-fp**: Canvas 指纹相关
- **webgl-fp**: WebGL 指纹相关
- **audio-fp**: AudioContext 指纹相关

预设配置参考框架的 `config/mock-rules.json`（如已安装外部框架）

### 步骤 5：循环判断

**继续循环的条件**：
- undefinedPaths 在减少（进展中）
- 错误信息在变化（说明在推进）
- 还有明确的可修复路径

**停止循环的条件**：
- `success: true` 且 undefinedPaths 为空或仅剩无关项
- 连续 2 轮 undefinedPaths 完全相同（陷入死循环）
- undefinedPaths 全部是 Proxy 监控的内部属性（如 `Symbol(*)` 开头）

**⚠️ `success: true` 不等于"能用"**：diagnose.js 的 success 只代表**脚本加载没报错**，不代表目标功能（签名、加密等）可用。必须进入步骤 6 做功能验证。

**向用户报告的时机**：
- 首次诊断后，告诉用户发现了哪些缺失环境
- 每轮补全后，报告进展（减少了多少 undefinedPaths）
- 遇到需要真实浏览器参数的情况
- 最终成功或无法继续时

### 步骤 6：功能验证

`success: true` 之后，必须验证目标功能是否真正可用。这一步在项目的 `env/run.js` 中完成，不是在 diagnose.js 中。

**验证流程**：

1. 编写 `env/run.js`，加载与 diagnose.js 相同的 env 模块 + 目标脚本
2. 按目标脚本的工作方式触发功能（如模拟 XHR 触发签名）
3. 检查输出是否符合预期（如 a_bogus 长度 192、格式正确）

**针对 hook 型 SDK（如 bdms.js）的验证要点**：

```
加载顺序（严格）：
  env 模块 → fake XMLHttpRequest → 目标 JS → hook 捕获代码 → init(配置) → 触发 XHR
                 ↑                                  ↑
          必须在目标 JS 之前              必须在目标 JS 之后
          （目标 JS 要 hook 它）          （目标 JS 可能重定义相关 API）
```

- **fake XMLHttpRequest**：在目标 JS 之前注入（目标 JS 加载时立即 hook prototype）
- **hook 捕获代码**（如 `URLSearchParams.append` hook）：在目标 JS **之后**注入（目标 JS 可能包含 polyfill 覆盖原生 API）
- **init 调用**：在目标 JS 加载之后、触发之前（传入步骤 0 采集的参数）
- **触发**：用标准 XHR `open → setRequestHeader → send` 流程

**常见的"加载成功但功能不工作"原因**：

| 现象 | 原因 | 排查方法 |
|------|------|---------|
| 签名值为 undefined | 缺少 crypto.js / performance.js | 检查 env 模块列表 |
| hook 执行了但不签名 | 缺少 init 参数（尤其是 paths） | 浏览器断点抓 init 参数（步骤 0） |
| 捕获 hook 没命中 | hook 被目标 JS 的 polyfill 覆盖 | 将 hook 移到目标 JS 加载之后 |
| JSVMP 静默失败 | 内部 try-catch 吞错误 | 在 JSVMP 错误出口 `f=3,void(l=` 注入 console.warn |
| 签名长度不对 | 环境指纹与真实浏览器有差异 | 用 js-reverse-mcp 采集真实浏览器参数覆盖默认值 |

### 步骤 7：封装 + HTTP 中间层 + 请求验证

步骤 6 确认签名值能生成后，需要封装为可调用的服务，并用实际请求验证签名是否被服务器接受。

**7a. sign.js — 签名接口模块**

从 `env/run.js` 提取签名逻辑，封装为可导出的函数：

```javascript
// env/sign.js
// ... 与 run.js 相同的环境构建 + 目标 JS 加载 ...

function sign(url) {
    window.__captured_a_bogus = null;
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
    xhr.send(null);
    return window.__captured_a_bogus;
}

module.exports = sign;

// 命令行模式
if (require.main === module) {
    const url = process.argv[2] || process.env.SIGN_URL;
    if (!url) { console.error('Usage: node sign.js <url>'); process.exit(1); }
    process.stdout.write(sign(url) || '');
}
```

**7b. server.js — HTTP 中间层**

通过 HTTP 服务调用签名，避免 subprocess 的环境差异问题：

```javascript
// env/server.js
const http = require('http');
const sign = require('./sign');
const PORT = 3456;

const server = http.createServer((req, res) => {
    if (req.method === 'POST' && req.url === '/sign') {
        let body = '';
        req.on('data', chunk => body += chunk);
        req.on('end', () => {
            try {
                const { url } = JSON.parse(body);
                const result = sign(url);
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ result: result || '', length: (result || '').length }));
            } catch (e) {
                res.writeHead(500, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ error: e.message }));
            }
        });
    } else { res.writeHead(404); res.end('Not Found'); }
});
server.listen(PORT, () => console.log(`sign server on http://localhost:${PORT}/sign`));
```

**7c. Python 验证脚本**

```python
# python/verify.py
from curl_cffi import requests
from urllib.parse import urlencode

SIGN_SERVER = "http://localhost:3456/sign"

def get_sign(url):
    resp = requests.post(SIGN_SERVER, json={"url": url}, impersonate="chrome136")
    return resp.json().get("result") or None

# headers / cookies / params 从浏览器抓包填入
headers = { "user-agent": "...", "referer": "..." }
cookies = { "ttwid": "...", "odin_tt": "..." }  # 必须用 dict
params = { "aid": "6383", "sec_user_id": "...", "msToken": "..." }

base_url = "https://目标域名/api/path/"
full_url = base_url + "?" + urlencode(params)
sign_value = get_sign(full_url)
params["a_bogus"] = sign_value

# ⚠️ params 用 dict 传 params=，cookies 用 dict 传 cookies=
response = requests.get(base_url, headers=headers, cookies=cookies, params=params, impersonate="chrome136")
print(response.status_code, response.json())
```

**验证顺序**：
1. **格式验证** — `node env/run.js`，签名长度与浏览器一致
2. **HTTP 中间层验证** — 启动 `node env/server.js`，curl 测试：
   ```bash
   curl -X POST localhost:3456/sign -H 'Content-Type: application/json' -d '{"url":"https://..."}'
   ```
3. **请求验证** — 运行 `python python/verify.py`，确认服务器返回正常数据

**⚠️ Python 请求注意事项**：
- **必须**用 `requests.get(base_url, params=params, cookies=cookies)` 标准写法
- **禁止**手动 `urllib.parse.quote(a_bogus)` 拼 URL — 编码不一致导致签名校验失败
- **禁止**把 cookie 字符串塞进 `headers["cookie"]` — 必须用 dict 传 `cookies=`
- **禁止**用 subprocess 调 sign.js — 必须通过 HTTP 中间层

## 关键注意事项

1. **diagnose.js 自动加载 ProxyMonitor.js**（提供 `watch()` 函数）；**ProxyEnv.js 仅在未指定 `--env` 时加载**（避免 `configurable:false` 冲突）
2. **env 模块路径相对于 `env/` 目录**，如 `bom/navigator.js` 而非 `env/bom/navigator.js`
3. **`dom/elements.js` 必须在 `dom/document.js` 之后**
4. **`webapi/network.js` 必须在 `webapi/xhr.js` 和 `webapi/fetch.js` 之后**
5. **对于 JSVMP 类脚本（如 a_bogus），`crypto.js` 和 `performance.js` 是关键依赖**
6. **环境必须在目标脚本之前加载**（diagnose.js 已确保这个顺序）
7. **consoleOutput 中的 `[ProxyMonitor]` 前缀日志**是框架监控输出，不是目标脚本的输出
8. **`success: true` ≠ 功能可用**：diagnose.js 只验证脚本能加载，不验证签名/加密等功能是否正常工作。必须在 `env/run.js` 中做功能验证（步骤 6）
9. **hook 捕获代码必须在目标 JS 之后注入**：目标 JS 可能包含 polyfill（如 URLSearchParams），会覆盖在它之前注入的 hook。正确顺序：`env 模块 → fake 全局对象 → 目标 JS → hook 捕获代码 → init → 触发`
10. **独立 SDK 的 init 参数决定签名行为**：不传正确 init 参数，SDK 加载成功、hook 生效，但签名被静默跳过（无报错）。见步骤 0

## 补环境的常见模式

### 模式 1：简单属性补全
undefinedPath 是简单属性（如 `navigator.userAgent`）→ 对应模块直接覆盖

### 模式 2：方法调用补全
undefinedPath 是方法（如 `document.createElement`）→ 需要加载完整的 DOM 模块

### 模式 3：链式属性补全
undefinedPath 很深（如 `window.crypto.subtle.digest`）→ 需要加载提供整个对象树的模块

### 模式 4：构造函数补全
error 包含 "X is not a constructor" → 需要加载定义该类的模块（如 `TextEncoder` → `encoding/textencoder.js`）

### 模式 5：指纹对抗
目标脚本检测 `navigator.webdriver`、Canvas 指纹等 → 加载模块 + 应用 anti-detect mock 预设

