スタティックライブラリ関数の検出とフック その2

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

概要

 前回は lib.exe をつかって libcmt.lib から printf.obj を抽出してから、printf() を検出しましたが、libcmt.lib を読み込んで直接 printf() を検出してみましょう。

lib ファイルのフォーマット

 lib ファイルは1つ以上の obj ファイルと、それらの obj ファイルが持つシンボルを検索するためのデータをアーカイブしたもので、UNIX の ar コマンドで作られたアーカイブファイルと同じ構造をしています。ただし ar コマンドでもライブラリファイルとしてアーカイブすることができるので、lib.exe は ar コマンドの亜種ということになります(GNU の ar コマンドは様々なフォーマットのライブラリに対応しており、lib.exe を包含する形となっています)。
 ar のアーカイブファイルで使われる管理用の情報(ヘッダなど)では文字しか使われません。例えばファイルサイズは "4096 " といった '\0' で終了していない10進表記の文字列で表されます。そのため、複数のテキストファイルをアーカイブしてできたファイルもまたテキストファイルという特徴があります。反面、文字列を取り出しにくい欠点があります。
 例えば以下の Hello.txt, World.txt の 2 つのファイルをアーカイブすると、ar.txt が出来上がります。
Hello.txt
Hello
World.txt
World!

ar.txt
!<arch>
Hello.txt/      1243824130  500   513   100000  5         `
Hello
World.txt/      1243824143  500   513   100000  6         `
world!

 ar.txt を見て分かるとおり、ar のアーカイブファイルは、最初にマジックストリングである "!<arch>\n"('\0'は含まず) から始まり、直後にアーカイブメンバヘッダとアーカイブメンバのセットが続きます。アーカイブされたファイルはアーカイブメンバとしてそのまま結合されます。また、アーカイブメンバヘッダは2バイト境界に揃える必要があり、パディングには '\n' が使用されます。

 マジックストリングとそのサイズは WinNT.H に以下のように定義されています。
#define IMAGE_ARCHIVE_START_SIZE             8
#define IMAGE_ARCHIVE_START                  "!<arch>\n"
 アーカイブメンバヘッダは WinNT.H に以下のように定義されています。先述したように全てのメンバは '\0' で終了しない文字列で表現され、残りは空白で埋められます。
typedef struct _IMAGE_ARCHIVE_MEMBER_HEADER {
    BYTE     Name[16];
    BYTE     Date[12];
    BYTE     UserID[6];
    BYTE     GroupID[6];
    BYTE     Mode[8];
    BYTE     Size[10];
    BYTE     EndHeader[2];
} IMAGE_ARCHIVE_MEMBER_HEADER, *PIMAGE_ARCHIVE_MEMBER_HEADER;

Name  アーカイブメンバの名前。アーカイブメンバがファイルの場合はファイル名になります。終了文字として '/' が使われます。ただし、Microsoft のツールで扱うライブラリファイルでは '/' で始まる名前は特殊な意味を持ちます(後述)。
Date  アーカイブメンバのタイムスタンプを10進文字列で表現したもの。
UserID  Microsoft のツールで使われるライブラリファイルでは使用しません。
GroupID  Microsoft のツールで使われるライブラリファイルでは使用しません。
Mode  Microsoft のツールで使われるライブラリファイルでは使用しません。
Size  アーカイブメンバのサイズを10進文字列で表現したもの。
EndHeader  ヘッダの終了を表すマジックストリングで "`\n" でなければいけません。

特殊なアーカイブメンバ

 ar のアーカイブファイルはその構造上、目的とするシンボルを探し出すのに適していません。目的のシンボルを含むメンバを最初から順に探しだすなんてことは馬鹿げています。そのため、アーカイブした obj ファイルに含まれる外部定義シンボルの名前のテーブルと、その外部定義シンボルがどのメンバ(obj ファイル)に含まれているかを素早く調べられるデータを持っています。
 そのデータは互換性を維持するための古い形式と、現在使われている新しい形式の2種類があり、どちらも "/ " という名前で必ずライブラリファイルの1番目(古い形式)と2番目(新しい形式)のメンバとして存在します。これらは第1リンカメンバ(First Linker Member)、第2リンカメンバ(Second Linker Member)と呼ばれています。
 また、アーカイブした obj ファイルの名前が15文字を越える場合は名前が "// " のロング名メンバと呼ばれるデータを持ち、そこに実際のファイル名が格納されます。その場合、15文字を超える名前をもつメンバの名前は '/' で始まり、ロング名メンバ内のオフセットを10進表記にした文字列が格納されます。例えば "/0 " はロング名メンバのデータの先頭に実際の名前が格納されていることを示します。なお、ロング名メンバに格納されている名前は '/' ではなく '\0' で終了しているので取り出しが簡単です。

