Operating Systems Development Series
Portable Executable (PE)
by Mike, 2011

はじめに

ようこそ!

どうも、長くなりそうです。

この章では、高度なトピックである PE 実行可能ファイル形式を取り上げます。PE リソース、ダイナミックリンク、その他をカバーする予定です。また、この章は、可能な限り完全な情報にするために、より多くの情報を含むように更新される予定です。

この章に含まれるほとんどの内容は、情報提供のみを目的としており、完全性を期すためと、読者がサポートを提供したい場合のために含まれているに過ぎません。また、提供された情報の多くは、公式の PE 仕様書にも記載されていることに注意してください。

この章を終えれば、ローダーを開発し、シングルタスク環境をサポートするために必要なものは全て揃うでしょう。

さぁ、はじめましょ

ファイルフォーマット

概要

Portable Executable (PE)ファイルフォーマットは、Windows や ReactOS のような Windows 系 OS を含むいくつかのオペレーティングシステムで使われている標準的な実行ファイルフォーマットです。また、EFI (Extensable Firmware Interface)搭載機で起動する際に使用される標準的なファイルフォーマットでもあります。

PE実行ファイル形式は、再配置、シンボルテーブル、リソース、ダイナミックバインディングなどをサポートする複雑な形式である。

用語

VA (バーチャルアドレス)

仮想アドレス(VA)とは、現在のプログラムの仮想アドレス空間(VAS)内の線形アドレスのことである。PE実行形式では、すべてのアドレスが仮想アドレスになります。これらのアドレスは32ビットのリニアアドレスである。

RVA (相対的仮想アドレス)

相対仮想アドレス(RVA)とは、実行プログラムのベースアドレスからの相対的なVAである。PE実行形式は多くの部分でRVAを使用していますので、RVAとは何か、RVAからリニアアドレスを取得する方法について知っておくことが重要です。RVAとは、ベースアドレスからのオフセットに過ぎません。従って、リニア・アドレスを得るには、ベース・アドレスにRVAを加えればよいのです。

Linear address = Base address + RVA

これはパース時に多くの部分でこの計算を行う必要があるので重要である。

セクションとセクション・テーブル

セクション

高度な実行ファイル形式では、一般にプログラムセクションを使用してリンクプロセスを簡略化し、ソフトウェアに構造を与えます。 セクションは、実行イメージまたはオブジェクトファイル内に命令とデータを格納するための標準的な方法を提供することによって、リンクプロセスを簡略化します。

セクションは通常、セクションの中にある要素に関連する名前を持っています。例えば、.dataは可変の未初期化データを格納する一般的なセクション名です。その他のセクション名には、歴史的な背景があります。例えば、.textは実行可能なコードやオブジェクトコードを含むセクションの典型的な名前です。.bssは一般的に、グローバルでプログラム全体の初期化データに使用されます。

C++ツールチェーンを例にとると、グローバル名前空間またはstaticとして定義された変数は.bssに格納されます。コンパイル後に生成されるバイトコードは.textに格納されます。

PE実行ファイル形式は、通常、以下のセクション名のうちの1つ以上を含んでいます。

  • .text
  • .data
  • .bss
  • .arch
  • .edata
  • .idata
  • .pdata
  • .rdata
  • .reloc
  • .rsrc
  • .sbss
  • .sdata
  • .srdata
  • .xdata

セクション表

プログラムファイルやオブジェクトファイルには、複数のセクションが含まれています。各セクションのベースロケーションとセクションの名前は、通常セクションテーブルに格納されます。セクション・テーブルは、構造体の単純なリンクリストやハッシュ・テーブルなど、さまざまな実装があります。

シンボルとシンボル・テーブル

シンボル

C++でプログラミングをしていると、有名な未定義シンボルのリンカーエラー(C言語では警告)に遭遇したことがあるでしょう。その通り、昔のMSVCではエラーなしで完全にコンパイルとリンクができました)。これは、リンク時に定義が解決できなかった関数を呼び出したり、変数を名前で参照したりした場合に起こります。

リンカは関数や変数をシンボルと呼んでいます。シンボルには名前と、それが何であるかという情報(例えばデータ型や値など)が含まれています。コンパイル時に、コンパイラはこれらのシンボルを追跡して、最終的なプロ グラムがリンクできることを保証しなければなりません。もし、現在の翻訳ユニットで定義されていないシンボルが使用されているが、EXTERNシンボルである場合、コンパイラはオブジェクトファイルにシンボルを書き込む際に、それをEXTERNシンボルとしてマークする必要がある。

リンクの段階で、EXTERNとマークされたシンボルにまだ値が関連付けられていない(シンボルが定義されていない)場合、リンカーは上記のエラーを出します。

シンボルは、プログラマがモジュール、翻訳ユニット、またはライブラリにまたがって変数や関数を定義するためのものです。

