> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 补环境
- URL: https://blog.vercanti.com/bu-huan-jing/
- Published: 2026-08-28T14:35:13.000Z
- Updated: 2026-08-28T14:58:19.000Z
- Description: 将从网站中提取的 JS 加密代码放到 Node.js 中独立运行时，由于缺少浏览器原生对象，通常会报错。补环境就是在 Node.js 中模拟出浏览器的运行环境，让代码能够正常执行。 使用 Proxy 拦截对任意属性的访问，自动打印缺失的环境属性，用于快速定位需要补的内容。 运行后控制台会输出所有访问了但不存在的属性，按需补充即可。 部分网站使用 Canvas 绘图结果作为设备指纹。 对于依赖完整 DOM 环境的代码，使用 jsdom 库更省力。适合页面脚本需要 document、navigator、Cookie 等完整 BOM/DOM 环境的场景。 先用
- Author: yellowdog
- Tags: js逆向, 技巧步骤

> 官方文档：<https://nodejs.org/en/docs/>  
> 适用场景：将浏览器 JS 加密代码移植到 Node.js 独立运行，补充缺失的浏览器 API

将从网站中提取的 JS 加密代码放到 Node.js 中独立运行时，由于缺少浏览器原生对象，通常会报错。补环境就是在 Node.js 中模拟出浏览器的运行环境，让代码能够正常执行。

---

## 环境差异

| 对象/API         | 浏览器           | Node.js           |
| -------------- | ------------- | ----------------- |
| window         | 存在（全局对象）      | 不存在               |
| document       | 存在            | 不存在               |
| navigator      | 存在            | 不存在               |
| location       | 存在            | 不存在               |
| localStorage   | 存在            | 不存在               |
| XMLHttpRequest | 存在            | 不存在               |
| canvas         | 存在            | 不存在               |
| fetch          | 存在            | Node 18+ 原生支持     |
| crypto         | window.crypto | require('crypto') |

---

## 基础补环境模板

```javascript
// env.js - 放在扣出的加密代码之前执行

// 补全全局对象
global.window = global;
global.self = global;

// 补 navigator
global.navigator = {
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    platform: 'Win32',
    language: 'zh-CN',
    languages: ['zh-CN', 'zh', 'en'],
    cookieEnabled: true,
    javaEnabled: function() { return false; },
    plugins: [],
    appName: 'Netscape',
};

// 补 location
global.location = {
    href: 'https://www.example.com/',
    origin: 'https://www.example.com',
    protocol: 'https:',
    host: 'www.example.com',
    hostname: 'www.example.com',
    pathname: '/',
    search: '',
    hash: '',
};

// 补 document
global.document = {
    cookie: '',
    domain: 'example.com',
    referrer: '',
    createElement: function(tag) { return {}; },
    getElementById: function() { return null; },
    querySelector: function() { return null; },
};

// 补 localStorage
global.localStorage = (function() {
    var store = {};
    return {
        getItem: function(key) { return store[key] || null; },
        setItem: function(key, val) { store[key] = String(val); },
        removeItem: function(key) { delete store[key]; },
        clear: function() { store = {}; },
    };
})();

// 补 sessionStorage
global.sessionStorage = global.localStorage;

// 补 screen
global.screen = {
    width: 1920,
    height: 1080,
    colorDepth: 24,
    pixelDepth: 24,
};

// 补 history
global.history = {
    length: 1,
    pushState: function() {},
    replaceState: function() {},
};

```

---

## Proxy 通杀补环境

使用 `Proxy` 拦截对任意属性的访问，自动打印缺失的环境属性，用于快速定位需要补的内容。

```javascript
// 使用 Proxy 检测缺失的环境变量
function createProxyEnv(name, target = {}) {
    return new Proxy(target, {
        get(obj, prop) {
            if (!(prop in obj)) {
                console.log(`[缺失] ${name}.${String(prop)}`);
                return undefined;
            }
            return obj[prop];
        },
        set(obj, prop, value) {
            obj[prop] = value;
            return true;
        }
    });
}

global.window = createProxyEnv('window', global);
global.navigator = createProxyEnv('navigator', {
    userAgent: 'Mozilla/5.0 ...'
});
global.document = createProxyEnv('document', {});

```

运行后控制台会输出所有访问了但不存在的属性，按需补充即可。

---

## Canvas 指纹补环境

部分网站使用 Canvas 绘图结果作为设备指纹。

```javascript
// 简单的 canvas 补环境
global.document.createElement = function(tag) {
    if (tag === 'canvas') {
        return {
            width: 0,
            height: 0,
            getContext: function() {
                return {
                    fillStyle: '',
                    fillRect: function() {},
                    fillText: function() {},
                    font: '',
                    textBaseline: '',
                    toDataURL: function() {
                        // 返回固定值或真实 canvas 计算结果
                        return 'data:image/png;base64,iVBORw0KGgo=';
                    }
                };
            },
            toDataURL: function() {
                return 'data:image/png;base64,iVBORw0KGgo=';
            }
        };
    }
    return {};
};

```

---

## jsdom 方案

对于依赖完整 DOM 环境的代码，使用 `jsdom` 库更省力。适合页面脚本需要 `document`、`navigator`、Cookie 等完整 BOM/DOM 环境的场景。