第2リンカメンバ

 図1は第2リンカメンバの構造と各フィールドの使われ方を示しています(名前や数値は適当です)。


図.1 第2リンカメンバ(画像クリックで拡大)

 メンバデータの先頭にはリンカメンバを除くシンボル数、直後に各メンバへのオフセットの配列が続きます。オフセットはメンバ数分あります。オフセット配列の直後にはシンボル数が続きます。そして、シンボルのインデックスをオフセットのインデックスにマップする配列が続きます。最後にシンボル名テーブルが続きます。インデックスもシンボル名もシンボルの数だけあります。
 _asctime から伸びている矢印は外部定義シンボルの "_asctime" を持っているメンバをどのように探し出すか示しています。まずシンボル名テーブルから _asctime を探し出して、そのインデックスを割り出します。そのシンボルのインデックスとインデックス配列を使ってオフセットのインデックスに変換します。今度はそのオフセットのインデックスとオフセット配列を使ってメンバのヘッダへのオフセットを取得します。

LibFile クラスの実装

 LibFile クラスは指定した外部定義シンボルを持つ obj ファイルをライブラリファイルから探しだすためのクラスです。宣言は以下のようになっています。

LibFile.h
   1 : #ifndef __LIB_FILE_H_INCLUDED__
   2 : #define __LIB_FILE_H_INCLUDED__
   3 : #include <PshPack1.h>
   4 : 
   5 : 
   6 : //! アーカイブメンバヘッダ(+ データの先頭)
   7 : struct ArchiveMemberHeader
   8 : {
   9 :   BYTE    name[16];       //!< 名前。
  10 :   BYTE    date[12];       //!< タイムスタンプ。
  11 :   BYTE    user_id[6];     //!< 使用しない。
  12 :   BYTE    group_id[6];    //!< 使用しない。
  13 :   BYTE    mode[8];        //!< 使用しない。
  14 :   BYTE    size[10];       //!< メンバのサイズ。
  15 :   BYTE    end_header[2];  //!< ヘッダのエンドマーカー。
  16 :   BYTE    member_data[1]; //!< メンバのデータの先頭。
  17 : 
  18 :   //! end_header と size に有効な値が入っているか調べる。
  19 :   bool IsValid() const;
  20 :   //! 名前を取得する。
  21 :   std::string GetName() const;
  22 :   //! メンバのサイズを取得する。
  23 :   size_t GetSize() const;
  24 : };
  25 : 
  26 : 
  27 : //! lib ファイルを扱うクラス。
  28 : class LibFile
  29 : {
  30 :   //===========================================================================
  31 :   // 公開メソッド
  32 :   //===========================================================================
  33 : public:
  34 :   //! コンストラクタ。既存の lib ファイルを開く。
  35 :   explicit LibFile(const wchar_t* i_file_path);
  36 : 
  37 :   //! 正しく開けたかどうか確認する。
  38 :   bool IsOpened() const;
  39 : 
  40 :   //! i_index 番目(0-based)のメンバヘッダへのポインタを取得する。
  41 :   const ArchiveMemberHeader* GetMemberHeaderPtr(int i_index) const;
  42 :   //! シンボルを含むアーカイブメンバのヘッダへのポインタを取得する。
  43 :   const ArchiveMemberHeader* FindSymbol(const std::string& i_symbol_name);
  44 : 
  45 : private:
  46 :   //! 有効なポインタかどうか調べる。
  47 :   bool IsValidPtr(const void* i_ptr, size_t i_size) const;
  48 : 
  49 :   //! lib ファイルかどうか調べる。
  50 :   static bool IsLibFile(const BYTE* i_ptr, size_t i_size);
  51 :   
  52 :   //! 第2リンカメンバからリンカメンバを除くメンバ数を取得する。
  53 :   static size_t GetMemberLength(const BYTE* i_second_linker_member_p);
  54 :   //! 第2リンカメンバからメンバへのオフセット配列へのポインタを取得する。
  55 :   static const DWORD* GetMemberOffsets(const BYTE* i_second_linker_member_p);
  56 :   //! 第2リンカメンバからシンボル数を取得する。
  57 :   static size_t GetSymbolLength(const BYTE* i_second_linker_member_p);
  58 :   //! 第2リンカメンバからシンボルのインデックスをメンバオフセットのイン
  59 :   //  デックスに変換する配列へのポインタを取得する。
  60 :   static const WORD* GetOffsetIndices(const BYTE* i_second_linker_member_p);
  61 :   //! 第2リンカメンバからシンボル名テーブルへのポインタを取得する。
  62 :   static const char* GetSymbolNames(const BYTE* i_second_linker_member_p);
  63 : 
  64 : private:
  65 :   bool                        m_is_opened;
  66 :   std::vector<BYTE>           m_lib_data;
  67 :   size_t                      m_member_length;
  68 :   const DWORD*                m_member_offsets_p;
  69 :   size_t                      m_symbol_length;
  70 :   const WORD*                 m_offset_index_from_symbol_index_p;
  71 :   std::vector<std::string>    m_symbol_names;
  72 : };
  73 : 
  74 : 
  75 : #include <PopPack.h>
  76 : #endif

 ArchiveMemberHeader 構造体は IMAGE_ARCHIVE_MEMBER_HEADER の代わりで、メンバへアクセスするための member_data と、データを扱いやすい型で取得するためのメソッドを追加したものです。
