ヒープ上でコードを実行する複雑なマルウェアのデバッグ

序章

このブログでは、ヒープ メモリ内の非決定論的アドレスでコードを実行する複雑な多段階サンプルの反復リモート デバッグ中に、「セーブ ポイント」を作成するための簡単なデバッグ戦術を共有します。 2 つの例を紹介します。1 つは人為的なもので、もう 1 つは複雑なモジュール式のマルウェア サンプル (MD5 ハッシュ: 830a09ff05eac9a5f42897ba5176a36a) で、POISONPLUG と呼ばれるファミリからのものです。 IDA Pro と WinDbg に焦点を当てますが、他のツールでも同じ効果を得る方法を説明します。この戦術を使用すると、さまざまなツールの長所を使用して複数のデバッガー間でプログラムの実行をハンドオフすることもできます (たとえば、バイナリのアンパック、メモリ マップのダンプ、アンチ RE との戦い、または通常のデバッグ)。

本質は、単にマルウェアを一時停止することです。準備として、マルウェアがデバッグ戦術にどのように影響し、これを必要とするかを最初に説明する必要があります。この説明は、マルウェアのデバッグを容易にし、POISONPLUG のケース スタディで最高潮に達する一般的な手法のレビューとして役立ちます。すでに IDA Pro を使用してマルウェアをリモートでデバッグするベテランのアナリストであり、ライブ マルウェアを一時停止してスナップショットを作成する方法の結論のみに関心がある場合は、最後の概要セクションにスキップしてください。

セーブポイントとしての VM とスナップショット

マルウェアによる損害を防ぐために、ほとんどのマルウェア リバース エンジニアは、分離された VM でデバッグします。これにより、デバッグ プロセス全体で VM のスナップショットをキャプチャし、ミスを犯した後に「セーブ ポイント」に戻ることができるという強力な戦術が生まれます。その後、アナリストは、マルウェアの動作を調査するために自由に積極的に実験することができます。エラーの唯一の結果は、アナリストが VM を元に戻し、同じ間違いを繰り返さないようにしなければならないことです。

リモートデバッグ

静的分析アーティファクトが保存されている同じシステムでマルウェアをデバッグすることは危険です。マルウェア (例: ランサムウェア) はメモや逆アセンブル データベースを破壊したり、マルウェア対策の RE 対策によってデータが失われたりする可能性があります (例: 再起動による)。したがって、デバッグと逆アセンブルおよびメモ取りに別々のシステムを使用することは理にかなっています。使用するツールによっては、アナリストは、テニスの試合の観客のように、逆アセンブラーの出力とデバッガーの表示を行ったり来たりする必要があります。これらの遷移は気が散ります。

IDA Pro をフロントエンドとして静的解析と動的解析を統合する

幸いなことに、IDA Pro (およびおそらく最新のほとんどの逆アセンブラー) は、デバッグ フロントエンドとして機能し、ライブ メモリに逆アセンブル アノテーションを重ねて、実行中のプログラムの状態を登録できます。これにより、アナリストは、観察結果に応じて逆アセンブル アノテーションを確認し、直接変更することができます。

実行時にメモリ マップを変更するマルウェア

動的分析手法の要件をさらに形作る 1 つのよくあるシナリオがあります。それは、ヒープ メモリを割り当て、そのメモリにコードを書き込み、そのコードを実行するマルウェアです。 C で書かれた単純なプログラムを示す図 1 を考えてみましょう。

 

Simple shellcode example program
図 1: 簡単なシェルコードのサンプル プログラム

プログラムは malloc を使用してメモリを割り当て、memcpy を使用してその場所に 6 バイトをコピーし、各バイトを論理的に反転し、関数としてバッファを呼び出し、最後にシェルコードの戻り値を返します (簡潔さと現実性のためにエラー チェックは省略されています)。図 2 は、メモリ内のデコードされたシェルコードを示しています。

 

単純なシェルコード関数が 42 を返す

図 2: シンプルなシェルコード関数が 42 を返す

このコードがないと、逆アセンブリ データベースはマルウェアのコードに関する有用な情報を欠き、その動作が少しブラック ボックスのままになります。この単純な例は一般的なパターンを示していますが、その単純な性質は、これを深刻な問題と見なすほど説得力のあるものではありません。より現実的な例は、手元にあるデバッグ戦術のより実質的な動機を提供します。

ケーススタディ: POISONPLUG

