11 月に、npm パッケージ event-stream が悪意のある依存関係 flatmap-stream によって悪用されました。 この事件の全容はここに書かれており、この投稿の焦点は、これを JavaScript のリバース エンジニアリングのケース スタディとして使用することです。 flatmap-stream に関連付けられた 3 つのペイロードは、記述しやすいほど単純でありながら、興味深いほど複雑です。 この投稿を理解するためにこの事件の背景を理解することは重要ではありませんが、詳細に多少なりとも精通していないと明らかでないかもしれない仮定を立てることにします。
ほとんどの JavaScript のリバース エンジニアリングは、デスクトップ OS で実行するバイナリ実行ファイルより簡単です (結局のところ、ソースが目の前にあるため)。ただし、理解しにくいように設計された JavaScript コードは、その意図を不明瞭にするために、何度か難読化が行われることがよくあります。 この難読化の一部は、スペースを節約するためにソースの全体的なバイト数を可能な限り削減するプロセスである「縮小」と呼ばれるものから生じます。 これには、変数を 1 文字の識別子に短縮し、true のような式を !0 のような短いが同等のものに変換することが含まれます。 最小化は、Web ブラウザーの起源のため、主に JavaScript のエコシステムに特有のものであり、ツールの再利用によりノード パッケージで時々見られますが、セキュリティ対策として意図されたものではありません。 一般的な縮小化および難読化手法の基本的な逆処理については、Shape のunminifyツールを参照してください。 専用の難読化パスは、難読化用に設計されたツールから提供される場合もあれば、開発者が手動で実行する場合もあります。
最初のステップは、分析のために分離されたソースを入手することです。 flatmap-stream パッケージは、パッケージの 1 つのバージョン (バージョン 0.1.1) にのみ悪意のあるペイロードが含まれていることを除いて、無害に見えるように特別に作成されました。 バージョン 0.1.2とバージョン 0.1.1を比較したり、2 つのタブで URL を切り替えるだけでも、ソースの変更をすぐに確認できます。 この記事の残りの部分では、追加されたソースをペイロード A と呼びます。以下はペイロード A のフォーマットされたソースです。
まず最初に: 悪意のあるコードを決して実行しないでください(隔離された環境を除く)。 私は、Shift パーサー スイートと JavaScript トランスフォーマーを使用してコードを動的にリファクタリングするのに役立つ独自のツールを作成しましたが、この投稿に沿って作業を進めるには Visual Studio Code などの IDE を使用することもできます。
JavaScript をリバース エンジニアリングする場合、精神的な負担を最小限に抑えることが重要です。 これは、すぐに価値を追加しない式やステートメントを削除し、自動または手動で最適化されたコードの DRY 性を逆転させることを意味します。 私たちは頭の中で JavaScript を静的に分析し、実行を追跡しているので、頭の中のスタックが深くなるほど、迷子になる可能性が高くなります。
最も簡単な方法の 1 つは、3 行目と 4 行目のように、require や process などのグローバル プロパティが割り当てられている変数を unminify することです。
これは、リファクタリング機能を備えた任意の IDE で実行できます (通常は、名前を変更する識別子の上で「F2」キーを押します)。 その後、関数定義 e が表示されますが、これは単に 16 進文字列をデコードするだけのようです。
最初の興味深いコード行は、文字列「2e2f746573742f64617461」をデコードする関数 e の結果から得られるファイルをインポートしているようです。
意図的に難読化された JavaScript では、リテラル文字列の値が隠蔽されることが多く、一目見ただけでは特に不吉な文字列やプロパティがはっきりと見えても、気付かれません。 ほとんどの開発者は、これは非常に低いハードルであると認識しているため、簡単に元に戻せるエンコードが採用されていることがよくありますが、ここでもそれは変わりません。 e 関数は単純に 16 進文字列を反転するだけなので、オンライン ツールまたは独自の便利な関数を使用して手動で実行できます。 e 関数が何をしているのかを確信している場合でも、攻撃者がデータによって引き起こされるセキュリティの脆弱性を発見していないという保証がないため、悪意のあるファイルで見つかった入力で e 関数を実行しない (抽出したとしても) ことをお勧めします。
その文字列を逆にすると、スクリプトには、配布された npm パッケージにあるデータ ファイル「./test/data」が含まれていることがわかります。
nをdataに名前変更し、e(n[2])からe(n[9])への呼び出しの難読化を解除すると、ここで何を扱っているのかがより明確になってきます。
これらの文字列が隠されている理由も簡単にわかります。単純なフラットマップ ライブラリで復号化への参照が見つかった場合、何かが間違っていることがすぐにわかります。
ここから、スクリプトが node.js の「crypto」ライブラリをインポートしていることがわかります。API を調べると、createDecipher の 2 番目の引数 (ここでは o) が復号化に使用されるパスワードであることがわかります。 これで、その引数とそれに続く戻り値を、API に基づいて適切な名前に変更できるようになりました。パズルの新しいピースが見つかるたびに、たとえ些細なことのように思える名前変更された変数であっても、リファクタリングまたはコメントによってそれを永続化することが重要です。 何時間も外国のコードを調べていると、場所がわからなくなったり、気が散ったり、誤ったリファクタリングのせいで後戻りする必要が生じたりすることが非常によくあります。 リファクタリング中に git を使用してチェックポイントを保存することも有益ですが、その決定はあなたにお任せします。 コードは次のようになります。e 関数は、分析に価値を追加しないため、ステートメント if (!o) {... と一緒に使用されなくなったため、削除されました。
また、f の名前を newModuleInstance に変更したことにも気づくでしょう。 このくらい短いコードであれば重要ではありませんが、数百行に及ぶ可能性のあるコードの場合は、すべてが可能な限り明確であることが重要です。
これで、ペイロード A はほぼ難読化解除され、ペイロード A の詳細を調べて、その機能について理解できるようになりました。
3 行目は外部データをインポートします。
4 行目は環境からパスワードを取得します。process.env を使用すると、ノード スクリプト内から変数にアクセスできます。npm_package_description は、package.json ファイルで定義されたスクリプトを実行するときにノードのパッケージ マネージャーである npm が設定する変数です。
5 行目は、npm_package_description の値をパスワードとして持つ decipher インスタンスを作成します。 つまり、暗号化されたペイロードは、このスクリプトが npm 経由で実行され、 package.json に特定の説明フィールドがある特定のプロジェクトに対して実行されている場合にのみ復号化できます。 それは大変だろうね。
6行目と7行目は外部ファイルの最初の要素を復号化し、変数「decrypted」に格納します。
8 行目から 11 行目は新しいモジュールを作成し、復号化されたデータをドキュメント化されていないメソッド _compile に渡します。 次に、このモジュールは外部データ ファイルの 2 番目の要素をエクスポートします。module.exports は、あるモジュールから別のモジュールにデータを公開するノードのメカニズムであるため、newModuleInstance.exports(data[1]) は外部データ ファイルにある 2 番目の暗号化されたペイロードを公開します。
この時点では、package.json のどこかにあるパスワードでのみ復号化可能な暗号化されたデータがあり、その復号化されたデータは _compile メソッドに渡されます。 ここで問題が残ります。パスワードが不明なデータをどうやって復号化するか? これは簡単な質問ではありません。AES256 暗号化をブルートフォースで簡単に実行できるとしたら、npm パッケージが乗っ取られるよりも多くの問題が発生するでしょう。 幸いなことに、私たちが扱っているのは、まったく未知のパスワードのセットではなく、どこかのpackage.json にたまたま入力された文字列です。package.json ファイルは npm パッケージ メタデータのファイル形式として始まったので、公式の npm レジストリから始めてもよいでしょう。 幸いなことに、すべてのパッケージ メタデータのストリームを提供する npm パッケージがあります。
ターゲット ファイルが npm パッケージ内にあるという保証はありません。多くの非 npm プロジェクトでは、node ベースのツールの構成を保存するために package.json が使用され、package.json の説明はバージョンごとに変わる可能性がありますが、開始するには良い場所です。 このペイロードを複数のキーで復号化すると意味不明な文字列になる可能性があるため、このブルートフォース攻撃プロセス中に復号化されたペイロードを検証する方法が必要です。 私たちが扱っているのは、 Module.prototype._compileに送られ、それがvm.runInThisContextに送られるものなので、出力は JavaScript であると想定でき、任意の数の JavaScript パーサーを使用してデータを検証できます。 パスワードが失敗した場合、または成功したがパーサーがエラーをスローした場合は、次の package.json に移動する必要があります。 便利なことに、Shape Security は JavaScript および Java 環境で使用するための独自の JavaScript パーサー セットを構築しました。 使用されたブルートフォース スクリプトは次のとおりです。
これを 92.1 秒間実行し、740543 個のパッケージを処理した結果、パスワード「A Secure Bitcoin Wallet」が見つかり、以下に含まれるペイロードが正常にデコードされました。
これはラッキーでした。 巨大なブルートフォース問題になる可能性があったものが、最終的には 100 万回未満の反復で済みました。 問題のキーを含む影響を受けたパッケージは、ビットコイン ウォレット Copay のクライアント アプリケーションであることが判明しました。 次の 2 つのペイロードは、アプリケーション自体をさらに深く掘り下げます。ターゲット アプリケーションがビットコインの保管に重点を置いていることを考えると、これがどこに向かうのかはおそらく推測できるでしょう。
このようなトピックに興味があり、他の 2 つのペイロードや将来の攻撃の分析を読みたい場合は、この投稿に「いいね」するか、Twitter の@jsoversonまでお知らせください。