
以下に続く作業は、マルウェア分析者の取り組みを妨害するために、マルウェアが特定の被害者に「キー付け」されることを可能にするPOCです。
このプロジェクトに含まれるアイデアやコードの悪意ある使用について、私は一切の責任を負いません。私はこの研究を、情報セキュリティ専門家の更なる教育、そしてマルウェア分析者、リバースエンジニア、ブルーチーム全体への追加のトレーニング・思考の糧として提供します。
マルウェアが被害者上で初回実行されるとき、その被害者の環境データを使用して実際のペイロード(RDLL)をAES暗号化します。その後、マルウェアが実行されるたびに同じ環境情報を収集し、マルウェア内にバイト配列として格納されたペイロードをAES復号し、実行します。復号またはペイロードの実行に失敗した場合、マルウェアは自身を削除します。リバースエンジニアやマルウェア分析者からの保護です。


このプロジェクトに未完成の感覚があったため、大きく書き直しました。元の研究と技術はこちらにあります。
主な変更点は以下の通りです。
このプロジェクトのソースコードから、他の場所で使用できるさまざまな要素を取り出すことができます。誰かの役に立てば幸いです。
BeatRevの最初のリリースにはいくつかの欠点があり、それに対処しようと決めました。
以前はStage2は独立した実行可能ファイルであり、Stage1の代替データストリーム(ADS)として保存されていました。被害者ごとのAES暗号化とその後の復号・実行を実現するために、Stage1が実行されるたびにADSを読み取り、復号し、ADSに書き戻し、CreateProcessを呼び出し、その後Stage2を再暗号化してADSとしてディスクに書き戻す必要がありました。これは多くのI/O操作であり、もちろんCreateProcessの呼び出しも理想的ではありませんでした。
たまたまSteven FewerのReflective DLLに関する研究を見つけ、これが適していると思いました。Stage2は現在RDLLです。保護したいマルウェア/シェルコードランナーなどはRDLL形式に移植され、Stage1内にバイト配列として格納され、実行時に復号されてStage1によって実行されます。これにより、バージョン1のすべてのI/O操作とCreateProcess呼び出しが排除され、歓迎すべき変更です。
Stage1には実際のAV回避対策が組み込まれていませんでした。これは意図的であり、余分な作業であり、この研究の目的ではなかったためです。書き直しの際、これを追加の課題として捉え、APIハッシュを追加してStage1のインポートアドレステーブルから関数を削除しました。これにより検出が改善され、Stage1はVirusTotalで4/66の検出率になりました。Stage1はすでに最初に実行された元のマシンにキー付けされており、AES暗号化によってファイルシグネチャが常に変化するため、アップロードしても問題ないと判断しました。
最近、マルウェア検出の手段としてエントロピーに注目し始めました。巨大なAES暗号化バイナリブロブが実行可能ファイルに与える非常に高いエントロピーを下げるために、UUIDとして保存されたシェルコードを統合する方法を検討しました。バイナリが文字列表現で保存されるため、実行可能ファイルの全体的なエントロピーが低くなります。この手法により、Stage0のエントロピーは約6.8、Stage1は約4.5(最大値8)になりました。
最後に、操作する必要があるすべての部品のため、完全なStage0を統合・生成することは大きな作業です。これを容易にするために、ビルダーアプリケーションを作成しました。これはStage0.cテンプレートファイル、Stage1スタブ、Stage2スタブ、および生のシェルコードファイル(Stage2がCobaltStrikeシェルコードを含むシェルコードランナーであることを前提に構築)を入力として、ターゲットで使用するコンパイル済みStage0ペイロードを生成します。
Stephen FewerのReflective DLLコードには、Visual Studioコンパイラ固有の命令が含まれています。この手法をMingWに移植することは可能だと思いますが、私にはそのスキルがありません。主な問題は、CobaltStrikeシェルコード(ステートレスで約265KB)をRDLL内に配置し、コンパイルする必要があることです。これを回避し、残りのプロセスとうまく統合するために、私のStage2 RDLLにはCSシェルコードのサイズに等しいメモリのグローバル変数チャンクを含めました。この約265KBのメモリチャンクには小さなプレースホルダーが含まれており、コンパイルされたバイナリ内で見つけることができます。src/Stage2内のコードにはこれが既に追加されています。
コンパイル後、このStage2スタブはkaliに転送され、バイナリパッチを実行して実際のCSシェルコードを適切なメモリ位置に書き込みます。これにより完全なStage2が生成されます。
前述のI/OとCreateProcessの混乱を避けるために、完全なStage2もStage0によってコンパイル済みStage1にパッチされる必要があります。これは、ターゲット上でStage2を暗号化できるようにするだけでなく、Stage2をディスクに別々に保存するのを防ぐために必要です。Stage2に対して前述したのと同じ概念が、ターゲット上でStage0によって実行され、最終的なStage1ペイロードが組み立てられます。各スタブ内のプレースホルダーを見つけるためにmemmem関数が使用されることに注意してください。この関数はWindowsでは利用できないため、カスタム実装を使用しました。Foxik384のコードに感謝します。
バイナリパッチを実行するには、必要なメモリを事前に割り当てる必要があります。これには複合効果があり、Stage1はStage2も含むのに十分な大きさでなければなりません。Stage2をUUID文字列に変換する追加ステップにより、Stage2はサイズが膨らみ、それを保持するためにStage1も同様に膨らみます。コンパイルサイズが約290KBのStage2 RDLLは、約1.38MBのStage0ペイロードと約700KBのStage1ペイロードになります。
ビルダーアプリケーションはx64 EXEの作成のみをサポートしています。しかし、もう少し作業を加えれば、理論的にはStage0をDLLにし、Stage1もDLLにすることで、ライフサイクル全体をスタンドアロン実行可能ファイルではなくDLLハイジャックとして存在させることができるでしょう。
以下の手順でこのPOCを使用できます。

