タイムトラベルトリアージ:.NETプロセスホロー化のケーススタディを用いたタイムトラベルデバッグ入門

著者:Josh Stroschein、Jae Young Kim


今日のマルウェアにおける難読化とマルチステージレイヤリングの広がりにより、アナリストは退屈で手動のデバッグセッションを強いられることが多くあります。例えば、AgentTeslaのような流行しているコモディティスティーラーを分析する主な課題は、マルウェアの特定ではなく、難読化された配信チェーンを素早く突き抜けて最終ペイロードに到達することです。

従来のライブデバッグとは異なり、タイムトラベルデバッグ(TTD)は、プログラムの実行の決定論的で共有可能な記録をキャプチャします。TTDの強力なデータモデルとタイムトラベル機能を活用することで、最終ペイロードにつながる重要な実行イベントに効率的にピボットできます。

このポストでは、TTDをあなたの分析に組み込み始めるために必要なWinDbgとTTDのすべての基本を紹介します。難読化されたマルチステージの.NETドロッパーがプロセスホロー化を実行する例を通じて、TTDがあなたのツールキットの一部であるべき理由を実証します。

タイムトラベルデバッグとは何か?

タイムトラベルデバッグ(TTD)は、MicrosoftがWinDbgの一部として提供するテクノロジーで、プロセスの実行をトレースファイルに記録し、前後に再生できます。実行を素早く巻き戻して再生できる能力により、デバッグセッションを常に再開したり、仮想マシンのスナップショットを復元する必要がなくなるため、分析時間が削減されます。TTDはまた、記録された実行データをクエリし、言語統合クエリ(LINQ)でフィルタリングして、シェルコード実行やプロセスインジェクションなどのマルウェア機能を実装するAPIの呼び出しやモジュールロードなど、関心のある特定のイベントを見つけることができます。

記録中、TTDはオペレーティングシステムとの完全な相互作用を可能にする透過的なレイヤーとして機能します。トレースファイルは完全な実行記録を保存し、同僚と共有できるため、ライブデバッグの結果に影響を与える可能性のある環境の違いを回避するコラボレーションが可能になります。

TTDは重大な利点を提供していますが、ユーザーは特定の制限を認識する必要があります。現在、TTDはユーザーモードプロセスに限定されており、カーネルモードデバッグには使用できません。TTDによって生成されるトレースファイルは独自の形式であるため、それらの分析は主にWinDbgに関連しています。最後に、TTDはプログラムの過去の実行フローを変更する意味での「真の」タイムトラベルを提供しません。条件または変数を変更して異なる結果を見たい場合は、既存のトレースが発生した内容の固定記録であるため、完全に新しいトレースをキャプチャする必要があります。

プロセスホロー化の兆候を持つマルチステージの.NETドロッパー

Microsoft .NETフレームワークは、長い間、高度に難読化されたマルウェアを開発するための脅威アクターの間で人気があります。これらのプログラムは、分析プロセスを複雑にするために、コードフラット化、暗号化、マルチステージアセンブリを使用することが多いです。この複雑さはプラットフォーム呼び出し(P/Invoke)によって増幅されています。これにより、マネージド.NETコードが管理されていないWindows APIに直接アクセスでき、作成者は試行錯誤されたエバージョン技術(例:プロセスホロー化)をコードに移植できます。

プロセスホロー化は、悪質なコードが別のプロセスを装って実行される、広がりのある効果的なコードインジェクション形式です。ダウンローダーチェーンの終わりでよく見られるのは、このテクニックが注入されたコードが無害なプロセスの正当性を引き継ぐことを可能にするため、基本的な監視ツールでマルウェアを発見することが難しくなります。

このケーススタディでは、TTDを使用して、プロセスホロー化を介して最終ステージを実行する.NETドロッパーを分析します。このケーススタディは、TTDが関連するWindows API関数を素早く表面化させることで、高度に効率的な分析を促進し、多数の.NET難読化レイヤーをバイパスしてペイロードを正確に特定できることを実証しています。