LibFile.cpp
  30 : //! end_header と size に有効な値が入っているか調べる。
  31 : bool ArchiveMemberHeader::IsValid() const
  32 : {
  33 :   return memcmp(end_header, "`\n", sizeof(end_header)) == 0 && GetSize() != 0;
  34 : }

LibFile.cpp
  40 : //! 名前を取得する。
  41 : std::string ArchiveMemberHeader::GetName() const
  42 : {
  43 :   char* p = reinterpret_cast<char*>(_alloca(sizeof(name) + 1));
  44 : 
  45 :   // '\0' で終端する文字列としてコピー
  46 :   memcpy(memset(p, '\0', sizeof(name) + 1), name, sizeof(name));
  47 :   // 両端の空白を除去
  48 :   StrTrimA(p, " "); 
  49 : 
  50 :   // '/' で始まっておらず、最後の文字が '/' の場合は '/' を除去。
  51 :   if( p[0] != '/' && p[strlen(p) - 1] == '/' )
  52 :     p[strlen(p) - 1] = '\0';
  53 : 
  54 :   return std::string(p);
  55 : }

LibFile.cpp
  61 : //! メンバのサイズを取得する。
  62 : size_t ArchiveMemberHeader::GetSize() const
  63 : {
  64 :   char* p = reinterpret_cast<char*>(_alloca(sizeof(size) + 1));
  65 : 
  66 :   // '\0' で終端する文字列としてコピー
  67 :   memcpy(memset(p, '\0', sizeof(size) + 1), size, sizeof(size));
  68 :   // 両端の空白を除去
  69 :   StrTrimA(p, " "); 
  70 : 
  71 :   return atoi(p);
  72 : }

 GetMemberLength(), GetMemberOffsets(), GetSymbolLength(), GetOffsetIndices(), GetSymbolNames() は第2リンカメンバから各フィールドを取り出すためのクラスメソッドで、メンバの m_member_length, m_member_offsets_p, m_symbol_length, m_offset_index_from_symbol_index_p, m_symbol_names に値を格納するために使用されます。
LibFile.cpp
  11 : //! 第2リンカメンバのメンバ数とオフセット配列。
  12 : struct MemberOffsets
  13 : {
  14 :   DWORD member_length;
  15 :   DWORD offsets[1];
  16 : };
  17 : 
  18 : //! 第2リンカメンバのシンボル数、メンバインデックスの配列。
  19 : struct OffsetIndexFromSymbolIndex
  20 : {
  21 :   DWORD symbol_length;
  22 :   WORD  offset_index_from_symbol_index[1];
  23 : };

LibFile.cpp
 303 : //! 第2リンカメンバからリンカメンバを除くメンバ数を取得する。
 304 : size_t LibFile::GetMemberLength(const BYTE* i_second_linker_member_p)
 305 : {
 306 :   return reinterpret_cast<const MemberOffsets*>(i_second_linker_member_p)->member_length;
 307 : }

