実行可能ファイルの構造

EXE ファイルをいじくるには、まず、ファイルの構造を知っておく必要がありますね。ここではそれを説明しましょう。


EXE ファイルの構造は PE フォーマット (Portable Executable format) と呼ばれます。この構造は、 EXE ファイルだけでなく、 DLL などでも使用されているものです。

PE フォーマットの大まかなファイル構造は以下のとおり。

ファイルヘッダMS-DOS スタブIMAGE_DOS_HEADER 構造体
MS-DOS 2.0 スタブプログラムなど
シグネチャ
COFF ファイルヘッダ (IMAGE_FILE_HEADER 構造体)
オプションヘッダ (IMAGE_OPTIONAL_HEADER 構造体) (データディレクトリを含む)
セクションテーブルセクションヘッダ1 (IMAGE_SECTION_HEADER 構造体)
セクションヘッダ2 (IMAGE_SECTION_HEADER 構造体)
セクションヘッダ3 (IMAGE_SECTION_HEADER 構造体)
セクションデータ
セクションデータ2
セクションデータ3

MS-DOS スタブ

ファイルの先頭には、「MS-DOS スタブ」と呼ばれる領域があり、ここには IMAGE_DOS_HEADER 構造体および MS-DOS 2.0 スタブプログラムが含まれています。この部分は、実行可能ファイルを MS-DOS 上で起動した場合でも、正常に起動されるようにするためのプログラムコードが含まれています。通常の Win32 プログラムは、 DOS 上で起動すると「 This program cannot be run in DOS mode 」というメッセージが表示されて終了されますが、これのためのプログラムコードがこの部分に含まれています。

あまり使用されることはありませんが、 IMAGE_DOS_HEADER 構造体は以下のように定義されています。