基本分析は、プロセスホロー化の活動を特定できることが多い重要な最初のステップです。例えば、サンドボックスを使用すると、疑わしいプロセスの起動が明らかになる可能性があります。マルウェア作成者は、これらが通常のシステム操作とシームレスに混在するため、ホロー化の対象として正当な.NETバイナリを頻繁に標的にします。この場合、VirusTotalのプロセスアクティビティを確認すると、サンプルがInstallUtil.exe%windir%\Microsoft.NET\Framework\<version>\に見つかります)を起動することがわかります。InstallUtil.exeは正当なユーティリティですが、疑わしいサンプルの子プロセスとしての実行は、潜在的なプロセスインジェクションへの初期調査を焦点を当てるのに役立つインジケーターです。

https://storage.googleapis.com/gweb-cloudblog-publish/images/time-travel-triage-fig1a.max-600x600.png

図1:VirusTotal サンドボックスに記録されたプロセスアクティビティ

プロセス・ドッペルゲンガーなどの新しく、より目立たないテクニックがありますが、アタッカーがプロセスインジェクションを使用する場合、プロセスホロー化の古典的なバージョンを使用することがまだあります。これは、その信頼性、相対的な単純性、そしてそれがまだ洗練度が低いセキュリティソリューションを効果的に回避するという事実のためです。古典的なプロセスホロー化のステップは以下の通りです。

  1. CreateProcessCREATE_SUSPENDEDフラグ付き):被害者プロセス(InstallUtil.exe)を起動しますが、実行前にプライマリスレッドを一時停止します。

  2. ZwUnmapViewOfSectionまたはNtUnmapViewOfSection:元の正当なコードをメモリから削除することで、プロセスを「ホロー化」します。

  3. VirtualAllocExおよびWriteProcessMemory:リモートプロセスで新しいメモリを割り当て、悪質なペイロードを注入します。

  4. GetThreadContext:一時停止されたプライマリスレッドのコンテキスト(状態とレジスタ値)を取得します。

  5. SetThreadContext:取得されたコンテキスト内のエントリーポイントレジスタを修正して、新しく注入された悪質なコードのアドレスを指すように、実行フローをリダイレクトします。

  6. ResumeThread:スレッドを再開し、悪質なコードが正当なプロセスであるかのように実行されます。

TTDを使用してこのアクティビティを確認するために、プロセス作成と、その後の子プロセスのアドレス空間への書き込みに関する検索に焦点を当てます。このサーチで実証されたアプローチは、関連する技術のTTDクエリを調整することで、他のテクニックのトリアージに適応できます。

マルウェアのタイムトラベルトレースの記録

TTDの使用を開始するには、まずプログラムの実行のトレースを記録する必要があります。トレースを記録するための主な2つの方法があります:WinDbgUIを使用するか、Microsoftが提供するコマンドラインユーティリティを使用します。コマンドラインユーティリティは、トレースを記録する最速でカスタマイズ可能な方法を提供し、このポストで探索するものです。

警告:マルウェアの実行ファイルのTTDトレースを記録する際は、マルウェアの動的分析を実行するための通常のすべての予防措置を取ってください。TTD記録はサンドボックステクノロジーではなく、マルウェアがホストと環境とのインターフェースを妨害なく行うことを許可します。

TTD.exeはトレースを記録するための推奨されるコマンドラインツールです。Windowsには組み込みのユーティリティが含まれていますが(tttracer.exe)、そのバージョンは機能が削減されており、主にシステム診断を目的としており、一般使用や自動化を目的としていません。すべてのWinDbgインストールがTTD.exeユーティリティを提供するわけではなく、システムパスに追加するわけでもありません。TTD.exeを取得する最速の方法は、Microsoftが提供するスタンドアロンインストーラーを使用することです。このインストーラーは、TTD.exeをシステムのPATH環境変数に自動的に追加し、コマンドプロンプトから利用可能にします。使用情報を表示するには、TTD.exe -helpを実行します。

トレースを記録する最速の方法は、単に適切な引数でターゲット実行ファイルを呼び出すコマンドラインを提供することです。次のコマンドを使用して、サンプルのトレースを記録します。

C:\Users\FLARE\Desktop\> ttd.exe 0b631f91f02ca9cffd66e7c64ee11a4b.bin
Microsoft (R) TTD 1.01.11 x64
Release: 1.11.532.0
Copyright (C) Microsoft Corporation. All rights reserved.
Launching '0b631f91f02ca9cffd66e7c64ee11a4b.bin'
    Initializing the recording of process (PID:2448) on trace file: C:\Users\FLARE\Desktop\0b631f91f02ca9cffd66e7c64ee11a4b02.run
    Recording has started of process (PID:2448) on trace file: C:\Users\FLARE\Desktop\0b631f91f02ca9cffd66e7c64ee11a4b02.run