このため、高級言語を使用すると名前の衝突が起こりやすいので、変数名や関数名には一般的に名前潰し(name mangling)が行われています。もちろん、アセンブリ言語には適用されませんが、適用される名前のマングリングは複数の要因に依存し、ツールチェーンによって異なります。

では、見てみましょう。右はC言語の関数宣言とそのマングル化されたシンボル名です。マングリングされた名前の中の数字は、パラメータのバイト数です。

void _cdecl function (int i); -> _function void _stdcall function(int i); -> _function@4 void _fastcall function(int i); -> @function@4

呼び出し規約が_cdeclの関数は、アンダースコアのみが先頭に付くことに注意してください。これにより、Cの関数をアセンブリ言語で簡単に定義でき、Cのコードで簡単にその関数を呼び出すことができます。

C++の名前のマングリングに関する標準はありません。コンパイラによっては、?h@YAXH@Zのようなシンボリックな名前を出すものもあれば、void h(int)という同じ関数に対して__7h__FiW?h$n(i)vといった名前を出すものもあります。このため、アセンブリ言語で使用するのは非現実的です。しかし、まだ可能性はあります。

シンボル表

セクションテーブルと同様にシンボルテーブルが存在します。シンボル・テーブルは、シンボル名やシンボルに関する情報(エクスポートされたシンボルかどうか、データ型、プロパティなど)をソフトウェアが検索できるようにするものである。シンボル・テーブルは通常、情報のリンクリストか、ハッシュ・テーブルで実装される。

構造

概要

MSVC++の章でPE実行形式の構造を見てきました。 PE実行形式をメモリにロードすると、そのメモリはロードしたファイルの正確なコピーを含むことになります。これは、PEファイル形式の最初の構造内の最初のバイトが、実際にファイルがメモリにロードされたところから最初のバイトに位置することを意味します。

例えば、PEファイルを1MBにロードした場合、メモリ内のフットプリントは次のようになります。

MSVCの章を読んでいる読者には、上記の画像は見慣れたものに見えるはずです。上の画像を見ると、PEファイルが1MBにロードされた場合、最初のディスク上の構造体であるIMAGE_DOS_HEADERがメモリ内のその位置から始まり、ファイル内の残りの構造体が続きます(パディングを含む)。

上の画像も単純化しすぎで、決してPEファイルフォーマットの完全な姿を示しているわけではありません。 PEファイルフォーマットの構造はかなり大きく、多くの構造体とテーブルで構成されています。

以下は、その完全な形式です。

  1. IMAGE_DOS_HEADER structure (Important)
  2. STUB program
  3. IMAGE_FILE_HEADER structure [COFF Header] (Important)
  4. IMAGE_OPTIONAL_HEADER structure (Important)
  5. Segment Table
  6. Resource Table
  7. Resident Name Table
  8. Module Reference Table
  9. Imported Names Table
  10. Entry Table
  11. Non Resident Name Table
  12. Segments
    1. Data
    2. Info

上の表は、ファイルの最初から最後までの完全なフォーマットを示しています。重要な項目は、プログラムを実行するために必要なパース方法を示しています。その他の情報は、情報提供のみを目的として提供されています。IMAGE_OPTIONAL_HEADER構造体の中で重要なのは、エントリポイントのアドレスを含むメンバと、画像のベースアドレスだけである。

このファイルの各セクションのパースについては、次節以降で詳しく説明する。また、テーブルやディレクトリを解析する際に使用される他の構造体も同様に紹介します。

IMAGE_DOS_HEADER構造体

IMAGE_DOS_HEADERはPEファイルの最初の構造体である。これは、プログラムファイルとそれをロードする方法に関するグローバルな情報を含んでいます。この構造体に含まれる情報のほとんどは、DOS ソフトウェアに関連するものであり、 後方互換性のためにのみサポートされています。

この構造体は,次のような形式になっている。

typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header uint16_t e_magic; // must contain "MZ" uint16_t e_cblp; // number of bytes on the last page of the file uint16_t e_cp; // number of pages in file uint16_t e_crlc; // relocations uint16_t e_cparhdr; // size of the header in paragraphs uint16_t e_minalloc; // minimum and maximum paragraphs to allocate uint16_t e_maxalloc; uint16_t e_ss; // initial SS:SP to set by Loader uint16_t e_sp; uint16_t e_csum; // checksum uint16_t e_ip; // initial CS:IP uint16_t e_cs; uint16_t e_lfarlc; // address of relocation table uint16_t e_ovno; // overlay number uint16_t e_res[4]; // resevered uint16_t e_oemid; // OEM id uint16_t e_oeminfo; // OEM info uint16_t e_res2[10]; // reserved uint32_t e_lfanew; // address of new EXE header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