```javascript
const { JSDOM } = require('jsdom');

const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
    url: 'https://www.example.com/',
    referrer: 'https://www.example.com/',
    contentType: 'text/html',
    runScripts: 'dangerously',   // 允许执行页面内嵌脚本
    pretendToBeVisual: true,     // 启用 requestAnimationFrame 等视觉 API
    userAgent: 'Mozilla/5.0 ...'
});

global.window    = dom.window;
global.document  = dom.window.document;
global.navigator = dom.window.navigator;
global.location  = dom.window.location;

```

> jsdom 的完整 API 说明（构造选项、VirtualConsole、CookieJar、资源拦截、Proxy 补环境、反检测等）请参考 → [jsdom完全指南](https://blog.vercanti.com/jsdom-wan-quan-zhi-nan-jsni-xiang-shi-zhan/)

---

## 常见报错及处理

| 报错信息                                          | 原因              | 解决方案                                     |
| --------------------------------------------- | --------------- | ---------------------------------------- |
| window is not defined                         | 缺少 window 对象    | global.window = global                   |
| document is not defined                       | 缺少 document     | 补 document 对象                            |
| navigator is not defined                      | 缺少 navigator    | 补 navigator 对象                           |
| Cannot read property 'userAgent' of undefined | navigator 未正确补全 | 补 navigator.userAgent                    |
| localStorage is not defined                   | 缺少 localStorage | 补 localStorage                           |
| btoa is not defined                           | Node 较老版本       | 使用 Buffer.from(str).toString('base64')   |
| atob is not defined                           | Node 较老版本       | 使用 Buffer.from(str, 'base64').toString() |

---

## 补环境工作流

```
1. 将加密函数代码复制到本地 .js 文件
2. 在文件头部引入 env.js（基础环境）
3. 使用 Proxy 包装全局对象，记录缺失属性
4. node 运行，按控制台输出逐步补充缺失的属性/方法
5. 确认加密函数能正常返回结果
6. 在 Python 中通过 subprocess 或 PyExecJS 调用该 Node.js 脚本

```

---

## Python 调用 Node.js

```python
import subprocess
import json

def call_js_encrypt(data):
    result = subprocess.run(
        ['node', 'encrypt.js', json.dumps(data)],
        capture_output=True,
        text=True
    )
    return result.stdout.strip()

# encrypt.js 最后一行：
# const input = JSON.parse(process.argv[2]);
# console.log(encrypt(input));

```

---

## 最佳实践

**先用 Proxy 探测再针对性补**：在 `global.window = new Proxy({}, { get(t,k){ console.log(k); return undefined } })` 运行目标代码，观察所有访问的属性后，再挑关键属性补充真实实现，避免盲目补充。

**补环境代码放在单独文件，与目标 JS 分离**：`env.js` 只包含浏览器环境模拟，通过 `require('./env')` 在目标 JS 前引入，保持目标 JS 不被修改，方便追踪目标代码更新时的变化。

**关键 API 优先补：`crypto`、`navigator`、`location`、`document`**：绝大多数加密代码会访问这 4 个对象；`crypto.getRandomValues` 用 `require('crypto').randomFillSync` 实现，`navigator.userAgent` 设为真实 UA 字符串。

**用 `vm.runInNewContext` 隔离全局变量污染**：多段目标 JS 代码独立运行时，用 `vm.runInNewContext(code, { window: {...} })` 各自隔离，避免变量名冲突；适合批量并发处理多个请求。

**Node.js 调用链加 `--timeout` 保护**：目标 JS 可能含无限循环或死锁的反调试，通过 `subprocess` 调用 Node.js 时设置超时（`timeout=5`），避免主进程被卡死。

---

## 常见陷阱

### 陷阱：补了 `window.document` 但 DOM 操作仍然失败

**现象：** 补了 `document = { querySelector: () => null }` 后，代码仍然报 `TypeError: document.getElementById is not a function`。  
**原因：** 补的对象没有覆盖代码所有用到的 DOM API，遇到未补的方法就报错。  
**解决：** 补一个 Proxy 代理替代 `document`，让所有方法调用默认返回空对象或 null，再逐步替换为真实实现；或用 jsdom 提供完整 DOM 实现。

### 陷阱：`window.location.href` 赋值报错

**现象：** 目标 JS 中有 `window.location.href = url` 导致 Node.js 报 `TypeError: Cannot set property href of [object Object]`。  
**原因：** 补的 `location` 对象的 `href` 是普通属性，部分代码用 setter 赋值跳转，而 Node.js 不需要实际跳转。  
**解决：** 用 `Object.defineProperty(location, 'href', { get: () => 'https://target.com', set: () => {} })` 让赋值变成空操作。

### 陷阱：`eval` 内的代码引用了外部变量但访问不到

**现象：** 目标 JS 在 `eval` 中运行部分代码，但变量是 `undefined`。  
**原因：** `eval` 的作用域与当前闭包相关，在 Node.js 的模块作用域中直接 `eval` 不能访问全局变量（需要 `global.xxx`）；或用 `vm.runInNewContext` 时上下文中缺少该变量。  
**解决：** 将全局变量挂到 `global` 上（`global.xxx = value`），或在 `vm.runInNewContext` 的 context 对象中包含所有需要的变量。

---

## 参见

[jsdom完全指南](https://blog.vercanti.com/jsdom-wan-quan-zhi-nan-jsni-xiang-shi-zhan/)  
[RPC远程调用](https://blog.vercanti.com/rpc-yuan-cheng-diao-yong/)  
[Proxy对象与Hook](https://blog.vercanti.com/proxy-dui-xiang-yu-hook/)  
[js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)