約6ヶ月前、私はAV/EDR回避に関してマルウェアについて多くのことを学び、行ってきましたが、リバースエンジニアリング/マルウェア分析を回避または打ち負かそうとすることにはほとんど時間を費やしていないことに気づきました。これにはいくつかの理由があります。
それでも、これは興味深い思考実験であり、マルウェア分析について実際に知っている数人の同僚がいて、アイデアをぶつけ合うことができました。AV/EDR回避とはまったく異なる規模の挑戦のように思え、挑戦してみることにしました。
私の最初の前提は、マルウェアが初回実行時に何らかの方法でその被害者マシンに「キー付け」されることでした。その後の実行試行は、ターゲット環境内の何かを評価し、マルウェア内の値と比較します。これら2つの要素が一致すれば、期待通りに実行されます。一致しない場合(サンプルがマルウェア分析者のサンドボックスに転送された場合など)、マルウェアは自身を削除します(LloydLabs と彼のdelete-self-pocの作業に大きく依存しています)。
この「キー」は被害者コンピュータに「固有」のものでなければなりません。理想的には、複数の情報を組み合わせ、さらに難読化します。例えば、コンピュータのホスト名と搭載RAM容量を収集し、これら2つの値を連結(例:Client018192MB)し、ユーザー定義関数でハッシュして数値(例:5343823956)を生成します。
収集する情報の選択肢は多数ありますが、ブルーチームが簡単に偽装できる値については考慮すべきです。例えば、MACアドレスは被害者の「固有」の識別子として魅力的に見えるかもしれませんが、MACアドレスはリバースエンジニアが自分のサンドボックスを元の被害者に一致させるために手動で簡単に設定できます。理想的には、選択および列挙される値は、リバースエンジニアが自分の環境で再現するのが難しいものであるべきです。
自己削除の魔法を使えば、マルウェアは自身をバッファに読み込み、プレースホルダー変数を見つけてこの数値で置き換え、自身を削除し、変更されたマルウェアを同じ場所にディスクに書き戻すことができます。Main内のif/else文と組み合わせることで、次回マルウェアが実行されるとき、以前に実行されたことを検出し、再度ホスト名とRAM容量を収集してハッシュ値を生成します。これが初回実行時にマルウェアに格納された数値(5343823956)と比較されます。一致する場合(マルウェアが元と同じマシンで実行されている場合)、期待通り実行されます。しかし、異なる値が返された場合、再び自己削除関数を呼び出してディスクから自身を削除し、作者をマルウェア分析者から保護します。
これは理論上は良いアイデアのように思えましたが、実際のマルウェア分析とリバースエンジニアリングの経験を持つ同僚と話すまで続きました。リバースエンジニアはマルウェア内の条件文(ValueFromFirstRun != GetHostnameAndRAM())を観察でき、期待される値が条件文の片側にハードコードされているため、レジスタを変更して期待される値を含めることで、保護メカニズム全体を完全にバイパスできると言われました。
この新しい知識により、思考実験は完全に頓挫し、そもそもこのような能力を使用する必要性がなかったため、プロジェクトは約6ヶ月間停止しました。
このプロジェクトはその後6ヶ月の間に数回再浮上しましたが、毎回ほんの一過性の考えに過ぎませんでした。リバースエンジニアリング/マルウェア分析に関する新しい知識は得ておらず、再びそのような能力の必要性もありませんでした。数日前に再びアイデアが浮かびましたが、これらの要因はどちらも変わっていませんが、今回はより多くの知識があり、アイデアを手放せませんでした。
前述のハードコード値に関する問題を念頭に置き、最終的にマルチステージ設計を採用することにしました。これらをStage0、Stage1、Stage2と呼びます。
Stage0: セットアップ。初期感染時に実行され、その後削除されます。
Stage1: ランナー。マルウェアが実行されるたびに実行されます。
Stage2: ペイロード。保護したい実際のマルウェア。プロセスを生成し、シェルコードを注入してビーコンを返します。
Stage0は、攻撃者によってターゲットに配信される新しい実行可能ファイルです。Stage1とStage2をAES暗号化されたバイト配列として含んでいます。これは、転送中のマルウェアを保護するため、または防御者が何らかの方法でStage0のコピーを入手した場合(本来は起こらないはず)に備えてです。AESキーとIVはStage0内に含まれているため、実際にはこれでは有能なブルーチームからStage1やStage2を保護できません。
Stage0 は以下のアクションを実行します。
この一連のイベントの終了時に、Stage0は終了します。ステップ2でディスクから削除され、メモリ上でも実行されていないため、Stage0は事実上消滅します。この手法を事前に知らなければ、残りのマルウェアのライフサイクルは既に混乱している以上に混乱を招くでしょう。
ステップ4では、プロセッサ名とMicrosoft ProductIDが収集されます。ProductIDはレジストリから取得され、この値は手動で変更できるため、ブルーチームが自分のサンドボックスをターゲット環境に合わせるのが容易になります。収集する環境情報によって、これは容易にも困難にもなります。
Stage1はStage0によってドロップされ、Stage0と同じ場所(名前も含む)に存在します。Stage2はStage1のADSとして保存されています。攻撃者/永続化メカニズムがその後マルウェアを実行すると、Stage1が実行されます。
Stage1 は以下のアクションを実行します。
Stage2は上書き可能になるために終了する必要があることに注意してください。自己削除のトリックは、既にADSとなっているファイルには機能しないようです。なぜなら、自己削除手法は実行可能ファイルのプライマリデータストリームの名前変更に依存しているからです。理想的には、Stage2はインジェクトまたはスポーン+インジェクトの実行可能ファイルです。
Stage1が同じ被害者から実行されていないことを検出し、自身/Stage2を削除して脅威アクターを保護するポイントが2つあります。1つ目は、収集した環境情報を使用してStage2を復号した後の実行可能ヘッダーのチェックです。理論上、このステップはリバースエンジニアによってバイパスされる可能性がありますが、最初の良いチェックです。2つ目の保護ポイントは、CreateProcess呼び出しの結果です。Stage2が適切に復号されなかったために失敗した場合、同様にマルウェアは削除されます。この呼び出しの結果を変更してリバースエンジニアによる削除を防ぐこともできますが、Stage2が暗号化されてアクセス不能であるという事実は変わりません。
Stage2はマルウェアチェーンの核心です。それ自体が本格的なシェルコードランナー/マルウェアです。この方法で暗号化および保護することにより、最終状態のマルウェアの動作は、リバースエンジニアやマルウェア分析者からよりよく難読化され、保護されます。開発中は、CobaltStrikeシェルコードを含む既存のシェルコードランナーの1つを使用しましたが、これは攻撃者が実行して保護したい任意のもので構いません。
このようなマルウェアライフサイクルで実際に何が達成されるのでしょうか?いくつかの興味深い点について話すことができます。
代替データストリームはNTFSファイルシステムに固有の機能です。つまり、初期感染後にマルウェアを転送するほとんどの方法では、Stage2がStage1のADSであるため、Stage2が除去されて失われます。Stage2を保存するには特別な注意が必要です。Stage2がなければ、多くのリバースエンジニアやマルウェア分析者は何が起こっているのか非常に混乱するでしょう。RARアーカイブはADSを保存でき、7ZやPeazipなどのツールはファイルとそのADSを抽出できます。
前述の通り、このライフサイクルを使用するマルウェアがブルーチームに届く頃には、Stage1の状態になっているはずです。Stage0は既に存在せず、Stage2はStage0によって収集された環境情報で暗号化されています。Stage0が存在したことさえ知らなければ、ライフサイクルの理解やStage2の復号にはかなりの不確実性が加わります。
理論上(繰り返しますが、リバースエンジニアリングの経験はありません)、Stage1は(ブルーチームが自己削除を繰り返すコピーをいくつか調べた後で)リバースエンジニアリング可能であり、Stage1がターゲットシステムから収集する情報を特定できるはずです。よく調整された対応があれば、ブルーチームはマルウェアの発信元の被害者を特定し、そこからその情報を収集し、プログラムにフィードして適切にAESキー/IVに変換し、Stage2を復号できるはずです。しかし、リバースエンジニアの相対的なスキルや、被害者マシンがその情報を回復できる状態で利用可能かどうかなど、多くの「もしも」があります。
アプリケーションホワイトリスティングは、このライフサイクルを著しく妨害するでしょう。Stage0/Stage1はDLLとしてサイドロードできるかもしれませんが、Stage2がADSであることはいくつかの問題を引き起こすと思われます。AWLに対してマルウェアをテストする環境がなく、すべてをDLL形式に移植する手間もかけていないため、断言できません。これらの問題を回避する創造的な方法は確かに存在するでしょう。
また、Stage2を実行するためのより賢い方法があると確信しています。ディスクにドロップしてCreateProcessを呼び出す代わりに、実行可能ファイルを手動でマッピングするか、Donutのようなツールを使用してシェルコードに変換するのが合理的なアイデアです。
開発中に、Stage1とStage2を入力として機能的なStage0を生成するビルダーアプリケーションを作成しました。ただし、これは提供されません。Stage0は読者の演習として除外され、Stage2はあなたが実行して保護したい任意のスタンドアロン実行可能ファイルです。このPOCは、読者の努力と裁量でさらに研究される可能性があります。提供するのは、このマルウェアのコンパイル済みコピーであるDropper64.exeです。Dropper64.exeはx64用にコンパイルされています。Dropper64.exeはStage0であり、Stage1とStage2を含んでいます。実行すると、Stage1とStage2はディスクに保存されますが、自動的には実行されません。再度Dropper64.exe(今度はStage1として)を実行する必要があります。Stage2はx64版のcalc.exeです。これは、これを見てみたいBlue Teamerの方々のために含めていますが、インシデント対応のシナリオでは99%の確率でStage1/Stage2を受け取ることになり、Stage0は消えていることを覚えておいてください。
これは、長い週末を費やした興味深いペットプロジェクトでした。デバッガや逆アセンブラの経験があれば、もっと高度で完全なものになっていたでしょうが、手持ちの手段で最善を尽くすものです。Blue Teamerや他のマルウェア開発者からの意見を聞くのを楽しみにしています。実際のAPTが行っていることを考えると、ここで車輪の再発明を過度に複雑にしてしまったことは間違いありませんが、その過程でいくつかのことを学びました。お読みいただきありがとうございます!