さてさて、この構造体には興味深いものがたくさんあります。オペレーティングシステムは通常CSにスタック空間とコード記述子の値を割り当てるので、CS:IPとSS:SPの初期メンバは無視する必要があります。これらのメンバは、DOSエリアやv8086モードを必要とするソフトウエアの時代に目立つようになった。

STUBプログラム

さて、それではIMAGE_DOS_HEADER構造体の直後にDOSスタブプログラムがあることに注目してください。 これは、実は便利なプログラムなのです。これは、DOSの中からWindowsのプログラムを実行しようとすると、「このプログラムはDOSモードでは実行できません」と表示するプログラムなのです。

このスタブプログラムを変更するには、/STUBというリンカーオプションを使用します。

/stub=myprog.exe

DOSが実行ファイルをロードしようとすると、IMAGE_DOS_HEADER構造体を解析し、有効なDOSプログラムであるため、DOSスタブプログラムを実行しようと試みます。Win32サブシステムの下で実行される場合、Windowsローダーはスタブプログラムを無視します。

画像_NT_HEADERS

STUB プログラムに続いて、PE ヘッダー構造体のフォーマットを含むIMAGE_NT_HEADERSという構造体があります。以下は、その構造体である。

typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;

署名は、"PE0 "にマッチする必要があります(ただし、"PE0 "はヌル文字)。IMAGE_FILE_HEADERには、ローダーが使用する追加情報とIMAGE_OPTIONAL_HEADER構造体の全サイズが格納される。IMAGE_OPTIONAL_HEADERは、ファイルの中で最も大きく、最も重要な構造体である。 また、定義されたサイズを持っていない。

e_lfanewはメモリ上のこの構造体へのRVAなので、この構造体の位置を特定するために、OSローダーは次のことを実行する必要があります。

IMAGE_DOS_HEADER* pFile = (IMAGE_DOS_HEADER*) imageBase; IMAGE_NT_HEADERS* pHeaders = (IMAGE_NT_HEADERS*) (pFile->e_lfanew + imageBase);

これは、imageBaseがプログラムファイルがメモリにロードされた場所を参照していると仮定しています。DOS などの古い OS では、このヘッダのメンバは認識されないので、これらの OS では無視されます。

この構造体には、他の2つのヘッダー構造体のフォーマットが含まれています。ここでは、そのうちの最初の構造体を見てみましょう。

IMAGE_FILE_HEADER

IMAGE_FILE_HEADERはCOFF (Common Object File Format)ヘッダー構造体です。次のような形式になっています。

