Speakeasy エミュレーション フレームワークをプログラムで使用してマルウェアをアンパックする

Andrew Davis は最近、 Speakeasyという彼の新しい Windows エミュレーション フレームワークの公開リリースを発表しました。ブログの紹介記事では、Speakeasy を一種の自動化されたマルウェア サンドボックスとして使用することに焦点を当てていましたが、このエントリでは、フレームワークの別の強力な使用法である、自動化されたマルウェアのアンパックについて取り上げます。コード例を使用して、Speakeasy をプログラムで使用して次のことを行う方法を示します。

  • サポートされていない Windows API をバイパスして、エミュレーションとアンパックを続行します
  • API フックを使用して動的に割り当てられたコードの仮想アドレスを保存する
  • コードフックを使用してコードの重要な領域に外科的に実行を指示する
  • アンパックされた PE をエミュレーターのメモリからダンプし、そのセクション ヘッダーを修正します。
  • Speakeasy にシンボリック情報を照会して、インポート テーブルの再構築を支援する

初期設定

Speakeasy とやり取りする 1 つの方法は、Speakeasy のSpeakeasyクラスのサブクラスを作成することです。図 1 は、このようなクラスを設定する Python コード スニペットを示しています。このクラスは、今後の例で展開されます。

輸入スピークイージー

クラス MyUnpacker(speakeasy.Speakeasy):
def __init__(self, config=None):
super(MyUnpacker, self).__init__(config=config)

図 1: Speakeasy サブクラスの作成

図 1 のコードは、デフォルト構成をオーバーライドするために使用できる Speakeasy 構成辞書を受け入れます。 Speakeasy には、いくつかの設定ファイルが付属しています。 Speakeasyクラスは、基礎となるエミュレータ クラスのラッパー クラスです。エミュレーター クラスは、バイナリがその PE ヘッダーに基づいて読み込まれるか、シェルコードとして指定されたときに自動的に選択されます。 Speakeasyをサブクラス化すると、インターフェースへのアクセス、拡張、変更が容易になります。また、エミュレーション前、エミュレーション中、エミュレーション後のステートフル データの読み取りと書き込みも容易になります。

バイナリのエミュレート

図 2 は、バイナリを Speakeasy エミュレータにロードする方法を示しています。

self.module = self.load_module(ファイル名)

図 2: エミュレーターへのバイナリーのロード

load_module関数は、ディスク上の提供されたバイナリのPeFileオブジェクトを返します。これは、 pefilePEクラスからサブクラス化された、 speakeasy/windows/common.pyで定義されたPeFileクラスのインスタンスです。または、ファイル名を指定するのではなく、データパラメーターを使用してバイナリのバイトを指定することもできます。図 3 は、読み込まれたバイナリをエミュレートする方法を示しています。

self.run_module(自己.モジュール)

図 3: エミュレーションの開始

API フック

Speakeasy フレームワークは、何百もの Windows API をサポートして出荷されており、さらに頻繁に追加されています。これは、 speakeasy/winenv/apiディレクトリ内の適切なファイルで定義された Python APIハンドラを介して実行されます。 APIフックをインストールして、エミュレーション中に特定の API が呼び出されたときに独自のコードを実行することができます。ハンドラーが存在するかどうかに関係なく、任意の API にインストールできます。 API フックを使用して既存のハンドラーをオーバーライドすることができ、オプションでそのハンドラーをフックから呼び出すことができます。 Speakeasy の API フック メカニズムは、エミュレーションに対する柔軟性と制御を提供します。アンパック コードをエミュレートしてアンパックされたペイロードを取得するコンテキスト内で、API フックのいくつかの使用法を調べてみましょう。

サポートされていない API のバイパス

Speakeasy は、サポートされていない Windows API 呼び出しを検出すると、エミュレーションを停止し、サポートされていない API 関数の名前を提供します。問題の API 関数がバイナリのアンパックに重要でない場合は、実行を継続できる値を返すだけの API フックを追加できます。たとえば、最近のサンプルのアンパック コードには、アンパック プロセスに影響を与えない API 呼び出しが含まれていました。そのような API 呼び出しの 1 つはGetSysColorに対するものでした。この呼び出しをバイパスして実行を継続できるようにするために、図 4 に示すように API フックを追加できます。

self.add_api_hook(self.getsyscolor_hook,
‘user32’,
‘GetSysColor’,
argc=1
)

