PEファイルフォーマット その5 ~ ベース再配置情報 ~

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

ベース再配置情報

 オプショナルヘッダにはイメージファイルがロードされるべきアドレスである ImageBase フィールドがありますが、既に別のイメージファイルがそのアドレスにロードされていたり、メモリの確保に失敗するなどして、ImageBase が指定するアドレスにロードできない場合があります。
 そのような場合、イメージファイルにベース再配置情報が含まれていれば、ローダはそれを用いて別のアドレスにイメージファイルをロードしてくれます。ベース再配置情報は、アドレス値を修正しなければいけない場所のリストで、例えば ImageBase で 0x10000000 が指定されているのに 0x10004000 にロードした場合、その差分の 0x4000 を加算してやらなければいけない場所のリストということになります。
 イメージファイルのヘッダやデータディレクトリに含まれるアドレスなどのほとんどは RVA(相対仮想アドレス)で記述されているので、ロードされるアドレスが変更されても何の問題もありませんが、プログラムコードで指定されるアドレスは RVA ではなくアドレス値そのものです。ベース再配置情報はそういったアドレス値を修正しなければいけない場所を指し示してくれるわけです。
 ベース再配置を行う必要が生じるのは主に DLL です。EXE ファイルは一番最初にロードされるイメージファイルのため、ImageBase におかしな値が指定されていない限りはロードに失敗することはありません。そのため、VisualC++ でコンパイルした EXE ファイルは通常はベース再配置情報をもっておらず、DLL は通常はベース再配置情報を持っています。リンカオプションの /FIXED オプションで再配置情報を持つかどうかを指定できます。/FIXED でベース再配置情報を持たせず、/FIXED:NO でベース再配置情報を持たせることになります。
 なお、COFF でも再配置情報というものがありましたが、それとは全くの別物なので注意してください。

ベース再配置情報の取り出し方と構造

 ベース再配置情報はオプショナルヘッダの DataDirectory の IMAGE_DIRECTORY_ENTRY_BASERELOC(5) 番目のエントリから辿ることができます。
 ベース再配置情報は下図を見るように他のデータと比べて少々特殊な構造をしています。最初に IMAGE_BASE_RELOCATION 構造体があり、その直後に再配置のタイプとオフセットが続きます。それを DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size バイトだけ繰り返します。


 IMAGE_BASE_RELOCATION は WinNT.h で以下のように定義されていますが、タイプとオフセットは定義されていません。自分で定義するか WORD 型の値からシフト演算で取り出す必要があります。
WinNT.h
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

VirtualAddress 再配置のベース RVA 。この値に、直後に続くタイプとオフセットのオフセット部分を加算すると再配置を行うべき RVA になります。
SizeOfBlock IMAGE_BASE_RELOCATION 構造体のサイズと直後に続くタイプとオフセットの合計バイト数。
TypeOffset コメントアウトされているので使用できませんが、直後に続くタイプとオフセットの配列を示しています。各要素は上位4ビットで再配置のタイプ、下位12ビットで VirtualAddress からのオフセットを示しています。

 再配置のタイプは WinNT.h で以下のように定義されておりますが、x86 用に使用されるのはパディング目的(つまり何もしなくて良い)の IMAGE_REL_BASED_ABSOLUTE と、差分を加算する IMAGE_REL_BASED_HIGHLOW の二つだけです。パディングは IMAGE_BASE_RELOCATION 構造体のデータを 4 バイト境界に揃えるために使用されます。
WinNT.h
//
// Based relocation types.
//

#define IMAGE_REL_BASED_ABSOLUTE              0
#define IMAGE_REL_BASED_HIGH                  1
#define IMAGE_REL_BASED_LOW                   2
#define IMAGE_REL_BASED_HIGHLOW               3
#define IMAGE_REL_BASED_HIGHADJ               4
#define IMAGE_REL_BASED_MIPS_JMPADDR          5
#define IMAGE_REL_BASED_MIPS_JMPADDR16        9
#define IMAGE_REL_BASED_IA64_IMM64            9
#define IMAGE_REL_BASED_DIR64                 10