typedef struct _IMAGE_FILE_HEADER { USHORT Machine; USHORT NumberOfSections; // Number of sections in section table ULONG TimeDateStamp; // Date and time of program link ULONG PointerToSymbolTable; // RVA of symbol table ULONG NumberOfSymbols; // Number of symbols in table USHORT SizeOfOptionalHeader; // Size of IMAGE_OPTIONAL_HEADER in bytes USHORT Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

この構造体はあまり複雑ではありません。上記のほとんどは、デバッガ(シンボルテーブルの解析)にのみ有用です。SizeOfOptionalHeaderは重要です - IMAGE_OPTIONAL_HEADERはサイズが定義されていないため、このメンバーで構造体のサイズを知ることができます。

Machineは以下の値のいずれかになります。

  • 0x014c (x86マシン)
  • 0x0200 (x64マシン)
  • 0x8664 AMD64マシン用

通常の場合、x86アーキテクチャ向けに開発しているので、0x014cであるべきである。

特性はビットフラグで構成されており、リンカがビット単位でORすることで、ローダに実行イメージの種類の異なる特性を知らせることができます。以下はその形式である。

  • Bit 0: 設定されている場合、イメージは再配置情報を持っていません。
  • Bit1: 設定されている場合、ファイルは実行可能です。
  • Bit2: 設定されている場合、イメージはCOFF行番号を持ちません。
  • Bit3: 設定されている場合、イメージはCOFFシンボルテーブルのエントリを持ちません。
  • Bit4:設定されている場合、イメージの作業セットを切り詰める。(Windowsのメモリ管理に特化。廃止)
  • Bit5: セットされた場合、ローダーは実行ファイルが2GB以上のVAを処理できると見なします。
  • Bit6: 設定された場合、ローダーはイメージが32ビットワードをサポートすると仮定します。
  • Bit7: セットされた場合、イメージはデバッグ情報を持ちません。
  • Bit8: セットされた場合、イメージはネットワークドライブから直接実行できない(Windows固有)
  • Bit9: 設定された場合、イメージはSYSTEMファイルとして扱われます。
  • Bit10: 設定すると、イメージはDLLファイルとして扱われます。
  • Bit11: 設定すると、イメージはシングルプロセッサのマシンでしか実行されません。
  • Bit12: 設定されている場合、ビッグエンディアンを使用する。

Windows のヘッダでは、IMAGE_FILE_RELOCS_STRIPPED や IMAGE_FILE_EXECUTABLE_IMAGE など、これらのフラグを設定する際に使用できる定数を定義して使用しています。

見ての通り、この構造体のほとんどは、ローダーが画像を読み込む方法に関する情報のみを提供するためのものです。しかし、ちょっと待ってください!リソース、シンボルテーブル、デバッグ情報......これはどこにあるのでしょうか?ああ、IMAGE_OPTIONAL_HEADERが定義されたサイズを持っていない理由を見よ。見てみましょう。

IMAGE_OPTIONAL_HEADER

うーん、これだ。これはファイルの中で最も複雑な構造です。しかし、この構造体は以前にも見たことがあるはずです。

struct _IMAGE_OPTIONAL_HEADER { USHORT Magic; // not-so-magical number UCHAR MajorLinkerVersion; // linker version UCHAR MinorLinkerVersion; ULONG SizeOfCode; // size of .text in bytes ULONG SizeOfInitializedData; // size of .bss (and others) in bytes ULONG SizeOfUninitializedData; // size of .data,.sdata etc in bytes ULONG AddressOfEntryPoint; // RVA of entry point ULONG BaseOfCode; // base of .text ULONG BaseOfData; // base of .data ULONG ImageBase; // image base VA ULONG SectionAlignment; // file section alignment ULONG FileAlignment; // file alignment USHORT MajorOperatingSystemVersion; // Windows specific. OS version required to run image USHORT MinorOperatingSystemVersion; USHORT MajorImageVersion; // version of program USHORT MinorImageVersion; USHORT MajorSubsystemVersion; // Windows specific. Version of SubSystem USHORT MinorSubsystemVersion; ULONG Reserved1; ULONG SizeOfImage; // size of image in bytes ULONG SizeOfHeaders; // size of headers (and stub program) in bytes ULONG CheckSum; // checksum USHORT Subsystem; // Windows specific. subsystem type USHORT DllCharacteristics; // DLL properties ULONG SizeOfStackReserve; // size of stack, in bytes ULONG SizeOfStackCommit; // size of stack to commit ULONG SizeOfHeapReserve; // size of heap, in bytes ULONG SizeOfHeapCommit; // size of heap to commit ULONG LoaderFlags; // no longer used ULONG NumberOfRvaAndSizes; // number of DataDirectory entries IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

まず、最後のメンバーであるDataDirectoryを見てください。定数IMAGE_NUMBEROF_DIRECTORY_ENTRIESは、長年にわたって変更することができますし、これまでも変更されてきました。これは、この構造体のサイズを変更することができるメンバーです。このメンバーについては、もう少し後で詳しく見てみましょう。

このヘッダーは明らかにオプションではないのに、なぜ「オプション」と呼ばれているのか、興味を持たれたかもしれません。これは、COFFオブジェクトファイルではオプションであるためです。実行可能なイメージではオプションではありませんが、オブジェクトファイルではオプショナルです :)

マジックは、以下のいずれかになります。

  • 0x10b: 32ビット実行イメージ
  • 0x20b: 64ビット実行形式イメージ
  • 0x107:ROMイメージ
通常の場合、0x10b となるはずです。

この構造体の多くのメンバーは、それほど複雑ではない。

subsystemメンバは Windows 固有のものである。プログラムが正しく実行されるために、どのようなサブシステムが必要かを Windows に伝える。以下の値のいずれかになります (完全を期すためにここに掲載します)。

  • 0: 不明
  • 1: ネイティブサブシステム
  • 2: GUIサブシステム
  • 3: CUIサブシステム
  • 5: OS/2 CUIサブシステム
  • 7: POSIX CUIサブシステム
  • 9: Windows CE GUIサブシステム
  • 10: EFI
  • 11: EFIブートドライバ
  • 12: EFIランタイムドライバ
  • 13: EFI ROM
  • 14: XBox
  • 16: ブートアプリケーション

DllCharacteristicsメンバは、ローダがDLLに関する情報を得るためのビットフラグを含む。以下のフォーマットで記述する。

  • Bit0~3:予約
  • Bit4:設定されている場合、DLLは再配置可能である
  • Bit5:設定された場合、コードの整合性チェックを強制する
  • Bit6: 設定されている場合、イメージはデータ実行防止(DEP)に対応している
  • Bit7: 設定されている場合、イメージは分離されるべきではありません。
  • Bit8: 設定されている場合、イメージは構造化例外処理(SEH)を使用しない
  • Bit9: 設定された場合、画像はバインドされない
  • Bit10: 予約済み
  • Bit11: 設定されている場合、イメージはWindows Driver Model (WDM)ドライバである。
  • Bit12: 予約
  • Bit13: イメージはターミナルサーバーを意識している

AddressOfEntryPointは重要なものです。このメンバには、イメージのエントリポイント関数のRVAが含まれています(DLLにはエントリポイントが不要なため、NULLでもかまいません)。

これだけです。.text.data.bssなどの他のメンバーについて興味があるかもしれません。また、DataDirectoryという厄介なメンバーもありますが、これはまだ見ていません。

これらのメンバーについては、後ほど詳しく見ていきます。とりあえず、プログラムの実行を見てみましょう。

プログラムの実行

この段階で、もしあなたがプログラムを実行したいだけなら、すべての情報は提供されています。プログラムをロードした後、ローダーがすべきことは、オプショナル・ヘッダからAddressOfEntryPointメンバを探し出し、そのアドレスを呼び出すことです。これはRVAであり、ローダーはこのアドレスをImageBaseに追加して、エントリポイント関数へのリニアアドレスを取得する必要があることを忘れないでください。

以下はその一例です。
//! loadedProgram is where the image was loaded to IMAGE_DOS_HEADER* pImage = (IMAGE_DOS_HEADER*) loadedProgram; //! go to NT HEADERS IMAGE_NT_HEADERS* pHeaders = (IMAGE_NT_HEADERS*)(loadedProgram + pImage->e_lfanew); //! get image base and entry point address from optional header int base = pHeaders->OptionalHeader.ImageBase; int entryPoint = pHeaders->OptionalHeader.AddressOfEntryPoint; //! entry point function is at base+entryPoint void (*entryFunction) () = (entryPoint + base); //! call program entry point entryFunction();

PE 実行ファイルを実行するのに必要なのは、これだけです :)