図 4: API フックの追加

MSDNによると、この関数は 1 つのパラメーターを取り、 DWORDとして表される RGB カラー値を返します。フックしている API 関数の呼び出し規則がstdcallでない場合は、オプションのcall_conv パラメータで呼び出し規則を指定できます。呼び出し規約の定数は、 speakeasy/common/arch.py ファイルで定義されています。 GetSysColorの戻り値はアンパック プロセスに影響しないため、単純に0を返すことができます。図 5 は、図 4 で指定されたgetsyscolor_hook関数の定義を示しています。

def getsyscolor_hook (self、emu、api_name、func、params):
0 を返す

図 5: GetSysColor フックが 0 を返す

API 関数でより洗練された処理が必要な場合は、ニーズに合った、より具体的で意味のあるフックを実装できます。フックの実装が十分に堅牢であれば、それを Speakeasy プロジェクトに API ハンドラとして提供することを検討してください。

API ハンドラーの追加

speakeasy/winenv/apiディレクトリ内には、対応するバイナリ モジュールの Python ファイルを含むusermodeおよびkernelmodeサブディレクトリがあります。これらのファイルには、各モジュールの API ハンドラが含まれています。 usermode/kernel32.pyでは、図 6 に示すように、 SetEnvironmentVariableに対して定義されたハンドラーが表示されます。

1: @apihook(‘SetEnvironmentVariable’, argc=2)
2: def SetEnvironmentVariable(self, emu, argv, ctx={}):
3:「」
4: BOOL SetEnvironmentVariable(
5: LPCTSTR lpName,
6: LPCTSTR lpValue
7: );
8: ”’
9: lpName、lpValue = argv
10: cw = self.get_char_width(ctx)
11: lpName と lpValue の場合:
12: 名前 = self.read_mem_string(lpName, cw)
13: val = self.read_mem_string(lpValue, cw)
14: argv[0] = 名前
15: argv[1] = 値
16: emu.set_env(名前、値)
17: 真を返す

図 6: SetEnvironmentVariable の API ハンドラー

ハンドラーは、API の名前と受け入れるパラメーターの数を定義する関数デコレーター (1 行目) で始まります。ハンドラーの開始時に、MSDN の文書化されたプロトタイプをコメントとして含めることをお勧めします (3 ~ 8 行目)。

ハンドラーのコードは、 argvパラメーターの要素を、対応する API パラメーターにちなんで名付けられた変数に格納することから始まります (9 行目)。ハンドラーのctxパラメーターは、API 呼び出しに関するコンテキスト情報を含む辞書です。 ‘ A ‘ または ‘ W ‘ で終わる API 関数 (例: CreateFileA ) の場合、 ctxパラメータをget_char_width関数 (10 行目) に渡すことで、文字幅を取得できます。この幅の値は、 read_mem_string (12 行目と 13 行目) などの呼び出しに渡すことができます。この呼び出しは、指定されたアドレスでエミュレーターのメモリを読み取り、文字列を返します。

argvパラメーターの文字列ポインター値を、対応する文字列値で上書きすることをお勧めします (14 行目と 15 行目)。これにより、Speakeasy は API ログにポインター値ではなく文字列値を表示できるようになります。 argv値の更新の影響を説明するために、図 7 に示す Speakeasy の出力を調べます。VirtualAllocエントリでは、シンボリック定数文字列PAGE_EXECUTE_READWRITEが値0x40を置き換えます。 GetModuleFileNameAおよびCreateFileAエントリでは、ポインター値はファイル パスに置き換えられます。

KERNEL32.VirtualAlloc(0x0, 0x2b400, 0x3000, “PAGE_EXECUTE_READWRITE”) -> 0x7c000
KERNEL32.GetModuleFileNameA(0x0, “C:Windowssystem32sample.exe”, 0x104) -> 0x58
KERNEL32.CreateFileA(“C:Windowssystem32sample.exe”, “GENERIC_READ”, 0x1, 0x0, “OPEN_EXISTING”, 0x80, 0x0) -> 0x84

図 7: Speakeasy API ログ

アンパックされたコード アドレスの保存

