NodeJS
简介
- 兼容性:Node 8.6.0+
- 代码:bytedance/Elkeid: rasp/node
Inspector
根据Node.js的官方文档得知,在Node.js 8之后加入了新的调试机制(Inspector API),在启动时加入”–inspect”参数或者向进程发送SIGUSR1信号,就会激活检查器。
node
kill -USR1 $(pidof node)
激活检查器后,终端会打印信息:
Debugger listening on ws://127.0.0.1:9229/f1f12ee0-4f35-4e61-836c-71d175a607e3
检查器监听本地端口9929,使用websocket协议,协议约定可见文档,连接检查器可以使用node自带的inspect client:
node inspect -p $(pidof node)
连接后可以通过”exec(‘console.log(123)’)”在目标进程中执行node代码,或者执行”repl”开启交互模式。但是自带的inspect client功能简单,如果需要更好的体验,可以使用chrome的调试模块,在chrome中访问”chrome://inspect”即可。
协议
node-inspect 就是检查器客户端的官方代码仓库,通过阅读 inspect_client.js发现,交互协议就是简单的json字符串,每个请求和响应通过id字段一一对应。 例如请求开启调试时,通过websocket协议发送字符串:
{
"id": 1,
"method": "Debugger.enable"
}
检查器返回响应:
{
"id": 1,
"result": {
"debuggerId": "(D750AD89818A150E3155AC1D6ECB7BB)"
}
}
如果你想在目标进程中执行代码,类似于”exec(‘console.log(123)’)”命令,你只需要使用Runtime.evaluate,发送请求:
{
"id": 2,
"method": "Runtime.evaluate",
"params": {
"expression": "console.log(123)",
"includeCommandLineAPI": true
}
}
参数需要设置”includeCommandLineAPI”,否则注入的代码无法使用require函数。
API
通过使用inspector在目标进程中执行"require('smith.js')",进行API的hook:
const path = require('path');
const inspector = require('inspector');
const { SmithClient, OperateEnum } = require('./client');
const { baseProcess, spawnProcess, spawnSyncProcess, connectProcess } = require('./process');
const fs = require('fs');
const net = require('net');
const dns = require('dns');
const child_process = require('child_process');
const smith_client = new SmithClient();
smith_client.on('message', (message) => {
switch (message.message_type) {
case OperateEnum.detect:
if (typeof require.main === 'undefined')
break;
const root = path.dirname(require.main.filename);
const package = path.join(root, 'package.json');
if (fs.existsSync(package)) {
const data = fs.readFileSync(package);
smith_client.postMessage(OperateEnum.detect, {'node': JSON.parse(data)});
}
break;
default:
break;
}
});
smith_client.connect();
function smithHook(fn, classID, methodID, process=baseProcess) {
return function(...args) {
const smithTrace = {
'class_id': classID,
'method_id': methodID,
'args': args.map(process),
'stack_trace': new Error().stack.split('\n').slice(1).map(s => s.trim())
}
smith_client.postMessage(OperateEnum.trace, smithTrace);
return fn.call(this, ...args);
}
}
child_process.ChildProcess.prototype.spawn = smithHook(child_process.ChildProcess.prototype.spawn, 0, 0, spawnProcess);
child_process.spawnSync = smithHook(child_process.spawnSync, 0, 1, spawnSyncProcess);
child_process.execSync = smithHook(child_process.execSync, 0, 2);
child_process.execFileSync = smithHook(child_process.execFileSync, 0, 3);
fs.open = smithHook(fs.open, 1, 0);
fs.openSync = smithHook(fs.openSync, 1, 1);
fs.readFile = smithHook(fs.readFile, 1, 2);
fs.readFileSync = smithHook(fs.readFileSync, 1, 3);
fs.readdir = smithHook(fs.readdir, 1, 4);
fs.readdirSync = smithHook(fs.readdirSync, 1, 5);
fs.unlink = smithHook(fs.unlink, 1, 6);
fs.unlinkSync = smithHook(fs.unlinkSync, 1, 7);
fs.rmdir = smithHook(fs.rmdir, 1, 8);
fs.rmdirSync = smithHook(fs.rmdirSync, 1, 9);
fs.rename = smithHook(fs.rename, 1, 10);
fs.renameSync = smithHook(fs.renameSync, 1, 11);
net.Socket.prototype.connect = smithHook(net.Socket.prototype.connect, 2, 0, connectProcess);
dns.lookup = smithHook(dns.lookup, 3, 0);
dns.resolve = smithHook(dns.resolve, 3, 1);
dns.resolve4 = smithHook(dns.resolve4, 3, 2);
dns.resolve6 = smithHook(dns.resolve6, 3, 3);
eval = smithHook(eval, 4, 0);
if (inspector.url()) {
setTimeout(() => {
inspector.close();
}, 500);
}
API列表:
- process
- child_process.ChildProcess.prototype.spawn
- child_process.spawnSync
- child_process.execSync
- child_process.execFileSync
- fs
- fs.open
- fs.openSync
- fs.readFile
- fs.readFileSync
- fs.readdir
- fs.readdirSync
- fs.unlink
- fs.unlinkSync
- fs.rmdir
- fs.rmdirSync
- fs.rename
- fs.renameSync
- net
- net.Socket.prototype.connect
- dns
- dns.lookup
- dns.resolve
- dns.resolve4
- dns.resolve6
数据格式
使用测试代码:
const fs = require('fs');
const net = require('net');
const dns = require('dns');
const child_process = require('child_process');
function testProcess() {
child_process.exec('ls');
}
function testFile() {
fs.open('/tmp/test', 'w', () => {});
fs.readFile('/tmp/test', () => {});
fs.readdir('/tmp', () => {});
fs.rename('/tmp/test', '/tmp/test1', () => {});
fs.unlink('/tmp/test1', () => {});
}
function testNetwork() {
net.connect(80, 'qq.com', () => {});
}
function testDns() {
dns.lookup('qq.com', () => {});
}
setInterval(() => {
testProcess();
testFile();
testNetwork();
testDns();
}, 3000);
运行代码:
node test.json
node <path>/injector.js <pid of node> 'require("<rasp node path>")'
数据输出:
[{
"pid": 3371235,
"runtime": "node.js",
"runtime_version": "v9.0.0",
"time": 1614065334018,
"message_type": 2,
"probe_version": "1.0.0",
"data": {
"class_id": 0,
"method_id": 0,
"args": ["/bin/sh -c ls"],
"stack_trace": ["at ChildProcess.spawn (/data00/home/liupan.patte/node-probe/smith.js:31:28)", "at exports.spawn (child_process.js:499:9)", "at Object.exports.execFile (child_process.js:209:15)", "at Object.exports.exec (child_process.js:139:18)", "at testProcess (/data00/home/liupan.patte/node-probe/test.js:7:19)", "at Timeout.setInterval [as _onTimeout] (/data00/home/liupan.patte/node-probe/test.js:27:5)", "at ontimeout (timers.js:478:11)", "at tryOnTimeout (timers.js:302:5)", "at Timer.listOnTimeout (timers.js:262:5)"]
}
}, {
"pid": 3371235,
"runtime": "node.js",
"runtime_version": "v9.0.0",
"time": 1614065334021,
"message_type": 2,
"probe_version": "1.0.0",
"data": {
"class_id": 1,
"method_id": 0,
"args": ["/tmp/test", "w", null],
"stack_trace": ["at Object.open (/data00/home/liupan.patte/node-probe/smith.js:31:28)", "at testFile (/data00/home/liupan.patte/node-probe/test.js:11:8)", "at Timeout.setInterval [as _onTimeout] (/data00/home/liupan.patte/node-probe/test.js:28:5)", "at ontimeout (timers.js:478:11)", "at tryOnTimeout (timers.js:302:5)", "at Timer.listOnTimeout (timers.js:262:5)"]
}
}, {
"pid": 3371235,
"runtime": "node.js",
"runtime_version": "v9.0.0",
"time": 1614065334022,
"message_type": 2,
"probe_version": "1.0.0",
"data": {
"class_id": 1,
"method_id": 3,
"args": ["/tmp/test", null],
"stack_trace": ["at Object.readFile (/data00/home/liupan.patte/node-probe/smith.js:31:28)", "at testFile (/data00/home/liupan.patte/node-probe/test.js:12:8)", "at Timeout.setInterval [as _onTimeout] (/data00/home/liupan.patte/node-probe/test.js:28:5)", "at ontimeout (timers.js:478:11)", "at tryOnTimeout (timers.js:302:5)", "at Timer.listOnTimeout (timers.js:262:5)"]
}
}, {
"pid": 3371235,
"runtime": "node.js",
"runtime_version": "v9.0.0",
"time": 1614065334022,
"message_type": 2,
"probe_version": "1.0.0",
"data": {
"class_id": 1,
"method_id": 5,
"args": ["/tmp", null],
"stack_trace": ["at Object.readdir (/data00/home/liupan.patte/node-probe/smith.js:31:28)", "at testFile (/data00/home/liupan.patte/node-probe/test.js:13:8)", "at Timeout.setInterval [as _onTimeout] (/data00/home/liupan.patte/node-probe/test.js:28:5)", "at ontimeout (timers.js:478:11)", "at tryOnTimeout (timers.js:302:5)", "at Timer.listOnTimeout (timers.js:262:5)"]
}
}, {
"pid": 3371235,
"runtime": "node.js",
"runtime_version": "v9.0.0",
"time": 1614065334022,
"message_type": 2,
"probe_version": "1.0.0",
"data": {
"class_id": 1,
"method_id": 11,
"args": ["/tmp/test", "/tmp/test1", null],
"stack_trace": ["at Object.rename (/data00/home/liupan.patte/node-probe/smith.js:31:28)", "at testFile (/data00/home/liupan.patte/node-probe/test.js:14:8)", "at Timeout.setInterval [as _onTimeout] (/data00/home/liupan.patte/node-probe/test.js:28:5)", "at ontimeout (timers.js:478:11)", "at tryOnTimeout (timers.js:302:5)", "at Timer.listOnTimeout (timers.js:262:5)"]
}
}, {
"pid": 3371235,
"runtime": "node.js",
"runtime_version": "v9.0.0",
"time": 1614065334022,
"message_type": 2,
"probe_version": "1.0.0",
"data": {
"class_id": 1,
"method_id": 7,
"args": ["/tmp/test1", null],
"stack_trace": ["at Object.unlink (/data00/home/liupan.patte/node-probe/smith.js:31:28)", "at testFile (/data00/home/liupan.patte/node-probe/test.js:15:8)", "at Timeout.setInterval [as _onTimeout] (/data00/home/liupan.patte/node-probe/test.js:28:5)", "at ontimeout (timers.js:478:11)", "at tryOnTimeout (timers.js:302:5)", "at Timer.listOnTimeout (timers.js:262:5)"]
}
}, {
"pid": 3371235,
"runtime": "node.js",
"runtime_version": "v9.0.0",
"time": 1614065334023,
"message_type": 2,
"probe_version": "1.0.0",
"data": {
"class_id": 2,
"method_id": 0,
"args": [
[{
"port": 80,
"host": "baidu.com"
}, null]
],
"stack_trace": ["at Socket.connect (/data00/home/liupan.patte/node-probe/smith.js:31:28)", "at Object.connect (net.js:107:35)", "at testNetwork (/data00/home/liupan.patte/node-probe/test.js:19:9)", "at Timeout.setInterval [as _onTimeout] (/data00/home/liupan.patte/node-probe/test.js:29:5)", "at ontimeout (timers.js:478:11)", "at tryOnTimeout (timers.js:302:5)", "at Timer.listOnTimeout (timers.js:262:5)"]
}
}, {
"pid": 3371235,
"runtime": "node.js",
"runtime_version": "v9.0.0",
"time": 1614065334023,
"message_type": 2,
"probe_version": "1.0.0",
"data": {
"class_id": 3,
"method_id": 0,
"args": ["baidu.com", {
"hints": 32
}, null],
"stack_trace": ["at /data00/home/liupan.patte/node-probe/smith.js:31:28", "at lookupAndConnect (net.js:1100:3)", "at Socket.connect (net.js:1034:5)", "at Socket.connect (/data00/home/liupan.patte/node-probe/smith.js:46:19)", "at Object.connect (net.js:107:35)", "at testNetwork (/data00/home/liupan.patte/node-probe/test.js:19:9)", "at Timeout.setInterval [as _onTimeout] (/data00/home/liupan.patte/node-probe/test.js:29:5)", "at ontimeout (timers.js:478:11)", "at tryOnTimeout (timers.js:302:5)", "at Timer.listOnTimeout (timers.js:262:5)"]
}
}, {
"pid": 3371235,
"runtime": "node.js",
"runtime_version": "v9.0.0",
"time": 1614065334023,
"message_type": 2,
"probe_version": "1.0.0",
"data": {
"class_id": 3,
"method_id": 0,
"args": ["baidu.com", null],
"stack_trace": ["at Object.lookup (/data00/home/liupan.patte/node-probe/smith.js:31:28)", "at testDns (/data00/home/liupan.patte/node-probe/test.js:23:9)", "at Timeout.setInterval [as _onTimeout] (/data00/home/liupan.patte/node-probe/test.js:30:5)", "at ontimeout (timers.js:478:11)", "at tryOnTimeout (timers.js:302:5)", "at Timer.listOnTimeout (timers.js:262:5)"]
}
}]
问题
修改module.exports进行hook,无法影响同模块中的函数调用。
// a.js
function A() {
console.log('A called');
}
function B() {
A();
}
module.exports = {
A,
B
};
// b.js
const m = require('./a')
m.B();
m.A = () => {console.log('A hooked')};
m.B();
替换了m.A之后,外部对模块a.A的调用会被hook,但是m.B中对A的调用依旧使用原函数。