| name | js-env-patcher |
| description | JS逆向沙箱补环境。给定一个混淆 JS 文件,自动完成"执行→诊断缺失环境→补环境→循环直到跑通"的完整流程。 当用户给出一个 JS 文件要求补环境、逆向分析、在沙箱中执行、或提到 a_bogus/JSVMP/指纹/反检测时触发此 skill。
|
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 等初始化方法
操作方法:
- 在浏览器中找到调用
SDK.init(params) 的位置(通常在胶水层/调度层)
- 设断点抓取 init 的参数:
set_breakpoint_on_text("SDK.init(", urlFilter: "sdk-glue")
→ 刷新页面 → evaluate_script 读取参数
- 将 init 参数记录到项目文档
docs/progress.md
init 参数的常见内容:
aid — 应用标识
paths / urls / patterns — URL 匹配规则(决定哪些请求需要签名)
boe / debug — 调试开关
- 版本号、功能开关等
⚠️ 教训:不传正确的 init 参数,SDK 可能加载成功、hook 生效,但签名逻辑被静默跳过(无报错),导致补环境看起来一切正常却始终拿不到签名值,极难排查。
步骤 1:首次诊断
node $SKILL_DIR/scripts/diagnose.js <目标脚本.js>
解读输出:
success: true + 无 undefinedPaths → 脚本已可运行,可能不需要补环境
success: false + error 包含 "is not defined" → 缺少全局对象
undefinedPaths 列表 → 需要补全的属性
步骤 2:选择 env 模块
- 读取
$SKILL_DIR/references/env-modules.md
- 对每个 undefinedPath 提取前缀(第一个
. 前的部分)
- 前缀匹配 → 找到对应模块
- 按
$SKILL_DIR/references/loading-order.md 中的标准顺序排列
- 自动补充依赖(如选了
dom/elements.js 必须加 dom/document.js)
步骤 3:带模块诊断
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 不在任何现有模块中时,创建补丁文件:
(() => {
'use strict';
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 的浏览器操作工具:
- 用
mcp__js-reverse__new_page 打开目标网站
- 用
mcp__js-reverse__evaluate_script 采集需要的属性值:
() => ({
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
})
- 将采集的值写入补丁文件
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 中。
验证流程:
- 编写
env/run.js,加载与 diagnose.js 相同的 env 模块 + 目标脚本
- 按目标脚本的工作方式触发功能(如模拟 XHR 触发签名)
- 检查输出是否符合预期(如 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 提取签名逻辑,封装为可导出的函数:
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 的环境差异问题:
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 验证脚本
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 = { "user-agent": "...", "referer": "..." }
cookies = { "ttwid": "...", "odin_tt": "..." }
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
response = requests.get(base_url, headers=headers, cookies=cookies, params=params, impersonate="chrome136")
print(response.status_code, response.json())
验证顺序:
- 格式验证 —
node env/run.js,签名长度与浏览器一致
- HTTP 中间层验证 — 启动
node env/server.js,curl 测试:
curl -X POST localhost:3456/sign -H 'Content-Type: application/json' -d '{"url":"https://..."}'
- 请求验证 — 运行
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 中间层
关键注意事项
- diagnose.js 自动加载 ProxyMonitor.js(提供
watch() 函数);ProxyEnv.js 仅在未指定 --env 时加载(避免 configurable:false 冲突)
- env 模块路径相对于
env/ 目录,如 bom/navigator.js 而非 env/bom/navigator.js
dom/elements.js 必须在 dom/document.js 之后
webapi/network.js 必须在 webapi/xhr.js 和 webapi/fetch.js 之后
- 对于 JSVMP 类脚本(如 a_bogus),
crypto.js 和 performance.js 是关键依赖
- 环境必须在目标脚本之前加载(diagnose.js 已确保这个顺序)
- consoleOutput 中的
[ProxyMonitor] 前缀日志是框架监控输出,不是目标脚本的输出
success: true ≠ 功能可用:diagnose.js 只验证脚本能加载,不验证签名/加密等功能是否正常工作。必须在 env/run.js 中做功能验证(步骤 6)
- hook 捕获代码必须在目标 JS 之后注入:目标 JS 可能包含 polyfill(如 URLSearchParams),会覆盖在它之前注入的 hook。正确顺序:
env 模块 → fake 全局对象 → 目标 JS → hook 捕获代码 → init → 触发
- 独立 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 预设