パックされたサンプルは、多くの場合、 VirtualAllocなどの関数を使用して、アンパックされたサンプルを格納するために使用されるメモリを割り当てます。アンパックされたコードの位置とサイズを取得する効果的な方法は、アンパック スタブで使用されるメモリ割り当て関数を最初にフックすることです。図 8 は、 VirtualAllocをフックして、API 呼び出しによって割り当てられる仮想アドレスとメモリ量を取得する例を示しています。

1: def virtualalloc_hook(self、emu、api_name、func、params):
2: ”’
3: LPVOID VirtualAlloc(
4: LPVOID lpAddress、
5: SIZE_T dwSize,
6: DWORD flAllocationType、
7: DWORD flProtect
8: );
9: ”’
10: PAGE_EXECUTE_READWRITE = 0x40
11: lpAddress、dwSize、flAllocationType、flProtect = params
12: rv = 関数 (パラメータ)
13: lpAddress == 0 および flProtect == PAGE_EXECUTE_READWRITE の場合:
14: self.logger.debug(“[*] スタブ VirtualAlloc 呼び出しをアンパックし、ダンプ情報を保存します”)
15: self.dump_addr = rv
16: self.dump_size = dwSize

17: rv を返す

図 8: メモリ ダンプ情報を保存するための VirtualAlloc フック

図 8 のフックは、12 行目のVirtualAllocに対する Speakeasy の API ハンドラを呼び出して、メモリを割り当てられるようにします。 API ハンドラーによって返される仮想アドレスは、 rvという名前の変数に保存されます。 VirtualAllocはアンパック プロセスに関係のないメモリを割り当てるために使用される可能性があるため、13 行目で追加のチェックを使用して、インターセプトされたVirtualAlloc呼び出しがアンパック コードで使用されたものであることを確認します。以前の分析に基づいて、 lpAddress0flProtectPAGE_EXECUTE_READWRITE ( 0x40 ) を受け取るVirtualAlloc呼び出しを探しています。これらの引数が存在する場合、仮想アドレスと指定されたサイズが 15 行目と 16 行目に格納されるため、アンパック コードが終了した後にメモリからアンパックされたペイロードを抽出するために使用できます。最後に、17 行目で、 VirtualAllocハンドラーからの戻り値がフックによって返されます。

API とコード フックを使用した手術コードのエミュレーション

Speakeasy は堅牢なエミュレーション フレームワークです。ただし、問題のあるコードの大部分を含むバイナリが見つかる場合があります。たとえば、サンプルがサポートされていない多くの API を呼び出したり、単にエミュレートに時間がかかりすぎたりする場合があります。両方の課題を克服する例を、次のシナリオで説明します。

MFC プロジェクトに隠れているスタブのアンパック

悪意のあるペイロードを偽装するために使用される一般的な手法には、大規模なオープンソース MFC プロジェクト内にそれらを隠すことが含まれます。 MFC はMicrosoft Foundation Classの略で、Windows デスクトップ アプリケーションの構築に使用される一般的なライブラリです。これらの MFC プロジェクトは、多くの場合、 Code Projectなどの一般的な Web サイトから任意に選択されます。 MFC ライブラリを使用するとデスクトップ アプリケーションを簡単に作成できますが、MFC アプリケーションはそのサイズと複雑さのためにリバース エンジニアリングが困難です。これらは、多数の異なる Windows API を呼び出す大規模な初期化ルーチンのため、エミュレートが特に困難です。以下は、Speakeasy を使用して Python スクリプトを作成し、MFC プロジェクト内のアンパック スタブを非表示にするカスタム パッカーのアンパックを自動化した経験の説明です。

パッカーのリバース エンジニアリングにより、C ランタイムと MFC の初期化後に発生するCWinAppオブジェクトの初期化中に、アンパック スタブが最終的に呼び出されることが明らかになりました。サポートされていない API をバイパスしようとした後、たとえ成功したとしても、エミュレーションには時間がかかりすぎて実用的ではないことに気付きました。初期化コードを完全にスキップして、アンパック スタブに直接ジャンプすることを検討しました。残念ながら、アンパック スタブのエミュレーションを成功させるには、C ランタイム初期化コードの実行が必要でした。

私の解決策は、C ランタイムの初期化の後、MFC の初期化ルーチンの初期にあったコード内の場所を特定することでした。図 9 に示す Speakeasy API ログを調べると、そのような場所は簡単に特定できました。グラフィック関連の API 関数GetDeviceCapsは、MFC 初期化ルーチンの早い段階で呼び出されます。これは、1) MFC はグラフィックスに依存するフレームワークであり、2) C ランタイムの初期化中にGetDeviceCapsが呼び出される可能性は低いことに基づいて推測されました。

