コンピュータウイルスの解析などに欠かせないリバースエンジニアリング技術ですが、何だか難しそうだな、という印象を抱いている人も多いのではないでしょうか。この連載では、「シェルコード」を例に、実践形式でその基礎を紹介していきます。(編集部)
第4回「Undocumentedなデータ構造体を知る」に引き続き、今回もシェルコードがWindowsのAPIを呼び出す方法について迫っていきたいと思います。
シェルコードでは、自由にAPIを呼び出すために以下の3ステップの処理を実行します。
前回は、download_exec.binを逆アセンブルしながら「kernel32.dllのベースアドレスを取得する」方法について解説しました。今回はさらに、LoadLibrary関数とGetProcAddress関数のアドレスの取得方法と、それらを用いた任意のAPIの呼び出し方法について見ていきましょう。今回は日本語の分量に負けないくらいのアセンブリが出てきますので、覚悟して読み進めてくださいね(笑)。
シェルコードの解析に入る前に、まずPEフォーマットについて簡単に触れたいと思います。kernel32.dllは、PE(Portable Executable)フォーマットと呼ばれる形式のファイルです。kernel32.dllが持つLoadLibrary関数やGetProcAddress関数のアドレスを探すには、このPEフォーマットを解釈していくことになります。
PEフォーマットについては、「Microsoft Portable Executable and Common Object File Format Specification」に詳しい情報がありますので、興味のある方はそちらも併せてご覧ください。
PEフォーマットの構成要素は、Microsoft Platform SDKに含まれるヘッダファイル、winnt.hで定義されています。PEフォーマットの全体像を把握したい方には、各構成要素の関連を図示した文書(http://www.openrce.org/reference_library/files/reference/PE%20Format.pdf)がお勧めです。
kernel32.dllはまず、winnt.hで定義されているIMAGE_DOS_HEADERから始まります。
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
……
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
この構造体の最後のメンバであるe_lfanew(IMAGE_DOS_HEADERの先頭から3Chバイト目)には、ファイル先頭からPEヘッダまでのオフセットが格納されています。つまりkernel32.dllのベースアドレスに、このe_lfanewの値を足すことで、PEヘッダのアドレスを取得できます。PEフォーマットの説明では、このようなモジュールのベースアドレスからのオフセットのことを、RVA(Relative Virtual Address)と呼ぶことが多いようです。
次に、PEヘッダを見てみましょう。winnt.hではIMAGE_NT_HEADERS32構造体として定義されています。
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signatureには、winnt.hで定義されているIMAGE_NT_SIGNATURE(0x00004550)が格納されています。さらにIMAGE_OPTIONAL_HEADER32構造体のメンバを見てみましょう。
typedef struct _IMAGE_OPTIONAL_HEADER {
……
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
IMAGE_OPTIONAL_HEADER32構造体の最後のメンバは、DataDirectoryという構造体の配列になります。この配列の一番目の要素、DataDirectory[0]に、エクスポート関数に関わる情報が格納されています。
では、DataDirectory[0]の型であるIMAGE_DATA_DIRECTORYを見てみましょう。
typedef struct _IMAGE_DATA_DIRECTORY {
+00 DWORD VirtualAddress;
+04 DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
DataDirectory[0].VirtualAddressが、以下のIMAGE_EXPORT_DIRECTORY構造体へのRVAとなっています。ちなみに、DataDirectory[0].VirtualAddressはPEヘッダ(IMAGE_NT_HEADER32)の先頭から78hバイト目です。
typedef struct _IMAGE_EXPORT_DIRECTORY {
+00 DWORD Characteristics;
+04 DWORD TimeDateStamp;
+08 WORD MajorVersion;
+0A WORD MinorVersion;
+0C DWORD Name;
+10 DWORD Base;
+14 DWORD NumberOfFunctions;
+18 DWORD NumberOfNames;
+1C DWORD AddressOfFunctions; // Export Address Table RVA
+20 DWORD AddressOfNames; // Name Pointer Table RVA
+24 DWORD AddressOfNameOrdinals; // Ordinal Table RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
このIMAGE_EXPORT_DIRECTORY構造体には、図1に示すようにエクスポート関数に関連する3つのテーブルのRVAが含まれています。
これらのテーブルを参照することで、kernel32.dllがエクスポートしているGetProcAddress関数やLoadLibrary関数のアドレスを取得できます。各テーブルの概要は以下のとおりです。
当該モジュールがエクスポートしている関数(変数)名のRVAが格納されています。IMAGE_EXPORT_DIRECTORY構造体のAddressOfNamesにこのテーブルのRVA、NumberOfNamesに要素数が格納されています。
このテーブルの要素は、「エクスポートアドレステーブル内で何番目の要素か」を意味する序数となっています。IMAGE_EXPORT_DIRECTORY構造体のAddressOfNameOrdinalsにこのテーブルのRVAが格納されています。また、序数テーブルとネームポインタテーブルの各要素は1対1で対応しているため、その要素数は、ネームポインタテーブルの要素数と同じくNumberOfNamesとなります。
当該モジュールがエクスポートしている関数(変数)のRVAの配列です。IMAGE_EXPORT_DIRECTORY構造体のAddressOfFunctionsにこのテーブルのRVA、NumberOfFunctionsに要素数が格納されています。
では、これら3つのテーブルを参照し、“LoadLibrary”という文字列からLoadLibrary関数のアドレスを取得する方法について見てみましょう。具体的には以下のような処理になります。
それでは、以上の流れを念頭に置いて、download_exec.binを読んでいきましょう。
Copyright © ITmedia, Inc. All Rights Reserved.