「リバースエンジニアリングバイブル」勉強メモ#5
PE ヘッダー
2014/11/06 : CTF
前回に続き、「リバースエンジニアリングバイブル」の勉強メモ第5弾です。
実行環境は、Windows7(64bit版)です。実行環境は、Windows7(64bit版)です。
5章 PE ヘッダー
実行ファイルがメモリのロードされて処理されるとき、PE を知らないと実行ファイルの情報がどうロードされるかについての疑問が解決できない。
「コードサイズと各セクションの位置を読み、IAT を得て API を呼び出すまでの過程を PE の知識なしにどう理解できるか?」
「PE を知らずにアンパッキングによってどうしてウイルスの分析をできるのか?」
「リバーシング妨害技術であるアンチデバッグについての知識が理解できるか?」
→リバースエンジニアリングで PE ヘッダーの勉強は必須であるという結論
PE : Portable Executable File Format
[誤植]
p.83 PE ファイル生成処理 図内
誤「Source.h」
正「Source.cpp」
PE の重要な構造体
IMAGE_DOS_HEADER
IMAGE_NT_HEADER
IMAGE_FILE_HEADER
IMAGE_OPTIONAL_HEADER
IMAGE_SECTION_HEADER
IMAGE_IMPORT_DESCRIPTOR
IMAGE_EXPORT_DIRECTORY
IMAGE_IMPORT_BY_NAME
IMAGE_THUNK_DATA32
[IMAGE_DOS_HEADER]
typedef struct _IMAGE_DOS_HEADER // DOS .EXE header
{
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER ;
この中で実際に使用するのは、最初の e_magic と最後の e_lfanew だけ。
・e_magic
e_magic フィールドはファイルが PE ファイルかどうかチェックすること以外に使うことはない。
#define IMAGE_DOS_SIGNATURE 0x4D5A // 「MZ」PE の先頭の値
MZ は PE を作った Mark Zbikowski のイニシャル。
・e_lfanew
IMAGE_NT_HEADER 構造体の位置を調べるために使用される値
実際の PE のオフセットがどこにあるかをこのフィールドに指定する。
[IMAGE_NT_HEADER]
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
・Signature
「PE\0\0」を表す「50 45 00 00」の4バイトの値。
ウイルス作成者たちが自分の都合で値を入れておくためによく利用されてきた。
最近リリースされたオペレーティングシステムでは、この部分に 50450000 以外の値が入っていれば実行されないが、昔はウイルスやマルウェアによって感染されたと判断する証拠として多く利用された。
[IMAGE_FILE_HEADER]
ファイルを実行するための最も基本的なデータが含まれている構造体。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWOR D NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
・Machine
このファイルがどの CPU で実行できるか。
・NumberOfSections
セクションがいくつあるか。
セクションとは、.text や .data といったコードセクションやデータセクションなどを指す。
通常、VisualStudio の MFC でオプションを何も変更せずにビルドしたファイルであれば、.text、.rdata、.data、.rsrc の4つのセクションが存在し、NumberOfSections の値も4となる。
パッキングやプロテクティングなどの理由でセクソン数が増えれば、この値の数値も変更される。
各セクションについての説明と役割がリバースエンジニアリングでは非常に重要。→IMAGE_SECTION_HEADER で説明
・TimeDateStamp
このファイルをビルドした日付。
Delphi の場合は、この値を記録せずに常に 1992 年と表示するので、Delphi で作成されたファイルは、このフィールドでビルド時間を確認することができない。
4バイトを占めるバイナリ表示なので、(関数で)変換しないと日付形式で表示できない。
・SizeOfOptionalHeader
IMAGE_OPTIONAL_HEADER32 の構造体の大きさ。
IMAGE_OPTIONAL_HEADER32 はPE をロードするための非常に重要な構造体を含んでいるが、この構造体はオペレーティングシステムごとにサイズが異なる場合があるで、PE ローダーでは SizeOfOptionalHeader 値を先に確認してから IMAGE_OPTIONAL_HEADER32 構造体のサイズを処理する。
・Characteristics
現在のファイルがどのような形式か。
DLL か EXE かを区別する用途程度に考えてよい。
[IMAGE_OPTIONAL_HEADER]
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
・Magic
目印。
32 ビットの場合は 0x10B が入り、64 ビットの場合は 0x20B が入る。
・MajorLinkerVersion
・MinorLinkerVersion
どのバージョンのコンパイラでビルドしたか。
VisualStudio 6.0 でビルドした場合は、6.0 となる。
MajorLinkerVersion が整数部を表し、MinorLinkerVersion が小数部を表す。
・SizeOfCode
コード全体のサイズ。
ウイルスやマルウェアは、このフィールドを参照して自分のコードを複製する場所の基準ポイントにする。
また、セキュリティソリューションでは、コードセクションの整合性チェックを実行するときに、このセクションの値を参照してチェック対象のサイズを割り出すことがある。
・ImageBase
ファイルが実行されるときに実際の仮想メモリにロードされるアドレス(PE ファイルがメモリにマッピングされるアドレス)。
EXE ファイルの場合は、特別なアドレス指定オプションがない限り、このアドレスはほとんど 0x400000 になる。
DLL の場合は、基本的に ImageBase アドレスは 0x10000000 番地に指定されているが、再配置される特性があるので再割り当てされることもある。
・AddressOfEntryPoint
実行ファイルがメモリ上で実行を開始するアドレス、すなわちエントリポイント( Entry Point )。
デバッガーは実行開始ポイントをこのアドレスに ImageBase を足した位置を指定して停止する。
EXE の場合、ここは WinMainCRTSetup になるのが一般的で、DLL の場合は DllMainCRTSetup にジャンプするコードが存在する(この後、WinMain() や DllMain() が始まる)。
OEP( Original Entry Point ):リバーサーに非常に重要なポイント
・BaseOfCode
実際のコードが実行されるアドレス。
ImageBase は、PE ファイル全体の開始アドレスであり、コード領域が開始されるベースアドレスはそれに BaseOfCode を足した値から始まる。
特別なことがない限り、0x1000 が指定される(もちろん、コンパイラのオプションでいくらでも変更できる)。
・SectionAlignment
・FileAlignment
セクションを配置するときの最小単位。
通常は 0x1000 が指定され、0x1000 単位に分割する。
隙間は0で埋める(パディング)。
FileAlignment はファイル上の間隔、SectionAlignment はメモリにロードされたときの間隔。
・SizeOfImage
EXE/DLL がメモリにロードされたときの全体サイズ。
SectionAlignment でパディングした値も含まれるため、この値は必ず SectionAlignment の倍数になる。
・SizeOfHeaders
PE ヘッダーのサイズを示すフィールド。
この値が 0x1000 であればアドレス計算は直感的にできるのでラクだが、0x400 とかだと電卓が必要になるので非常に厄介。
・Subsystem
#define IMAGE_SUBSYSTEM_NATIVE 1 // *.sys などのドライバーモジュール
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Windows GUI で、ウィンドウを持っているモジュール
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // コンソールアプリケーション
・DataDirectory
IMAGE_DATA_DIRECTORY の構造体で、VirtualAddress と Size という名前のフィールドが含まれている。
エクスポートディレクトリまたはインポートディレクトリ、リソースディレクトリ、IAT など、それぞれの仮想アドレスとサイズがこのフィールドでわかる。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
IMAGE_NUMBEROF_DIRECTORY_ENTRIES は 16 で定義されていて、それぞれの配列は次のとおり。
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
RVA( Relative Virtual Address ):相対アドレスのことで、絶対アドレスからベースアドレスを引いた値。ベースアドレスを基準にしてどれだけ離れているかを示す。
・IMAGE_SECTION_HEADER
セクションヘッダーは、主に各セクションの名前、開始アドレスとサイズなどの情報を管理する構造体である。
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
ImageBase を取得するには、GetModuleHandle() という API が利用できる。
NULL を与えると、現在のモジュール(EXE ファイル)の ImageBase を返すので、これを PIMAGE_DOS_HEADER にキャストすると、PE として利用できる。
[脱字]
p.107 2行目
誤「MEM_WRITE などものがある。・・・」
正「MEM_WRITE などのものがある。・・・」
IAT( Import Address Table ):DLL の関数のポインターを集めたテーブルを作っておいて、コードのセクションでこのテーブルを参照するように管理する。
・IMAGE_IMPORT_DESCRIPTOR
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
インポートの構造体。
1つの EXE や DLL の PE にインポートされる DLL、つまり、バイナリで使用する kernel32.dll、user32.dll などの DLL が存在する場合、DLL の数だけこの構造体が存在する。
アンパッキング手法の中で最も基本的な作業は、(パッカーがアンパックすることで壊した)インポートデータを回復することである。
.idata というセクションに IAT が含まれているはずだと思われがちだが、実際には、コンパイラのビルドの状態により、他のセクションの .rdata などに IAT が含まれている場合も多い。
したがって、全てのセクションを探索してインポートデータを探すことになる。
PE 関連のコードを直接作成し、デバッガーを起動して1行ずつトレースしながら、スタックはどう変わっていって、メモリのどの値をどう取得するかなどを直接身をもって体験する学習を繰り返したほうが、PE を完全に自分のものにできる。
記述に際しては、細心の注意をしたつもりですが、間違いやご指摘がありましたら、こちらからお知らせいただけると幸いです。
←「リバースエンジニアリングバイブル」勉強メモ#6
←「リバースエンジニアリングバイブル」勉強メモ#4
« 戻る