タイムトラベル トリアージ: .NET プロセスの空洞化のケーススタディを使用したタイムトラベル デバッグの概要
Mandiant
※この投稿は米国時間 2025 年 11 月 14 日に、Google Cloud blog に投稿されたものの抄訳です。
昨今のマルウェアでは、難読化や多段階のレイヤリングが一般的になっているため、アナリストは面倒な手作業のデバッグを行わなければならないことが多くなっています。たとえば、AgentTesla のような広く蔓延している汎用の情報窃取型マルウェア(コモディティ スティーラー)を分析する際の主な課題は、マルウェアを特定することではなく、難読化された配信チェーンを迅速に切り抜けて最終的なペイロードに到達することです。
従来のライブ デバッグとは異なり、Time Travel Debugging(TTD)は、プログラムの実行の再現性があり、共有可能な実行記録をキャプチャします。TTD の強力なデータモデルとタイムトラベル機能を活用することで、最終的なペイロードにつながる主要な実行イベントに効率的に焦点を合わせることができます。
この投稿では、TTD を分析に取り入れるために必要な WinDbg と TTD の基本をすべて紹介します。プロセスの空洞化を実行する難読化されたマルチステージの .NET ドロッパーを例に、このツールがツールキットに加える価値がある理由を示します。
タイム トラベル デバッグとは
Microsoft が WinDbg の一部として提供するテクノロジーである Time Travel Debugging(TTD)は、プロセスの実行をトレース ファイルに記録し、そのトレース ファイルを前向き、後向きに再生できます。実行をすばやく巻き戻して再生できるため、デバッグ セッションの再起動や、、仮想マシンのスナップショットの復元を繰り返す必要がなくなり、分析時間が短縮されます。また、TTD では、記録された実行データをクエリし、Language Integrated Query(LINQ)でフィルタリングして、モジュールの読み込みや、シェルコードの実行やプロセス インジェクションなどのマルウェア機能を実装する API の呼び出しなど、関心のある特定のイベントを見つけることができます。
記録中、TTD はオペレーティング システムとの完全なインタラクションを可能にする透過的なレイヤとして機能します。トレース ファイルには完全な実行記録が保存されるため、これを同僚と共有すると共同作業に役立ちます。また、ライブ デバッグの結果に影響する可能性のある環境の違いも回避できます。
TTD には大きなメリットがありますが、ユーザーはある程度の制限事項を認識しておく必要があります。現在、TTD はユーザーモード プロセスに限定されており、カーネルモードのデバッグには使用できません。TTD によって生成されるトレース ファイルは独自の形式であるため、その分析は主に WinDbg に関連付けられます。最後に、TTD はプログラムの過去の実行フローを変更するという意味での「真の」タイムトラベルを提供しません。条件や変数を変更して異なる結果を確認したい場合は、既存のトレースは発生したことを固定的に記録したものであるため、完全に新しいトレースをキャプチャする必要があります。
プロセスの空洞化の兆候が見られるマルチステージの .NET ドロッパー
Microsoft .NET フレームワークは、高度に難読化されたマルウェアを開発するために、脅威アクターの間で長年にわたって人気があります。これらのプログラムでは、分析プロセスを複雑にするために、コードの平坦化、暗号化、多段階アセンブリがよく使用されます。この複雑さは、マネージド .NET コードからアンマネージド Windows API に直接アクセスできる Platform Invoke(P/Invoke)によってさらに助長されます。これにより、作成者はプロセスの空洞化などの実績のある回避手法をコードに移植できます。
プロセスの空洞化は、広く蔓延している効果的なコード インジェクションの形式で、悪意のあるコードが別のプロセスのふりをして実行されます。この手法では、注入されたコードが正常なプロセスの正当性を前提とできるため、基本的なモニタリング ツールではマルウェアを特定しにくくなります。そのため、ダウンローダ チェーンの最後に使用されることが一般的です。
このケーススタディでは、TTD を使用して、プロセスの空洞化を介して最終段階を実行する .NET ドロッパーを分析します。このケーススタディでは、TTD が関連する Windows API 関数を迅速に特定することで、.NET 難読化の多数のレイヤをバイパスしてペイロードを特定し、非常に効率的な分析をどのように促進するかを示しています。
基本的な分析は、プロセスの空洞化の可能性を特定できる重要な第一歩です。たとえば、サンドボックスを使用すると、疑わしいプロセスの起動を検出できます。マルウェア作成者は、正規の .NET バイナリを空洞化の標的として頻繁に利用します。これは、正規の .NET バイナリが通常のシステム運用にシームレスに溶け込むためです。この場合、VirusTotal でプロセス アクティビティを確認すると、サンプルが InstallUtil.exe(%windir%\Microsoft.NET\Framework\<version>\ にある)を起動していることがわかります。InstallUtil.exe は正規のユーティリティですが、疑わしい悪意のあるサンプルの子プロセスとして実行されることは、初期調査でプロセス インジェクションの可能性に焦点を当てるのに役立つ指標となります。