データディレクトリ

概要

リソース、シンボルテーブル、デバッグ情報、インポート、エクスポートテーブルなどは、 オプションのヘッダにあるDataDirectoryメンバからアクセスすることができます。このメンバーはIMAGE_DATA_DIRECTORY の配列であり、 これらの情報を含む他の構造体にアクセスするために使用されます。IMAGE_DATA_DIRECTORYは以下のような形式になっています。

typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; // RVA of table DWORD Size; // size of table } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

DataDirectoryはIMAGE_DATA_DIRECTORYの 配列であることを忘れないでください。この配列の各エントリによって、アクセスしたい異なるデータにアクセスすることができます。

以下は、インデックスのエントリです。

  • 0: エクスポートディレクトリ
  • 1: インポートディレクトリ
  • 2: Resourceディレクトリ
  • 3: Exceptionディレクトリ
  • 4: Securityディレクトリ
  • 5: ベースリロケーションテーブル
  • 6: デバッグディレクトリ
  • 7: 説明文字列
  • 8: マシン値(MIPS GP)
  • 9:TLSディレクトリ
  • 10: ロードコンフィグレーションディレクトリ
  • 14: COM+ データディレクトリ

例えば、エクスポートテーブルを読み込む場合は、DataDirectory[0]を参照する。リソースを読み込む場合は、DataDirectory[2].VirtualAddressを参照します。

これらのセクションには、特定のデータを解析するために必要な独自の構造体が含まれています。 より便利なものをいくつか見てみましょう。

エクスポートテーブルの読み込み

エクスポートテーブルには、ライブラリやDLLからエクスポートされたすべての関数と、そのDLL内の関数アドレス、関数名、序数などが含まれています。Win32 API 関数GetProcAddress()は、序数または名前によってモジュールのエクスポートテーブルを解析し、そこからアドレスを返すことによって動作します。 これは、エクスポートテーブルを読むことが有用である方法の 1 つです。

エクスポートテーブルを解析するためには、まずエクスポートディレクトリの構造を取得する必要があります。これはDataDirectory[0] を取得することで行われます。

PIMAGE_DATA_DIRECTORY DataDirectory = &OptionalHeader->DataDirectory [0]; PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY) (DataDirectory->VirtualAddress + ImageBase);

IMAGE_DATA_DIRECTORY構造体のVirtualAddressはRVAなので、イメージベースに追加する必要があることに注意してください。これで、exportDirectoryは、この素敵な構造体を指すようになります。

typedef struct _IMAGE_EXPORT_DIRECTORY { uint32_t Characteristics; uint32_t TimeDateStamp; uint16_t MajorVersion; uint16_t MinorVersion; uint32_t Name; uint32_t Base; uint32_t NumberOfFunctions; uint32_t NumberOfNames; uint32_t** AddressOfFunctions; uint32_t** AddressOfNames; uint16_t** AddressOfNameOrdinal; }IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

こちらは簡単ですね。AddressOfFunctionsは関数アドレスの配列を指すRVAです。 ただし、関数アドレスもRVAです。AddressOfNamesは関数名のリストへのポインタです。 しかし、これらのアドレスはすべてRVAですので、関数名とアドレスを正しく取得するためにイメージベースに追加する必要があります。

AddressOfNameOrdinalは序数列のリストへのRVAです。序数は、アドレスではなく、エクスポートされた関数を表す単なる番号なので、RVAではありません。

