PE(Portable Executable)ファイルフォーマット その2 ~ インポート情報とインポート関数の列挙 ~

サンプルプログラム(EnumImp.zip)のダウンロード

インポート情報

 イメージファイルには、そのイメージファイルをロードする際に必要な DLL とその DLL からインポートする関数や変数の情報が含まれています。インポート関数や変数といちいち表現するのも面倒なので、便宜上インポートシンボルと言いますが、例えば、以下のコード:

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
  return MessageBoxA(NULL, "World!", "Hello", MB_OK);
}

 上記のコードをコンパイルして出来上がった EXE ファイルでは下図のようなインポート情報が含まれることになります。

 見てのとおり、「USER32.DLL から MessageBoxA() API をインポートするよ」というデータです。KERNEL32.DLL の部分はスタートアップルーチンが必要とするインポート関数です。
 赤色の文字で「ロードまたはバインドされた後はインポートシンボルの実アドレスで上書きされる。」とあります。  インポートアドレステーブルの各要素は、イメージファイルがロードまたはバインド(後述)される前はインポートする関数の名前への RVA や序数を保持していて、ロードまたはバインドされた後はそのインポートシンボルのアドレスを保持します。このインポートシンボルのアドレスで上書きする役目を担っているのはローダであったり、バインドを行うプログラムです。
 そして MessageBoxA の呼び出しは、このインポートアドレステーブルに格納されている MessageBoxA() API のアドレスを介して行われるというわけです。
 API のフックでよく使われる手口である「IAT の書き換えによる API のフック」というのはこのインポートアドレステーブルに格納されている関数のアドレスを上書きすることで行われます。それに関しては次回に取り上げます。

 それぞれに関してもう少し詳しく見ていきましょう。

バインド

 バインドというのは、イメージファイルのロードを早くするために、ロード前にイメージファイル内のインポートアドレステーブルをインポートシンボルの実アドレスで上書きしてしまう行為です。BIND ユーティリティや IMAGEHELP.DLL の BindImageEx() API などで行えます。
 下図は上がバインド前、下がバインド後の EXE ファイルの内容です。0x9664 という RVA の値から MessageBoxA() API のアドレスである 0x77D3058A に変わっています。


 さて、このバインドという行為、インポートシンボルのアドレスが変わってしまうとどうなるのでしょうか?例えば、Windows Update によって USER32.DLL が更新されてしまい、MessageBoxA() API のアドレスが 0x77D3058A で無くなってしまったら?他にも Vista 以降では KERNEL32.DLL のロードアドレスは Windows を起動するたびに変わってしまいます。そもそも、別のコンピュータで動作させたら?
 ローダはきちんと、DLL のタイムスタンプなどを見てインポートアドレステーブルに上書きされているインポートシンボルのアドレスが正しい値かどうかをチェックします。そして、正しい値でなかった場合は、バインド前(つまり序数やヒント/名前へのRVA が格納されている状態)のインポートアドレステーブルのコピーであるインポートネームテーブル(インポートルックアップテーブルとも呼ばれます)と呼ばれるものを使ってインポートシンボルのアドレスを取得しなおします。インポートネームテーブルはリンク時に生成されるもので、バインド時に生成されるものではありません。
 インポートネームテーブルは存在しなくても問題ありませんが、バインドはできなくなってしまいます。Borland 製の古いリンカはこのインポートネームテーブルを生成しません。

インポートデスクリプタ

 インポート情報はインポートディレクトリテーブルから始まります。そしてインポートディレクトリテーブルのエントリ(レコード)をインポートデスクリプタと呼びます。
 インポートデスクリプタはインポートしている DLL の数 + 1 だけ存在し、+ 1 の分は終了を表す全てのフィールドが 0 のレコードとなっています。
 インポートデスクリプタは WinNT.H で以下のように定義されています。

WinNT.h
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    };
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

