検索
連載

PEフォーマットを解釈せよ!リバースエンジニアリング入門(5)(1/3 ページ)

コンピュータウイルスの解析などに欠かせないリバースエンジニアリング技術ですが、何だか難しそうだな、という印象を抱いている人も多いのではないでしょうか。この連載では、「シェルコード」を例に、実践形式でその基礎を紹介していきます。(編集部)

PC用表示 関連情報
Share
Tweet
LINE
Hatena

Windows APIの呼び出し方法に迫れ!

 第4回「Undocumentedなデータ構造体を知る」に引き続き、今回もシェルコードがWindowsのAPIを呼び出す方法について迫っていきたいと思います。

 シェルコードでは、自由にAPIを呼び出すために以下の3ステップの処理を実行します。

  1. kernel32.dllのベースアドレスを取得する
  2. (kernel32.dllがエクスポートしている)LoadLibrary関数とGetProcAddress関数のアドレスを取得する
  3. LoadLibrary関数とGetProcAddress関数を利用して任意のAPIを呼び出す

 前回は、download_exec.binを逆アセンブルしながら「kernel32.dllのベースアドレスを取得する」方法について解説しました。今回はさらに、LoadLibrary関数とGetProcAddress関数のアドレスの取得方法と、それらを用いた任意のAPIの呼び出し方法について見ていきましょう。今回は日本語の分量に負けないくらいのアセンブリが出てきますので、覚悟して読み進めてくださいね(笑)。

PEフォーマットとエクスポートテーブル

 シェルコードの解析に入る前に、まず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が含まれています。

図1 エクスポート関数に関するテーブル(クリックすると拡大します)
図1 エクスポート関数に関するテーブル(クリックすると拡大します)

 これらのテーブルを参照することで、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関数のアドレスを取得する方法について見てみましょう。具体的には以下のような処理になります。

  1. ネームポインタテーブルから文字列“LoadLibrary”を探し、該当した要素がネームポインタテーブル内で何番目か(ここでは【インデックス】と呼びます)という情報を取得。インデックス】と呼びます)という情報を取得。
  2. 序数テーブル内で【インデックス】番目の値を取得。これがLoadLibrary関数の【序数】になります。序数】になります。
  3. エクスポートアドレステーブル内で【序数】番目の要素を取得。これがLoadLibrary関数のRVAとなります。
  4. 3で得られたRVAにkernel32.dllのベースアドレスを加算し、LoadLibrary関数のアドレスを算出。

 それでは、以上の流れを念頭に置いて、download_exec.binを読んでいきましょう。

       | 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る