TTDが記録を開始すると、トレースは2つの方法のいずれかで完了します。まず、プロセスの終了(例:プロセス終了、未処理例外など)によってトレースが自動的に停止されます。次に、ユーザーが手動で介入できます。記録中、TTD.exeは小さなダイアログボックス(図2を参照)を表示し、2つの制御オプションが含まれています。

  • トレース オフ:トレースを停止し、プロセスから切り離し、プログラムの実行を続行します。

  • アプリを終了:トレースを停止し、プロセスも終了します。

https://storage.googleapis.com/gweb-cloudblog-publish/images/time-travel-triage-fig2a.max-700x700.png

図2:TTDトレース実行制御ダイアログ

TTDトレースを記録すると、次のファイルが生成されます。

  • <trace>.run:トレースファイルは、圧縮された実行データを含む独自の形式です。トレースファイルのサイズは、プログラムのサイズ、実行の長さ、およびロードされる追加リソースの数などの他の外部要因によって影響されます。

  • <trace>.idx:インデックスファイルにより、デバッガーはトレース全体の順序付きスキャンをバイパスして、トレース中の特定の時点を素早く見つけることができます。インデックスファイルは、WinDbgでトレースファイルが最初に開かれるときに自動的に作成されます。一般に、Microsoftは、インデックスファイルは通常トレースファイルの2倍のサイズであることを示唆しています。

  • <trace>.out:トレース記録中に生成されたログを含むトレースログファイル。

トレースが完了すると、.runファイルはWinDbgで開くことができます。

TTDトレースのトリアージ:焦点をデータに移す

TTDの基本的な利点は、手動のコードステップから実行データ分析への焦点のシフトです。このデータ駆動アプローチで迅速かつ効果的なトリアージを実行するには、基本的なTTDナビゲーションとDebugger Data Modelのクエリの両方に習熟していることが必要です。ナビゲーションの基本とDebugger Data Modelの探索から始めましょう。

トレースのナビゲーション

基本的なナビゲーションコマンドは、WinDbg UIのホームタブの下にあります。

https://storage.googleapis.com/gweb-cloudblog-publish/images/time-travel-triage-fig3.max-500x500.png

図3:基本的なWinDbg TTDナビゲーションコマンド

実行制御のための標準的なWinDbgコマンドと短縮キーは、以下の通りです。

TTDトレースを再生すると、正規フロー制御コマンドを補完する逆フロー制御コマンドが有効になります。各逆フロー制御補完は、正規フロー制御コマンドにダッシュ()を追加することによって形成されます。

  • g-:戻る実行–トレースを逆方向に実行

  • g-u:アップバック実行–最後の呼び出し命令までトレースを逆方向に実行

  • t-:逆方向ステップイン–シングルステップイン逆方向

  • p-:逆方向ステップオーバー–シングルステップオーバー逆方向

タイムトラベル(!tt)コマンド

基本的なナビゲーションコマンドを使用するとステップバイステップでトレースを移動できますが、タイムトラベルコマンド!tt)は、特定のトレース位置への正確なナビゲーションを有効にします。これらの位置は、様々なTTDコマンドの出力で提供されることが多いです。TTDトレース内の位置は、#:#形式(例:E:7D5)の2つの16進数で表されます。

  • 最初の部分は、モジュールロードや例外など、主要な実行イベントに通常対応するシーケンシング番号です。

  • 2番目の部分は、その主要な実行イベント以降に実行されたイベントまたは命令の数を示すステップ数です。

このポストの後半で、タイムトラベルコマンドを使用して、プロセスホロー化の例の重要なイベントに直接ジャンプし、手動命令トレースを完全にバイパスします。

TTDデバッガーデータモデル

WinDbgデバッガーデータモデルは、デバッガー情報をオブジェクトのナビゲート可能なツリーとして公開する拡張可能なオブジェクトモデルです。デバッガーデータモデルは、ユーザーがWinDbgでデバッガー情報にアクセスする方法に基本的な変化をもたらし、生テキストベースの出力を処理することから構造化されたオブジェクト情報と相互作用することへと変わります。データモデルはLINQをサポートし、クエリと複雑な実行情報の大量をフィルタリングするために効率的にソートできます。デバッガーデータモデルはまた、コマンドを通じてデバッガーデータモデルにアクセスする方法を反映するAPIを使用して、JavaScriptによる自動化も簡素化します。