LibFile.cpp
 313 : //! 第2リンカメンバからメンバへのオフセット配列へのポインタを取得する。
 314 : const DWORD* LibFile::GetMemberOffsets(const BYTE* i_second_linker_member_p)
 315 : {
 316 :   return reinterpret_cast<const MemberOffsets*>(i_second_linker_member_p)->offsets;
 317 : }

LibFile.cpp
 323 : //! 第2リンカメンバからシンボル数を取得する。
 324 : size_t LibFile::GetSymbolLength(const BYTE* i_second_linker_member_p)
 325 : {
 326 :   size_t        member_length   = GetMemberLength(i_second_linker_member_p);
 327 :   const DWORD*  member_offsets  = GetMemberOffsets(i_second_linker_member_p);
 328 : 
 329 :   return reinterpret_cast<const OffsetIndexFromSymbolIndex*>(
 330 :            &member_offsets[member_length]
 331 :          )->symbol_length;
 332 : }

LibFile.cpp
 338 : //! 第2リンカメンバからシンボルのインデックスをメンバオフセットのイン
 339 : //  デックスに変換する配列へのポインタを取得する。
 340 : const WORD* LibFile::GetOffsetIndices(const BYTE* i_second_linker_member_p)
 341 : {
 342 :   size_t        member_length   = GetMemberLength(i_second_linker_member_p);
 343 :   const DWORD*  member_offsets  = GetMemberOffsets(i_second_linker_member_p);
 344 : 
 345 :   return reinterpret_cast<const OffsetIndexFromSymbolIndex*>(
 346 :            &member_offsets[member_length]
 347 :          )->offset_index_from_symbol_index;
 348 : }

LibFile.cpp
 354 : //! 第2リンカメンバからシンボル名テーブルへのポインタを取得する。
 355 : const char* LibFile::GetSymbolNames(const BYTE* i_second_linker_member_p)
 356 : {
 357 :   size_t        symbol_length  = GetSymbolLength(i_second_linker_member_p);
 358 :   const WORD*   offset_indices = GetOffsetIndices(i_second_linker_member_p);
 359 :   
 360 :   return reinterpret_cast<const char*>(&offset_indices[symbol_length]);
 361 : }

 コンストラクタでは lib ファイルの読み込みと、上記のメソッドを使って第2リンカメンバの解析を行うのみとなっています。
LibFile.cpp
 139 : //! コンストラクタ。lib ファイルを読み込む。
 140 : LibFile::LibFile(const wchar_t* i_file_path)
 141 : : m_is_opened(false),
 142 :   m_lib_data(),
 143 :   m_member_length(0),
 144 :   m_member_offsets_p(NULL),
 145 :   m_symbol_length(0),
 146 :   m_offset_index_from_symbol_index_p(NULL),
 147 :   m_symbol_names()
 148 : {
 149 :   do
 150 :   {
 151 :     if( LoadFile(i_file_path, m_lib_data) == false )
 152 :     {
 153 :       _RPTF1(_CRT_WARN, "%S を開けませんでした。\n", i_file_path);
 154 :       break;
 155 :     }
 156 :     
 157 :     if( IsLibFile(&m_lib_data[0], m_lib_data.size()) == false )
 158 :     {
 159 :       _RPTF1(_CRT_WARN, "%S はライブラリファイルではありません。\n", i_file_path);
 160 :       break;
 161 :     }
 162 :     
 163 :     // 第2リンカメンバのメンバヘッダへのポインタを取得。
 164 :     const ArchiveMemberHeader* header_p = GetMemberHeaderPtr(1);
 165 :     if( header_p == NULL || header_p->GetName() != "/" )
 166 :     {
 167 :       _RPTF0(_CRT_WARN, "第2リンカメンバを含んでいません。\n");
 168 :       break;
 169 :     }
 170 :     
 171 :     // 第2リンカメンバをアクセスしやすい形で保持しておく。
 172 :     m_member_length = GetMemberLength(header_p->member_data);
 173 :     m_member_offsets_p = GetMemberOffsets(header_p->member_data);
 174 :     m_symbol_length = GetSymbolLength(header_p->member_data);
 175 :     m_offset_index_from_symbol_index_p = GetOffsetIndices(header_p->member_data);
 176 :     
 177 :     // シンボル名テーブルを std::vector<std::string> に変換
 178 :     m_symbol_names.reserve(m_symbol_length);
 179 :     const char* p = GetSymbolNames(header_p->member_data);
 180 :     for(size_t i = 0; i < m_symbol_length; i++)
 181 :     {
 182 :       m_symbol_names.push_back(p);
 183 :       p += strlen(p) + 1;
 184 :     }
 185 :     
 186 :     m_is_opened = true;
 187 :   }
 188 :   while( 0 );
 189 : }

 第2リンカメンバを解析してしまえば、シンボルを含むメンバを割り出すのは簡単です。その処理は FindSymbol() メソッドとして定義されています。
 std::lower_bound() は STL のバイナリサーチを行う template 関数です。iterator からシンボルのインデックス(symbol_index)を割り出し、オフセット配列のインデックス(offset_index)に変換し、さらにオフセット(member_offset)に変換しています。