現実的な例として、MD5 ハッシュ 830a09ff05eac9a5f42897ba5176a36a (VirusTotal から入手可能) のサンプルを考えてみましょう。このマルウェアは、シェルコードをデコードして呼び出すスレッドを作成し、シェルコードを解凍して、変更された DLL モジュールのエントリ ポイントを呼び出します。次に、モジュールは 6 つの追加モジュールをアンパックしてから、最終的にそれらのモジュールの 1 つの関数を呼び出します。複数のモジュールの DllEntryPoint 機能はそれぞれ、複数のアンチ RE スレッドを作成します。これらのスレッドは、一般的な分析ツールを検出し、それに応じてマルウェアを終了させようとします。マルウェアを完全に解凍した後、 Flare-dbg の Tyler Dean の injectfindや私自身の Flare-qdb (クエリ指向デバッガー)などのツールは、メモリ内のすべての読み取り/書き込み/実行 (R/W/X) マッピングを公開できます。この場合、マルウェア モジュールを直接指します。図3は、この時点までマルウェアのサブセットをデバッグし、そのR/W/X割り当てをダンプするflare-qdbの出力を示しています。

 

解凍後の POISONPLUG R/W/X メモリ位置

図 3: 開梱後の POISONPLUG R/W/X メモリの場所

図 4 は、このサンプルから解凍されたシェルコード ベースのローダーを示しています。これは、複雑で難読化されており、注釈を付けるのに時間がかかります。

 

POISONPLUG のシェルコードベースのローダー

図 4: POISONPLUG のシェルコード ベースのローダー

このシェルコードは、このマルウェア ファミリに固有のいくつかのアンチ RE 機能を実装しており、これのコピーを使用して、変更された/カスタムの PE-COFF ヘッダーを含む 7 つのモジュールをまとめて解凍します。メモリ内で実行可能ファイル全体を見つけた場合の一般的な対応は、ファイルをダンプして独自の逆アセンブリ データベースを作成することです。ただし、モジュールは、ページング ファイルのマッピングに格納された関数ポインターのリストを使用して、スパゲッティ コード方式で互いの関数の「エクスポート」を見つけて呼び出し、制御フローと機能セマンティクスを意図的に難読化します。図 5 は、各レーンが 1 つの実行可能コード モジュールを表し、各レーン内のボックスがそのモジュール内の個別の関数エントリ ポイント RVA を表す例を示しています。

 

構成を取得してデコードするための部分相互作用図

図 5: 構成を取得してデコードするための部分的な相互作用図

モジュール 0 のオフセット 0x11f2 にあるコードは、単に他のモジュールを呼び出して、最終的に独自のモジュール (オフセット 0x1d42) 内のコードに到達します。別の逆アセンブリ データベースにダンプすると、実行パスをたどるには、完全に異なる逆アセンブリ データベース間で Alt+Tab キーを押す必要があるため、アナリストの気が散ります。

これらのタイプの複雑なサンプルは、これまでに説明したデバッグ戦術に二重の問題を引き起こします…

課題 1: ヒープからのコードの同期

最初の問題は、通常、メモリに書き込まれたコードが元の逆アセンブリ出力ですぐに利用できず、別の逆アセンブリ データベースへのダンプが常に適切であるとは限らないことです。また、反転防止手段を無力化して、コード化されたすべてのモジュールをヒープ メモリに展開するまでサンプルをシェファードするのも大変な作業になる可能性があります。デバッグに誤りがあると、修正して分析を再開するために多くの追加作業が必要になる場合があります。ライブ メモリは、デバッグ セッションの存続期間を超えて保持できる場合、リバース エンジニアリングを早める可能性のあるリソースです。幸いなことに、アンパックされたモジュールを単一の逆アセンブル データベースで便利に利用できるようにするというこの最初の問題は、少なくとも IDA Pro では簡単に解決できます。

  1. 動的に割り当てられた各コード領域にアクセスして、そのセグメント属性を変更し (Alt+ S )、それぞれをローダー セグメントとしてマークします。
  2. 動的に割り当てられたメモリを逆アセンブリ データベースに取り込みます ([デバッガ] > [メモリスナップショットの取得] > [ローダーセグメント])。

デバッグ セッションを開始せずに作業を進めている場合、IDA の [セグメント属性の変更] ダイアログで [ローダーセグメント] チェックボックスが省略されます。図 6 は、デバッグ セッション中のこのダイアログを示しており、ローダーセグメントのチェックボックスが強調表示されています。

 

デバッグ セッション中のセグメント属性ダイアログの変更

図 6: デバッグ セッション中のセグメント属性の変更ダイアログ

図 7 に示すように、ライブ メモリをプルした後、デバッグ セッションの終了後でも、ヒープ割り当て内のアンパックされたモジュールとコードを読み取って注釈を付けることができます。

 