図 1: VirusTotal サンドボックスで記録されたプロセス アクティビティ
Process Doppelgänging のような新しいステルス技術があるにもかかわらず、攻撃者がプロセス インジェクションを使用する場合、その信頼性、比較的単純であること、そして高度ではないセキュリティ ソリューションを効果的に回避できるという事実から、依然としてプロセスの空洞化の古典的なバージョンが使用されることがよくあります。従来のプロセスの空洞化の手順は次のとおりです。
-
CreateProcess(CREATE_SUSPENDEDフラグ付き): 被害者のプロセス(InstallUtil.exe)を起動しますが、実行前にそのプライマリ スレッドを一時停止します。 -
ZwUnmapViewOfSectionまたはNtUnmapViewOfSection: メモリから元の正規のコードを削除して、プロセスを「空洞化」します。 -
VirtualAllocExとWriteProcessMemory: リモート プロセスに新しいメモリを割り当て、悪意のあるペイロードを注入します。 -
GetThreadContext: 中断されたプライマリ スレッドのコンテキスト(状態とレジスタ値)を取得します。 -
SetThreadContext: 取得したコンテキスト内のエントリ ポイント レジスタを、新たにインジェクションされた悪意のあるコードのアドレスを指すように変更することで、実行フローをリダイレクトします。 -
ResumeThread: スレッドを再開し、悪意のあるコードを正規のプロセスであるかのように実行させます。
TTD を使用したサンプルでこのアクティビティを確認するため、プロセスの作成と、その後の子プロセスのアドレス空間への書き込みに検索を絞ります。この検索で示されたアプローチは、TTD クエリを調整してその手法に関連する API を検索することで、他の手法をトリアージするために応用できます。
マルウェアのタイムトラベル トレースを記録する
TTD の使用を開始するには、まずプログラムの実行のトレースを記録する必要があります。トレースを記録するには、WinDbg UI を使用する方法と、Microsoft が提供するコマンドライン ユーティリティを使用する方法の 2 つがあります。コマンドライン ユーティリティは、トレースを記録する最も迅速でカスタマイズ可能な方法です。この投稿では、その方法について説明します。
警告: マルウェア実行可能ファイルの 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
リリース: 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.runTTD の記録が開始されると、トレースは次のいずれかの方法で終了します。まず、マルウェアが終了すると(プロセス終了、未処理の例外など)、トレースが自動的に停止します。2 つ目は、ユーザーが手動で介入できることです。記録中、TTD.exe には、2 つの制御オプションを含む小さなダイアログ(図 2)が表示されます。
-
Tracing Off: トレースを停止してプロセスから切り離し、プログラムの実行を続行できるようにします。
- アプリを終了: トレースを停止し、プロセスも終了します。


