遠慮なくご相談ください:InlineExecute-Assembly による Fork&Run .NET 実行の回避

夜遅くまでコードを書いて作業しながらコンピューターの画面を見ている男性

気に入っている人もあれば、嫌う人もいますが、現時点では、.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を作成した理由、その主な機能、注意事項、敵対者シミュレーションやレッドチームを実施する際にどのように役立つかについて詳しく説明しましょう。

The DX Leaders

AI活用のグローバル・トレンドや日本の市場動向を踏まえたDX、生成AIの最新情報を毎月お届けします。登録の際はIBMプライバシー・ステートメントをご覧ください。

ご登録いただきありがとうございます。

ニュースレターは日本語で配信されます。すべてのニュースレターに登録解除リンクがあります。サブスクリプションの管理や解除はこちらから。詳しくはIBMプライバシー・ステートメントをご覧ください。

InlineExecute-Assembly を使用する理由

InlineExecute-Assemblyを構築する理由は非常にシンプルです。私は、敵対者シミュレーション・チームがプロセス内で.NETアセンブリを実行し、Cobalt Strikeを使用して成熟した環境で運用する際に、上記のOPSECの落とし穴の一部を回避できるようにする方法を求めていました。また、現在の.NETツールのほとんどに変更を加える必要があることで、チームに余分な開発時間がかかりないようにするためのツールも必要でした。安定性も必要でした。複雑なBOFは安定していますが、数少ないビーコンの1つを環境の中に失うことは避けたいものです。基本的には、Cobalt Strikeの実行アセンブリー・モジュールと同様に、オペレーターにとって可能な限りスムーズに動作する必要があります。

オフィスでミーティングをするビジネスチーム

IBMお客様事例

お客様のビジネス課題(顧客満足度の向上、営業力強化、コスト削減、業務改善、セキュリティー強化、システム運用管理の改善、グローバル展開、社会貢献など)を解決した多岐にわたる事例のご紹介です。

主な機能

共通言語ランタイム(CLR)の読み込み

わかっています、ある意味明白です。AIがなければ、これまで以上に前進することはできなかったでしょう。冗談はさておき、CLRがどのように動作し、何が行われているのかの複雑さは、それだけでブログの記事になりそうなので、ここでは、アンマネージコードでCLRをロードする際にBOFが使用するものを、非常に高いレベルでレビューすることにします。

CLRの読み込み中のスクリーンショット

CLR をロードする

上記の簡略化された製品の画面に示すように、BOFがCLRをロードするために実行する主な手順は次のとおりです。

  1. ICLRMetaHostインターフェースの取得に使用されるCLRCreateInstanceを呼び出します。
  2. その後、ICLRMMetaHost ->GetRuntimeを使用して、要求する.NETのバージョンのランタイム情報を取得します。アセンブリが.NETバージョン3.5以下で構築された場合は、v2.0.50727をリクエストし、アセンブリが.NET 4.0以降で構築された場合は、v4.0.30319をリクエストします。実際、BOFには、.NETアセンブリが自動的に使用するバージョンを把握するのに役立つ関数がありますが、それについては後で説明します。
  3. ランタイム情報を取得したら、ICLRRruntimeInfo->IsLoadableを使用して、ランタイムをプロセスにロードできるかどうかを確認します。これでは、他のランタイムがすでにロードされている可能性がある場合も考慮され、ランタイムをプロセス内でロードできる場合、BOOL値を1(true)に設定します。
  4. すべてが確認できたら、ICLRRRuntimeInfo->GetInterfaceを実行してCLRをプロセスにロードし、ICorRunTimeHostへのインターフェースを取得します。
  5. 最後に、CLRを開始するICorRuntimeHost->Startを呼び出します。

これでCLRは初期化されましたが、お気に入りの.NETアセンブリを実際に実行する前に、まだやるべきことが少しあります。Microsoft社が「アプリケーションが実行される分離された環境」と説明するAppDomainインスタンスを作成する必要があります。言い換えれば、これは、エクスプロイテーション後の.NETアセンブリーのロードと実行に使用されます。

