补环境
将从网站中提取的 JS 加密代码放到 Node.js 中独立运行时,由于缺少浏览器原生对象,通常会报错。补环境就是在 Node.js 中模拟出浏览器的运行环境,让代码能够正常执行。 使用 Proxy 拦截对任意属性的访问,自动打印缺失的环境属性,用于快速定位需要补的内容。 运行后控制台会输出所有访问了但不存在的属性,按需补充即可。 部分网站使用 Canvas 绘图结果作为设备指纹。 对于依赖完整 DOM 环境的代码,使用 jsdom 库更省力。适合页面脚本需要 document、navigator、Cookie 等完整 BOM/DOM 环境的场景。 先用
官方文档: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') |
基础补环境模板
// 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 拦截对任意属性的访问,自动打印缺失的环境属性,用于快速定位需要补的内容。
// 使用 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 绘图结果作为设备指纹。
// 简单的 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 环境的场景。
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完全指南
常见报错及处理
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
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
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 对象中包含所有需要的变量。