Characteristics  この値が 0 の場合はインポートディレクトリの終了を意味する特性となります。非 0 の場合は下の OriginalFirstThunk として使用されます。ただし、これを鵜呑みにすると痛い目を見ます。インポートネームテーブルは存在しなくても良いことを思い出してください。実質、このフィールドは無意味です。
OriginalFirstThunk  バインドの項で説明したインポートネームテーブルへの RVA が格納されています。0 の場合は、インポートネームテーブルが存在しません。
TimeDateStamp  バインドには古い形式と新しい形式があって、古い形式ではインポートシンボルがバインドされた日付のタイムスタンプが格納されます。新しい形式では -1 が格納されます。この値が 0 の場合はバインドされていません。
ForwarderChain 古い形式でバインドされているときに使われる情報。
Name インポートする DLL の名前の RVA が格納されています。
FirstThunk インポートアドレステーブルへの RVA が格納されています。

インポートネームテーブル(Import Name Table:INT)とインポートアドレステーブル(Import Address Table:IAT)

 インポートネームテーブルはバインドの項で説明したように、バインド前(つまり序数やヒント/名前へのRVA が格納されている状態)のインポートアドレステーブルのコピーです。リンク時に生成されます。インポートネームテーブルはバインド後やロード後もインポートシンボルのアドレスで上書きされるようなことはありません。
 インポートアドレステーブルはロード前やバインド前ではインポートシンボルの序数やヒント/名前への RVA が格納されていて、ロード後やバインド後はインポートシンボルの実アドレスで上書きされる領域です。
 で、インポートネームテーブルもインポートアドレステーブルも IMAGE_THUNK_DATA 構造体の配列となっていて、エントリ数はインポートデスクリプタが示す DLL からインポートしている関数の数 + 1 だけ存在します。+ 1 はインポートデスクリプタと同じく、終了を表す値が 0 のレコードです。
 IMAGE_THUNK_DATA 構造体は WinNT.H で以下のように定義されていて、実態はポインタサイズ(32bit)の値の共用体です。64bit では DWORD から ULONGLONG になります。

WinNT.h
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;

 Function はロードまたはバインド後のインポートシンボルのアドレス、Ordinal はインポートシンボルの序数 、AddressOfData はヒント/名前への RVA を意味しています。ForwarderString は転送文字列への RVA だそうですが、良く分かりません。私は ForwaderString として使われている IMAGE_THUNK_DATA を見たことありません。
 インポートシンボルが序数によって示される場合は、IAT/INT の要素には最上位ビットが 1 で下位16ビットでインポートシンボルの序数を示した値が格納されています。例えば 0x80000012 は序数 0x12 のエクスポートシンボルをインポートすることを意味します。
 エントリが序数かどうかと序数を取得するためのマクロが WinNT.H に定義されています。
WinNT.h
#define IMAGE_ORDINAL_FLAG64 0x8000000000000000
#define IMAGE_ORDINAL_FLAG32 0x80000000
#define IMAGE_ORDINAL64(Ordinal) (Ordinal & 0xffff)
#define IMAGE_ORDINAL32(Ordinal) (Ordinal & 0xffff)
#define IMAGE_SNAP_BY_ORDINAL64(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG64) != 0)
#define IMAGE_SNAP_BY_ORDINAL32(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG32) != 0)

 名前による指定の場合では、エントリには「ヒント/名前」への RVA が格納されています。ヒントはエクスポート関数をすばやく検索するために使われる値です。「ヒント/名前」には最初の WORD でヒントが、次に続く BYTE の配列でインポートシンボル名が示されています。BYTE の配列ですが関数名は '\0' で終了しています。なお、ヒント/名前は WORD 境界にないといけません。
 「ヒント/名前」は WinNT.h で以下のように定義されています。
WinNT.h
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

インポート情報の取り出し方

 PE ファイルのオプショナルヘッダには DataDirectory というフィールドがありましました。この DataDirectory はインポート情報やエクスポート情報、リソースなど特別なデータへの RVA とサイズを保持しています。この DataDirectory のエントリがどの情報を指しているかは WinNT.H で以下のように定義されています。

