Node.js 运行环境
本文专注于逆向还原场景下的 Node.js 使用,包括如何在本地运行扣出的 JS 代码、利用内置模块辅助逆向分析,以及补全浏览器环境。 vm 模块允许在隔离的沙箱上下文中执行代码,是运行不受信任 JS 代码的常用方式。 vm.runInNewContext(code, sandbox, options) vm.Script 适合需要多次执行同一段代码的场景(预编译): vm.createContext(sandbox) 将一个普通对象提升为 vm 上下文(contextify): 逆向场景推荐使用 vm 而非 eval,原因:扣出的浏览器代码通常会访问
官方文档:https://nodejs.org/en/docs/
适用版本:Node.js 20 LTS+(2026-05-08 核实)
本文专注于逆向还原场景下的 Node.js 使用,包括如何在本地运行扣出的 JS 代码、利用内置模块辅助逆向分析,以及补全浏览器环境。
运行扣出的 JS 代码
基本执行
# 直接运行脚本文件
node script.js
# 执行单行代码
node -e "console.log(require('crypto').createHash('md5').update('hello').digest('hex'))"
# 交互式 REPL
node
vm 模块
vm 模块允许在隔离的沙箱上下文中执行代码,是运行不受信任 JS 代码的常用方式。
vm.runInNewContext(code, sandbox, options)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
code |
string |
必填 | 要执行的 JS 代码字符串 |
sandbox |
object |
{} |
沙箱对象,代码中的全局变量来自此对象 |
options.filename |
string |
'evalmachine.<anonymous>' |
错误堆栈中显示的文件名 |
options.lineOffset |
number |
0 |
错误堆栈中的行偏移量 |
options.timeout |
number |
无限制 | 执行超时(毫秒),超时抛出错误 |
options.breakOnSigint |
boolean |
false |
Ctrl+C 时是否中断执行 |
options.contextName |
string |
'VM Context' |
上下文名称(调试用) |
const vm = require('vm');
const sandbox = {
window: {},
result: null
};
vm.runInNewContext(`
function encrypt(data) {
return data.split('').reverse().join('');
}
result = encrypt('hello');
`, sandbox);
console.log(sandbox.result); // 'olleh'
vm.Script
适合需要多次执行同一段代码的场景(预编译):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
code |
string |
必填 | 要编译的 JS 代码字符串 |
options.filename |
string |
'evalmachine.<anonymous>' |
错误堆栈文件名 |
options.lineOffset |
number |
0 |
行偏移量 |
options.columnOffset |
number |
0 |
列偏移量 |
options.cachedData |
Buffer |
无 | 预编译字节码缓存 |
const vm = require('vm');
const script = new vm.Script(`
var count = (typeof count === 'undefined') ? 0 : count;
count++;
`);
const ctx = vm.createContext({ count: 0 });
for (let i = 0; i < 5; i++) {
script.runInContext(ctx);
}
console.log(ctx.count); // 5
vm.createContext(sandbox)
将一个普通对象提升为 vm 上下文(contextify):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sandbox |
object |
{} |
作为全局对象的基础对象,执行后属性会被写入此对象 |
const vm = require('vm');
const ctx = vm.createContext({
window: {},
console: console, // 允许沙箱内使用 console
require: require // 谨慎:允许沙箱访问 require 会破坏隔离性
});
const script = new vm.Script('window.x = 42;');
script.runInContext(ctx);
console.log(ctx.window.x); // 42
vm 与直接 eval 的区别
| 对比项 | eval |
vm.runInNewContext |
|---|---|---|
| 作用域 | 共享当前作用域,可访问外部变量 | 独立沙箱,无法访问外部变量 |
| 全局对象 | 共享 global |
使用传入的 sandbox 对象 |
| 错误隔离 | 无隔离,错误会污染当前进程 | 相对隔离(但不是完全安全隔离) |
| 适用场景 | 简单快速执行,信任代码 | 执行不确定代码,需要控制全局环境 |
| 性能 | 较高 | 略低(上下文创建有开销) |
逆向场景推荐使用 vm 而非 eval,原因:扣出的浏览器代码通常会访问 window、document 等全局变量,通过 sandbox 可以精确控制这些变量的值。
模块系统
CommonJS
// 导出
// utils.js
function md5(str) { /* ... */ }
module.exports = { md5 };
// 或
module.exports.md5 = function(str) { /* ... */ };
// 导入
const utils = require('./utils');
utils.md5('hello');
// 导入内置模块
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
ESM
Node.js 支持 ESM,但有以下限制:
- 文件扩展名必须为
.mjs,或package.json中设置"type": "module" - 不能直接使用
require(需要用createRequire代替) - 顶层
await可用
// ESM 导入导出
import { encrypt } from './crypto.mjs';
export function sign(data) { return encrypt(data); }
// 在 ESM 中使用 require
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const fs = require('fs'); // 现在可以用 require 了
动态 require 加载文件
// 动态加载(路径在运行时确定)
const moduleName = 'crypto';
const mod = require(moduleName);
// 加载并执行 JS 文件(常用于加载扣出的 bundle)
const bundle = require('./extracted_bundle.js');
// 强制重新加载(绕过缓存)
delete require.cache[require.resolve('./target.js')];
const fresh = require('./target.js');
逆向还原常用内置模块
fs:读写文件
fs.readFileSync(path, options)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
path |
string | Buffer | URL |
必填 | 文件路径 |
options.encoding |
string | null |
null |
编码格式,如 'utf8';为 null 时返回 Buffer |
options.flag |
string |
'r' |
文件系统标志 |
fs.writeFileSync(path, data, options)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
path |
string | Buffer | URL |
必填 | 文件路径,不存在则创建 |
data |
string | Buffer | Uint8Array |
必填 | 要写入的内容 |
options.encoding |
string |
'utf8' |
编码格式 |
options.flag |
string |
'w' |
'w' 覆盖,'a' 追加 |
options.mode |
integer |
0o666 |
文件权限 |
const fs = require('fs');
// 读取扣出的 JS 文件
const code = fs.readFileSync('./bundle.js', 'utf8');
// 读取二进制文件(如 wasm)
const wasmBuf = fs.readFileSync('./target.wasm'); // 返回 Buffer
// 将结果写入文件
fs.writeFileSync('./result.txt', JSON.stringify(results, null, 2), 'utf8');
// 追加日志
fs.writeFileSync('./log.txt', `[${new Date().toISOString()}] ${msg}\n`, { flag: 'a' });
path:路径拼接
const path = require('path');
// 拼接路径(跨平台)
const filePath = path.join(__dirname, 'files', 'bundle.js');
// 解析绝对路径
const absPath = path.resolve('./bundle.js');
// 获取目录名、文件名、扩展名
path.dirname('/home/user/file.js'); // '/home/user'
path.basename('/home/user/file.js'); // 'file.js'
path.extname('/home/user/file.js'); // '.js'
crypto:内置加密模块
crypto.createHash(algorithm)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
algorithm |
string |
必填 | 算法名,如 'md5', 'sha1', 'sha256', 'sha512' |
返回 Hash 对象,调用 .update(data).digest(encoding) 获取结果。
crypto.createHmac(algorithm, key)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
algorithm |
string |
必填 | 算法名,如 'sha256' |
key |
string | Buffer | KeyObject |
必填 | HMAC 密钥 |
crypto.createCipheriv(algorithm, key, iv)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
algorithm |
string |
必填 | 算法名,如 'aes-128-cbc', 'aes-256-cbc' |
key |
string | Buffer |
必填 | 密钥,长度需与算法匹配 |
iv |
string | Buffer | null |
必填 | 初始化向量(ECB 模式传 null) |
const crypto = require('crypto');
// MD5
const md5 = crypto.createHash('md5').update('hello').digest('hex');
// SHA256
const sha256 = crypto.createHash('sha256').update('hello', 'utf8').digest('hex');
// HMAC-SHA256
const hmac = crypto.createHmac('sha256', 'secret_key')
.update('data')
.digest('hex');
// AES-128-CBC 加密
const key = Buffer.from('0123456789abcdef'); // 16 字节
const iv = Buffer.from('fedcba9876543210'); // 16 字节
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
let encrypted = cipher.update('plaintext', 'utf8', 'hex');
encrypted += cipher.final('hex');
// AES-128-CBC 解密
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
// 生成随机字节
const randomBytes = crypto.randomBytes(16).toString('hex');
https / http:发请求
const https = require('https');
// GET 请求
function get(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
}).on('error', reject);
});
}
// POST 请求(带 headers)
function post(url, body, headers = {}) {
const bodyStr = JSON.stringify(body);
const urlObj = new URL(url);
return new Promise((resolve, reject) => {
const req = https.request({
hostname: urlObj.hostname,
path: urlObj.pathname + urlObj.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(bodyStr),
...headers
}
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
});
req.on('error', reject);
req.write(bodyStr);
req.end();
});
}
对比 axios:内置 https 模块更底层,无需安装依赖,适合简单请求或生产环境;axios 更简洁,适合开发调试阶段。逆向脚本推荐 axios 提高效率,生产还原脚本用内置模块避免依赖。
Buffer:二进制数据处理
Buffer.from(data, encoding)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
data |
string | Array | ArrayBuffer |
必填 | 数据来源 |
encoding |
string |
'utf8' |
当 data 为字符串时的编码:'utf8', 'hex', 'base64', 'latin1' |
buf.toString(encoding, start, end)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
encoding |
string |
'utf8' |
目标编码:'hex', 'base64', 'utf8', 'latin1' |
start |
number |
0 |
开始字节索引 |
end |
number |
buf.length |
结束字节索引(不含) |
// 字符串 -> Buffer -> hex
const buf = Buffer.from('hello', 'utf8');
console.log(buf.toString('hex')); // '68656c6c6f'
console.log(buf.toString('base64')); // 'aGVsbG8='
// hex 字符串 -> Buffer -> 原始字节
const hexBuf = Buffer.from('68656c6c6f', 'hex');
console.log(hexBuf.toString('utf8')); // 'hello'
// base64 解码
const b64Buf = Buffer.from('aGVsbG8=', 'base64');
console.log(b64Buf.toString('utf8')); // 'hello'
// 拼接多个 Buffer
const combined = Buffer.concat([buf1, buf2]);
// Buffer 与 Uint8Array 互转
const uint8 = new Uint8Array(buf);
const backToBuf = Buffer.from(uint8.buffer, uint8.byteOffset, uint8.byteLength);
补全浏览器环境
浏览器 JS 代码运行在 Node.js 时,通常会访问 window、document、navigator 等全局对象。需要手动补全,按报错逐步添加。
常用 polyfill 写法
// 最常用的基础补全
global.window = global;
global.self = global;
// location 对象
global.location = {
href: 'https://www.example.com/',
origin: 'https://www.example.com',
protocol: 'https:',
host: 'www.example.com',
hostname: 'www.example.com',
port: '',
pathname: '/',
search: '',
hash: ''
};
// navigator 对象
global.navigator = {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
platform: 'Win32',
language: 'zh-CN',
languages: ['zh-CN', 'zh', 'en'],
cookieEnabled: true,
onLine: true,
vendor: 'Google Inc.'
};
// document 基础补全
global.document = {
cookie: '',
referrer: 'https://www.example.com/',
title: '',
URL: 'https://www.example.com/',
domain: 'www.example.com',
createElement: function(tag) { return { style: {} }; },
getElementById: function(id) { return null; },
querySelector: function(sel) { return null; },
querySelectorAll: function(sel) { return []; }
};
// screen 对象
global.screen = {
width: 1920,
height: 1080,
colorDepth: 24,
pixelDepth: 24
};
// atob / btoa(Node.js >= 16 已内置,旧版本需手动补)
if (typeof atob === 'undefined') {
global.atob = (str) => Buffer.from(str, 'base64').toString('latin1');
global.btoa = (str) => Buffer.from(str, 'latin1').toString('base64');
}
// performance
global.performance = {
now: function() { return Date.now(); },
timing: { navigationStart: Date.now() }
};
// localStorage / sessionStorage(简单 mock)
global.localStorage = (function() {
const store = {};
return {
getItem: (k) => store[k] || null,
setItem: (k, v) => { store[k] = String(v); },
removeItem: (k) => { delete store[k]; },
clear: () => { Object.keys(store).forEach(k => delete store[k]); }
};
})();
jsdom 快速集成
对于需要更完整 DOM 环境的场景(如代码操作 DOM 元素),直接使用 jsdom。详细用法参见 jsdom完全指南。
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',
pretendToBeVisual: true
});
// 将 jsdom 的 window 挂载到 global
const { window } = dom;
global.window = window;
global.document = window.document;
global.navigator = window.navigator;
global.location = window.location;
// 现在可以运行浏览器代码了
eval(fs.readFileSync('./bundle.js', 'utf8'));
调试技巧
--inspect / --inspect-brk 开启 Chrome DevTools 调试
# 启动调试服务器,等待连接后才执行代码
node --inspect-brk script.js
# 启动调试服务器,不等待,代码立即执行
node --inspect script.js
# 指定端口(默认 9229)
node --inspect-brk=0.0.0.0:9230 script.js
连接方式:
- 在 Chrome 地址栏输入
chrome://inspect - 点击 "Configure..." 添加
localhost:9229 - 在 "Remote Target" 下点击 "inspect" 打开 DevTools
--inspect-brk 会在第一行代码处暂停,适合调试启动阶段的问题。--inspect 不暂停,适合在代码中手动设置断点(debugger 语句)。
console.log 调试 vs 断点调试
| 对比项 | console.log |
断点调试 |
|---|---|---|
| 适用场景 | 快速验证、批量输出 | 深入分析执行流程 |
| 效率 | 需要重复运行 | 可实时查看所有变量 |
| 对代码的侵入性 | 需要修改代码 | 不修改代码 |
| 异步代码 | 输出顺序可能混乱 | 可暂停,清晰追踪 |
逆向建议:先用 console.log 快速确认入参和返回值,再用断点调试定位内部逻辑。
// 包装目标函数,自动打印参数和返回值
function wrapFn(fn, name) {
return function() {
const args = Array.from(arguments);
console.log(`[${name}] 调用参数:`, args);
const result = fn.apply(this, arguments);
console.log(`[${name}] 返回值:`, result);
return result;
};
}
target.encrypt = wrapFn(target.encrypt, 'encrypt');
最佳实践
用 --inspect 调试扣出的 JS 代码:node --inspect-brk target.js 启动后在 Chrome 打开 chrome://inspect,可以用 DevTools 调试 Node.js 代码,包括断点、变量查看、Call Stack,比 console.log 高效得多。
内置 crypto 模块替代浏览器 crypto.subtle:逆向代码中 crypto.createHash('md5')、crypto.createHmac 等是 Node.js 内置 API,无需安装额外包;浏览器的 crypto.subtle 对应 webcrypto,Node 18+ 的 globalThis.crypto 也可用。
vm.runInNewContext 隔离全局状态:多段目标代码需要并行执行时,用 vm.runInNewContext(code, sandbox) 各自隔离全局变量;比在同一个 global 上运行更安全,避免相互污染。
process.argv 传参比环境变量更简单:在 Python 中通过 subprocess.run(['node', 'sign.js', json.dumps(params)]) 调用,Node.js 里 const params = JSON.parse(process.argv[2]) 接收,比用文件或 stdin 更直接。
ES Modules 支持用 .mjs 或 "type": "module":目标代码用 import/export 时,文件改为 .mjs 扩展名或在 package.json 加 "type": "module" 即可运行,不需要 Babel 转译。
常见陷阱
陷阱:require 在 ESM 模块中不可用
现象: .mjs 文件中用 require('crypto') 报 ReferenceError: require is not defined。
原因: ES Modules 中没有 require,需要用 import crypto from 'crypto' 或 createRequire(import.meta.url) 兼容。
解决: import { createRequire } from 'module'; const require = createRequire(import.meta.url); 后就可以用 require。
陷阱:console.log 输出的对象在某些情况下不完整
现象: console.log(bigObject) 只显示 [Object: null prototype] 或截断了嵌套内容。
原因: Node.js 的 console.log 默认只显示 2 层嵌套,超过层级的显示为 [Object]。
解决: console.log(JSON.stringify(bigObject, null, 2)) 完整输出;或 require('util').inspect(bigObject, { depth: null })。
陷阱:Node.js 版本不兼容目标代码语法
现象: 运行目标 JS 报 SyntaxError: Unexpected token '?.'(可选链)或 Private fields are not supported。
原因: 较老的 Node.js 版本不支持新 JS 语法;Node.js 14 不支持可选链,Node.js 12 不支持逻辑赋值运算符。
解决: 升级到 Node.js 20+ LTS;或用 npx babel target.js --presets @babel/preset-env 将代码降级后运行。