typedef struct _IMAGE_DOS_HEADER {
    WORD   e_magic;
    WORD   e_cblp;
    WORD   e_cp;
    WORD   e_crlc;
    WORD   e_cparhdr;
    WORD   e_minalloc;
    WORD   e_maxalloc;
    WORD   e_ss;
    WORD   e_sp;
    WORD   e_csum;
    WORD   e_ip;
    WORD   e_cs;
    WORD   e_lfarlc;
    WORD   e_ovno;
    WORD   e_res[4];
    WORD   e_oemid;
    WORD   e_oeminfo;
    WORD   e_res2[10];
    LONG   e_lfanew;
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Win32 の実行可能ファイルにおいて重要となるのは、まず、ファイルの先頭にある2バイト(すなわち、構造体の e_magic メンバ)の値が 0x5A4D (IMAGE_DOS_SIGNATURE と定義されている)であるということです。これは、文字列で表したときに "MZ" となります。これが別の値であった場合には、そのファイルは実行可能ファイルや DLL ではないと判断しましょう。

もう1つ重要となるのは、ファイル先頭から 0x3C (60) バイトの位置にある4バイト数値(構造体の e_lfanew メンバ)の値です。この値は、 MS-DOS スタブの次にくるべき「シグネチャ」の領域の、ファイル先頭からのオフセットが格納されています。シグネチャの位置や、それに続く COFF ファイルヘッダの位置は、 MS-DOS スタブのサイズによって変わってくるので、この値を参照する必要があります。

シグネチャ

MS-DOS スタブに続く4バイトの領域には、シグネチャと呼ばれるものが格納されます。シグネチャとは、実行イメージの種類を識別するためのもので、 PE ファイル(通常の Win32 の実行可能ファイルや DLL )の場合は4バイト値 0x00004550 (IMAGE_NT_SIGNATURE と定義されている)が格納されます。この値は文字列 "PE\0\0" に相当します。(この場合の "\0" はヌル文字です。) この値が別の値である場合には、このファイルが PE ファイルでないと判断しましょう。

COFF ファイルヘッダ

シグネチャに続いて、「COFF ファイルヘッダ」と呼ばれる20バイトの領域があります。この領域を単に「ファイルヘッダ」と呼ぶこともあります。

COFF というのは、“Common Object File Format”のことで、コンパイラなどが生成するオブジェクトファイルで使用されている形式の1つですが、 PE ファイルの構造は COFF ファイルとよく似ており、この「COFF ファイルヘッダ」もその1つです。

この領域は、 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;

いくつかメンバを持ちますが、重要なものをピックアップしていくことにしましょう。

この構造体の最初の2バイト(Machineメンバ)は、マシンタイプを識別する値が格納されます。例えば、 Intel 386 以降(またはその互換)の CPU マシン上で実行されることを目的とする実行ファイルでは 0x014C (IMAGE_FILE_MACHINE_I386) が格納されます。

次の2バイト(NumberOfSectionsメンバ)には、この実行イメージに含まれる「セクション」の数が格納されます。ファイルを解析する上では重要な数ですので、覚えておきましょう。

次の4バイト(TimeDateStampメンバ)には、ファイルが作成された日時が格納されることになっています。この値の形式は正式にはきめられていないようですが、 C/C++ コンパイラ・リンカの多くは、 time_t 形式(世界標準時での、1970年1月1日の午前0時0分0秒からの秒数)を格納するようです。

構造体の16バイト目にある2バイト値(SizeOfOptionalHeaderメンバ)には、 COFF ファイルヘッダに続いて配置されている「オプションヘッダ」のサイズが格納されます。普通、オプションヘッダのサイズは 224 ですが、正しく解析するにはここに格納されている値を使用するようにします。

その次の2バイト値(Characteristicsメンバ)には、イメージの特性を示すビットフラグを組み合わせた値が格納されます。実行可能ファイルや DLL では、実行イメージであることを示す 0x0002 (IMAGE_FILE_EXECUTABLE_IMAGE) が設定されています。また、 DLL の場合には 0x2000 (IMAGE_FILE_DLL) が指定されます。

オプションヘッダ

COFF ファイルヘッダに続いて、オプションヘッダが存在します。オプションヘッダは IMAGE_OPTIONAL_HEADER 構造体として定義されています。

typedef struct _IMAGE_OPTIONAL_HEADER {

    // 標準フィールド

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    // NT 固有フィールド

    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;

構造体の先頭の2バイト(Magic メンバ)には、イメージファイルの状態を識別すつ値が格納されます。通常の Win32 用の実行ファイルでは 0x010B が格納されています。

構造体の4バイト目から連続する3つの4バイト値(SizeOfCode メンバ、 SizeOfInitializedData メンバおよび SizeOfUninitializedData メンバ)には、それぞれ、コードセクション、初期化されたデータセクション、初期化されていないデータセクションのサイズが格納されます。

次の4バイト(AddressOfEntryPoint メンバ)には、イメージファイルのエントリポイントの RVA (相対仮想アドレス)が格納されます。エントリポイントとは、イメージファイルがメモリ上に読み込まれて、最初に実行されるコードのことを指します。

次の4バイト(BaseOfCode メンバおよび BaseOfData メンバ)には、メモリにロードされたときの実行可能コードセクションおよび初期化されたデータセクションの RVA がそれぞれ格納されます。

続く4バイト(ImageBase メンバ)には、イメージファイルのベースアドレスが格納されます。ベースアドレスとは、イメージがロードされて、プロセスの仮想アドレス空間にマッピングされるときの先頭アドレスのことです。このアドレスは常に 64KB の整数倍になります。通常、EXE ファイルでは 0x00400000 が、 DLL では 0x10000000 がデフォルトのアドレスとして指定されています。ただし、ベースアドレスは、イメージがそのアドレスにロードされることが望ましい、ということを表すもので、必ずしもそのアドレスにロードされるわけではありません。また、イメージファイル中で使われるすべての RVA (相対仮想アドレス)は、このベースアドレスからの相対値で表されています。

次の2つの4バイト値(SectionAlignment メンバおよび FileAlignment メンバ)には、 PE ファイルのセクションの、メモリ内におけるアライメント値(セクションアライメント値)およびファイル内におけるアライメント値(ファイルアライメント値)がそれぞれ格納されます。ファイル内ではセクションをファイルアライメントの倍数で区切らなければならず、また、セクションアライメントの倍数に切り上げなければならない値も存在するため、イメージファイルを書き換えるときにはこれらの値が必要になります。

構造体の56バイト目にある4バイト値(SizeOfImage メンバ)は、ヘッダおよびセクションを含むイメージ全体のサイズになります。このサイズは、実際にメモリにロードされたときのサイズであるため、ヘッダ全体のサイズおよび個々のセクションのサイズをそれぞれセクションアライメント値の倍数に切り上げたものを足し合わさなければなりません。

次の4バイト(SizeOfHeaders メンバ)には、 MS-DOS スタブから、シグネチャ、 COFF ファイルヘッダ、オプションヘッダ、およびセクションテーブル中のすべてのセクションヘッダまでを合わせたヘッダ領域の、ファイル中でのサイズが格納されます。すなわち、この値は、ファイルの先頭から、最初のセクションデータまでのオフセットということになります。ファイル中でのサイズであるので、この値はファイルアライメントの倍数になっています。

構造体の68バイト目にある2バイト値(Subsystem メンバ)は、実行イメージのためのサブシステムを表す値です。通常は 2 または 3 が格納されているはずです。 2 の場合は Windows GUI アプリケーションであることを、 3 の場合は Windows コンソールアプリケーションであることを表します。

オプションヘッダには、データディクショナリと呼ばれる部分が含まれています。これは、さまざまな情報が格納されている RVA とそのサイズを格納しているもので、 IMAGE_DATA_DIRECTORY 構造体の配列で表されています。オプションヘッダの92バイト目にある4バイト値(NumberOfRvaAndSizes メンバ)は、ディクショナリに含まれるエントリ(要素)の数が格納されます。そして、直後の96バイト目から、 IMAGE_DATA_DIRECTORY 構造体の配列になっています。 IMAGE_DATA_DIRECTORY 構造体は以下のように定義されています。

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

今回はリソース書き換えが最終的な目的であるので、データディクショナリのエントリの中で重要となるのは、インデックス 2 のエントリ(すなわち3番目のエントリ/オプションヘッダ先頭から112バイト目の位置)です。この部分には、リソーステーブル(すなわち、実際のリソースセクションのセクションデータ)の RVA およびサイズが格納されています。リソースデータの位置はこの RVA から決定されます。また、リソース情報を書き換えた後には、この部分の書き換えも忘れないようにしなければいけません。

セクションテーブル(セクションヘッダ)

少々長くなりましたが、オプションヘッダは以上です。次はセクションテーブルです。

セクションテーブルは、オプションヘッダの直後に続いて存在します。セクションテーブルの先頭位置を求めるには、オプションヘッダの先頭位置に、オプションヘッダのサイズ( COFF ファイルヘッダ IMAGE_FILE_HEADER 構造体の SizeOfOptionalHeader メンバに格納されている値)を加えます。

セクションテーブルの実体は、 IMAGE_SECTION_HEADER 構造体で定義されるセクションヘッダの配列になります。そのエントリ(要素)の数は、 COFF ファイルヘッダ IMAGE_FILE_HEADER 構造体の NumberOfSections メンバに格納されています。セクションヘッダには、セクションについての情報が格納されています。 IMAGE_SECTION_HEADER 構造体は以下のように定義されています。

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[8];
    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;

セクションヘッダの先頭の8バイト(Name メンバ)には、そのセクションの名前を表す ASCII 文字列が格納されています。この文字列は 8 文字以内になります。イメージファイルを生成するコンパイラ・リンカのなかには、ピリオドで始まる名前を付けるものが多いようです。(".text" や ".bbs" など) しかし、必ずしもピリオドで始まっていなくてはならないということはありません。また、多くのイメージファイルではリソースデータが含まれるセクションに ".rsrc" という名前が付けられていますが、必ずしもこの名前であるわけではないので注意しましょう。(とはいっても、一般的に使用されるコンパイラはこの名前を付けるのですが。 HSP の実行イメージでもリソースセクションの名前は ".rsrc" になっています。)

その次の4バイト(PhysicalAddressVirtualSize の共用体で表されます)には、実行イメージの場合には、メモリ上にロードされたときの、セクションデータのサイズが格納されることになっています。ただ、必ずしもサイズが格納されるとは限らないようで、アドレスであったり、 0 であったりする実行イメージも存在するみたいですが。

次の4バイト(VirtualAddress メンバ)には、メモリにロードされたときの、イメージのベースアドレスに対するセクションの先頭バイトの RVA が格納されます。すなわち、デースアドレス(オプションヘッダの ImageBase メンバ)にこの RVA を加えたアドレスに、このセクションのデータがロードされることになります。

その次の4バイト(SizeOfRawData メンバ)には、ファイル上におけるセクションデータのサイズが格納されます。ファイル中のセクションデータのサイズはファイルアライメントの倍数になっているはずですから、この値も、ファイルアライメント値の倍数になります。

次の4バイト(PointerToRawData メンバ)には、ファイルの先頭からセクションデータへのファイルオフセットが格納されます。この値もまた、ファイルアライメント値の倍数になります。

セクションヘッダの36バイト目にある4バイト値(Characteristics メンバ)は、このセクションの性質をあらわすビットフラグが格納されています。代表的なフラグには以下のものがあります。

最初の3つのフラグは、セクションデータの内容を表すもので、それぞれ、実行コード、初期化済みデータ、未初期化のデータが含まれることを示します。最後の3つのフラグは、セクションがメモリ上にロードされたときのデータへのアクセス指定で、それぞれ、実行アクセス、読み取りアクセス、書き込みアクセスを指定します。実行イメージがメモリ上にロードされたとき、例えば、 IMAGE_SCN_MEM_WRITE フラグを持たないセクションが格納されたメモリブロックに対して書き込みを行なおうとすると、アクセス違反が起こるようになります。通常、リソースデータを持つセクションでは少なくとも IMAGE_SCN_CNT_INITIALIZED_DATA フラグと IMAGE_SCN_MEM_READ フラグが設定されているはずです。

セクションデータ

セクションテーブルの後には、実際のセクションデータが格納されることになります。原則としては、セクションの数はセクションヘッダの数と同じです。すなわち、 COFF ファイルヘッダの NumberOfSections メンバで指定される値になります。ただし、セクションによっては、データを持たない(セクションデータのサイズが 0 である)ものも存在します。この場合は、そのセクションのセクションデータはファイル中に存在しません。

セクションデータの内容は、そのセクションにどんな種類の情報が含まれるのかによってさまざまです。リソースデータの構造についてはまた後で述べることにしましょう。


少々複雑でしたが、実行ファイルの構造については以上です。次回は、リソースデータの構造について考えます。