PEファイルフォーマット その4 ~ エクスポート情報 ~

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

エクスポート情報

 イメージファイルがエクスポートシンボル(エクスポート関数と変数)を持っている場合は、イメージファイル内にエクスポートディレクトリと呼ばれるデータを持っています。エクスポートディレクトリにはエクスポートシンボルの名前や序数、アドレスなどシンボルの解決に十分な情報が含まれています。

 エクスポートディレクトリは WinNT.h で以下のように定義されています。

WinNT.h
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Characteristics 使用されていません。
TimeDateStamp このイメージファイルが作成された日付のタイムスタンプ。
MajorVersion このイメージファイルのメジャーバージョン。
MinorVersion
Name このイメージファイルの名前への RVA。
Base 序数のベース値。序数によるインポートの場合や GetProcAddress() で序数を指定した場合などは序数からこの値を引くと、AddressOfFunctions が指す配列(エクスポートアドレステーブル)のインデックスになります。
NumberOfFunctions AddressOfFunctions が指す配列(エクスポートアドレステーブル)の要素数。
NumberOfNames AddressOfNames が指す配列(名前ポインタテーブル)の要素数。名前を持つエクスポートシンボルの数になります。
AddressOfFunctions エクスポートアドレステーブルの RVA 。エクスポートアドレステーブルには、個々のエクスポートシンボルの RVA が格納されています 。
AddressOfNames 名前ポインタテーブルの RVA 。名前ポインタテーブルには、個々のエクスポートシンボルの名前の RVA が格納されています。また、目的のエクスポート関数を探すときにバイナリサーチできるように名前の昇順でソートされています。
AddressOfNameOrdinals 序数テーブルの RVA 。序数テーブルという名前ですが、名前ポインタテーブルのインデックスをエクスポートアドレステーブルのインデックスに変換するためのマップです。したがって、要素数は NumberOfNames の数だけあります。

エクスポート情報の取り出し方

 インポート情報と同様にオプショナルヘッダの DataDirectory の IMAGE_DIRECTORY_ENTRY_EXPORT(0) 番目にエクスポートディレクトリの RVA とエクスポート情報のサイズが格納されています。
 VirtualAddress が 0 だった場合はエクスポートシンボルを持っていません。

エクスポートシンボルの転送

 エクスポートシンボルが転送されている場合、エクスポートアドレステーブルに格納されている値はエクスポートシンボルの RVA ではなく、"NTDLL.RtlHeapAlloc" などの転送先名の RVA となります。
 エクスポートアドレステーブルに格納されている RVA が、エクスポート情報内(DataDirectory の VirtualAddress から Size を足した範囲まで)に含まれている場合、エクスポートシンボルは転送されていることを示します。
 エクスポートシンボルの転送に関しては「エクスポート転送で遊んでみる」を参照してください。

PEFile クラスへの機能追加

 背景が赤っぽい色になっている部分が前回から追加された部分です。エクスポート関数を列挙するメソッド GetExportSymbols() や、GetProcAddress() の代わりとなるメソッド GetExportSymbolPFromOrdinal(), GetExportSymbolPFromName() が追加されています。
 GetProcAddress() は OS がロードしたイメージファイルでないとエクスポートシンボルのアドレスを取得できないので、GetExportSymbolPFromOrdinal(), GetExportSymbolPFromName() を実装する必要があります。

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;       //!< 名前によるインポートの場合、その名前。
  };
};

//! GetExportSymbols() で返される vector の要素の型
struct ExportSymbol
{
  DWORD         symbol_rva;   //!< エクスポートシンボルの RVA 。
  void*         symbol_p;     //!< エクスポートシンボルへのポインタ。
  DWORD         ordinal;      //!< 序数。
  const char*   name;         //!< エクスポートシンボル名。序数のみの場合 "" 。
  const char*   forwarded_to; //!< 転送されている場合は転送先名。いない場合 "" 。
};

