イメージファイルがエクスポートシンボル(エクスポート関数と変数)を持っている場合は、イメージファイル内にエクスポートディレクトリと呼ばれるデータを持っています。エクスポートディレクトリにはエクスポートシンボルの名前や序数、アドレスなどシンボルの解決に十分な情報が含まれています。
エクスポートディレクトリは 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 を足した範囲まで)に含まれている場合、エクスポートシンボルは転送されていることを示します。
エクスポートシンボルの転送に関しては「エクスポート転送で遊んでみる」を参照してください。
背景が赤っぽい色になっている部分が前回から追加された部分です。エクスポート関数を列挙するメソッド 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
まず列挙関連のほうから。GetExportDirP() はエクスポートディレクトリへのポインタを取得するメソッドです。
PEFile.cpp
//! エクスポートディレクトリへのポインタを取得する。 IMAGE_EXPORT_DIRECTORY* PEFile::GetExportDirP() { return reinterpret_cast<IMAGE_EXPORT_DIRECTORY*>( GetDirEntryDataP(IMAGE_DIRECTORY_ENTRY_EXPORT) ); }
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; }
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; }
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; }
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; }
次は 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 を取得しています。
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; }