PEFile クラスへの機能追加

 今回はベース再配置情報を取得するための GetBaseRelocations() メソッド、擬似ロードを行うための PseudoLoad() メソッド、PseudoLoad() メソッドの補助である BindImportSymbols() メソッドが追加されています。

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);
  
  //! ベース再配置情報を取得する。
  std::vector<DWORD*> GetBaseRelocations();
  //! 擬似ロードを行う。エントリポイントへのポインタを返す。
  void* PseudoLoad(void* i_rebase_address = NULL);
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);
  //! インポートシンボルのバインド
  bool BindImportSymbols();
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

ベース再配置情報の取得

 まずは GetBaseRelocations() と言いたいところですが、IMAGE_BASE_RELOCATION の代わりに使用する構造体から。IMAGE_BASE_RELOCATION だと各情報が取り出しにくいので、メンバとメソッドを追加した BaseRelocation 構造体を定義しました。

PEFile.cpp
//! IMAGE_BASE_RELOCATION の代わりに使用する構造体。メンバとメソッドを追加。
struct BaseRelocation
{
  DWORD   virtual_address;  //!< 再配置の開始 RVA
  DWORD   size_of_block;    //!< この再配置ブロックのサイズ
  WORD    fixup_records[1]; //!< 上位4ビットでタイプ、下位12ビットでオフセット
  
  //! fixup_records のレコード数を取得する。
  size_t GetFixupLength()
  {
    return (size_of_block - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
  }
  
  //! i_index 番目の fixup_records の再配置タイプを取得する。
  DWORD GetType(int i_index)
  {
    return fixup_records[i_index] >> 12;
  }
  
  //! i_index 番目の RVA を取得する。
  DWORD GetRVA(int i_index)
  {
    return (fixup_records[i_index] & ((1 << 12) - 1)) + virtual_address;
  }
};

 GetBaseRelocations() は再配置しなければいけない場所のポインタの vector を返します。32bit なので DWORD* としていますが、64bit なら QWORD* になります。PEFile クラスは x86 用なので ULONG_PTR は敢えて使用しません。
PEFile.cpp
//! ベース再配置情報を取得する。
std::vector<DWORD*> PEFile::GetBaseRelocations()
{
  std::vector<DWORD*> retval;
  DWORD offset        = 0;
  DWORD reloc_size    = GetDirEntryDataSize(IMAGE_DIRECTORY_ENTRY_BASERELOC);
  BYTE* first_reloc_p = GetDirEntryDataP(IMAGE_DIRECTORY_ENTRY_BASERELOC);
  
  if( first_reloc_p )
  {
    while( offset < reloc_size )
    {
      BaseRelocation&
        reloc = *reinterpret_cast<BaseRelocation*>(first_reloc_p + offset);
      
      for(size_t i = 0; i < reloc.GetFixupLength(); i++)
      {
        if( reloc.GetType(i) == IMAGE_REL_BASED_HIGHLOW )
          retval.push_back(GetDataP<DWORD*>(reloc.GetRVA(i)));
      }
      
      offset += reloc.size_of_block;
    }
  }
  
  return retval;
}

ベース再配置情報をもったイメージファイルの擬似ロード

 自前でメモリに読み込んだイメージファイルの IAT をインポートシンボルのアドレスで上書きし、読み込んだアドレスへベース再配置してやれば、実際に実行できるモジュールの出来上がりです。
 IAT の上書きを行うのが BindImportSymbols() メソッドで、それを呼び出した後に再配置情報を適用するのが PseudoLoad() です。ただし、PseudoLoad() はエントリポイントを呼びはしません。エントリポイントに限らず任意の関数を呼び出すことができるからです(※Cランタイム関数を使うにはスタートアップルーチンを呼び出して初期化する必要あり)。