スクリーンショット:AppDomainが作成中、アセンブリがロード/実行中

AppDomainが作成され、アセンブリがロード/実行されている

上記の簡略化されたスクリーンショットに示されているように、BOF が当社の .NET アセンブリを読み込んで呼び出すために実行する主な手順は次のとおりです。

  1. ICorRuntimeHost->CreateDomainを使用して独自のAppDomainを作成します
  2. IUnknown->QueryInterface(pAppDomainThunk)を使用して、AppDomainインターフェースへのポインターを取得します。
  3. SafeArrayを作成し、.NETアセンブリ・バイトをそれにコピーします。
  4. AppDomain->Load_3経由でアセンブリをロードします。
  5. Assembly->EntryPoint経由でアセンブリー内のエントリー・ポイントを取得します。
  6. 最後に、MethodInfo->Invoke_3経由でアセンブリを呼び出します

これで、アンマネージ コードによる .NET 実行について高度な理解が得られたことと思いますが、これではまだ運用上健全なツールにはまったく遠いため、BOF に実装されたいくつかの主要な機能を見て、BOF を「まあまあ」から「完全に正当な」ものにしていきます。

Console STDOUTを名前付きパイプまたはメール・スロットにリダイレクト:ツールの変更を回避

おそらく、これがなぜ重要なのか疑問に思われるでしょう。まあ、あなたも私と同じように時間を大切にしているなら、エントリ ポイントが、通常はコンソールの標準出力にパイプされるだけのデータをすべて含む文字列を返すように、ほぼすべての .NET アセンブリを変更するのに時間を費やしたくないですよね。そうだろうと思いましたよ。これを回避するには、標準出力を名前付きパイプまたはメール スロットにリダイレクトし、出力が書き込まれた後に読み取って、元の状態に戻す必要があります。この方法なら、cmd.exe または powershell.exe の場合と同じように、変更されていないアセンブリを実行できます。さて、コードを説明する前に、@N4k3dTurtl3 によるプロセス実行アセンブリとメール スロットに関するブログ記事に感謝したいと思います。これがそもそも、この技術が初めて登場したときに、私のプライベートの C インプラントにこの技術を実装する道に私を導いたもので、数か月後には同じ機能を BOF に移植しました。さて、小道具が用意できたので、stdoutを以下の名前付きパイプにリダイレクトすることでこれがどのように実現されるかの簡単な例を見てみましょう。

スクリーンショット:コンソールの標準出力を名前付きパイプにリダイレクトし、元に戻す

コンソールの標準出力を名前付きパイプにリダイレクトして元に戻す

アセンブリの.NETバージョンを決定する

ICLRMetaHost ->GetRuntime 経由で CLR をロードするときに、必要な .NET フレームワークのバージョンを指定する必要があったことを覚えていますか。.NET アセンブリーがどのバージョンでコンパイルされたかに依存することをことを覚えていますか。毎回どのバージョンが必要かを手動で指定するのはあまり楽しいことではないと思いませんか。幸運なことに、@b4rtikは、execute-assemblyモジュール内でMetasploitフレームワーク用にこれを扱うクールな関数を実装しており、私たち自身のツールに簡単に実装できます。以下にそれを示します。

スクリーンショット:.NETアセンブリを読み取り、CLRをロードする際に必要な.NETバージョンを決定するのに役立つ関数

.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バージョン変数を設定するためのif/elseステートメント

マルウェア対策スキャン・インターフェース(AMSI)のパッチ適用