デバッグ メモリ スナップショットから保存されたヒープの関数コード

図 7: デバッグ メモリ スナップショットから保存されたヒープの関数コード

課題 2: 非決定論的メモリ マップ

2 つ目の問題は、動的に割り当てられたメモリでコードを実行するサンプルから発生します。デバッグの誤りから回復するには、プログラムを再度デバッグする必要がありますが、モジュールは実行ごとに異なるアドレスを占有することがよくあります。したがって、IDA Pro で元のアドレスに作成された便利な注釈は、新しいコードの場所にはありません。図 8 は、図 7 と同じコードを含む例を示していますが、その後のプログラムの実行中に別のアドレスにロードされます。アナリストは、分析を続行するために、すべてを認識および/または再ラベル付けする必要があります。これはスクリプト化できますが、時間のかかる注意散漫になります。

 

別のアドレスにある同じコードには注釈がありません

図 8: 別のアドレスにある同じコードに注釈がない

コードがデバッグ セッション間でさまざまなアドレスに表示される理由は、VirtualAlloc などの Windows のメモリ割り当て関数が、プログラムの実行ごとに常に一貫したアドレスを返すとは限らないためです。たとえば、プログラムが最初に実行されると、アドレス 0xe000 でメモリを取得し、2 回目は 0x11a000 でメモリを取得する場合があります。複数のモジュールを持つ複雑なマルウェアの場合、これが問題になります。

IDA Pro がそれぞれ 1 つの仮想アドレスに関連付けた既存の静的解析アノテーションを引き続き構築できるように、デバッグ セッションごとにメモリ マップを統一したいと考えています。残念ながら、VirtualAlloc は、割り当てる領域の開始アドレスを示すオプションの lpAddress パラメータを受け入れますが、メモリがそのアドレスで既に予約され、コミットされていない場合を除き、これは単なる提案にすぎません。 lpAddress パラメーターを目的の値に強制しても、ほとんど (私の経験では決して) 成功しません。

または、以前のように仮想マシンのスナップショットを使用して「セーブ ポイント」を作成する方法に戻るとよいでしょう。残念ながら、ネットワークを介してリモートでデバッグする場合、仮想マシンのスナップショットを元に戻すプロセスにより、デバッグ サーバーと IDA Pro の間の TCP 接続が切断され、マルウェアがデバッガーの制御下で続行できなくなります。

…シーンを配置する場所

新しい技術を導入するための舞台が整いました。まず、ここまでの経緯を簡単に要約します。

  • ホスト システムへの損傷を避けるために VM でデバッグする必要がある
  • 静的解析と動的解析を統合するためのデバッグ フロントエンドとして IDA Pro を使用することを好む
  • 静的解析アーティファクトとドキュメントへの損傷を避けるために、リモート デバッグを使用する必要がある
  • 複数のデバッグ セッションで繰り返しデバッグする必要がある
  • 逆アセンブル アノテーションを有効にするには、デバッグ セッションのメモリ マップと一致させる必要があります。

マルウェアの動作とアナリストの好みにより、私たちは窮地に追い込まれたようです。マルウェアを繰り返し実行すると、非決定論的なメモリ マップが逆アセンブリ データベースの注釈と一致しなくなります。また、IDA Pro を使用して静的ビューをライブ リモート デバッグと統合すると、VM スナップショットを使用してセーブ ポイントとして機能させることが妨げられるようです。アナリストは何をすべきか?

マルウェアをパークする

繰り返し再接続してデバッグを再開できる VM スナップショットをキャプチャするには、プログラム内のすべてのスレッドの中断カウントを増やし、デバッガーを接続解除します。デバッグ サーバーは TCP 接続を適切に閉じ、再接続するまでプログラムは中断されたままになります。次に、VM のスナップショットをキャプチャします。最後に、VM を元に戻し、再接続し、実行を再開して、分析を続けることができます。このようにして、マルウェアを 1 回パークし、その動作を理解するまで何度もクラッシュさせることができます。

結局のところ、スレッドを中断するための IDA Pro の機能 (右クリック -> 中断) は、デバッガーをデタッチした後、その効果を維持しません。代わりに、IDA のデバッガー バックエンドとして WinDbg を具体的に使用します ( Hex-Rays のサイトの指示を参照してください)。