図 2: TTD トレース実行制御ダイアログ
TTD トレースを記録すると、次のファイルが生成されます。
-
<trace>.run: トレースファイルは、圧縮された実行データを含むプロプライエタリな形式です。トレース ファイルのサイズは、プログラムのサイズ、実行時間、およびロードされる追加リソースの数などの外部要因によって影響を受けます。 -
<trace>.idx: インデックス ファイルを使用すると、デバッガはトレース全体を順番にスキャンすることなく、トレース内の特定の時点をすばやく見つけることができます。インデックス ファイルは、WinDbg でトレース ファイルを初めて開いたときに自動的に作成されます。一般に、Microsoft は、インデックス ファイルのサイズは通常、トレース ファイルの 2 倍であるとしています。 -
<trace>.out: トレースの記録中に生成されたログを含むトレースログファイル。
トレースが完了すると、.run ファイルを WinDbg で開くことができます。
TTD トレースのトリアージ: データに焦点を移す
TTD の基本的な利点は、手動でのコード ステップ作業から実行データ分析へと焦点を移せることです。このデータドリブンなアプローチで迅速かつ効果的なトリアージを行うには、基本的な TTD ナビゲーションとデバッガ データモデルのクエリの両方に習熟している必要があります。まず、ナビゲーションの基本とデバッガのデータモデルについて見ていきましょう。
トレースの操作
基本的なナビゲーション コマンドは、WinDbg UI の [ホーム] タブにあります。