 BindImportSymbols() はインポート情報を理解していれば問題ないでしょう。

PEFile.cpp
//! インポートシンボルのバインド
bool PEFile::BindImportSymbols()
{
  bool                      retval    = true;
  std::vector<const char*>  dll_names = GetImportDllNames();
  
  for(size_t dll_index = 0; retval && dll_index < dll_names.size(); dll_index++)
  {
    HMODULE dll_handle = GetModuleHandleA(dll_names[dll_index]);
    // GetModuleHandle() が失敗なら LoadLibrary() してみる。
    if( dll_handle == NULL )
      dll_handle = LoadLibraryA(dll_names[dll_index]);

    // ハンドルが取得できないなら失敗。
    if( dll_handle == NULL )
    {
      retval = false;
      break;
    }

    std::vector<ImportSymbol> symbols = GetImportSymbols(dll_names[dll_index]);
    for(size_t symbol_index = 0; symbol_index < symbols.size(); symbol_index++)
    {
      *symbols[symbol_index].iat_entry_p = GetProcAddress(
                                             dll_handle,
                                             symbols[symbol_index].name
                                           );
      // シンボルを解決できなかった。
      if( *symbols[symbol_index].iat_entry_p == NULL )
      {
        retval = false;
        break;
      }
    }
  }
  
  return retval;
}

 PseudoLoad() はベース再配置を行うアドレスを指定できます。NULL を指定するとメモリに読み込まれたアドレスを元にベース再配置を行いますが、NULL 以外だとそのアドレスを元にベース再配置を行います。別のプロセスに割り当てたメモリのアドレスを指定し、WriteProcessMemory() でベース再配置したイメージファイルを書き込んでやればそのプロセスで実行できるモジュールが出来るからです。もっとも、別プロセスに同じ DLL が同じアドレスにロードされていることが前提になるので KERNEL32.DLL などシステム DLL しか使用していないものに限られますが。BindImportSymbols() を改良してその制限を取っ払ってみてください。
PEFile.cpp
//! 擬似ロードを行う。エントリポイントへのポインタを返す。
void* PEFile::PseudoLoad(void* i_rebase_address)
{
  void*                 retval    = NULL;
  std::vector<DWORD*>   relocs    = GetBaseRelocations();

  if( relocs.size() && BindImportSymbols() )
  {
    DWORD rebase_address = i_rebase_address
                         ? reinterpret_cast<DWORD>(i_rebase_address)
                         : reinterpret_cast<DWORD>(m_load_base);

    // ベース再配置を行う。
    DWORD diff = rebase_address - GetOptionalHeaderP()->ImageBase;
    for(size_t i = 0; i < relocs.size(); i++)
    {
      *relocs[i] += diff;
    }
    
    retval = reinterpret_cast<BYTE*>(
               rebase_address + GetOptionalHeaderP()->AddressOfEntryPoint
             );
  }
  
  return retval;
}

自身を削除するサンプルプログラム

 ロードされているイメージファイル(モジュール)は、ファイルにロックがかけられていて削除することができません。しかし、PEFile クラスで擬似ロードを行い、擬似ロードされた中で自モジュールを FreeLibrary() で解放すればファイルのロックが外れます。そうすれば削除も書き換えも自由に行えるということです。
 こういうのは本来なら機械語を直接書き込まなければいけないという敷居の高い(かつ手間がかかる)ものですが、擬似ロードがその敷居を低くしてくれます。好きな言語で DLL を作れば良いだけです。
 とはいえ、FreeLibrary() で解放できるのは DLL だけで EXE は解放できません。そこで RunDLL32.EXE の力を借りることになります。削除処理を行うのは DLL で、その DLL を呼び出す RunDLL32.EXE を起動する EXE が別途必要になります(バッチやコマンドプロンプトから直接起動しても良いですが)。

DLL 側