スレッドの状態を表示するための WinDbg コマンドは ~ (チルダ) です。 ~ コマンドは、表示するスレッドを指定するオプションの数値引数 (例: ~3) を受け入れるか、または ~* を指定してすべてのスレッドの完全なステータスを表示することができます。 WinDbg は、スレッドを中断および再開するためのコマンド ~ nおよび ~ mもサポートしています。これらは数値またはアスタリスクの引数も許可するため、 ~* nを使用してデタッチする前にすべてのスレッドを中断し、 ~* mを使用して再アタッチ時にスレッドを再開することができます。図 9 は、スレッドの状態を表示し、すべてのスレッドを中断し、最後にそれらの状態をもう一度表示した後の IDA/WinDbg の出力を示しています。

 

スレッドの状態の表示、中断、および再表示

図 9: スレッド ステータスの表示、一時停止、および再表示

~*n コマンドを発行すると、サスペンド カウントが 1 から 2 に増加します。これで、デバッガーがプロセスから切り離され、すべてのスレッドのサスペンド カウントが減分された場合 (通常どおり)、各スレッドの人為的に引き上げられたサスペンド カウントはゼロより大きいままになります。したがって、NT ディスパッチャはプロセス内のスレッドの実行をスケジュールせず、プロセスは中断状態のままになります。

これで、中断したところからデバッグを再開するために繰り返し元に戻すことができる VM スナップショットをキャプチャできます。図 10 は、VM スナップショットを元に戻し、[デバッガー] -> [プロセスへアタッチ…] をクリックした後の IDA Pro のプロセス アタッチ ダイアログを示しています。

 

中断されたプロセスへのアタッチ

図 10: 中断されたプロセスへのアタッチ

これらの「セーブ ポイント」は、保存するディスク容量がある限り、さまざまな時点で作成できます。

この手順の 1 つの注意点は、再接続してからデバッグを続行しようとするまでの間、スレッドを再開するのを忘れがちだということです。この手順を忘れると、図 11 の [お待ちください…] モーダル ダイアログが表示されます。

 

中断されたプロセスのデバッグ

図 11: 中断されたプロセスのデバッグ

リバース エンジニアは、間違いを犯してマルウェアを自由に実行させた後にのみこのダイアログを表示することに慣れているかもしれませんが、この場合、プログラムは実際には命令を実行していません。これを修正するには、IDA Pro の [お待ちください…] ダイアログの [サスペンド] ボタンをクリックし、すべてのスレッド (WinDbg: ~*m) を再開して、サスペンド カウントを減らします。その後、実行は通常どおり続行できます。

概要

IDA Pro + WinDbg リモート デバッグ セッション内で実行しているプログラムを一時停止して、再利用可能な VM スナップショットをキャプチャするには:

  1. すべてのスレッドを中断します (WinDbg: ~*n)
  2. プロセスから切り離す (IDA Pro: Deb u gger -> Detach from process)
  3. VM のスナップショットをキャプチャする

中断されたプログラムを再開するには:

  1. リモート プロセスに接続します (IDA Pro: Deb u gger -> A ttach to process…)
  2. すべてのスレッドを再開します (WinDbg: ~*m)
  3. 通常どおりデバッグを再開します

WinDbg コマンドの使用に関心がない場合は、代わりに SysInternals の Process Explorer を使用して、デバッグ VM でプロセスを一時停止し、IDA Pro を使用して単純にデタッチできます。必要に応じて、Python ctypes スクリプトまたはネイティブ プログラムを記述して、関連する Windows API を直接使用することもできます (具体的には、TH32_SNAPTHREAD フラグ、OpenThread、SuspendThread、および ResumeThread を指定した CreateToolhelp32Snapshot を使用)。

この戦術により、複数の (場合によってはカスケード) アンパックされたコード領域を持つ複雑なマルチステージ シェルコードまたはモジュラー マルウェアに対処することができます。同じメモリ マップを維持しながら、デバッグ セッションでセーブ ポイントを作成できるため、逆アセンブル アノテーションは常にデバッグ セッションのメモリ マップと一致したままになります。また、デタッチする前に各デバッガーでスレッドの一時停止カウントをゼロ以外の値に維持できる場合、あるデバッガーでマルウェアの実行を一時停止し、別のデバッガーで実行することもできます。

締めくくる前に、Tarik Soulami の著書『Inside Windows Debugging』(Microsoft Press、2012 年) で WinDbg スレッド管理について説明してくれたことに感謝したいと思います。リバース エンジニアとしての道のりで、より困難なデバッグ シナリオに直面し始めている場合は、レパートリーを増やし、WinDbg と Windows 自体の強力なデバッグ機能をさらに理解するために、「Windows の内部デバッグ」を取り上げることを強くお勧めします。

参照: https://www.mandiant.com/resources/blog/debugging-complex-malware-that-executes-code-on-the-heap

コメント

タイトルとURLをコピーしました