WinNT.h
#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 とサイズはこれらのうち、IMAGE_DIRECTORY_ENTRY_IMPORT 番目のエントリに格納されています。イメージファイルがロードされたアドレスにこのエントリが指す RVA を加算するとインポート情報へのポインタということになります。

PEFile クラスの実装

 では実際に PE ファイルからインポート情報を取り出してみましょう。取り出すためのクラス PEFile を実装します。このクラスに徐々に機能を追加していきます。

PEFile.h
#ifndef __PEFILE_H_INCLUDED__
#define __PEFILE_H_INCLUDED__
#include <pshpack1.h>


//! GetImportSymbols() で返される vector の要素の型
struct ImportSymbol
{
  DWORD   iat_entry_rva;    //!< IAT エントリの RVA 。
  void**  iat_entry_p;      //!< IAT エントリへのポインタ。
  WORD    hint;             //!< ヒント
  union
  {
    struct
    {
      WORD import_by_name;  //!< 非 0 の場合、名前によるインポート。
      WORD ordinal;         //!< import_by_name が 0 の場合、序数
    };
    const char* name;       //!< 名前によるインポートの場合、その名前。
  };
};


//! PE ファイルから情報を取得するためのクラス。
class PEFile
{
public:
  //! コンストラクタ。正常に開けたかどうかは GetLoadBase() で確認する。
  explicit PEFile(const wchar_t* i_file_name);
  //! デストラクタ
  virtual ~PEFile();
  
  //! ロードアドレスを取得する。
  BYTE* GetLoadBase();
  
  //! DOS ヘッダへのポインタを取得。
  IMAGE_DOS_HEADER* GetDosHeaderP();
  //! NT ヘッダへのポインタを取得。
  IMAGE_NT_HEADERS32* GetNtHeadersP();
  //! ファイルヘッダへのポインタを取得。
  IMAGE_FILE_HEADER* GetFileHeaderP();
  //! オプショナルヘッダへのポインタを取得。
  IMAGE_OPTIONAL_HEADER32* GetOptionalHeaderP();
  //! i_index 番目のセクションヘッダへのポインタを取得。
  IMAGE_SECTION_HEADER* GetSectionHeader(int i_index);
  
  //! i_index 番目のデータディレクトリエントリが指すデータへのポインタを取得する。
  BYTE* GetDirEntryDataP(int i_index);
  //! i_index 番目のデータディレクトリエントリが指すデータへのサイズを取得する。
  DWORD GetDirEntryDataSize(int i_index);
  
  //! インポートディレクトリへのポインタを取得する。
  IMAGE_IMPORT_DESCRIPTOR* GetImportDirP();
  //! インポートしている DLL 名を取得する。
  std::vector<const char*> GetImportDllNames();
  //! 指定した DLL からインポートしているシンボルを取得する。
  std::vector<ImportSymbol> GetImportSymbols(const char* i_dll_name);

protected:
  //! イメージファイルをメモリにマッピングする。
  static BYTE* MapImage(const wchar_t* i_file_name);
  
  //! RVA をポインタに変換するテンプレートメソッド
  template<class T> T GetDataP(DWORD i_rva)
  { return reinterpret_cast<T>(m_load_base + i_rva); }
  
protected:
  bool                   m_need_to_free;    //!< m_load_base を解放する必要があるか?
  BYTE*                  m_load_base;       //!< イメージのロードされたアドレス
  IMAGE_DOS_HEADER*      m_dos_header_p;    //!< DOS ヘッダへのポインタ
  IMAGE_NT_HEADERS*      m_nt_headers_p;    //!< NT ヘッダへのポインタ
  IMAGE_SECTION_HEADER*  m_section_table_p; //!< セクションテーブルへのポインタ
};