//! PE ファイルから情報を取得するためのクラス。
class PEFile
{
public:
  //! コンストラクタ。正常に開けたかどうかは GetLoadBase() で確認する。
  explicit PEFile(const wchar_t* i_file_name);
  //! モジュールハンドルからのコンストラクタ。
  explicit PEFile(HMODULE i_module_handle);
  //! デストラクタ
  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);
  //! インポートシンボルをフックする。成功した場合 IAT エントリへのポインタを返す。
  void** HookImportSymbol(void* i_old_p, void* i_new_p);

  //! エクスポートディレクトリへのポインタを取得する。
  IMAGE_EXPORT_DIRECTORY* GetExportDirP();
  //! エクスポートシンボルを取得する。
  std::vector<ExportSymbol> GetExportSymbols();
  //! 序数からエクスポートシンボルへのポインタを取得する。
  void* GetExportSymbolPFromOrdinal(WORD i_ordinal);
  //! エクスポート名からシンボルへのポインタを取得する。
  void* GetExportSymbolPFromName(const char* i_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); }
  //! i_index 番目のエクスポートシンボルの名前を取得する。失敗した場合は "" を返す。
  const char* FindExportSymbolName(size_t i_index);
  //! i_index 番目のエクスポートシンボルの RVA を取得する。
  DWORD GetExportSymbolRVA(size_t i_index);
  //! エクスポートが転送されているかどうか調べる。
  bool IsExportForwarded(DWORD i_symbol_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

GetExportSymbols()

 まず列挙関連のほうから。GetExportDirP() はエクスポートディレクトリへのポインタを取得するメソッドです。

PEFile.cpp
//! エクスポートディレクトリへのポインタを取得する。
IMAGE_EXPORT_DIRECTORY* PEFile::GetExportDirP()
{
  return reinterpret_cast<IMAGE_EXPORT_DIRECTORY*>(
           GetDirEntryDataP(IMAGE_DIRECTORY_ENTRY_EXPORT)
         );
}

 続いて、エクスポートシンボルを列挙する GetExportSymbols() 。主な処理は FindExportSymbolName(), GetExportSymbolRVA(), IsExportForwarded() に任せてあります。
PEFile.cpp
//! エクスポートシンボルを取得する。
std::vector<ExportSymbol> PEFile::GetExportSymbols()
{
  std::vector<ExportSymbol> retval;
  IMAGE_EXPORT_DIRECTORY*   export_dir_p = GetExportDirP();
  
  if( export_dir_p )
  {
    for(size_t i = 0; i < export_dir_p->NumberOfFunctions; i++)
    {
      ExportSymbol  symbol = { 0 };

      symbol.symbol_rva   = GetExportSymbolRVA(i);
      if( symbol.symbol_rva )
      {
        symbol.symbol_p     = GetDataP<void*>(symbol.symbol_rva);
        symbol.ordinal      = i + export_dir_p->Base;
        symbol.name         = FindExportSymbolName(i);
        symbol.forwarded_to = IsExportForwarded(symbol.symbol_rva)
                            ? reinterpret_cast<char*>(symbol.symbol_p)
                            : "";

        retval.push_back(symbol);
      }
    }
  }
  while( 0 );
  
  return retval;
}

 FindExportSymbolName() は名前ポインタテーブルを走査して、エクスポートアドレステーブルのインデックスに対応する名前を見つけるメソッドです。序数テーブルのようなマップがないのでいちいち走査しなければいけません。
PEFile.cpp
//! i_index 番目のエクスポートの名前を取得する。失敗した場合は "" を返す。
const char* PEFile::FindExportSymbolName(size_t i_index)
{
  const char*               retval        = "";
  IMAGE_EXPORT_DIRECTORY*   export_dir_p  = GetExportDirP();
  
  if( export_dir_p )
  {
    DWORD* name_rvas_p = GetDataP<DWORD*>(export_dir_p->AddressOfNames);
    WORD*  indices_p   = GetDataP<WORD*>(export_dir_p->AddressOfNameOrdinals);
    for(DWORD i = 0; i < export_dir_p->NumberOfNames; i++)
    {
      if( indices_p[i] == i_index )
      {
        retval = GetDataP<const char*>(name_rvas_p[i]);
        break;
      }
    }
  }
  
  return retval;
}

 GetExportSymbolRVA() は指定されたインデックスのエクスポートシンボルの RVA を取得するメソッド。わざわざ分ける必要もないのですが・・・。
PEFile.cpp
//! i_index 番目のエクスポートシンボルの RVA を取得する。
DWORD PEFile::GetExportSymbolRVA(size_t i_index)
{
  DWORD                   retval        = 0;
  IMAGE_EXPORT_DIRECTORY* export_dir_p  = GetExportDirP();
  
  if( export_dir_p
  &&  0 <= i_index && i_index < export_dir_p->NumberOfFunctions )
  {
    retval = GetDataP<DWORD*>(export_dir_p->AddressOfFunctions)[i_index];
  }
  
  return retval;
}

 IsExportForwarded() はエクスポート転送されているかどうか調べるメソッド。先の説明のとおりです。
PEFile.cpp
//! エクスポートが転送されているかどうか調べる。
bool PEFile::IsExportForwarded(DWORD i_symbol_rva)
{
  bool  retval = false;

  if( GetLoadBase() )
  {
    IMAGE_DATA_DIRECTORY& dir_entry = GetOptionalHeaderP()->DataDirectory[
                                        IMAGE_DIRECTORY_ENTRY_EXPORT
                                      ];

    // エクスポートディレクトリが存在していて RVA がその範囲内。
    retval = dir_entry.VirtualAddress && dir_entry.Size
          && dir_entry.VirtualAddress <= i_symbol_rva
          && i_symbol_rva < dir_entry.VirtualAddress + dir_entry.Size;
  }
  
  return retval;
}

GetExportSymbolPFromOrdinal(), GetExportSymbolPFromName()

 次は GetProcAddress() の代わりとなるメソッド。まず序数のほうから。

PEFile.cpp
//! 序数からエクスポートシンボルへのポインタを取得する。
void* PEFile::GetExportSymbolPFromOrdinal(WORD i_ordinal)
{
  void*                     retval        = NULL;
  IMAGE_EXPORT_DIRECTORY*   export_dir_p  = GetExportDirP();
  
  if( export_dir_p )
  {
    DWORD symbol_rva = GetExportSymbolRVA(i_ordinal - export_dir_p->Base);
    if( symbol_rva )
    {
      if( IsExportForwarded(symbol_rva) )  // 転送されている場合。
      {
        char* forwarded_to       = GetDataP<char*>(symbol_rva);
        char* symbol_name_p      = strstr(forwarded_to, ".") + 1;
        char  dll_name[MAX_PATH] = { 0 };

        // DLL 名をコピーして拡張子を付与する。
        memcpy(dll_name, forwarded_to, symbol_name_p - forwarded_to);
        strncat(dll_name, "DLL", sizeof(dll_name) - 1);
        
        HMODULE module_handle = GetModuleHandleA(dll_name);
        if( module_handle == NULL )
          module_handle = LoadLibraryA(dll_name);
        
        if( module_handle )
          retval = PEFile(module_handle).GetExportSymbolPFromName(symbol_name_p);
      }
      else  // 転送されていない場合
      {
        retval = GetDataP<void*>(symbol_rva);
      }
    }
  }
  
  return retval;
}
 まず、指定された序数から Base を引いた値をエクスポートアドレステーブルのインデックスとして、GetExportSymbolRVA() に渡して RVA を取得しています。
 その RVA を元に転送されているかどうか調べ、転送されていた場合は転送先の DLL のロードとシンボルのアドレスの取得を行っています。

 次は GetExportSymbolPFromName() 。
PEFile.cpp
//! エクスポート名からシンボルへのポインタを取得する。
void* PEFile::GetExportSymbolPFromName(const char* i_name)
{
  void*                     retval        = NULL;
  IMAGE_EXPORT_DIRECTORY*   export_dir_p  = GetExportDirP();
  
  if( export_dir_p )
  {
    DWORD*  name_rvas_p = GetDataP<DWORD*>(export_dir_p->AddressOfNames);
    WORD*   indices_p   = GetDataP<WORD*>(export_dir_p->AddressOfNameOrdinals);
    
    // バイナリサーチを行う。
    int     left    = 0;
    int     right   = export_dir_p->NumberOfNames - 1;
    while( left <= right )
    {
      int   middle      = (left + right) / 2;
      char* name        = GetDataP<char*>(name_rvas_p[middle]);
      int   comp_result = strcmp(i_name, name);

      if( comp_result == 0 )  // 発見!
      {
        // インデックスを序数に変換して、序数からポインタを取得する。
        retval = GetExportSymbolPFromOrdinal(
                   static_cast<WORD>(indices_p[middle] + export_dir_p->Base)
                 );
        break;
      }
      else if( comp_result > 0 )
      {
        left = middle + 1;
      }
      else
      {
        right = middle - 1;
      }
    }
  }
  
  return retval;
}
 引数で渡された名前をバイナリサーチして、名前ポインタテーブルのインデックスをエクスポートアドレステーブルのインデックスに変換し、そのインデックスを序数に変換して GetExportSymbolPFromOrdinal() に渡しています。

エクスポートシンボルの列挙

 今回はソースだけ。パラメータで渡された DLL をロードして、エクスポートシンボルを列挙します。実行結果は長くなるので自分で確認してください。

EnumExp.cpp
#include <windows.h>
#include <stdio.h>
#include <vector>
#include <string>
#include "PEFile.h"


//! メインルーチン
int main(int argc, char** argv)
{
  for(int i = 1; i < argc; i++)
  {
    printf("%s\n", argv[i]);
    HMODULE module_handle = LoadLibrary(argv[i]);

    std::vector<ExportSymbol>
      export_symbols = PEFile(module_handle).GetExportSymbols();

    if( export_symbols.size() )
    {
      printf("  序数  RVA      名前\n");
      for(size_t j = 0; j < export_symbols.size(); j++)
      {
        printf("  %-5d %08X %s\n",
               export_symbols[j].ordinal,
               export_symbols[j].symbol_rva,
               export_symbols[j].name
              );
        if( export_symbols[j].forwarded_to[0] )
          printf("%*s %s\n", 16, "→", export_symbols[j].forwarded_to);
      }
      
    }
    printf("\n");
  }
  return 0;
}