NodeJS

简介

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的调用依旧使用原函数。