#include <poppack.h>
#endif

 まずは MapImage() メソッドから説明を。MapImage() メソッドはコンストラクタで呼ばれているスタティック関数で、イメージファイルをメモリにマップする役割を担っています。
 イメージファイルをそのままメモリに読み込んだのでは、データの取り出しが煩雑になってしまいます。データの取り出しには RVA を含んでいるセクションを探し出し、そのセクションデータのオフセットにデータの RVA を加算してからセクションの RVA を引いて行うことになってしまいます。
 そんなことをするより、イメージファイルがまるでロードされたかのような配置でメモリに読み込むことで、データの取り出しが楽になるということです。また、実際にロードされている EXE や DLL からデータを取り出す手順と同じものになり、ファイルの時とメモリにロードされた時で処理を分ける必要がなくなります。逆に EXE ファイルとして保存するのは面倒になりますが。
PEFile.cpp
//! イメージファイルをメモリにマッピングする。
BYTE* PEFile::MapImage(const wchar_t* i_file_name)
{
  BYTE*     retval          = NULL;
  
  do
  {
    // ファイル全体を読み込む。
    std::vector<BYTE> file_data;
    if( LoadFile(i_file_name, file_data) == false )
      break;

    // DOS ヘッダの取得と確認。
    IMAGE_DOS_HEADER* dos_header_p = reinterpret_cast<IMAGE_DOS_HEADER*>(
                                       &file_data[0]
                                     );
    if( dos_header_p->e_magic != *reinterpret_cast<WORD*>("MZ")
    ||  dos_header_p->e_lfanew == 0 )
    {
      _RPTF0(_CRT_WARN, "実行可能ファイルではありません。\n");
      break;
    }
    
    // NT ヘッダの取得と確認。
    IMAGE_NT_HEADERS32* nt_headers_p = reinterpret_cast<IMAGE_NT_HEADERS*>(
                                         &file_data[dos_header_p->e_lfanew]
                                       );
    if( nt_headers_p->Signature != *reinterpret_cast<DWORD*>("PE\0\0")
    ||  nt_headers_p->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC )
    {
      _RPTF0(_CRT_WARN, "PE ファイルではありません。\n");
      break;
    }
    
    // セクションテーブルへのポインタを取得。
    IMAGE_SECTION_HEADER* section_table_p = reinterpret_cast<IMAGE_SECTION_HEADER*>(
                                              nt_headers_p + 1
                                            );
    
    // SizeOfImage 分だけメモリを確保。
    retval  = reinterpret_cast<BYTE*>(
                VirtualAlloc(
                  NULL,
                  nt_headers_p->OptionalHeader.SizeOfImage,
                  MEM_COMMIT,
                  PAGE_READWRITE
                )
              );
    if( retval == NULL )
    {
      _RPTF1(_CRT_WARN, "メモリの割り当て[%dbytes]に失敗しました。\n",
             nt_headers_p->OptionalHeader.SizeOfImage
             );
      break;
    }

    // ヘッダをコピー
    memcpy(retval, &file_data[0], nt_headers_p->OptionalHeader.SizeOfHeaders);
    // セクションデータをコピー
    for(DWORD i = 0; i < nt_headers_p->FileHeader.NumberOfSections; i++)
    {
      // セクションデータがある場合。
      if( section_table_p[i].PointerToRawData )
      {
        memcpy(&retval[section_table_p[i].VirtualAddress],
               &file_data[section_table_p[i].PointerToRawData],
               section_table_p[i].SizeOfRawData
              );
      }
    }
  }
  while( 0 );
  
  return retval;
}

 次にコンストラクタ。上記の MapImage() でファイルを読み込んだら各種ヘッダへのポインタをメンバに格納しています。その1を読んでいれば特に問題ないでしょう。