 RunDllEntryPointW() は RunDLL32.EXE から呼び出してもらう関数です。自モジュールのファイルを PEFile クラスで読み込んで、擬似ロードを行い、擬似ロードされたほうの DeleteSelf() 関数を呼んでいます。

DelSelfDll.cpp
//! RunDll32.EXE から呼び出してもらう関数。
extern "C" __declspec(dllexport) void CALLBACK RunDllEntryPointW(
  HWND,
  HINSTANCE,
  LPWSTR        i_command_line,
  int
)
{
  // DLL のハンドルを取得。
  MEMORY_BASIC_INFORMATION  mbi = { 0 };
  VirtualQuery(&RunDllEntryPointW, &mbi, sizeof(mbi));
  HMODULE module_handle = reinterpret_cast<HMODULE>(mbi.AllocationBase);

  // DLL のパスを取得。
  wchar_t path[MAX_PATH] = { 0 };
  GetModuleFileNameW(module_handle, path, MAX_PATH);

  // 自身のファイルを開いて擬似ロードを行う。
  PEFile pe_file(path);
  if( pe_file.PseudoLoad() )
  {
    // 擬似ロードされたイメージファイルでの DeleteSelf() へのポインタを取得。
    DWORD del_self_rva = reinterpret_cast<BYTE*>(&DeleteSelf)
                       - reinterpret_cast<BYTE*>(module_handle);
    void* DelSelf_p = pe_file.GetLoadBase() + del_self_rva;
    
    // 擬似ロードされたイメージファイルのほうの DeleteSelf() を呼び出す。
    ((void(*)(HINSTANCE, LPWSTR, LPWSTR))DelSelf_p)(
      module_handle,
      path,
      i_command_line
    );
  }
}

 DeleteSelf() はコマンドラインで渡されたファイルの削除と、自モジュールの解放および削除を担当する関数です。
DelSelfDll.cpp
/*! ファイルが存在する場合に削除する。
 *
 *  @return ファイルが存在し、削除に失敗したときだけ false を返す。
 */
static bool DeleteFileIfExists(LPWSTR i_path)
{
  // ファイルが存在しない場合は true を返す。
  if( PathFileExistsW(i_path) == FALSE )
    return true;

  // 10 回リトライ
  for(int i = 0; i < 10; i++)
  {
    if( DeleteFileW(i_path) )
      return true;
    
    Sleep(100); // 0.1 秒待機してロックが解除されるのを待つ。
  }
  
  return false;
}


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


//! コマンドラインで渡されたファイルと自身を削除する。
static void DeleteSelf(
  HMODULE   i_module_handle,
  LPWSTR    i_path,
  LPWSTR    i_command_line
)
{
  bool succeeded = false;

  // コマンドラインで渡されたファイルを削除。
  if( DeleteFileIfExists(i_command_line) )
  {
    // 自分自身の削除
    FreeLibrary(i_module_handle);
    if( DeleteFileIfExists(i_path) )
      MessageBoxW(NULL, L"削除しました", L"確認", MB_ICONINFORMATION);
  }

  ExitProcess(0);
}

EXE 側

 "rundll32.exe DelSelf.DLL,RunDllEntryPoint 自プログラムのパス" というコマンドライン文字列を構築して、CreateProcess() で起動しているだけの単純なものです。なので、バッチやコマンドラインから直接起動しても構いません。
 自身は RunDLL32.EXE を起動した直後に終了するので DLL 側の DelSelf() 関数が呼ばれる頃にはファイルのロックは解除されています。
 実行して「削除しました。」とメッセージボックスが表示されれば成功です。

DelSelfExe.cpp
#include <windows.h>


//! メインルーチン
int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
{
  wchar_t               command_line[MAX_PATH * 4] = { 0 };
  PROCESS_INFORMATION   process_info = { 0 };
  STARTUPINFOW          startup_info = { 0 };
  
  lstrcpyW(command_line, L"rundll32.exe DelSelf.DLL,RunDllEntryPoint ");
  GetModuleFileNameW(NULL,
                     command_line + lstrlenW(command_line),
                     MAX_PATH
                    );

  if( CreateProcessW(
        NULL,
        command_line,
        NULL,
        NULL,
        FALSE,
        0,
        NULL,
        NULL,
        &startup_info,
        &process_info
      ) != FALSE )
  {
    CloseHandle(process_info.hThread);
    CloseHandle(process_info.hProcess);
  }
  else
  {
    MessageBoxW(NULL, L"RUNDLL32.EXE の起動に失敗しました。", command_line, MB_OK);
  }

  return 0;
}