LibFile.cpp
 242 : //! シンボルを含むアーカイブメンバのヘッダへのポインタを取得する。
 243 : const ArchiveMemberHeader* LibFile::FindSymbol(const std::string& i_symbol_name)
 244 : {
 245 :   const ArchiveMemberHeader*  retval = NULL;
 246 : 
 247 :   if( m_is_opened )
 248 :   {
 249 :     std::vector<std::string>::iterator itr = std::lower_bound(
 250 :                                                m_symbol_names.begin(),
 251 :                                                m_symbol_names.end(),
 252 :                                                i_symbol_name
 253 :                                              );
 254 :     if( itr != m_symbol_names.end() && *itr == i_symbol_name )
 255 :     {
 256 :       int symbol_index  = itr - m_symbol_names.begin();
 257 :       int offset_index  = m_offset_index_from_symbol_index_p[symbol_index] - 1;
 258 :       int member_offset = m_member_offsets_p[offset_index];
 259 : 
 260 :       retval = reinterpret_cast<const ArchiveMemberHeader*>(
 261 :                  &m_lib_data[0] + member_offset
 262 :                );
 263 :       if( IsValidPtr(retval, sizeof(*retval)) == false
 264 :       ||  retval->IsValid() == false )
 265 :       {
 266 :         retval = NULL;
 267 :       }
 268 :     }
 269 :   }
 270 :   
 271 :   return retval;
 272 : }

前回の hookdll.cpp の修正

 前回は libcmt.lib から抽出した printf.obj を読み込むようになっていましたが、libcmt.lib を読み込んで printf.obj のメンバを読み込むように修正します。

hookdll.cpp 修正前
 112 :     // obj ファイルを開く
 113 :     ObjFile   obj_file(L"printf.obj");

hookdll.cpp 修正後
 113 :     // ライブラリファイルを開く
 114 :     LibFile lib_file(L"libcmt.lib");
 115 :     if( lib_file.IsOpened() == false )
 116 :     {
 117 :       Error(L"libcmt.lib ファイルのオープンに失敗しました。\n");
 118 :       break;
 119 :     }
 120 :     
 121 :     // _printf を探す。
 122 :     const ArchiveMemberHeader* header_p = lib_file.FindSymbol("_printf");
 123 :     if( header_p == NULL )
 124 :     {
 125 :       Error(L"_printf シンボルが見つかりませんでした。\n");
 126 :       break;
 127 :     }
 128 :     
 129 :     // obj ファイルを開く
 130 :     ObjFile   obj_file(header_p->member_data, header_p->GetSize());

 数行の修正で済みました。libcmt.lib ファイルを実行ファイルと同じフォルダにコピーして実行すると前回と同じ結果が得られます。

最後に

 これで obj ファイルとライブラリファイルのフォーマットを説明したことになります。obj ファイルには弱外部参照(weak external reference)や COMDAT セクションなど説明していない項目がありますが、リンカを作る最低限の知識が得られたことになります。次回はそのうちリンカもどきを作ってみましょう。
 なお、ExeFreezer のアンパックとローダ部は C++ で書いて VC でコンパイルしたものをもどきでリンクしたものを使っています。パッカーを全て高級言語で書けるのはずいぶん楽なものです。パッカーやEXE感染型ウィルス(!)なんかは簡単に作れてしまいます。