Windows Defenderアプリケーション・コントロール(WDAC)は、不正なコード(マルウェアや信頼できない実行ファイルやスクリプトなど)がシステム上で実行されるのを防ぐためのWindowsのセキュリティー機能です。これは、明示的に信頼された実行ファイル、スクリプト、およびドライバーのみがシステム上で実行できるようにするポリシーを適用する、アプリケーションのホワイトリスト・メカニズムです。これは、X-Force Red敵対的シミュレーション・チームがテストを行っている環境のように、セキュリティとシステムの整合性が重要視される、高保証環境または厳密に管理された環境で頻繁に使用されます。
数週間前、私の同僚であるBobby Cookeが、信頼できるElectronアプリケーションをバックドア化することで最も厳格なWDACポリシーさえも回避する方法を詳述したブログ記事を公開しました。ElectronアプリケーションがNode.jsをどのように使用し、どのようにバックドア化できるのかを理解するために、彼のブログ記事をお読みいただくことをお勧めします。
さらに、彼はその調査の一環として、Node.jsベースのコマンド・アンド・コントロール・フレームワークであるLoki C2をオープンソース化しました。Loki C2を開発したBobbyとDran Tranの素晴らしい成果のおかげで、X-Force敵対的シミュレーション・チームは、WDACを採用する強化された環境でのエンゲージメントに関するコード実行に成功しました。
では、この研究はどこに関係するのでしょうか?前述の手法には欠点が1つあります。それは、JavaScriptコードの実行のみに制限されており、DLLの読み込みやEXEの実行などのネイティブ・コードを実行できないことです。また、シェルコードを実行してステージ2 C2ペイロードを起動することもできません。このブログ記事では、これらの制限を回避するために、私たちが利用したテクニックについて説明します。
まず、Bobbyと私は、Electronアプリケーションによってロードされた署名付きNode.jsモジュールのリバース・エンジニアリングを開始し、低レベルで命令レベルのコード実行を許可する脆弱性を探しました。いくつかの初期調査の後、jeffsshの提案により、私の注目はNode.jsとChromeで使用されているV8エンジンに移りました。
Node.jsモジュールで脆弱性を発見する代わりに、N-dayで V8 エンジンをエクスプロイトするのはどうでしょうか?
この攻撃シナリオは、よく知られているもので、脆弱ではあるが信頼できるバイナリーを持ち込み、信頼されているという事実を悪用してシステムへの足がかりを築くというものです。この場合、V8が脆弱なバージョンの信頼できるElectronアプリケーションを使用し、ペイロードとしてステージ2を実行するV8エクスプロイトにmain.jsを置き換えます、するとネイティブなシェルコードを実行できるのです。悪用されたアプリケーションが信頼できるエンティティー(Microsoftなど)によってホワイトリストに登録/署名されており、採用されているWDACポリシーの下での実行が通常は許可されている場合、悪意のあるペイロードの経路として使用される可能性があります。
このアプローチには、シェルコードを自由に実行できることに加えて、ブラウザーのようなプロセスのコンテキストでシェルコードを実行できるというメリットもあります。ジャスト・イン・タイム(JIT)コード用にマッピングされたRWXメモリがあるなど、EDR(エンドポイントの検知と対応)によって疑わしいものとしてフラグが立てられるかもしれない動作は、ブラウザでは正常のものに認識されます。
このアプローチは非常に簡単に思えましたが、私には率直な疑問がいくつかありました。公開されているChrome V8 N-dayエクスプロイトは、Electronアプリ内で実際に機能するのでしょうか?Chromeで使用されているV8エンジンはNode.jsで使用されているV8エンジンとどのように異なるでしょう?エクスプロイトにはどのような修正が必要でしょう?これをどうやってデバッグすればいいでしょう?
ElectronアプリにおけるV8エクスプロイトのエクスプロイテーションに関する既存の公開作業が存在することがわかりましたが、私にとって非常に悲しいことに、それが完了した後まで気づきませんでした。Turb0は、公開されているv8エクスプロイトと、それに対応する読み取り/書き込みプリミティブをElectronアプリケーション内で動作させる(ある意味苦痛を伴う)プロセスをカバーした素晴らしい仕事をしています。Turb0のブログ記事では、私が対処しなければならなかった問題について、既に深い技術的詳細が多く説明されています。これは、確認することを強く推奨します。このブログ記事の残りの部分では、WDACバイパスを作成するという特定の目標でWindowsをターゲットにすること、および私が実際に使用するためのエクスプロイトの運用化で出くわしたことに関連した、エクスプロイト開発サイクルの残りの段階に焦点を当てています。
私が最初に行う必要があったことは、正確なターゲットを把握することでした。信頼されているElectronアプリケーションを選択し、それをエクスプロイトする脆弱性を選択する必要がありました。これまでにブラウザのエクスプロイテーションのエクスペリエンスがほとんどなかったため、選択した脆弱性には出発点として使用できるパブリック・エクスプロイトが必要でした。
V8のバージョンがV8 Electron使用のバージョンにどのように対応付けられているのか、またそれが本当に脆弱なのかどう見分けるのかもわかりませんでした。ElectronのV8のバージョンは、ChromeのV8の最新バージョンより遅れていることがよくあります。Electronの保守担当者は、新しいバージョンの重要なセキュリティー・パッチを、特定のElectronのリリース用に凍結したバージョンにバックポートします。つまり、Electronが古いバージョンのV8を使用しているとしても、修正がバックポートされている可能性があるため、必ずしもバグに対して脆弱であるとは限りません。彼らが適応した選りすぐりのパッチはここに保存 されています。
最も簡単なアプローチは、アプリケーションのバージョンのリリース後にパッチが適用された脆弱性を使用することだと判断しました。そうすれば、そのバージョンのアプリがパッチを適用済みである可能性は絶対にありません。調べてみたところ、過去2年分のVSCodeリリースのダウンロードが見つかりました。Microsoftが署名済みで脆弱性のあるアプリケーションの選択肢が多数ありました。😊
まず、最近のV8エクスプロイトPoCを採用し、脆弱なElectronアプリにバックドアを設定して、main.jsをそのエクスプロイトに置き換え、期待しながら結果を見ました。おそらくこれは、簡単過ぎる作業です、そうですよね?私は、最低でもクラッシュすると考えていました。驚くべきことではありませんが、アプリを起動したときに、何も起こりませんでした。不本意ながら私には、何が起こっているのかをより深いレベルで理解するために、V8を構築する必要があることはわかっていました。V8を独自に構築することで、デバッグ・バージョン(d8)を構築し、エクスプロイトの深層を把握した上で、ターゲットとしていた特定のバージョンに合わせて調整することができます。
私の最初の目標は、エクスプロイトが機能することが知られている環境をそのまま再現する「グラウンド・トゥルース」を確立することでした。そして、そのバージョンと私が目標としていたバージョンの違いを調べ、何が問題だったのかを理解することができました。
公開されているV8エクスプロイトのほとんどは、Linuxを標的としていることがわかりました。そこで、まず、V8をLinux上でコンパイルし、自分が選択したパブリック・エクスプロイトがターゲットにしている正確なコミットを確認しました。その後、エクスプロイトを実行して、それが機能することを確認しました。幸いにも、うまくいきました。これで、私にはグランド・トゥルースが手に入りました。
そこから、ターゲットにしていたV8のバージョン(Electronアプリで使用されているのと同じもの)をLinuxでコンパイルしました。このエクスプロイトは、すぐには機能しませんでした。自分でプロジェクトを構築するメリットは、必要なだけコードを振り返ることができる点です。特に、V8には、V8 JavaScriptエンジンのスタンドアロン・シェルであるd8があり、主にブラウザまたはNode.js環境の外部でJavaScriptおよびWebAssemblyコードのテスト、デバッグ、実行に使用されます。d8 は内部デバッグ主要な機能を備えており、
これを使用することで、関心のあるオブジェクトのアドレスを表示し、パブリック・エクスプロイトのハードコードされたオフセットを調整することができました。これで、一定の段階に到達しました。エクスプロイトをWindowsに移植するだけです。
Windows上で古いバージョンのV8をコンパイルすることは、たくさんの頭痛の種となりました。依存関係に関する多くの問題を修正する必要があったため、あいまいなコード修正をいくつか行いました。それで、詳細な情報を見失いました。私の脳が自分自身を守るためにそれらをブロックしたのです。何時間もの苦労の末、ようやく必要なバージョンをコンパイルすることができました。驚くことに、Linuxで変更されたエクスプロイトは、調整なしでWindows上でも機能したのです。
これで残りの仕事は、Electroアプリでエクスプロイトをテストし、息を止めて見守ることでしたが、それではうまくいきませんでした。しかし、その理由は?
最初は、ターゲットがクラッシュしたため、私には期待感がありました。結局のところ、私はLinuxペイロードをWindowsに適応させていなかったので、興味深いことが起こることは期待できませんでした。動作を確認するために、エクスプロイトをアドレス0x4141414141で実行するように変更しました。これは、命令ポインターのアドレスを制御することでプログラムの制御を獲得したことを確認/証明するために、エクスプロイトの記述者が使用する一般的な手法です。しかし、WinDbgがクラッシュした後、私は望んでいたものを確認することができませんでした。ターゲットの関数ポインターを上書きするときにセグメンテーション障害が発生していました。
先に私が話した、Electronの選り好みV8コミットのことをご記憶でしょうか?アプリは私がエクスプロイトに使用していたバグに対して脆弱であったものの、公開されたエクスプロイトで使用されたサンドボックス回避方法には、選り好みのパッチが既に適用されていることがわかりました。V8サンドボックス/メモリー・ケージについて詳しくない場合は、こちらで詳細を読むことができます。基本的にこれは、脆弱性が発生した場合に、V8のエクスプロイテーションをより困難にする方法です。
何が起こっていたのかを実現するために、ターゲットバージョンのV8を再度構築し、今回は選り好みのパッチを適用する必要がありました。Node.jsは、セキュリティー・パッチに加えて、Electronが使用するV8のバージョンに特定のNode.jsパッチも適用します。ElectronとNode.jsがさまざまな依存関係にどのように対処するかがすぐに明確ではなかったため、これを行う必要があることに気づくのに時間がかかりました。
コンパイルしていたV8のバージョンがターゲットと*同じ*であることを確認しようとしたり、最近のサンドボックス回避手法についても読みこんだりするのに1日か2日経過したあと、進歩がありました。ターゲットに対し機能する回避テクニックを見つけることができました。エクスプロイトを調整した後、ようやく命令ポインターを制御してアプリをクラッシュさせることができました。うれしい勝利で、終わりが見えました...
この時点で、残されているのは、パブリックのエクスプロイト・ペイロードを変更して、代わりにC2ペイロードを実行することだけでした。この一見単純に見える変更は、私が思っていた以上に面倒なものであることが判明しました。パブリック・エクスプロイトのLinuxペイロードは、サイズがわずか数バイトのシェルをポップアップする単純なものでした。C2のペイロードはそれよりはるかに大きいものでした。
シェルコードでのコーディングについて知っている人なら、Windowsシェルコードの記述がLinuxのシェルコードよりも面倒であることを知っているでしょう。主な理由は、Linuxのように位置に依存しない方法で直接シスコールを行う簡単な方法がないためです。ペイロードは浮動小数点配列内で「JOP smuggled」である必要もありました。
明らかに、ステージC2ペイロード全体(数千バイトの大きさ)はこのように実行できませんでした。そのため、実行可能なページをマッピングし、最終的なペイロードをそこにコピーして、そこにジャンプするブートストラッピング・ペイロードを作成する必要がありました。
ブートストラッピング・ペイロードの問題は、プログラム制御はできていたものの、実行されたペイロードに引数を渡す方法がなかったということです。そのため、注入されたシェルコードでは、コピー元の最終ペイロードのアドレスがわかりません。私は「引数持ち込み」と名付けた手段によってこれを回避しました。
上書きされたJSFunctionオブジェクトのアドレスがrcxレジスタに格納されることはわかっていました。そこで、任意の書き込みプリミティブを使用して、マップされたページを、必要のないオブジェクトのフィールドの1つに格納しました。いくつかのオフセットを上書きするとクラッシュしたため、これには少し試行錯誤が必要でした。コピーする値とコピー先のオフセットについても同じことを行いました。フィールドのオフセットをシェルコードにハードコードすることで、ペイロードのコピー元を知らせることができます。ペイロードは、n回呼び出しました。nはコピーするバイト数です。
V8の最適化コンパイラーであるTurboFanが、私の計画をいくらか邪魔してきました。TurboFanの最適化により、同じ値の複数の浮動小数点に変換される命令のシーケンスを持ち込んでも、メモリ内にその値のインスタンスは1つだけになります。そのため、指示を繰り返す頻度に制限が課せられました。この問題を回避するために、シェルコードを可能な限りコンパクトにし、また、命令を繰り返す絶対的な必要がある場合は、浮動小数点値が異なり繰り返しエントリーがないようにすることで、挿入する命令の場所が変わるようにしました。
また、ステージ2のペイロードが大きすぎる場合にシェルコードのコピーで問題が発生しました。これはおそらく、失敗した同じJSFunctionを呼び出すのが必要な回数のせいで、TurboFanがこれを最適化しようとしためです。最終的に、1つの大きなループではなく複数のループを「WriteShellcode」にコピー・アンド・ペーストすることで、この問題を回避できました。非常に見かけは悪いですが、これが成功したのです。その後でBobbyとDylanが、C2ペイロードを、BLOBストレージからより大型のペイロードを取得するステージャーに変更したため、最終ペイロードはディスクに保管する必要がなくなりました。これは、main.jsのファイルサイズを妥当なものに抑えるのにも役立ちました。
エクスプロイトを実際に運用するための準備には、常にさまざまな環境でのテストが含まれます。エンゲージメントの文脈については、ただ、それがWDACに対応している可能性が高いWindowsシステムであるということだけで、ペイロードがどの環境で実行されるのかはわかりませんでした。したがって、エクスプロイトはOSに関係なく機能する必要がありました。アプリケーションのV8バージョンとすべての依存関係がアプリ内に含まれているため、可変性はそれほど大きくないことを私は確信していました。その仮定には誤りがありました。
私が理解できない理由で、脆弱な関数ポインターの上書き先オフセットがWindowsのバージョン間で変更されていました。これの意味が不明だったのは、私の理解によれば、オフセット距離はV8 JITエンジンによって決定され、そのライブラリはアプリケーション・パッケージから直接ロードされるからです。これは、OSに関係なく、まったく同じV8ライブラリーがロードされることを意味します。事態をさらに複雑にしているのは、そのバリエーションがいかなるパターンに従ってもいるようには見えないことです。Windowsの一部のバージョン(古いものと新しいもの)では、オフセットが4バイトずれていることがありました。JavaScriptエクスプロイト内から適切なオフセットを収集する方法が(私が知る限り)なかったため、これは特に面倒でした。計算する唯一の方法は、デバッグ・シェルを利用してメモリー・アドレスを読み取り、計算を行うことでしたが、実稼働環境のElectronアプリケーションからは、当然のことながら、その選択肢はありませんでした。TLDR: オフセットの変動はエクスプロイトのランタイム時に計算できません。
一貫性のないオフセットの問題を回避するために、BobbyとDylanはエクスプロイトを再設計し、main.jsがエクスプロイトを複数回起動し、成功するまでさまざまなオフセットを試すようにしました。これは、初期コード・プロセスにループを実行させることによって実現されました。このループにより、一意のオフセットを使用してエクスプロイトを試みる子プロセスが生成されました。エクスプロイトが失敗した場合、子プロセスは終了します。エクスプロイトが成功した場合、シェルコードはステージ2 C2をデプロイする前に、Mutexファイルの実行と書き込みを行います。エクスプロイトが成功すると、最初のプロセスはループから抜け出し、永久にスリープ状態になります。
これは、間違ったオフセットの試みがクラッシュを引き起こすことを意味しますが、私たちのテストによって、ユーザーが目視できるエラーは発生せず、アプリケーションの機能が引き続きシームレスに動作しているように見えることが判明しました。最もクリーンなソリューションではなく、クラッシュの影響でややノイズが多いものの、時間が非常に重要でした。これは私たちのビジネスで「JIT xdev」と呼ばれているものであり、私たちのニーズのために完璧に機能しました。
私たちは、これが発見されて、誰かがアプリケーションのmain.jsエントリー・ポイントを分析した場合に、そのエクスプロイトが明らかになることは、当然望んでいませんでした。それを回避するために、エクスプロイト・コードにJavaScript難読化ツールを適用し、人間が目に対しては理解できないものにしましたチームのペイロードCI/CDパイプラインを管理するChris Spehnの才能と献身のおかげで、このペイロードの配信を合理化し、ペイロードが生成されるたびにコードを再難読化することができました。これにより、毎回異なるエクスプロイト・コードを含むアプリケーションを、無限に再利用できるようになりました。これにより、ペイロードが署名されなくなりました。これは特に役立つ点となりました。なにしろ初めてこの機能を使用しようとしたときには、残念なことに、ユーザーがフィッシングEメール(🙁)にフラグを立てたのですから。興味深いことに、クライアントのブルー・チームは、フィッシングEメールからアプリケーションを分析したものの、アプリケーションの目的については収集せず、埋め込まれたV8エクスプロイトも特定しませんでした。
関連するすべてのV8ライブラリはElectronアプリケーション内にバンドルされているはずなので、JIT化された関数オフセットがOSに依存していた理由は、まだよく理解できません。この理由についてご存じの人がいたら、ぜひご連絡ください。
Electronは、実行時にアプリケーションのすべてのファイルの整合性を検証する、整合性に関する実験的な機能をロール・アウトしてきています。これは、macOSではバージョン16から、Windowsではバージョン30から利用できるようになっています。アプリケーション開発者は、このElectronヒューズを有効にして、アプリケーション・ファイルが改ざんされないようにすることができます。改ざんされていれば、プロセスは自動的に終了し、何も実行されません。
この主要な機能により、main.jsを含むElectronアプリのパッケージ・ファイルの変更が防止され、ここで説明した手法が阻止されます。ただし、最も一般的なアプリケーションには、まだこれは実装されていません。この機能がより広く使用されるような時が来ても、整合性ヒューズ前の古いバージョンのアプリケーションは引き続き脆弱であり、この攻撃に対して使用可能であることに注意してください。
Bobby Cook & Dylan Tran – エクスプロイトの運用化を支援
Dylan Tran – 図の作成
Chris Spehn – このペイロードのCI/CDパイプラインへの統合(そして、チームのためにしてくれた他のすべての報われないDevOps作業)。
jeffssh – インスピレーション
j j - V8ハッカーのマスターであり、その多作なV8 PoCは大いに役立ちました