.NET 攻撃の手口について語る際にAMSIについて語らないわけにはいきません。AMSI とは何か、またそれをバイパスするすべての方法については何度も説明されているため詳しく説明しません。ただし、BOF 経由で何を実行するかに応じて AMSI にパッチを適用することが必要になる可能性がある理由について少し説明します。たとえば、難読化を一切行わずにSeatbeltを実行しようとすると、出力が返されず、ビーコンが機能していないことがすぐにわかります。こうなると、もうどうしようもありません。これは、AMSI がアセンブリを捕捉し、それが悪意のあるものであると判断し、騒がしいホーム パーティーに抗議するようにアセンブリをシャットダウンしたためです。これは理想的ではないですよね?AMSI に関しては、ConfuserXInvisibility Cloakなどを使用した .NET ツールの難読化と、さまざまな手法を使用した AMSI の無効化の、2 つの適切なオプションがあります。今回は、メモリ内の amsi.dll にパッチを適用して E_INVALIDARG を返し、スキャン成果 0 を作成するRastaMouseを使います。これは、該当のブログ記事で指摘されているように、通常は AMSI_RESULT_CLEAN として解釈されます。以下に x64 プロセスのコードの簡略化されたバージョンを見てみましょう。

スクリーンショット:AmsiScanBufferのメモリ内パッチ適用

AmsiScanBufferのメモリ内パッチ適用

上のスクリーンショットにあるように、次のことを行うだけです。

  1. amsi.dllをロードし、AmsiScanBufferへのポインターを取得します。
  2. メモリ保護を変更します。
  3. amsiPatch[]バイトにパッチを適用します。
  4. メモリ保護を元の状態に戻します。

これをツールに実装することで、以下のように–amsiフラグを使用してデフォルトバージョンのSeatbelt.exeを実行し、AMSI検知を回避することができます。

スクリーンショット:InlineExecute-Assemby AMSI バイパスの例

InlineExecute-Assemby AMSI バイパスの例

Windows 用イベント トレーシング(ETW)のパッチ適用

防御側にとって幸いなことに、ETW を使用して悪意のある .NET の手口を検出する場合、AMSI 以外にも役立つものがあります。残念ながら、AMSI と同様に、これも攻撃者が簡単に回避できる可能性があり、@xpn はこれがどのように実行されるかについて非常に素晴らしい研究を行いました。以下に、ETW にパッチを適用して完全に無効にする方法の簡略化された例を示します。

スクリーンショット:EtwEventWriteのメモリ内パッチ適用

EtwEventWriteのメモリ内パッチ適用

上のスクリーンショットからわかるように、手順はAMSIのパッチ適用とほとんど同じなので、今回の手順については割愛します。–etw フラグを実行する前と後のスクリーンショットを以下に示します。

スクリーンショット:–etwフラグを使用して inlineExecute-Assembly を実行する前に、Process Hacker を使用して PowerShell.exe のプロパティを表示

inlineExecute-Assembly を –etw フラグで実行する前に、Process Hacker を使用して PowerShell.exe のプロパティを表示します。

スクリーンショット:–etwフラグを使用したinline-Execute-Assemblyの実行

–etwフラグを使用してinline-Execute-Assemblyを実行します。

スクリーンショット:inlineExecute-Assemblyを実行した後に、Process Hackerを使用して同じPowerShell.exeのプロパティを表示

Process Hacker を使用して、inlineExecute-Assembly を実行した後に同じ PowerShell.exe のプロパティを表示します。

一意のAppDomain、名前付きパイプ、メール・スロット

デフォルトでは、作成されたAppDomain、名前付きパイプ、またはメール・スロットには、デフォルト値「totesLegit」が使用されます。これらの値は、提供されているアグレッサー スクリプトで変更するか、コマンド ライン フラグを使用して臨機応変に変更することで、テストしている環境内でよりよく溶け込むようにすることができます。コマンド・ラインでの変更例を以下に示します。

Beaconでのコマンド「inlineExecute-Assembly --dotnetassembly /root/Desktop/MessageBoxCS.exe」の実行を示すターミナルのスクリーンショット。出力には、inlineExecute-Assemblyの実行、homeというホスト(16319バイトを送信)、「Hello From .NET!」という出力の受信、「inlineExecute-Assembly Finished」という完了メッセージなどのステータス・メッセージが含まれます。