デバッガーオブジェクトモデル式を表示するdx)コマンドは、WinDbgのコマンドウィンドウからデバッガーデータモデルと相互作用するための主な方法です。モデルは発見可能性に適しており、ルートDebuggerオブジェクトから始まることで、それを通じて走査を開始できます。

0:000> dx Debugger
Debugger
    Sessions
    Settings
    State
    Utility
    LastEvent

コマンド出力は、Debuggerオブジェクトのプロパティである5つのオブジェクトを一覧表示します。出力内の名前はリンクのように見えますが、デバッガーマークアップ言語(DML)を使用してマークアップされています。DMLは、関連するコマンドを実行するリンクを使用して出力を充実させます。出力のSessionsオブジェクトをクリックすると、次のdxコマンドを実行してそのオブジェクトを展開します。

0:000> dx -r1 Debugger.Sessions
Debugger.Sessions                
    [0x0]            : Time Travel Debugging: 0b631f91f02ca9cffd66e7c64ee11a4b.run

-r#引数は、指定されない場合のデフォルト深度が1の#レベルまでの再帰を指定します。例えば、前のコマンドで再帰を2レベルに増やすと、次の出力が得られます。

0:000> dx -r2 Debugger.Sessions
Debugger.Sessions                
    [0x0]            : Time Travel Debugging: 0b631f91f02ca9cffd66e7c64ee11a4b.run
        Processes       
        Id               : 0
        Diagnostics     
        TTD             
        OS              
        Devices         
        Attributes

-g引数は、反復可能なオブジェクトをデータグリッドに表示します。各要素はグリッド行で、各要素の子プロパティはグリッド列です。

0:000> dx -g Debugger.Sessions
https://storage.googleapis.com/gweb-cloudblog-publish/images/time-travel-triage-fig4a.max-500x500.png

図4:セッションのグリッド表示(列が切り詰められています)

デバッガーおよびユーザー変数

WinDbgは、利便性のためにいくつかの事前定義されたデバッガー変数を提供し、DebuggerVariablesプロパティを通じてリストされます。

0:000> dx Debugger.State.DebuggerVariables
Debugger.State.DebuggerVariables                
   cursession       : Time Travel Debugging: 0b631f91f02ca9cffd66e7c64ee11a4b.run
    curprocess       : 0b631f91f02ca9cffd66e7c64ee11a4b.exe [Switch To]
    curthread        [Switch To]
    scripts         
    scriptContents   : [object Object]
    vars            
    curstack        
    curframe         : ntdll!LdrInitializeThunk [Switch To]
    curregisters    
    debuggerRootNamespace