図 3: WinDbg TTD の基本的なナビゲーション コマンド
実行を制御するための標準の WinDbg コマンドとショートカットは次のとおりです。
-
g: Go(F5)– 実行を再開 -
gu: Go Up / Step Out(Shift+F11)– 現在の関数が完了するまで実行 -
t: Trace / Step Into (F11orF8) – シングル ステップ イン -
p: Step / Step Over (F10) – シングル ステップ オーバー
TTD トレースを再生すると、通常のフロー制御コマンドを補完する逆フロー制御コマンドが有効になります。各逆方向の各フロー制御コマンドは、通常のフロー制御コマンドにダッシュ(-)を追加することで形成されます。
-
g-: Go back – トレースを逆方向に実行する -
g-u: Step Out Back - 最後の呼び出し命令までトレースを逆方向に実行 -
t-: Step Into Back – 後ろ向きにシングル ステップ -
p-: Step Over Back – 後ろ向きにシングル ステップ
タイムトラベル(!tt)コマンド
基本的なナビゲーション コマンドを使用すると、トレースをステップごとに移動できますが、タイムトラベル コマンド(!tt)を使用すると、特定のトレース位置に正確にジャンプできます。これらの位置は、さまざまな TTD コマンドの出力で提供されることがよくあります。TTD トレースの位置は、#:# 形式の 2 つの 16 進数で表されます(例: E:7D5) ここに:
-
最初の部分はシーケンス番号で、通常はモジュールの読み込みや例外などの主要な実行イベントに対応します。
-
2 つ目の部分はステップ数で、その主要な実行イベント以降に実行されたイベントまたは命令の数を示します。
この投稿では後ほど、タイム トラベル コマンドを使用して、プロセスの空洞化の例における重要なイベントに直接ジャンプし、手動での命令トレースを完全にバイパスします。
TTD デバッガのデータモデル
WinDbg デバッガのデータモデルは、デバッガ情報をオブジェクトのナビゲート可能なツリーとして公開する、拡張可能なオブジェクト モデルです。デバッガのデータモデルにより、WinDbg でデバッガ情報にアクセスする方法に根本的な変化をもたらします。ユーザーは、未加工のテキストベースの出力を処理するのではなく、構造化されたオブジェクト情報とやり取りできるようになります。データモデルは、クエリとフィルタリングに LINQ をサポートしており、ユーザーは大量の実行情報を効率的に並べ替えることができます。また、デバッガ データモデルは、コマンドを通じてデバッガ データモデルにアクセスする方法を反映した API を使用して、JavaScript による自動化を簡素化します。
Display Debugger Object Model Expression (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

図 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: 現在のデバッガ セッション。Debugger.Sessions[<session>]と同等。一般的に使用される項目は次のとおりです。 -
@$cursession.Processes: セッション内のプロセスのリスト。 -
@$cursession.TTD.Calls: トレース中に発生した呼び出しをクエリするメソッド。 -
@$cursession.TTD.Memory: トレース中に発生したメモリ操作をクエリする方法。 -
@$curprocess: 現在のプロセス。@$cursession.Processes[<pid>]と同等。よく使用される項目は次のとおりです。 -
@$curprocess.Modules: 現在読み込まれているモジュールのリスト。 -
@$curprocess.TTD.Events: トレース中に発生したイベントのリスト。
デバッガのデータモデルを調査してプロセスの空洞化を特定する
TTD のコンセプトの基本を理解し、調査の準備が整ったので、プロセスの空洞化の証拠を探します。まず、Calls メソッドを使用して、特定の Windows API 呼び出しを検索できます。この検索は、.NET サンプルでも有効です。プロセスの空洞化などの手法を実行するには、マネージド コードが P/Invoke を介してアンマネージド Windows API とインターフェースする必要があるためです。
プロセスの空洞化は、作成フラグの値が 0x4 の CreateProcess の呼び出しによって、一時停止状態のプロセスを作成することから始まります。次のクエリは、Calls メソッドを使用して、トレース内の kernel32 モジュールの CreateProcess* への各呼び出しのテーブルを返します。ワイルドカード(*)により、クエリは CreateProcessA または CreateProcessW への呼び出しと一致します。
0:000> dx -g @$cursession.TTD.Calls("kernel32!CreateProcess*")

このクエリは多数のフィールドを返しますが、そのすべてが調査に役立つわけではありません。これに対処するには、元のクエリに Select LINQ クエリを適用します。これにより、表示する列を指定して名前を変更できます。
0:000> dx -g @$cursession.TTD.Calls("kernel32!CreateProcess*").Select(c => new { TimeStart = c.TimeStart, Function = c.Function, Parameters = c.Parameters, ReturnAddress = c.ReturnAddress})

結果には、位置 58243:104D から始まる CreateProcessA への呼び出しが 1 つ示されています。戻りアドレスに注目してください。これは .NET バイナリであるため、ジャストインタイム(JIT)コンパイラによって実行されるネイティブ コードは、アプリケーションのメインイメージのアドレス空間にはありません(.NET 以外のイメージの場合にはあります)。通常、効果的なトリアージ手順は、Where LINQ クエリで結果をフィルタリングし、戻り先アドレスをプライマリ モジュールに限定して、マルウェアから発信されていない 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 は別の仮定を立てますが、これが誤った結果につながる可能性があります。主に、最大 4 つの QWORD パラメータと、戻り値も QWORD であることを想定しています。この想定は、引数が通常スタックに渡される 32 ビット(4 バイト)値である 32 ビット プロセス(x86)では一致しません。TTD はスタック上の引数を正しく検出しますが、隣接する 2 つの 32 ビット引数を 1 つの 64 ビット値として誤認します。
これを解決する方法の一つは、スタック上の引数を手動で調査することです。まず、!tt コマンドを使用して、CreateProcessA の関連する呼び出しの先頭に移動します。
0:000> !tt 58243:104D
(b48.12a4): ブレーク 命令 例外 - コード 80000003 (初回/2 回目の 例外処理は 利用できません)
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 コマンドは、 4 のオフセットを ESP レジスタに追加してこの値をスキップし、関数引数を適切に配置します。
0:000> dd /c 1 esp+4 L0A
0055de18 0055de74 <-- アプリケーション 名
0055de1c 0055de70
0055de20 0055e0ac
0055de24 0055e078
0055de28 00000000
0055de2c 08080004 <-- 作成フラグ - 0x4 (CREATE_SUSPENDED)
0055de30 00000000
0055de34 0055de40
0055de38 0055e0c0
0055de3c 0055e068dwCreationFlags 引数 (6 番目の引数)のビットマスクに設定された値 0x4 (CREATE_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 (Go Up) コマンドを使用して呼び出しからステップアウトし、値が入力された構造体を調べます。
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 <-- ターゲット プロセス ハンドル
[0x1] : 0x400000 <-- ターゲット アドレス
[0x2] : 0x9810af0 <-- ソースバッファ
[0x3] : 0x200 <-- 書き込みサイズ
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] : 0x200WriteProcessMemory の関数シグネチャは次のとおりです。
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 HEADER VALUES
14C machine (i386)
3 セクション数
66220A8D time date stamp Fri Apr 19 06:09:17 2024
----- SNIPPED -----
OPTIONAL HEADER VALUES
10B マジック #
11.00 リンカー バージョン
----- 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
セクション ヘッダー #1
.text name
3B434 仮想 サイズ
2000 仮想 アドレス
3B600 未加工データのサイズ
200 ファイル ポインタ から 未加工 データ
----- SNIPPED -----
セクション ヘッダー #2
.rsrc name
546 仮想サイズ
3E000 仮想アドレス
600 サイズ の 未加工 データ
3B800 ファイル ポインタ から 未加工 データ
----- SNIPPED -----
セクション ヘッダー #3
.reloc name
C 仮想サイズ
40000 仮想アドレス
200 未加工データのサイズ
3BE00 ファイル ポインタ ( 未加工 データ)
----- SNIPPED -----COR20 ヘッダー ディレクトリ(.NET ヘッダーへのポインタ)の存在は、これが .NET 実行可能ファイルであることを示します。 .text (0x2000)、 .rsrc (0x3E000)、 .reloc (0x40000)の相対仮想アドレスも、 WriteProcessMemory 呼び出しのターゲット アドレスと一致しています。
新しく発見された PE ファイルは、writemem コマンドを使用してメモリから抽出できます。
0:000> .writemem c:\users\flare\Desktop\headers.bin 0x9810af0 L0x200
200 バイトを書き込みます。
0:000> .writemem c:\users\flare\Desktop\text.bin 0x984cb10 L0x3b600
3b600 バイトを書き込みます。.......................................................................................................................
0:000> .writemem c:\users\flare\Desktop\rsrc.bin 0x387d9d0 L0x600
600 バイトを書き込みます。
0:000> .writemem c:\users\flare\Desktop\reloc.bin 0x3927178 L0x200
200 バイトを書き込みます。16 進数エディタを使用すると、各セクションを元のオフセットに配置することでファイルを再構築できます。dnSpy で生成された .NET 実行可能ファイル(SHA256: 4dfe67a8f1751ce0c29f7f44295e6028ad83bb8b3a7e85f84d6e251a0d7e3076)を簡単に分析すると、その構成データが明らかになります。
----- SNIPPED -----
// トークン: 0x0400000E RID: 14
public static bool EnableKeylogger = Convert.ToBoolean("false");
// トークン: 0x0400000F RID: 15
public static bool EnableScreenLogger = Convert.ToBoolean("false");
// トークン: 0x04000010 RID: 16
public static bool EnableClipboardLogger = Convert.ToBoolean("false");
// トークン: 0x0400001C RID: 28
public static string SmtpServer = "<REDACTED";
// トークン: 0x0400001D RID: 29
public static string SmtpSender = "<REDACTED>";
// トークン: 0x04000025 RID: 37
public static string StartupDirectoryName = "eXCXES";
// トークン: 0x04000026 RID: 38
public static string StartupInstallationName = "eXCXES.exe";
// トークン: 0x04000027 RID: 39
public static string StartupRegName = "eXCXES";
----- SNIPPED -----まとめ: 分析を加速する TTD
このケーススタディでは、TTD 実行トレースを検索可能なデータベースとして扱うことのメリットを示します。ペイロード配信をキャプチャし、特定の API 呼び出しについてデバッガ データモデルを直接クエリすることで、.NET ドロッパーの多層化された難読化を迅速に回避しました。対象を絞ったデータモデル クエリと LINQ フィルタ(CreateProcess* と WriteProcessMemory*)、それに低レベルのコマンド(!dh、.writemem)を組み合わせることで、隠された AgentTesla ペイロードを分離・抽出することが可能になり、重要な構成の詳細を数分で取得できました。
この分析で使用したツールと環境(最新バージョンの WinDbg と TTD を含む)は、FLARE-VM インストール スクリプトからすぐに利用できます。この事前構成された環境で分析ワークフローを合理化することをおすすめします。
TTD トレースは、元のサンプルとともに VirusTotal からダウンロードできます。
- Mandiant、 執筆者: Josh Stroschein、Jae Young Kim
