PEファイル
Windowsの実装ファイルの形式。exe,dll等
ローダがプログラムをロードするのに必要な情報が格納されたヘッダと、プログラムコードやリソースデータが記録されているセクションで構成されている。
RVA(Relative Virtual Address)相対仮想アドレス
プログラムがメモリにロードされたアドレスからの相対アドレスを表している。RVAはファイルの先頭からのオフセットとは異なる。
DOS MZヘッダ
PEファイルフォーマットの先頭にある領域で、winnt.h内ではIMAGE_DOS_HEADERとして定義されている。サイズは64バイトで、19個のメンバから構成されている。e_magicはDOS MZヘッダの先頭2バイト分でWindows実行ファイルの場合は(MZ (0x4D, 0x5A))という値がシグネチャとして設定されている。e_lfanewは最後のメンバで、PEヘッダへのオフセット値担っている。
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;
PEヘッダ
winnt.hでIMAGE_NT_HEADERとして定義されていて、Signature, FileHeader, OptionalHeaderの3つのメンバで構成されている。SignatureはPEヘッダの先頭4バイトで、PEファイルの場合[PE(0x50, 0x45, 0x00, 0x00)]という値が設定されている。
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
IMAGE_FILE_HEADER
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
NumberOfSections
後述するセクションの数を示す
SizeOfOptionalHeader
後述するOptionalHeaderの数
OptionalHeader
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; 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_HEADER, *PIMAGE_OPTIONAL_HEADER;
AddressOfEntryPoint
プログラムの実行開始アドレス(RVA)
ImageBase
プログラムがメモリ上にロードされる時の理想的な開始アドレス。実行ファイルの場合は0x00400000, DLLファイルの場合は0x10000000になるのが一般的。ロードしようとしたアドレスが使用されていたら再配置が行われる。
SectionAlignment
メモリ上にロードされるときに各セクションが配置されるアラインメント。各セクションの開始アドレスはSectionA;ignmentの倍数になる。SectionAlignmentが0x1000, 最初のセクションの開始アドレスが0x00401000,サイズが0x20バイトだとすると、次のセクションは0x00402000から始まり、その間はNULLで埋められるらしい。
FileAlignment
ディスク上で各セクションが配置されるアラインメント。各セクションの開始アドレスはFileAlignmentの倍数になる。
SizeOfImage
メモリ上にロードされたプログラムのサイズ。ロードされる前の実行ファイルのサイズとは異なる。
DataDirectory
IMAGE_DATA_DIRECTORY構造体。各エントリがある位置のRVAとサイズで構成されている。配列の長さは16でDataDirectory[0]にはエクスポートテーブル、DataDirectory[1]にはインポートテーブルのエントリが保持されている。
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
先ほどのe_lfanewの値が0x000000E0だったので、PEヘッダの先頭はそこからになっています。Singatureは[PE]という値に。
FileHeaderのnumberOfSectionsは0x0008なのでこのプログラムには8つのセクションがあることがわかります。
Characteristicsは0x0102なので、32ビットアーキテクチャのファイルで実行可能であることがわかります。
次にOptionalHeaderに注目します。AddressOfEntryPointが0x00011055, ImageBaseが0x00400000なので、このプログラムはメモリの0x00400000からロードされ0x00411055に位置するコードから実行されることがわかります。
インポートテーブルのDataDirectory[1]はVirtualAddressが0x0001A1E4でSizeが0x64なので、ImageBaseの値より、メモリ上の0x0041A1E4から0x64分がインポートテーブルとなることがわかります。
セクションテーブル
IMAGE_SECTION_HEADERとして定義されている。コードやデータ、リソース情報など種類ごとにまとめられたセクションの領域のサイズや開始アドレス、属性を指定している、1つのセクションテーブルには1つのセクションの情報だけ格納されているので、セクションテーブル自体はNumberOfSectionsの数だけある。
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;
Name
セクションにつけられる名前。8バイト。ASCIIである必要はなく、NULL(0x00)で終わる必要もない。.textだとコードセクション、.idataだとインポートセクションというケースが多いが、Nameの値はプログラムの実行において意味がないのでそのまま判断するのは危険らしい。
VirtualSize
プログラムがメモリ上にロードされた時のセクションのサイズ。一般にSizeOfRawDataよりも小さな値となる。
VirtualAddress
プログラムがメモリ上にロードされた時のセクションの先頭アドレスのRVA。
SizeOfRawData
ディスク上におけるセクションのサイズ
PointerToRawData
ディスク上におけるセクションの先頭へのオフセット。
Characteristics
セクションの属性を表す属性の論理和。よく見られるものは以下。
フラグ | 値 | 意味 |
---|---|---|
IMAGE_SCN_CNT_CODE | 0x00000020 | 実行コードを含む |
IMAGE_SCN_MEM_EXECUTE | 0x20000000 | 実行可能 |
IMAGE_SCN_MEM_READ | 0x40000000 | 読み取り可能 |
IMAGE_SCN_MEM_WRITE | 0x80000000 | 書き込み可能 |
SizeOfOptionalHeaderは0xE0でOptionalHeaderの先頭は0xF8から始まっているので、0x1D8からセクションテーブルが始まっていることがわかります。
NumberOfSectionは0x08なので8つのセクションテーブルとそれに対応するセクションが存在します。
セクションテーブルはそれぞれ40バイトとなっている。.text(Name)に着目すると、このプログラムをメモリにロードした時、.textというセクションは0x00411000から始まり(VirtualAddressが0x11000でこれはRVAなので、ImageBaseの0x400000を加える)、そのサイズは0x4EF4(VirtualSize)であることがわかります。ロードする前のファイルがディスク上にある状態では0x400(PointerToRawData)から0x5000バイト(SizeOfRawData)分が.textのセクションである。
セクション
プログラムの根本をなすコードやデータ、リソース情報などが含まれている。役割に応じていくつかの種類がある。
コードセクション
実行されるコードのバイナリデータが記録されている領域。
データセクション
文字列や定数など、プログラム中で使われるデータが記録されている領域。
リソースセクション
プログラムで使用されるリソースに関連する情報が記録されている領域
エクスポートセクション
DLLなどのエクスポート関数の情報が記録されている領域
インポートセクション
外部DLLなどから関数をインポートしてくる場合、インポートしている関数に関する情報が記録されている領域。
TLS(Thread Local Storage)セクション
スレッド固有のデータ領域に関する情報が記録されている領域。
インポートセクション
外部DLLからインポートする関数の情報が含まれている。インポートテーブルにはインポートしている関数の情報があり、IMAGE_IMPORT_DESCRIPTOR構造体の配列になっている。インポートテーブルの長さは決まっていないため、終端は全て0で埋められている。
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) }; DWORD TimeDateStamp; // 0 if not bound, DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk
インポートルックアップテーブル(Import Lookup Table:ILT)のRVA. ILTはIMAGE_THUNK_DATA構造体の配列。4バイトの共用体となっており、ILTではインポートする関数のヒント(Hint)と関数名の構造体であるIMAGE_IMPORT_BY_NAME構造体へのポインタ(AddressOfData)として使われる。
typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE DWORD Function; // PDWORD DWORD Ordinal; DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Name
インポートしているDLL名のRVA
FirstThunk
インポートアドレステーブル(Import Address Table:IAT)のRVA. IATはプログラムがロードされる前後で書き換えられる。ロードされる前はIMAGE_THUNK_DATA構造体の配列になっていて、ILTと同一の値が格納されている。ロード後は、実際にインポートする各関数のアドレスに書き換えられる。
dumpbin
Visual Studioに付属しているPEフォーマットを表示してくれるもの。今までのようなことをする必要がなくなりそう。
Vitual Studioコマンドプロンプトを開いて以下のコマンドを入力
>dumpbin.exe /HEADERS hoge.exe