よく使用される変数は以下の通りです。

  • @$cursession.Processes:セッション内のプロセスのリスト。

  • @$cursession.TTD.Calls:トレース中に発生した呼び出しをクエリするメソッド。

  • @$cursession.TTD.Memory:トレース中に発生したメモリ操作をクエリするメソッド。

    • @$curprocess.Modules:現在ロードされているモジュールのリスト。

    • @$curprocess.TTD.Events:トレース中に発生したイベントのリスト。

    デバッガーデータモデルの調査によるプロセスホロー化の特定

    TTDの概念に対する基本的な理解とトレースの準備ができたことで、プロセスホロー化の証拠を探すことができます。まず、Callsメソッドを使用して、特定のWindows APIコールを検索できます。.NETサンプルの場合、マネージドコードはP/Invokeを通じてアンマネージドWindows APIとインターフェースし、プロセスホロー化のようなテクニックを実行する必要があるため、このサーチは効果的です。

    プロセスホロー化は、CreateProcessへの呼び出しで、作成フラグ値0x4で一時停止状態でプロセスを作成することで始まります。次のクエリはCallsメソッドを使用して、トレース内のカーネル32モジュールの各CreateProcess*への呼び出しのテーブルを返します。ワイルドカード(*)により、CreateProcessAまたはCreateProcessWへの呼び出しの両方がクエリに一致します。

    0:000> dx -g @$cursession.TTD.Calls("kernel32!CreateProcess*")
    https://storage.googleapis.com/gweb-cloudblog-publish/images/time-travel-triage-fig5a.max-500x500.png

    このクエリは多くのフィールドを返しますが、すべてが調査に役立つわけではありません。これに対応するために、元のクエリにSelectLINQクエリを適用できます。これにより、どの列を表示するか、そしてそれらの名前を変更することができます。

    0:000> dx -g @$cursession.TTD.Calls("kernel32!CreateProcess*").Select(c => new { TimeStart = c.TimeStart, Function = c.Function, Parameters = c.Parameters, ReturnAddress = c.ReturnAddress})
    https://storage.googleapis.com/gweb-cloudblog-publish/images/time-travel-triage-fig6a.max-500x500.png

    結果は、位置58243:104Dから始まるCreateProcessAへの1つの呼び出しを示しています。戻り値アドレスに注目してください:これは.NETバイナリなので、ジャストインタイム(JIT)コンパイラによって実行されるネイティブコードは、アプリケーションのメインイメージアドレス空間(非.NETイメージのように)には配置されません。通常、効果的なトリアージステップは、WhereLINQクエリでフィルタリングし、戻り値アドレスをプライマリモジュールに制限して、マルウェアから発生しないAPIコールを除外することです。しかし、このフィルタWhereは、JITコンパイルされたコードの動的な性質のため、分析時に信頼性が低いです。

    次に関心のあるポイントはParametersフィールドです。折りたたまれた値{..}のDMLリンクをクリックすると、対応するdxコマンドを通じてParametersが表示されます。

    0:000> dx -r1 @$cursession.TTD.Calls("kernel32!CreateProcess*").Select( c => new { TimeStart = c.TimeStart, Parameters = c.Parameters, ReturnAddress = c.ReturnAddress})[0].Parameters
    @$cursession.TTD.Calls("kernel32!CreateProcess*").Select( c => new { TimeStart = c.TimeStart, Parameters = c.Parameters, ReturnAddress = c.ReturnAddress})[0].Parameters
        [0x0]            : 0x55de700055de74
        [0x1]            : 0x55e0780055e0ac
        [0x2]            : 0x808000400000000
        [0x3]            : 0x55de4000000000

    関数の引数は特定のCallsオブジェクトで値の配列として利用可能です。しかし、パラメータを調査する前に、TTDが行う仮定についていくつか調べる価値があります。全体的に、これらの仮定はプロセスが32ビットか64ビットかによって影響されます。プロセスのビット数を確認する簡単な方法は、DebuggerInformationオブジェクトを検査することです。

    0:00> dx Debugger.State.DebuggerInformation
    Debugger.State.DebuggerInformation                
        ProcessorTarget  : X86 <--- Process Bitness
        Bitness          : 32
        EngineFilePath   : C:\Program Files\WindowsApps\<SNIPPED>\x86\dbgeng.dll
        EngineVersion    : 10.0.27871.1001

    出力の重要な識別子はProcessorTargetです。この値は、デバッガーを実行しているホストオペレーティングシステムが64ビットかどうかに関係なく、トレースされたゲストプロセスのアーキテクチャを示しています。

    TTDは、プログラムデータベース(PDB)ファイルで提供されるシンボル情報を使用して、パラメーターの数、その型、および関数の戻り値の型を決定します。ただし、この情報は、PDBファイルにプライベートシンボルが含まれている場合にのみ利用可能です。Microsoftは多くのライブラリ用のPDBファイルを提供していますが、これらはしばしば公開シンボルであり、パラメータを正しく解釈するために必要な関数情報が不足しています。TTDがもう1つの仮定を行い、これは不正確な結果につながる可能性があります。主に、最大4つのQWORDパラメータを仮定し、戻り値もQWORDであることを想定しています。この仮定は、32ビットプロセス(x86)で不一致を作成し、引数は通常、スタック上で渡される32ビット(4バイト)の値です。TTDは正しくスタック上の引数を見つけていますが、2つの隣接する32ビット引数を単一の64ビット値として誤解釈しています。

    これを解決する1つの方法は、スタック上の引数を手動で調査することです。まず!ttコマンドを使用して、CreateProcessAへの関連呼び出しの開始にナビゲートします。

    0:000> !tt 58243:104D
    (b48.12a4): Break instruction exception - code 80000003 (first/second chance not available)
    Time Travel Position: 58243:104D
    eax=00bed5c0 ebx=039599a8 ecx=00000000 edx=75d25160 esi=00000000 edi=03331228
    eip=75d25160 esp=0055de14 ebp=0055df30 iopl=0         nv up ei pl zr na pe nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
    KERNEL32!CreateProcessA:
    75d25160 8bff            mov     edi,edi

    戻り値アドレスは関数呼び出しの開始時のスタックの最上部にあるため、以下のddコマンドは、ESPレジスタに4のオフセットを追加することにより、この値をスキップして関数の引数を適切にアラインします。

    0:000> dd /c 1 esp+4 L0A
    0055de18  0055de74  <-- Application Name
    0055de1c  0055de70
    0055de20  0055e0ac
    0055de24  0055e078
    0055de28  00000000
    0055de2c  08080004  <-- Creation Flags - 0x4 (CREATE_SUSPENDED)
    0055de30  00000000
    0055de34  0055de40
    0055de38  0055e0c0
    0055de3c  0055e068

    dwCreationFlags引数(6番目の引数)のビットマスクで設定された0x4CREATE_SUSPENDED)の値は、プロセスが一時停止状態で作成されることを示しています。

    以下のコマンドは、poi演算子を使用してesp+4を逆参照し、アプリケーション名文字列ポインタを取得してから、daコマンドを使用してASCII文字列を表示します。

    0:000> da poi(esp+4)
    0055de74  "C:\Windows\Microsoft.NET\Framewo"
    0055de94  "rk\v4.0.30319\InstallUtil.exe"

    このコマンドは、ターゲットアプリケーションがInstallUtil.exeであることを明らかにし、これは基本分析の結果と一致しています。

    新しく作成されたプロセスへのハンドルを取得することも有用です。これにより、それに対して実行されたその後の操作を特定できます。ハンドル値は、最後の引数として渡されたPROCESS_INFORMATION構造体へのポインタ(上記の参照出力で0x55e068)を通じて返されます。この構造体には、以下の定義があります。

    typedef struct _PROCESS_INFORMATION {
      HANDLE hProcess;
      HANDLE hThread;
      DWORD  dwProcessId;
      DWORD  dwThreadId;
    }

    CreateProcessAへの呼び出しの後、この構造体の最初のメンバーには、プロセスへのハンドルが設定されるはずです。gu(アップ実行)コマンドを使用して呼び出しをステップアウトし、設定された構造体を確認します。

    0:000> gu
    Time Travel Position: 58296:60D
    0:000> dd /c 1 0x55e068 L4
    0055e068  00000104 <-- handle to process
    0055e06c  00000970
    0055e070  00000d2c
    0055e074  00001c30

    このトレースでは、CreateProcessは一時停止されたプロセスのハンドルとして0x104を返しました。

    プロセスホロー化に関して、トリアージの目的として最も興味深い操作は、メモリの割り当てと、その後のメモリへの書き込みです。これは一般的にWriteProcessMemoryへの呼び出しを通じて実行されます。前のCallsクエリは、WriteProcessMemoryへの呼び出しを特定するために更新できます。

    0:000> dx -g @$cursession.TTD.Calls("kernel32!WriteProcessMemory*").Select( c => new { TimeStart = c.TimeStart, ReturnAddress = c.ReturnAddress, Params = c.Parameters})
    =============================================================
    =          = (+) TimeStart = (+) ReturnAddress = (+) Params =
    =============================================================
    = [0x0]    - 6A02A:4B4     - 0x15032e2         - {...}      =
    = [0x1]    - 6E516:A91     - 0x15032e2         - {...}      =
    = [0x2]    - 729A2:511     - 0x15032e2         - {...}      =
    = [0x3]    - 76E2D:750     - 0x15032e2         - {...}      =
    = [0x4]    - 7B2DF:C1C     - 0x15032e2         - {...}      =
    =============================================================
    

    クエリは4つの結果を返します。以下のクエリは、WriteProcessMemoryへの各呼び出しの引数を展開します。

    0:000> dx -r1 @$cursession.TTD.Calls("kernel32!WriteProcessMemory*").Select( c => new { TimeStart = c.TimeStart, ReturnAddress = c.ReturnAddress, Params = c.Parameters})[0].Params
    @$cursession.TTD.Calls("kernel32!WriteProcessMemory*").Select( c => new { TimeStart = c.TimeStart, ReturnAddress = c.ReturnAddress, Params = c.Parameters})[0].Params                
        [0x0]            : 0x104        <-- Target process handle
        [0x1]            : 0x400000     <-- Target Address
        [0x2]            : 0x9810af0    <-- Source buffer
        [0x3]            : 0x200        <-- Write size
    0:000> dx -r1 @$cursession.TTD.Calls("kernel32!WriteProcessMemory*").Select( c => new { TimeStart = c.TimeStart, ReturnAddress = c.ReturnAddress, Params = c.Parameters})[1].Params
    @$cursession.TTD.Calls("kernel32!WriteProcessMemory*").Select( c => new { TimeStart = c.TimeStart, ReturnAddress = c.ReturnAddress, Params = c.Parameters})[1].Params                
        [0x0]            : 0x104
        [0x1]            : 0x402000
        [0x2]            : 0x984cb10
        [0x3]            : 0x3b600
    0:000> dx -r1 @$cursession.TTD.Calls("kernel32!WriteProcessMemory*").Select( c => new { TimeStart = c.TimeStart, ReturnAddress = c.ReturnAddress, Params = c.Parameters})[2].Params
    @$cursession.TTD.Calls("kernel32!WriteProcessMemory*").Select( c => new { TimeStart = c.TimeStart, ReturnAddress = c.ReturnAddress, Params = c.Parameters})[2].Params                
        [0x0]            : 0x104
        [0x1]            : 0x43e000
        [0x2]            : 0x387d9d0
        [0x3]            : 0x600
    0:000> dx -r1 @$cursession.TTD.Calls("kernel32!WriteProcessMemory*").Select( c => new { TimeStart = c.TimeStart, ReturnAddress = c.ReturnAddress, Params = c.Parameters})[3].Params
    @$cursession.TTD.Calls("kernel32!WriteProcessMemory*").Select( c => new { TimeStart = c.TimeStart, ReturnAddress = c.ReturnAddress, Params = c.Parameters})[3].Params                
        [0x0]            : 0x104
        [0x1]            : 0x440000
        [0x2]            : 0x3927a78
        [0x3]            : 0x200

    WriteProcessMemoryには、以下の関数シグネチャがあります。

    BOOL WriteProcessMemory(
      [in]  HANDLE  hProcess,
      [in]  LPVOID  lpBaseAddress,
      [in]  LPCVOID lpBuffer,
      [in]  SIZE_T  nSize,
      [out] SIZE_T  *lpNumberOfBytesWritten
    );

    WriteProcessMemoryへの呼び出しを調査すると、ターゲットプロセスハンドルが0x104(一時停止されたプロセス)であることがわかります。2番目の引数は、ターゲットプロセスのアドレスを定義します。これらの呼び出しの引数は、PE読み込みに共通するパターンを明らかにします。マルウェアは、PEヘッダーに続いて関連するセクションを仮想オフセットで書き込みます。

    ターゲットプロセスのメモリをこのトレースから分析することはできないことに注意する価値があります。子プロセスの実行を記録するには、-childrenフラグをTTD.exeユーティリティに渡します。これにより、すべての子プロセスを含む、実行中に起動された各プロセスのトレースファイルが生成されます。

    ターゲットプロセスの基本アドレス(0x400000)に対する最初のメモリ書き込みは、0x200バイトです。このサイズはPEヘッダーと一致し、ソースバッファ(0x9810af0)の内容を調査することでそれを確認します。

    0:000> db 0x9810af0
    09810af0  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ..............
    09810b00  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ........@.......
    09810b10  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
    09810b20  00 00 00 00 00 00 00 00-00 00 00 00 80 00 00 00  ................
    09810b30  0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68  ........!..L.!Th
    09810b40  69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f  is program canno
    09810b50  74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20  t be run in DOS 
    09810b60  6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00  mode....$.......

    !dh拡張機能を使用して、このヘッダー情報を解析できます。

    0:000> !dh 0x9810af0
    File Type: EXECUTABLE IMAGE
    FILE HEADER VALUES
         14C machine (i386)
           3 number of sections
    66220A8D time date stamp Fri Apr 19 06:09:17 2024
    ----- SNIPPED -----
    OPTIONAL HEADER VALUES
         10B magic #
       11.00 linker version
             ----- SNIPPED -----
           0 [       0] address [size] of Export Directory
       3D3D4 [      57] address [size] of Import Directory
       ----- SNIPPED -----
           0 [       0] address [size] of Delay Import Directory
        2008 [      48] address [size] of COR20 Header Directory
    SECTION HEADER #1
       .text name
       3B434 virtual size
        2000 virtual address
       3B600 size of raw data
         200 file pointer to raw data
    ----- SNIPPED -----
    SECTION HEADER #2
       .rsrc name
         546 virtual size
       3E000 virtual address
         600 size of raw data
       3B800 file pointer to raw data
    ----- SNIPPED -----
    SECTION HEADER #3
      .reloc name
           C virtual size
       40000 virtual address
         200 size of raw data
       3BE00 file pointer to raw data
    ----- SNIPPED -----

    COR20ヘッダーディレクトリ(.NETヘッダーへのポインタ)の存在は、これが.NET実行ファイルであることを示しています。.text0x2000)、.rsrc0x3E000)、.reloc0x40000)の相対仮想アドレスも、WriteProcessMemory呼び出しのターゲットアドレスと一致します。

    新たに発見されたPEファイルは、writememコマンドを使用してメモリから抽出できます。

    0:000> .writemem c:\users\flare\Desktop\headers.bin 0x9810af0 L0x200
    Writing 200 bytes.
    0:000> .writemem c:\users\flare\Desktop\text.bin 0x984cb10 L0x3b600
    Writing 3b600 bytes.......................................................................................................................
    0:000> .writemem c:\users\flare\Desktop\rsrc.bin 0x387d9d0 L0x600
    Writing 600 bytes.
    0:000> .writemem c:\users\flare\Desktop\reloc.bin 0x3927178 L0x200
    Writing 200 bytes.

    16進数エディタを使用して、各セクションを生のオフセットに配置することでファイルを再構成できます。結果の.NET実行ファイル(SHA256:4dfe67a8f1751ce0c29f7f44295e6028ad83bb8b3a7e85f84d6e251a0d7e3076)をdnSpyで簡単に分析すると、その設定データが明らかになります。

    ----- SNIPPED -----
    // Token: 0x0400000E RID: 14
    public static bool EnableKeylogger = Convert.ToBoolean("false");
    // Token: 0x0400000F RID: 15
    public static bool EnableScreenLogger = Convert.ToBoolean("false");
    // Token: 0x04000010 RID: 16
    public static bool EnableClipboardLogger = Convert.ToBoolean("false");
    // Token: 0x0400001C RID: 28
    public static string SmtpServer = "<REDACTED";
    // Token: 0x0400001D RID: 29
    public static string SmtpSender = "<REDACTED>";
    // Token: 0x04000025 RID: 37
    public static string StartupDirectoryName = "eXCXES";
    // Token: 0x04000026 RID: 38
    public static string StartupInstallationName = "eXCXES.exe";
    // Token: 0x04000027 RID: 39
    public static string StartupRegName = "eXCXES";
    ----- SNIPPED -----

    結論:分析アクセラレータとしてのTTD

    このケーススタディは、TTD実行トレースを検索可能なデータベースとして扱うことの利点を実証しています。ペイロード配信をキャプチャし、Debugger Data Modelを直接クエリして特定のAPIコールを行うことにより、.NETドロッパーの複数層の難読化を素早くバイパスしました。ターゲットのデータモデルクエリとLINQフィルタ(CreateProcess*およびWriteProcessMemory*用)と低レベルコマンド(!dh.writemem)の組み合わせにより、隠されたAgentTeslaペイロードを分離して抽出でき、数分で重要な設定の詳細を得ることができました。

    この分析で使用されるツールと環境(最新バージョンのWinDbgとTTDを含む)は、FLARE-VMインストールスクリプトを使用して容易に利用可能です。このあらかじめ設定された環境を使用して、分析ワークフローを効率化することをお勧めします。

    TTDトレースはVirusTotal からダウンロードでき、元のサンプルもあります。

    投稿分類:

    翻訳元: https://cloud.google.com/blog/topics/threat-intelligence/time-travel-debugging-using-net-process-hollowing/

    ソース: cloud.google.com