0x43e0a7: ‘kernel32.FlsGetValue(0x0)’ -> 0x4150
0x43e0e3: ‘kernel32.DecodePointer(0x7049)’ -> 0x7048
0x43b16a: ‘KERNEL32.HeapSize(0x4130, 0x0, 0x7000)’ -> 0x90
0x43e013: ‘KERNEL32.TlsGetValue(0x0)’ -> 0xfeee0001
0x43e02a: ‘KERNEL32.TlsGetValue(0x0)’ -> 0xfeee0001
0x43e02c: ‘kernel32.FlsGetValue(0x0)’ -> 0x4150
0x43e068: ‘kernel32.EncodePointer(0x44e215)’ -> 0x44e216
0x43e013: ‘KERNEL32.TlsGetValue(0x0)’ -> 0xfeee0001
0x43e02a: ‘KERNEL32.TlsGetValue(0x0)’ -> 0xfeee0001
0x43e02c: ‘kernel32.FlsGetValue(0x0)’ -> 0x4150
0x43e068: ‘kernel32.EncodePointer(0x704c)’ -> 0x704d
0x43c260: ‘KERNEL32.LeaveCriticalSection(0x466f28)’ -> なし
0x422151: ‘USER32.GetSystemMetrics(0xb)’ -> 0x1
0x422158: ‘USER32.GetSystemMetrics(0xc)’ -> 0x1
0x42215f: ‘USER32.GetSystemMetrics(0x2)’ -> 0x1
0x422169: ‘USER32.GetSystemMetrics(0x3)’ -> 0x1
0x422184: ‘GDI32.GetDeviceCaps(0x288, 0x58)’ -> なし

図 9: Speakeasy API ログで MFC コードの先頭を特定

この段階で実行をインターセプトするために、図 10 に示すようにGetDeviceCapsの API フックを作成しました。このフックは、関数が 2 行目で初めて呼び出されていることを確認します。

1: def mfc_init_hook(self、emu、api_name、func、params):
2: self.trigger_hit でない場合:
3: self.trigger_hit = True
4: self.h_code_hook = self.add_code_hook(self.start_unpack_func_hook)
5: self.logger.debug(“[*] MFC init api ヒット、unpack 関数を開始”)

図 10: GetDeviceCaps の API フック セット

4 行目は、 Speakeasyクラスのadd_code_hook関数を使用したコード フックの作成を示しています。コード フックを使用すると、エミュレートされる各命令の前に呼び出されるコールバック関数を指定できます。 Speakeasy では、 beginパラメータとendパラメータを指定して、コード フックが有効になるアドレス範囲をオプションで指定することもできます。

コード フックが 4 行目に追加された後、 GetDeviceCapsフックが完了し、サンプルの次の命令が実行される前に、 start_unpack_func_hook関数が呼び出されます。この機能を図 11 に示します。

1: def start_unpack_func_hook(self、emu、addr、size、ctx):
2: self.h_code_hook.disable()
3: unpack_func_va = self.module.get_rva_from_offset(self.unpack_offs) + self.module.get_base()
4: self.set_pc(unpack_func_va)

図 11: 命令ポインタを変更するコード フック

コード フックは、エミュレータ オブジェクト、現在の命令のアドレスとサイズ、およびコンテキスト ディクショナリを受け取ります (1 行目)。 2 行目で、コード フックはそれ自体を無効にします。コード フックは各命令で実行されるため、エミュレーションが大幅に遅くなります。したがって、それらは慎重に使用し、できるだけ早く無効にする必要があります。 3 行目で、フックはアンパック関数の仮想アドレスを計算します。この計算を実行するために使用されるオフセットは、正規表現を使用して特定されました。簡潔にするために、例のこの部分は省略されています。

self.module属性は、図 2 に示すサンプル コードで以前に設定されていました。これはpefilePEクラスからサブクラス化されているため、3 行目のget_rva_from_offset()などの便利な関数にアクセスできます。この行には、 self の使用例も含まれています。 .module.get_base()を使用して、モジュールのベース仮想アドレスを取得します。