PEFile.cpp
//! コンストラクタ。正常に開けたかどうかは GetLoadBase() で確認する。
PEFile::PEFile(const wchar_t* i_file_name)
: m_need_to_free(true),
  m_load_base(NULL),
  m_dos_header_p(NULL),
  m_nt_headers_p(NULL),
  m_section_table_p(NULL)
{
  do
  {
    // イメージファイルをメモリにマップする。
    m_load_base = MapImage(i_file_name);
    if( m_load_base == NULL )
    {
      _RPTF1(_CRT_WARN, "[%S]のロードに失敗しました。", i_file_name);
      break;
    }
    _RPTF1(_CRT_WARN, "m_load_base: %p\n", m_load_base);

    // DOS ヘッダへのポインタを取得。
    m_dos_header_p = reinterpret_cast<IMAGE_DOS_HEADER*>(m_load_base);
    
    // NT ヘッダへのポインタを取得。
    m_nt_headers_p = reinterpret_cast<IMAGE_NT_HEADERS32*>(
                         m_load_base + m_dos_header_p->e_lfanew
                     );
      
    // セクションテーブルへのポインタを取得。
    m_section_table_p = reinterpret_cast<IMAGE_SECTION_HEADER*>(
                          m_nt_headers_p + 1
                        );
  }
  while( 0 );
}

 デストラクタではイメージをマップするために割り当てたメモリを解放しています。m_need_to_free は今は気にしないでください。後々、役に立つようになります。
PEFile.cpp
//! デストラクタ
PEFile::~PEFile()
{
  if( m_need_to_free && m_load_base )
  {
    VirtualFree(m_load_base, 0, MEM_RELEASE);
    m_load_base = NULL;
  }
}

 GetDosHeaderP(), GetNtHeadersP(), GetFileHeaderP(), GetOptionalHeaderP(), GetSectionHeaderP() はコンストラクタで保存しておいた各ヘッダへのポインタを返すものです。ソースは省きます。

 GetDirEntryDataP(), GetDirEntryDataSize() はオプショナルヘッダの DataDirectory から指定したエントリの RVA を取り出して、そのデータへのポインタを返すメソッドです。
PEFile.cpp
//! i_index 番目のデータディレクトリエントリが指すデータへのポインタを取得する。
BYTE* PEFile::GetDirEntryDataP(int i_index)
{
  if( GetLoadBase()
  &&  0 <= i_index && i_index < IMAGE_NUMBEROF_DIRECTORY_ENTRIES
  &&  GetOptionalHeaderP()->DataDirectory[i_index].VirtualAddress
  &&  GetOptionalHeaderP()->DataDirectory[i_index].Size )
  {
    return m_load_base + GetOptionalHeaderP()->DataDirectory[i_index].VirtualAddress;
  }
  else
  {
    return NULL;
  }
}


//=============================================================================


//! i_index 番目のデータディレクトリエントリが指すデータへのサイズを取得する。
DWORD PEFile::GetDirEntryDataSize(int i_index)
{
  if( GetLoadBase() && 0 <= i_index && i_index < IMAGE_NUMBEROF_DIRECTORY_ENTRIES )
    return GetOptionalHeaderP()->DataDirectory[i_index].Size;
  else
    return 0;
}

 GetImportDirP() は GetDirEntryDataP() に IMAGE_DIRECTORY_ENTRY_IMPORT を渡して戻り値をキャストするだけの単純なもの。
PEFile.cpp
//! インポートディレクトリへのポインタを取得する。
IMAGE_IMPORT_DESCRIPTOR* PEFile::GetImportDirP()
{
  return reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(
           GetDirEntryDataP(IMAGE_DIRECTORY_ENTRY_IMPORT)
         );
}

 GetImportDllNames() はインポートしている DLL を列挙して vector<const char*>として返してくれるメソッドです。インポートディレクトリの構造が分かっていれば特に問題ないでしょう。
PEFile.cpp
//! インポートしている DLL 名を取得する。
std::vector<const char*> PEFile::GetImportDllNames()
{
  std::vector<const char*>  retval;
  IMAGE_IMPORT_DESCRIPTOR*  imp_descs_p = GetImportDirP();
  
  if( imp_descs_p )
  {
    for(int i = 0; imp_descs_p[i].FirstThunk != 0; i++)
    {
      retval.push_back(GetDataP<const char*>(imp_descs_p[i].Name));
    }
  }
  
  return retval;
}

 GetImportSymbols() は指定した DLL からインポートしているシンボルを列挙して vector<ImportSymbol> として返してくれるメソッドです。