エクスポートテーブルを正しく解析するためには、ループで行う必要があります。例えば

PDWORD FunctionNameAddressArray = ((DWORD)ExportDirectory->AddressOfNames) + ((PBYTE)imageBase); PWORD FunctionOrdinalAddressArray = (DWORD)ExportDirectory->AddressOfNameOrdinal + (PBYTE)imageBase; PDWORD FunctionAddressArray = (DWORD)ExportDirectory->AddressOfFunctions + (PBYTE)imageBase; //! search for function in exports table for ( i = 0; i < ExportDirectory->NumberOfFunctions; i++ ) { LPSTR FunctionName = FunctionNameAddressArray [i] + (PBYTE)imageBase; if (strcmp (FunctionName, funct) == 0) { WORD Ordinal = FunctionOrdinalAddressArray [i]; DWORD FunctionAddress = FunctionAddressArray [Ordinal]; return (PBYTE) (FunctionAddress + (PBYTE)imageBase); } }

これは、DLLのサポートに便利なGetProcAddress()を実装するために使用できます。

インポートテーブルの読み込み

さて...エクスポートテーブルを読むのはそれほど難しくはないでしょう?インポートテーブルを読むのはそれほど難しくありませんが、エクスポートテーブルより少し複雑です。OK、OK、インポートテーブルを読んで何になるのでしょうか?読むというより、書き込むのです。プログラムのインポートテーブルにエントリを書き込むことで、GetProcAddress()を呼び出すことなく、ライブラリやDLLをまたいだ関数コールを可能にします。Windowsでは、遅延ロードされたDLLとシステムDLLでこれを実行します。

インポートテーブルを読み込むには、インポートディレクトリ構造を見つける必要があります。これはDataDirectory[1]にあります。

PIMAGE_DATA_DIRECTORY DataDirectory = &OptionalHeader->DataDirectory [1]; PIMAGE_IMPORT_DESCRIPTOR importDirectory = (PIMAGE_IMPORT_DESCRIPTOR) (DataDirectory->VirtualAddress + ImageBase);

importDirectoryは、記述子の配列を指すことに注意することが重要である。これらのエントリはそれぞれ、インポートされたモジュール、例えばインポートDLLを表しています。この構造体を見てみましょう。

typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { uint32_t Characteristics; // 0 for terminating null import descriptor uint32_t OriginalFirstThunk; // RVA to INT }; uint32_t TimeDateStamp; // Time/Date of module, or other properties (see below) uint32_t ForwarderChain; // Forwarder chain ID uint32_t Name; // Module name uint32_t FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR *PIMAGE_IMPORT_DESCRIPTOR;

Name、OriginalFirstThunkFirstThunkはRVAであることに注意することが重要です。つまり、データを適切に解析するために、画像ベースにアドレス(これらはポインタです)を追加する必要があります。Nameはkernel32.dllのようなインポートモジュール名を指し示すRVAです。これはヌル文字で終端しています。

インポート記述子の配列で作業していることを思い出してください。この配列に含まれるインポート記述子の数をどうやって知ることができるでしょうか?配列はNULLのIMAGE_IMPORT_DESCRIPTORで終わっているので、各エントリをループする簡単な方法は次のとおりです。

IMAGE_IMPORT_DESCRIPTOR* lpImportDesc; while (! lpImportDesc->FirstThunk) { //! work with lpImportDesc here lpImportDesc++; // move to next entry }

TimeDateStampには、適切な時間/日付、または以下の値のいずれかを指定します。

  • 0: モジュールがバインドされていない
  • -1: 画像がバインドされている。実時間/日付スタンプを格納

ForwarderChainはDLL間の呼び出しを他のDLLに転送するDLL Forward Referencingをサポートする場合にのみ使用されます。たとえば、Windowskernel32.dllの一部の呼び出しは、他のDLLに転送されます。

FirstThunkはIATを指し、OriginalFirstThunkはインポートされたすべての関数を表す構造体の配列を指します。 これがインポートネームテーブル(INT)です。これらのメンバは両方ともRVAです。

別の構造体が出てくることはおわかりですね見てみましょう。

typedef struct _IMAGE_THUNK_DATA { union { uint32_t* Function; // address of imported function uint32_t Ordinal; // ordinal value of function PIMAGE_IMPORT_BY_NAME AddressOfData; // RVA of imported name DWORD ForwarderStringl // RVA to forwarder string } u1; } IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;

OriginalFirstThunkはIMAGE_THUNK_DATA構造体の配列を指し示すRVAです。

うっ、やった、また構造体だ。でも、これは小さいものです。

typedef struct _IMAGE_IMPORT_BY_NAME { uint16_t Hint; // Possible ordinal number to use uint8_t Name[1]; // Name of function, null terminated } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

以上で完了です。最初のパラメータは0でも構いませんが、これは関数が使用する可能性のある序数をローダーに示唆するものに過ぎません。Nameは、関数の名前を表す文字の配列です。

ここからが本題です。IATは、関数を表すアドレスのリストに過ぎません。関数とは?このIMAGE_THUNK_DATA配列の中にある関数です。IMAGE_THUNK_DATAの構造体を見て、それが関数名を表す単なるアニオンであることに気づいてください。これがインポート名テーブル(INT)です。

例えば、IMAGE_THUNK_DATA[3]にある関数の現在のアドレスを取得したい場合を考えてみましょう。そのアドレスはIATの3番目のドワードになり、IMAGE_IMPORT_DESCRIPTOR->FirstThunkで読み取ることができます。

そこで、関数名とアドレスを取得してみます。

unsigned int count=0; while (lpThunk->u1.Function) { //! get the function name char* lpFunctionName = (char*)((uint8_t*)imageBase + (uint32_t)lpThunk->u1.AddressOfData.Name); //! go into the IAT to get this functions address uint32_t* addr = (uint32_t*)((uint8_t*)imageBase + lpImportDesc->FirstThunk) + count; // lpFunctionName now points to the null terminated function name // addr now points to the address of this function count++; lpThunk++; }

画像結合

ここが面白いところです。IATには、実行時またはビルド時に、インポートされた関数のアドレスを入力することができます。拘束されたイメージとは、ビルド時にIATが関数に拘束されるイメージのことである。非拘束型イメージとは、ロード時にOSローダーによってIATが埋められるイメージのことです。

バウンデッドイメージの場合、外部DLL内の関数を呼び出すには、以下のようにします。

__declspec (dllimport) void function (); function (); // calls myDll:function()

画像がバインドされていない場合、IATはジャンクを含んでいます。その場合、上記のコードが動作するようにIATを更新するのはOSローダーの責任となります。これは、ロードされたDLLモジュールのエクスポートテーブルを読み込んで(GetProcAddress()を呼び出し)、そのインポート関数のIATエントリを上書きすることで実行できます。IAT の上書きは、上記の方法で行うことができます - 関数の IAT エントリを取得したら、それを上書きするだけです :) 。

この方法は、DLLや他のモジュールにフックをインストールする際にも有効です。

サポートリソース

はじめに

Windows カーネルがディスクから何もロードせずに画像を表示し、XML 設定ファイルを扱うことができることを不思議に思ったことはありませんか?リソースを追加する作業をしたことがあるが、OSでサポートすることは可能だろうかと思ったことはないだろうか。その答えは、"もちろん!"です。

しかし、リソースのパースは、他のディレクトリタイプより少し複雑です。他のセクションと同様に、基本的なIMAGE_RESOURCE_DIRECTORY構造体があり、オプションのヘッダーのDataDirectoryメンバーから取得することができます。

PIMAGE_DATA_DIRECTORY DataDirectory = &OptionalHeader->DataDirectory [2]; PIMAGE_RESOURCE_DIRECTORY resourceDirectory = (PIMAGE_RESOURCE_DIRECTORY) (DataDirectory->VirtualAddress + ImageBase);

これらのセクションにアクセスする方法がパターン化されていることにお気づきですか?そうそう、新しい構造体について説明します。

typedef struct _IMAGE_RESOURCE_DIRECTORY { uint32_t Characteristics; uint32_t TimeDateStamp; uint16_t MajorVersion; uint16_t MinorVersion; uint16_t NumberOfNamedEntries; uint16_t NumberOfIdEntries; IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[1]; } IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

この構造体は、最後の3つを除いて、あまり面白いフィールドを持っていません。

Win32のリソースを扱ったことがある人なら、リソースがIDや名前で識別できることをご存知かもしれません。この構造体の 2 つのメンバによって、これらのエントリの数とエントリの合計量 (NumberOfNamedEntries + NumberOfIdEntries) を知ることができ、すべてのエントリをループする際に役立ちます。 おそらく推測できるように、エントリは DirectoryEntries 配列に格納されています。DirectoryEntriesIMAGE_RESOURCE_DIRECTORY_ENTRY構造体の配列で構成されており、その形式は次のとおりです。

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY { union { struct { DWORD NameOffset:31; DWORD NameIsString:1; }; DWORD Name; WORD Id; }; union { DWORD OffsetToData; struct { DWORD OffsetToDirectory:31; DWORD DataIsDirectory:1; }; }; } IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

さて、これは醜い構造体です。この構造体は、1つのリソース、つまりリソースディレクトリを表しています。

リソース・ディレクトリの構造

リソースなのかリソースディレクトリなのか?ちょっと立ち止まってみましょう。(リソースはツリーとして格納されることを知っておくことが重要です。このツリーは次のような構造になっています。

  • Root directory
    • Resource group 1 Directory
      • Resource 1
      • Resource 2
    • Resource group 2 Directory
      • Resource 1
      • Resource 2
    • Resource group 3 Directory
      • Resource 1
      • Resource 2
    • ...etc...

リソースグループにはいくつかの種類があり、このグループに含まれるリソースの種類を知ることができます。 以下、グループIDを示します。

  • 1 - Cursor
  • 2 - Bitmap
  • 3 - Icon
  • 4 - Menu
  • 5 - Dialog
  • 6 - String
  • 7 - Font directory
  • 8 - Font
  • 9 - Accelerator
  • 10 - RcData
  • 11 - Message table
  • 16 - Version
  • 17 - DlgInclude/li>
  • 19 - Plug and Play
  • 20 - VXD
  • 21 - Animated Cursor
  • 22 - Animated Icon
  • 23 - HTML
  • 24 - Manifest
リソースを見つけるには、このツリーを横断する必要があります。このツリーには3層しかないと仮定すれば、難しいことではありません。

まず、リソースディレクトリのすべてのエントリをループすることについて見てみましょう。

//! get first entry in directory IMAGE_RESOURCE_DIRECTORY_ENTRY* lpResourceEntry = lpResourceDir->DirectoryEntries; //! loop through all entries int entries = lpResourceDir->NumberOfIdEntries + lpResourceDir->NumberOfNamedEntries; while (entries-- != 0) { //! look for bitmap resource (id=2) if (lpResourceEntry->Id == 2) { //! see below } lpResourceEntry++; }

これだけなら簡単でしょう?IMAGE_RESOURCE_DIRECTORY_ENTRYIdメンバは、グループ ID を格納するために使用されます。ビットマップを探すなら、ルートディレクトリのビットマップグループにあるので、ID=2のエントリーを探せばいいのです。

IMAGE_RESOURCE_DIRECTORY_ENTRYはリソースエントリとディレクトリの両方を表すので、それが何であるかを見分けるにはどうしたらよいでしょうか。もちろん、DataIsDirectoryメンバです。このメンバが設定されている場合、それはディレクトリです。このメンバがセットされている場合、それはディレクトリです。ああ、しかし、それがディレクトリである場合、どのようにディレクトリを読むことができますか?見てみましょう。

if (lpResourceEntry->DataIsDirectory) { lpResourceEntry = lpResourceEntry->OffsetToDirectory; lpResourceEntry += startOfResourceSection; }

これも悪くはない。エントリーがディレクトリの場合、上記はOffsetToDirectoryから新しいディレクトリへのオフセットを取得し、それをstartOfResourceSection に追加しています!?そう、これはオフセットであって、RVAではないのです。そうだ.なぜマイクロソフトは、なぜ!

リソースセクションの開始位置は、実際にはIMAGE_RESOURCE_DIRECTORY_ENTRY配列の最初のメンバーのアドレスです。 したがって、このアドレスをOffsetToDirectoryから得たオフセットに追加すれば、このディレクトリのIMAGE_RESOURCE_DIRECTORY構造へのポインタを取得することができるわけです。はい、それからこれらのディレクトリエントリを読み込む全処理が始まります :)

特定のリソースのためにディレクトリをパースしている最中であれば、ディレクトリ内のすべてのリソースエントリをループするだけです。resourceEntry IDフィールドが、探そうとしているリソースID(ここではプログラム固有のID)と一致すれば、リソース・データを見つけたことになります。

リソースデータは...ゾクゾクするような構造で格納されています。ディレクトリエントリ構造のOffsetToDataメンバから取得することができます。OffsetToDirectoryメンバと同様、これもリソースセクションの開始点からのオフセットです。

ポインタを取得すると、リソースデータを取り出すことができます。その構造体を見てみよう。

typedef struct _IMAGE_RESOURCE_DATA_ENTRY { uint32_t OffsetToData; uint32_t Size; uint32_t CodePage; uint32_t Reserved; } IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

以上です。OffsetToDataは実際のリソースデータへのRVAで、Sizeはそのデータのサイズ(バイト数)です。例えば、ビットマップリソースを探している場合、OffsetToDataはビットマップのBITMAPINFOHEADER構造体を指すRVAとなり、どのビットマップローダでも扱うことができます。

まとめ

この章はこれで終わりです。今後、デバッグデータ、COMDATSなどのセクションを追加し、更新していく予定です。

この章にはデモはありません。PE 実行可能ファイルフォーマットの内部動作に興味があり、それを使って作業したい人のために主にリリースされます。本編では、プログラムをロードして実行するだけかもしれないので、その他の情報は完全性のためにのみ提供されます。デモのためにテキストで提供されるすべてのコードは、動作確認済みです(若干の修正あり)。

次の章では、PE実行ファイル形式を使用し、ユーザーモードプログラムをサポートするためのローダーを構築します。その後、マルチタスクに進みます!