気に入っている人もあれば、嫌う人もいますが、現時点では、.NET tradecraftが予想よりも少し長く続くことは驚くにはあたりませんでした。.NETフレームワークはMicrosoftのオペレーティング・システムに不可欠な要素であり、.NETの最新リリースは.NET Coreです。Coreは、.NETをLinuxとmacOSにも導入する、.NETフレームワークの後続となるクロスプラットフォームのフレームワークです。これにより、.NETは、敵対者やレッドチームの間で、エクスプロイテーションのトレードクラフトとして、これまで以上に人気が高まっています。このブログでは、フォークアンド実行手法を使用する従来の組み込み実行アセンブリー・モジュールと比較して、Cobalt Strikeを介してプロセス内で.NETアセンブリーを実行できるようにする新しいBeacon Object File(BOF)について説明します。
敵対者シミュレーションソフトとして有名なCobalt Strikeは、PowerShellの検知機能の向上により、レッドチームがPowerShellツールからC#に移行する傾向を認識し、2018年のCobalt Strikeバージョン3.11でexecute-assemblyモジュールを導入しました。これにより、オペレーターは、これらのツールをディスクにドロップするという追加のリスクを負うことなく、メモリ内で実行することにより、エクスプロイテーション後の .NETアセンブリの力を活用できるようになりました。アンマネージド・コード経由で.NETアセンブリをメモリに読み込む機能は、リリース時点では目新しいものでも未知でもありませんでしたが、Cobalt Strikeによってこの機能が主流になり、エクスプロイテーション後のトレードクラフトにおける.NETの人気をさらに高めることができたと言えます。
Cobalt StrikeのExecute-assemblyモジュールは、新しい犠牲プロセスを生成し、悪用後の悪意のあるコードをその新しいプロセスに注入し、悪意のあるコードを実行し、終了したら新しいプロセスを強制終了するフォークアンドラン手法を使用します。これにはメリットと欠点の両方があります。フォーク・アンド・ラン方式のメリットは、Beaconエージェント・プロセスの外側で実行が行われることです。これは、エクスプロイテーション後のアクションで何か問題が発生したり、把握されたりした場合でも、インプラントが存続する可能性がより高くなるということです。簡単に言えば、インプラントの全体的な安定性が非常に向上しますしかし、セキュリティー・ベンダーがこのフォーク・アンド・ランの動作を捉えたことにより、Cobalt Strikeが認めている、OPSECの高価なパターンが追加されました。
2020年6月にリリースされたバージョン4.1の時点で、Cobalt Strikeは、ビーコン・オブジェクト・ファイル(BOF)の導入により、この問題に対処するための新機能を導入しました。BOFを使用すると、オペレーターは、前述のよく知られた実行パターンや、cmd.exe/powersheet.exeの使用などのOPSECの失敗を、ビーコン・インプラントと同じプロセス内でメモリ内のオブジェクト・ファイルを実行することにより回避できます。BOFの内部の仕組みについてはここでは触れませんが、以下に、参考になるブログ記事をいくつか紹介します。
上記のブログを読んでいただければ、BOFは私たちが望んでいた救いの恵みではなかったことがわかります。これらの素晴らしい.NETツールをすべて書き直して、BOFに変えることを夢見ていたなら、その夢は今や打ち砕かれています。ごめんなさい。しかし、私の意見では、BOFが提供できる素晴らしいものがいくつかあるので、希望は失われません。私は最近、BOFでできることの限界を押し広げて、とても楽しい(そして少しフラストレーションも感じました)中で、BOFでできることの限界を押し広げようとしています。まず、CredBandit を作成し、LSASSのようなプロセスの完全なメモリダンプを実行し、既存のBeacon通信チャネルを通じて送信します。InlineExecute-Assemblyは、お気に入りの .NET ツールを変更することなく、ビーコン プロセス内で .NET アセンブリを実行できます。BOFを作成した理由、その主な機能、注意事項、敵対者シミュレーションやレッドチームを実施する際にどのように役立つかについて詳しく説明しましょう。
IBMニュースレター
AI活用のグローバル・トレンドや日本の市場動向を踏まえたDX、生成AIの最新情報を毎月お届けします。登録の際はIBMプライバシー・ステートメントをご覧ください。
ニュースレターは日本語で配信されます。すべてのニュースレターに登録解除リンクがあります。サブスクリプションの管理や解除はこちらから。詳しくはIBMプライバシー・ステートメントをご覧ください。
InlineExecute-Assemblyを構築する理由は非常にシンプルです。私は、敵対者シミュレーション・チームがプロセス内で.NETアセンブリを実行し、Cobalt Strikeを使用して成熟した環境で運用する際に、上記のOPSECの落とし穴の一部を回避できるようにする方法を求めていました。また、現在の.NETツールのほとんどに変更を加える必要があることで、チームに余分な開発時間がかかりないようにするためのツールも必要でした。安定性も必要でした。複雑なBOFは安定していますが、数少ないビーコンの1つを環境の中に失うことは避けたいものです。基本的には、Cobalt Strikeの実行アセンブリー・モジュールと同様に、オペレーターにとって可能な限りスムーズに動作する必要があります。
わかっています、ある意味明白です。AIがなければ、これまで以上に前進することはできなかったでしょう。冗談はさておき、CLRがどのように動作し、何が行われているのかの複雑さは、それだけでブログの記事になりそうなので、ここでは、アンマネージコードでCLRをロードする際にBOFが使用するものを、非常に高いレベルでレビューすることにします。
CLR をロードする
上記の簡略化された製品の画面に示すように、BOFがCLRをロードするために実行する主な手順は次のとおりです。
これでCLRは初期化されましたが、お気に入りの.NETアセンブリを実際に実行する前に、まだやるべきことが少しあります。Microsoft社が「アプリケーションが実行される分離された環境」と説明するAppDomainインスタンスを作成する必要があります。言い換えれば、これは、エクスプロイテーション後の.NETアセンブリーのロードと実行に使用されます。
AppDomainが作成され、アセンブリがロード/実行されている
上記の簡略化されたスクリーンショットに示されているように、BOF が当社の .NET アセンブリを読み込んで呼び出すために実行する主な手順は次のとおりです。
これで、アンマネージ コードによる .NET 実行について高度な理解が得られたことと思いますが、これではまだ運用上健全なツールにはまったく遠いため、BOF に実装されたいくつかの主要な機能を見て、BOF を「まあまあ」から「完全に正当な」ものにしていきます。
おそらく、これがなぜ重要なのか疑問に思われるでしょう。まあ、あなたも私と同じように時間を大切にしているなら、エントリ ポイントが、通常はコンソールの標準出力にパイプされるだけのデータをすべて含む文字列を返すように、ほぼすべての .NET アセンブリを変更するのに時間を費やしたくないですよね。そうだろうと思いましたよ。これを回避するには、標準出力を名前付きパイプまたはメール スロットにリダイレクトし、出力が書き込まれた後に読み取って、元の状態に戻す必要があります。この方法なら、cmd.exe または powershell.exe の場合と同じように、変更されていないアセンブリを実行できます。さて、コードを説明する前に、@N4k3dTurtl3 によるプロセス実行アセンブリとメール スロットに関するブログ記事に感謝したいと思います。これがそもそも、この技術が初めて登場したときに、私のプライベートの C インプラントにこの技術を実装する道に私を導いたもので、数か月後には同じ機能を BOF に移植しました。さて、小道具が用意できたので、stdoutを以下の名前付きパイプにリダイレクトすることでこれがどのように実現されるかの簡単な例を見てみましょう。
コンソールの標準出力を名前付きパイプにリダイレクトして元に戻す
ICLRMetaHost ->GetRuntime 経由で CLR をロードするときに、必要な .NET フレームワークのバージョンを指定する必要があったことを覚えていますか。.NET アセンブリーがどのバージョンでコンパイルされたかに依存することをことを覚えていますか。毎回どのバージョンが必要かを手動で指定するのはあまり楽しいことではないと思いませんか。幸運なことに、@b4rtikは、execute-assemblyモジュール内でMetasploitフレームワーク用にこれを扱うクールな関数を実装しており、私たち自身のツールに簡単に実装できます。以下にそれを示します。
.NET アセンブリを読み取り、CLR をロードするときに必要な .NET バージョンを判断するのに役立つ関数
基本的にこの関数が行うことは、アセンブリ バイトが渡されると、それらのバイトを読み取り、16 進数値 76 34 2E 30 2E 33 30 33 31 39 を検索します。これを ASCII に変換すると v4.0.30319 になります。おそらく、それは見覚えがあるでしょう。アセンブリの読み取り時にその値が見つかった場合、関数は 1 または true を返し、見つからない場合は 0 または false を返します。これを使用すると、以下のコード例に示すように、1/true または 0/false のどちらが返されるかによって、どのバージョンをロードするかを簡単に判断できます。
.NETバージョン変数を設定するためのif/elseステートメント
.NET 攻撃の手口について語る際にAMSIについて語らないわけにはいきません。AMSI とは何か、またそれをバイパスするすべての方法については何度も説明されているため詳しく説明しません。ただし、BOF 経由で何を実行するかに応じて AMSI にパッチを適用することが必要になる可能性がある理由について少し説明します。たとえば、難読化を一切行わずにSeatbeltを実行しようとすると、出力が返されず、ビーコンが機能していないことがすぐにわかります。こうなると、もうどうしようもありません。これは、AMSI がアセンブリを捕捉し、それが悪意のあるものであると判断し、騒がしいホーム パーティーに抗議するようにアセンブリをシャットダウンしたためです。これは理想的ではないですよね?AMSI に関しては、ConfuserXやInvisibility Cloakなどを使用した .NET ツールの難読化と、さまざまな手法を使用した AMSI の無効化の、2 つの適切なオプションがあります。今回は、メモリ内の amsi.dll にパッチを適用して E_INVALIDARG を返し、スキャン成果 0 を作成するRastaMouseを使います。これは、該当のブログ記事で指摘されているように、通常は AMSI_RESULT_CLEAN として解釈されます。以下に x64 プロセスのコードの簡略化されたバージョンを見てみましょう。
AmsiScanBufferのメモリ内パッチ適用
上のスクリーンショットにあるように、次のことを行うだけです。
これをツールに実装することで、以下のように–amsiフラグを使用してデフォルトバージョンのSeatbelt.exeを実行し、AMSI検知を回避することができます。
InlineExecute-Assemby AMSI バイパスの例
防御側にとって幸いなことに、ETW を使用して悪意のある .NET の手口を検出する場合、AMSI 以外にも役立つものがあります。残念ながら、AMSI と同様に、これも攻撃者が簡単に回避できる可能性があり、@xpn はこれがどのように実行されるかについて非常に素晴らしい研究を行いました。以下に、ETW にパッチを適用して完全に無効にする方法の簡略化された例を示します。
EtwEventWriteのメモリ内パッチ適用
上のスクリーンショットからわかるように、手順はAMSIのパッチ適用とほとんど同じなので、今回の手順については割愛します。–etw フラグを実行する前と後のスクリーンショットを以下に示します。
inlineExecute-Assembly を –etw フラグで実行する前に、Process Hacker を使用して PowerShell.exe のプロパティを表示します。
–etwフラグを使用してinline-Execute-Assemblyを実行します。
Process Hacker を使用して、inlineExecute-Assembly を実行した後に同じ PowerShell.exe のプロパティを表示します。
デフォルトでは、作成されたAppDomain、名前付きパイプ、またはメール・スロットには、デフォルト値「totesLegit」が使用されます。これらの値は、提供されているアグレッサー スクリプトで変更するか、コマンド ライン フラグを使用して臨機応変に変更することで、テストしている環境内でよりよく溶け込むようにすることができます。コマンド・ラインでの変更例を以下に示します。
一意のAppDomain名と一意の名前付きパイプ名を使用したInlineExecute-Assemblyの例
一意のAppDomain名のChangedMeの例
一意の名前付きパイプLookAtMeの例
実行が成功した後に削除されるAppDomainの例
実行が成功した後に削除される名前付きパイプの例
このセクションは、私が GitHub リポジトリで述べた内容とほとんど同じ内容になりますが、このツールを使用する際に覚えておくべきことがいくつかあることを繰り返すことが重要だと感じました。
以下に防御上の考慮事項をいくつか示します。