最後に、4 行目でset_pc関数を使用して命令ポインターが変更され、コードのアンパックでエミュレーションが続行されます。図 10 と図 11 のコード スニペットにより、C ランタイムの初期化が完了した後に実行をアンパック コードにリダイレクトし、MFC 初期化コードを回避することができました。

アンパックされた PE のダンプと修正

エミュレーションがアンパックされたサンプルの元のエントリ ポイントに到達したら、PE をダンプして修正します。通常、フックは、図 8 の 15 行目に示すように、アンパックされた PE のベース アドレスをクラスの属性に保存します。アンパックされた PE の PE ヘッダーに正しいエントリ ポイントが含まれていない場合、真のエントリ ポイントもエミュレーション中にキャプチャする必要があります。図 12 は、エミュレーターのメモリをファイルにダンプする方法の例を示しています。

open(self.output_path, “wb”) をアップとして:
mm = self.get_address_map(self.dump_addr)
up.write(self.mem_read(mm.get_base(), mm.get_size()))

図 12: アンパックされた PE のダンプ

すでにメモリにロードされている PE をダンプする場合、セクションの配置が異なるため、ディスク上と同じレイアウトにはなりません。その結果、ダンプされた PE のヘッダーを変更する必要がある場合があります。 1 つの方法は、各セクションのPointerToRawData値をそのVirtualAddressフィールドと一致するように変更することです。各セクションのSizeOfRawData値は、PE のオプション ヘッダーで指定されたFileAlignment値に準拠するためにパディングする必要がある場合があります。結果の PE が正常に実行される可能性は低いことに注意してください。ただし、これらの努力により、ほとんどの静的分析ツールが正しく機能するようになります。

ダンプされた PE を修復するための最後の手順は、インポート テーブルを修正することです。これは、独自のブログ投稿に値する複雑なタスクであるため、ここでは詳しく説明しません。ただし、最初のステップでは、エミュレーター メモリ内のライブラリ関数名とそのアドレスのリストを収集します。 GetProcAddress API がアンパッカー スタブでアンパックされた PE のインポートを解決するために使用されていることがわかっている場合は、図 13 に示すようにget_dyn_imports関数を呼び出すことができます。

api_addresses = self.get_dyn_imports()

図 13: 動的インポートの取得

それ以外の場合は、図 14 に示すように、 get_symbols関数を呼び出して、エミュレーター クラスにクエリを実行し、そのシンボル情報を取得できます。

シンボル = self.get_symbols()

図 14: エミュレーター クラスからシンボル情報を取得する

このデータを使用して、解凍された PE の IAT を検出し、インポート関連のテーブルを修正または再構築できます。

すべてを一緒に入れて

マルウェア サンプルをアンパックする Speakeasy スクリプトの作成は、次の手順に分けることができます。

  1. 解凍スタブをリバース エンジニアリングして、1) 解凍されたコードが存在する場所またはそのメモリが割り当てられている場所、2) 実行が解凍されたコードに転送される場所、および 3) サポートされていない API、遅いなどの問題を引き起こす可能性のある問題のあるコードを特定します。エミュレーション、またはアンチ分析チェック。
  2. 必要に応じて、フックを設定して問題のあるコードをバイパスします。
  3. 仮想アドレスを識別するフックを設定し、必要に応じて、展開されたバイナリのサイズを設定します。
  4. アンパックされたコードの元のエントリ ポイントの実行時または実行後にエミュレーションを停止するフックを設定します。
  5. Windows API の仮想アドレスを収集し、PE のインポート テーブルを再構築します。
  6. PE のヘッダーを修正し (該当する場合)、さらに分析するためにバイトをファイルに書き込みます。

UPX サンプルを解凍するスクリプトの例については、Speakeasy リポジトリのUPX 解凍スクリプトを確認してください。

結論

Speakeasy フレームワークは、アナリストがマルウェアのアンパックなどの複雑な問題を解決できるようにする、使いやすく柔軟で強力なプログラミング インターフェイスを提供します。 Speakeasy を使用してこれらのソリューションを自動化すると、大規模に実行できます。 Speakeasy フレームワークの自動化に関するこの紹介を楽しんでいただき、それを使用して独自のマルウェア分析ソリューションを実装するきっかけになったことを願っています。

参照: https://www.mandiant.com/resources/blog/using-speakeasy-emulation-framework-programmatically-to-unpack-malware

Comments

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