一意のAppDomain名と一意の名前付きパイプ名を使用したInlineExecute-Assemblyの例

スクリーンショット:一意のAppDomain名のChangedMeの例

一意のAppDomain名のChangedMeの例

スクリーンショット:一意の名前付きパイプLookAtMeの例

一意の名前付きパイプLookAtMeの例

スクリーンショット:実行が成功した後に削除されるAppDomainの例

実行が成功した後に削除されるAppDomainの例

スクリーンショット:実行が成功した後に削除される名前付きパイプの例

実行が成功した後に削除される名前付きパイプの例

補足

このセクションは、私が GitHub リポジトリで述べた内容とほとんど同じ内容になりますが、このツールを使用する際に覚えておくべきことがいくつかあることを繰り返すことが重要だと感じました。

  1. 私はこれを可能な限り安定させようと努めてきましたが、クラッシュしたりビーコンが停止したりしないという保証はありません。何か問題が発生した場合にビーコンが存続する追加の豪華なフォークアンドランはありません。これがBOFとのトレードオフです。そうは言っても、アセンブリが適切に動作することを確認するために事前にアセンブリをテストすることがいかに重要であるかは、強調してもしきれません。
  2. BOF はプロセス内で実行され、実行中にビーコンを引き継ぐため、長時間実行されるアセンブリに使用する前にはこれを考慮する必要があります。結果が返ってくるまでに時間がかかる作業を選んだ場合、結果が戻ってアセンブリが実行を終えるまで、ビーコンはアクティブにならず、コマンドを実行できません。これはスリープ設定にも忠実ではありません。たとえば、スリープが 10 分に設定されていて BOF を実行しても、BOF の実行が終了するとすぐに結果が返されます。
  3. PE をメモリにロードするツール (SafetyKatz など) に変更を加えない限り、ビーコンが機能しなくなる可能性が高くなります。これらのツールの多くは、終了する前に犠牲プロセスからコンソール出力を送信できるため、実行アセンブリで正常に動作します。それらが処理中のBOF経由で退出すると、私たちのプロセスが停止し、ビーコンが停止されます。これらは動作するように修正できますが、これらのアセンブリは実行アセンブリで実行することをお勧めします。なぜなら、他のOPSECに配慮しないものがプロセスにロードされてしまい、削除されない可能性があるからです。
  4. アセンブリが Environment.Exit を使用している場合は、プロセスとビーコンが強制終了されるため、これを削除する必要があります。
  5. 名前付きパイプとメール・スロットは一意である必要があります。データが返されず、ビーコンがまだ動作している場合は、おそらく別の名前付きパイプまたはメール スロット名を選択する必要があります。

防御に関する考慮事項

以下に防御上の考慮事項をいくつか示します。

  1. これは、AMSIおよびETWメモリ・パッチ適用を実行するときにPAGE_EXECUTE_READWRITEを使用します。これは意図的に行われたもので、PAGE_EXECUTE_READWRITE のメモリ保護を備えたメモリ範囲を持つプログラムはほとんどないため、警告となるはずです。
  2. 作成された名前付きパイプのデフォルト名は「totesLegit」です。これは意図的に行われたものであり、署名検知を使用してこれをフラグ付けできます。
  3. 作成されたメールスロットのデフォルト名は「totesLegit」です。これは意図的に行われたものであり、署名検知を使用してこれをフラグ付けできます。
  4. 読み込まれたAppDomainのデフォルト名は「totesLegit」です。これは意図的に行われたものであり、署名検知を使用してこれをフラグ付けできます。
  5. .NETの悪意ある利用検出に関する良いヒントを以下に示します。@bohopsによるものについてはこちら、F-Secureによるものについてはこちら、およびこちらをご覧ください。
  6. CLR がロードされるべきではないアンマネージ プロセスなど、疑わしいプロセスへの .NET CLR のロードを探します。
  7. イベント・トレーシングの詳細。
  8. 他の既知のCobalt Strike Beacon IOCまたはC2エグレス/コミュニケーションIOCを探します。