PEFile.cpp
//! 指定した DLL からインポートしているシンボルを取得する。
std::vector<ImportSymbol> PEFile::GetImportSymbols(const char* i_dll_name)
{
  std::vector<ImportSymbol>   retval;
  IMAGE_IMPORT_DESCRIPTOR*    imp_descs_p = GetImportDirP();

  // インポートネームテーブルを持っているか、PEFile クラスが自前でロードした
  // イメージファイルのみ列挙可能。
  if( imp_descs_p && (imp_descs_p[0].OriginalFirstThunk || m_need_to_free) )
  {
    for(int i = 0; imp_descs_p[i].FirstThunk != 0; i++)
    {
      const char* dll_name = GetDataP<const char*>(imp_descs_p[i].Name);
      if( _stricmp(dll_name, i_dll_name) == 0 )  // 指定された DLL を発見!
      {
        // インポートアドレステーブルへのポインタを取得。
        DWORD* iat_p = GetDataP<DWORD*>(imp_descs_p[i].FirstThunk);
        // インポートネームテーブルへのポインタを取得。
        // INT が存在しない場合は代わりに IAT へのポインタを使用する。
        DWORD* int_p = imp_descs_p[i].OriginalFirstThunk
                     ? GetDataP<DWORD*>(imp_descs_p[i].OriginalFirstThunk)
                     : iat_p;

        // インポートシンボルを列挙
        for(int table_index = 0; int_p[table_index] != 0; table_index++)
        {
          // INT のレコードがイメージ内に収まっているか確認する。
          if( int_p[table_index] < GetOptionalHeaderP()->SizeOfImage )
          {
            ImportSymbol  symbol = { 0 };

            symbol.iat_entry_rva = imp_descs_p[i].FirstThunk
                                 + sizeof(DWORD) * table_index;
            symbol.iat_entry_p   = reinterpret_cast<void**>(&iat_p[table_index]);

            // 序数によるインポート
            if( IMAGE_SNAP_BY_ORDINAL32(int_p[table_index]) ) 
            {
              symbol.ordinal = static_cast<WORD>(IMAGE_ORDINAL32(int_p[table_index]));
            }
            // 名前によるインポート
            else
            {
              IMAGE_IMPORT_BY_NAME*
                by_name_p = GetDataP<IMAGE_IMPORT_BY_NAME*>(int_p[table_index]);
              symbol.hint = by_name_p->Hint;
              symbol.name = reinterpret_cast<char*>(by_name_p->Name);
            }

            retval.push_back(symbol);
          }
        }
      }
    }
  }
  
  return retval;
}
 インポートディレクトリへのポインタを取得し、インポートシンボルの名前や序数、IAT のアドレスを取得しています。
 ディレクトリエントリのループ条件に import_descriptor_p->FirstThunk を使っています。インポートデスクリプタの終端は全ての値が 0 の NULL レコードであらわされます。必ず値の入っている FirstThunk が 0 になったらループ終了ということです。OriginalFirstThunk はインポートネームテーブルの RVA ですので、必ず存在するとは限りません。注意が必要です。
 列挙にはインポートネームテーブルを優先してインポートシンボル名や序数を取得します。インポートアドレステーブルがインポートシンボルのアドレスで上書きされていても大丈夫なようにです。  インポートネームテーブルへのポインタ(int_p)とインポートアドレステーブルへのポインタ(iat_p)は、IMAGE_THUNK_DATA* ではなく DWORD* にしています。64bit に対応するには注意が必要です。

インポートシンボルを表示するサンプル

 パラメータで指定したファイルのインポートシンボルを一覧表示するプログラムです。
EnumImp.cpp
#include <windows.h>
#include <stdio.h>
#include <vector>
#include <string>
#include "PEFile.h"


int wmain(int argc, wchar_t** argv)
{
  for(int i = 1; i < argc; i++)
  {
    PEFile                    pe_file(argv[i]);
    std::vector<const char*>  dll_names = pe_file.GetImportDllNames();

    printf("%S\n", argv[i]);
    for(size_t i = 0; i < dll_names.size(); i++)
    {
      printf("  %s\n", dll_names[i]);
      
      printf("    IAT RVA  HINT NAME\n");
      std::vector<ImportSymbol>
        import_symbols = pe_file.GetImportSymbols(dll_names[i]);
      for(size_t j = 0; j < import_symbols.size(); j++)
      {
        printf("    %08X ", import_symbols[j].iat_entry_rva);
        if( import_symbols[j].import_by_name )
          printf("%04d %s\n", import_symbols[j].hint, import_symbols[j].name);
        else
          printf("Ordinal %d\n", import_symbols[j].ordinal);
      }
      printf("\n");
    }
  }

  return 0;
}


実行結果
>EnumImp.exe EnumImp.exe
EnumImp.exe
  KERNEL32.dll
    IAT RVA  HINT NAME
    00010000 0052 CloseHandle
    00010004 0693 ReadFile
    00010008 0356 GetFileSizeEx
    0001000C 0086 CreateFileW
    00010010 0899 VirtualFree
    00010014 0897 VirtualAlloc
    00010018 0727 RtlUnwind
    0001001C 0862 TerminateProcess
    00010020 0322 GetCurrentProcess
    00010024 0878 UnhandledExceptionFilter
    00010028 0842 SetUnhandledExceptionFilter
    0001002C 0569 IsDebuggerPresent
    00010030 0679 RaiseException
    00010034 0534 HeapFree
    00010038 0489 GetVersionExA
    0001003C 0528 HeapAlloc
    00010040 0419 GetProcessHeap
    00010044 0152 EnterCriticalSection
    00010048 0593 LeaveCriticalSection
    0001004C 0383 GetModuleHandleA
    00010050 0416 GetProcAddress
    00010054 0869 TlsGetValue
    00010058 0867 TlsAlloc
    0001005C 0870 TlsSetValue
    00010060 0868 TlsFree
    00010064 0556 InterlockedIncrement
    00010068 0808 SetLastError
    0001006C 0326 GetCurrentThreadId
    00010070 0369 GetLastError
    00010074 0552 InterlockedDecrement
    00010078 0260 GetCPInfo
    0001007C 0253 GetACP
    00010080 0403 GetOEMCP
    00010084 0575 IsValidCodePage
    00010088 0185 ExitProcess
    0001008C 0932 WriteFile
    00010090 0441 GetStdHandle
    00010094 0381 GetModuleFileNameA
    00010098 0382 GetModuleFileNameW
    0001009C 0246 FreeEnvironmentStringsA
    000100A0 0629 MultiByteToWideChar
    000100A4 0341 GetEnvironmentStrings
    000100A8 0247 FreeEnvironmentStringsW
    000100AC 0343 GetEnvironmentStringsW
    000100B0 0272 GetCommandLineA
    000100B4 0273 GetCommandLineW
    000100B8 0804 SetHandleCount
    000100BC 0358 GetFileType
    000100C0 0439 GetStartupInfoA
    000100C4 0129 DeleteCriticalSection
    000100C8 0532 HeapDestroy
    000100CC 0530 HeapCreate
    000100D0 0675 QueryPerformanceCounter
    000100D4 0479 GetTickCount
    000100D8 0323 GetCurrentProcessId
    000100DC 0458 GetSystemTimeAsFileTime
    000100E0 0854 Sleep
    000100E4 0916 WideCharToMultiByte
    000100E8 0538 HeapReAlloc
    000100EC 0540 HeapSize
    000100F0 0580 LCMapStringA
    000100F4 0581 LCMapStringW
    000100F8 0442 GetStringTypeA
    000100FC 0445 GetStringTypeW
    00010100 0372 GetLocaleInfoA
    00010104 0594 LoadLibraryA
    00010108 0547 InitializeCriticalSection
    0001010C 0290 GetConsoleCP
    00010110 0307 GetConsoleMode
    00010114 0238 FlushFileBuffers
    00010118 0795 SetFilePointer
    0001011C 0921 WriteConsoleA
    00010120 0309 GetConsoleOutputCP
    00010124 0931 WriteConsoleW
    00010128 0823 SetStdHandle
    0001012C 0083 CreateFileA