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

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

※プログラムの性質上ウィルスと誤認される場合がありますのでご注意ください。NOD32 ではひっかかってしまいました。

概要

 API のフックは IAT の書き換えやブレークポイントの設置などで比較的簡単にできますし、やり方に関する情報も結構あるんじゃないでしょうか。API フック用のライブラリなんかもどこかで公開されてたと思います。
 が、スタティックライブラリ関数のフックなんてのはなかなか聞いたことないのではないかなと思います。今回は、printf() などスタティックライブラリに含まれる関数を実行可能ファイル内から探し出してきてフックする方法について説明します。
 スタティックライブラリ関数の検出はフックのほかにも逆アセンブルやエミュレータの開発に役に立つかと思います。
 逆アセンブル時にニーモニックだけでなく関数名を表示すれば解析しやすくなります。この機能をもった逆アセンブラは既にありますね。
 エミュレータの開発では、機械語レベルやレジスタレベルでエミュレートせずに機能単位で置き換えることによって開発が容易になります(たぶん)。例えば、ハード A でグラフィック用ライブラリとして OpenGL を提供していました。ハード B はハード A とは互換性がありませんが、同じく OpenGL を提供していました。その場合、ハード A 用に開発された実行可能ファイルから OpenGL の関数を探しだして、ハード B 用の OpenGL 関数呼び出しに置き換えてやればよいわけです。

仕組み

 実行可能ファイルにはリンクした中間ファイルの一部がそのまま含まれています。その一部というのは当然コード部分になりますが、それを実行可能ファイル内から探し出してきて、別の関数への jmp 命令に置き換えるという訳です。
 つまり、検出するには対象となる実行可能ファイルにリンクされたライブラリ関数と同じものが必要になります。
 また、中間ファイルや実行可能ファイルのフォーマットは OS やコンパイラによって異なります。今回は、VisualC++ の特定のバージョンで 32bit 用にコンパイルされた実行可能ファイルを対象とします。

Common Object File Format(COFF)

 中間ファイルについて知らないと、探したい関数のデータを取り出せませんので、まずはそちらから。図1 は VC が出力する中間ファイルである obj ファイルのフォーマットで、COFF(Common Object File Format)と呼ばれます。図2 は Hello, world がどのように obj ファイルになるかを端的に示したものです。リンクがどのような手順で行われるか見えてきませんか?

図.1

図.2

 まず、COFF ファイルヘッダがあり、直後にセクションテーブルが続きます。他は各オフセット値から位置を割り出します。各データを順に見ていきましょう。

ファイルヘッダ

 ファイルヘッダの定義は WinNT.h に以下のように定義されています。

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Machine  このファイルがどのマシンを対象として作られたものか表す数値です。WinNT.h に IMAGE_FILE_MACHINE_XXX として定義されています。x86 なら IMAGE_FILE_MACHINE_I386(0x014c) か IMAGE_FILE_MACHINE_UNKNOWN(0) になります。
NumberOfSections  セクション数。Machine が IMAGE_FILE_MACHINE_UNKNOWN で、セクション数が IMPORT_OBJECT_HDR_SIG2(0xffff) の場合はインポート用の擬似COFFファイルに なります。
TimeDateStamp  ファイルのタイムスタンプ。
PointerToSymbolTable  シンボルテーブルへのオフセット。ファイルの先頭からのオフセットになります。
NumberOfSymbols  このファイルに含まれるシンボル数。
SizeOfOptionalHeader  オプショナルヘッダのサイズ。obj ファイルでは通常オプショナルヘッダを持たない ので 0 になります。
Characteristics ファイルの特性。

セクションテーブル

 セクションというのはコードや文字列リテラル、変数などその分類ごとに分けられた区画のことです。例えば、コードは読取専用で実行可能な特性をもつセクションに、文字列リテラルやリソースデータは読取専用のセクションに、グローバル変数は読み書き可能なセクションに、といった具合に分けられて配置されます。
 セクションの特性やデータへのオフセットなどの情報が収められているのがセクションヘッダで、セクションヘッダの配列がセクションテーブルになります。
 また、セクションデータは必要に応じて再配置情報を持ちます。再配置情報はリンクされるまで値が決まらないようなデータの場所やその値の元となるシンボル情報のインデックスを持っています。
 セクションヘッダは WinNT.h に以下のように定義されています。

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name  セクション名。
VirtualSize  obj ファイルでは使われません。
VirtualAddress  obj ファイルでは使われません。
SizeOfRawData  セクションデータのサイズ。
PointerToRawData  セクションデータへのオフセット。ファイルの先頭からのオフセットになります。 セクションデータが存在しない場合は 0 が格納されます。
PointerToRelocations  再配置情報へのオフセット。
PointerToLinenumbers  行番号情報へのオフセット。行番号情報は古い形式のデバッグ情報です。最近の デバッグ情報は別の形式で生成されるため、通常は 0 です。
 cl に /Z7 オプションをつけてコンパイルすると生成されます。
NumberOfRelocations  再配置情報の数。
NumberOfLinenumbers  行番号情報の数。
Characteristics  セクションの特性を表すフラグの集合。各フラグは IMAGE_SCN_XXXX として WinNT.h に定義されています。
 たとえば、コードが配置されるセクションなら、 コードを含む IMAGE_SCN_CNT_CODE フラグ、読取専用を示す IMAGE_SCN_MEM_READ フラグ、実行可能であることを示す IMAGE_SCN_MEM_EXECUTE フラグがセット されます。

 再配置情報は、WinNT.h に以下のように定義されています。
typedef struct _IMAGE_RELOCATION {
    union {
        DWORD   VirtualAddress;
        DWORD   RelocCount;
    } DUMMYUNIONNAME;
    DWORD   SymbolTableIndex;
    WORD    Type;
} IMAGE_RELOCATION;
typedef IMAGE_RELOCATION UNALIGNED *PIMAGE_RELOCATION;

VirtualAddress  再配置しなければいけない位置のオフセット。セクションデータの先頭からの オフセットです。
RelocCount  再配置情報数が FFFF を越える場合、 セクションの特性に IMAGE_SCN_LNK_NRELOC_OVFL フラグがセットされます。その場合、再配置情報 の最初の一つ目は再配置情報数を表すために使用され、RelocCount に FFFF を 越える再配置情報数がセットされています。
SymbolTableIndex  再配置で置き換える値の元となるシンボルのインデックス。
Type  再配置情報のタイプ。x86 用では常に IMAGE_REL_I386_REL32(0x0014) が使われ、 SymbolTableIndex が示すシンボルのアドレスへの相対値となります。x86 用では IMAGE_REL_I386_REL32(0x0014) と IMAGE_REL_I386_DIR32(0x0006) が使用されます。IMAGE_REL_I386_REL32 は SymbolTableindex が示すシンボルのアドレスへの相対値で置き換え、IMAGE_REL_I386_DIR32 はシンボルのアドレス値を元の値に加算したもので置き換えます。 - 2009/06/19 修正

シンボルテーブル

 シンボルテーブルは固定長(18byte)のシンボル情報の配列です。
 シンボル情報には、シンボル名やシンボルの型、外部定義なのか外部参照なのか、外部定義シンボルの場合はどのセクションに定義されているか、などの情報が含まれます。
 また、シンボルの種類によっては同じく 18byte の補助シンボル情報をいくつか持ちます。補助シンボル情報はシンボル情報の直後に続き、それぞれの意味によって形式は異なります。例えば、シンボルが関数の場合で、デバッグ情報出力を有効にしてコンパイルした場合は関数の開始位置と終了位置を示す補助シンボル情報がつきます。
 なお、ファイルヘッダがもつシンボル数はシンボル情報の数と補助シンボル情報の数の合計値です。
 シンボル情報の定義は WinNT.h に以下のように定義されています。
typedef struct _IMAGE_SYMBOL {
    union {
        BYTE    ShortName[8];
        struct {
            DWORD   Short;     // if 0, use LongName
            DWORD   Long;      // offset into string table
        } Name;
        DWORD   LongName[2];    // PBYTE [2]
    } N;
    DWORD   Value;
    SHORT   SectionNumber;
    WORD    Type;
    BYTE    StorageClass;
    BYTE    NumberOfAuxSymbols;
} IMAGE_SYMBOL;

N  '\0'を含まないシンボル名の文字数が8以内の場合は ShortName にシンボル名が格納 されます。
 シンボル名が 8 文字を越えるかどうかは Short の値を調べれば分かります。 Short の値が 0 以外の場合は 8 文字以内、0 の場合は 8 文字を越えるシンボル名 (ロングシンボル名)を持つことになります。
 シンボル名が 8 文字を越える場合、Long はロングシンボル名へのオフセットです。 オフセットはロングシンボル名テーブルの先頭からのオフセットです。
Value  シンボルの種類によって意味が変わる値です。例えばセクションデータを持つ シンボルの場合は、シンボルがセクションデータのどの位置にあるかをあらわす オフセット値になります。セクションデータをもたない未初期化の配列などの 場合は、そのサイズを意味します。
SectionNumber  このシンボルがどのセクションに定義されているかを示す番号で、1-based の セクションテーブルのインデックスです。値が IMAGE_SYM_UNDEFINED(0) の場合は セクションに属さないデータや別のどこかで定義されている外部シンボルへの参照を 意味します。他にも -1, や -2 など特別な値をとることもあります。
Type  上位バイト、下位バイトでシンボルの型をあらわします。ただし、Microsoft の ツールは関数を意味する 0x20 かそれ以外の 0x00 しか値をセットしません。
StorageClass  記憶域クラスを示す値です。WinNT.h に IMAGE_SYM_CLASS_XXX として色々定義され ていますが、重要なのは外部シンボルであることを示す IMAGE_SYM_CLASS_EXTERNAL(2) ぐらいです。Value か SectionNumber に値が入って いる場合は外部定義シンボル、どちらも 0 の場合は外部シンボルへの参照と なります。
NumberOfAuxSymbols  補助シンボル情報の数。

ロングシンボル名テーブル

 ロングシンボル名テーブルはシンボルテーブルの直後に続きます。そして、最初の4バイトにテーブルサイズを持ち、続いて '\0' で終わる文字列が連なります。
 なお、ロングシンボル名テーブルの終わりはファイルの終端という決まりがあります。

 obj ファイルの説明は以上です。もっと詳しく知りたい人は Microsoft Portable Executable and Common Object File Format Specificationを参照してください。探せば他にもいろいろでてくると思います。

obj ファイルから各データを取得する

 obj ファイルから各データを簡単に取り出せるように ObjFile クラスつくりました。宣言は以下のようになっています。
ObjFile.h
   1 : #ifndef __OBJ_FILE_H_INCLUDED__
   2 : #define __OBJ_FILE_H_INCLUDED__
   3 : 
   4 : 
   5 : //! Common Object File Format のファイルを扱うクラス。
   6 : class ObjFile
   7 : {
   8 :   //===========================================================================
   9 :   // 公開メソッド
  10 :   //===========================================================================
  11 : public:
  12 :   //! コンストラクタ。obj ファイルを読み込む。
  13 :   explicit ObjFile(const wchar_t* i_obj_path);
  14 :   //! コンストラクタ。メモリ上の obj データをコピーして保持する。
  15 :   explicit ObjFile(const void* i_obj_data, size_t i_size);
  16 :   //! デストラクタ
  17 :   virtual ~ObjFile();
  18 :   
  19 :   //! 正しく開けたかどうか確認する。
  20 :   bool IsOpened() const;
  21 : 
  22 :   //! シンボル数を取得する。
  23 :   int GetSymbolCount() const;
  24 :   //! シンボル情報のポインタを取得する。
  25 :   const IMAGE_SYMBOL* GetSymbolP(int i_index) const;
  26 : 
  27 :   //! i_index 番目(0-based)のシンボル名を取得する。
  28 :   const std::string& GetSymbolName(int i_index) const;
  29 :   //! i_index 番目(0-based)の非修飾シンボル名を取得する。
  30 :   const std::string& GetUndecoratedName(int i_index) const;
  31 :   //! i_index 番目(0-based)のシンボル名が修飾されているかどうか調べる。
  32 :   bool IsDecorated(int i_index) const;
  33 : 
  34 :   //! シンボル名から外部定義シンボルのインデックスを取得する。
  35 :   /**
  36 :    *  @return 見つかった場合はそのシンボルのインデックス。見つからなかった場合
  37 :    *          は -1 を返す。
  38 :    */
  39 :   int FindSymbol(const char* i_symbol_name) const;
  40 : 
  41 :   //! i_index 番目(0-based)のセクションヘッダへのポインタを取得する。
  42 :   const IMAGE_SECTION_HEADER* GetSectionHeaderP(int i_index) const;
  43 :   //! i_index 番目(0-based)のセクションデータへのポインタを取得する。
  44 :   const BYTE* GetSectionDataP(int i_index) const;
  45 :   //! i_index 番目(0-based)のシンボルが外部定義シンボルかどうか調べる。
  46 :   bool IsExternalSymbol(int i_index) const;
  47 :   
  48 :   //! i_index 番目(0-based)のセクションの再配置情報の数を取得する。
  49 :   int GetRelocationCount(int i_section_index) const;
  50 :   //! i_index 番目(0-based)のセクションの再配置情報テーブルを取得する。
  51 :   const IMAGE_RELOCATION* GetRelocations(int i_index) const;
  52 :   
  53 :   //! 再配置情報を考慮してセクションデータが等しいか調べる。
  54 :   bool IsEqualSection(
  55 :     int           i_section_index,
  56 :     const BYTE*   i_data,
  57 :     int           i_size
  58 :   ) const;
  59 :   
  60 :   //! i_image_p から i_symbol_index 番目のシンボルのセクションを探す。
  61 :   BYTE* FindSymbolData(
  62 :     int     i_symbol_index,
  63 :     BYTE*   i_image_p,
  64 :     size_t  i_image_size
  65 :   ) const;
  66 : 
  67 :   //===========================================================================
  68 :   // 非公開メソッド
  69 :   //===========================================================================
  70 : private:
  71 :   //! コピーコンストラクタは使用禁止とする。
  72 :   ObjFile(const ObjFile&);
  73 :   //! 代入演算子も使用禁止とする。
  74 :   const ObjFile& operator=(const ObjFile&);
  75 :   
  76 :   //! ポインタが m_obj_data 内を指しているかどうか調べる。
  77 :   bool IsValidPtr(const void* i_ptr, size_t i_size) const;
  78 : 
  79 :   //! m_obj_data の中身を解析して、各メンバに値をセットする。
  80 :   void Analyze();
  81 :   
  82 :   //! 非修飾名を取得する。
  83 :   static bool UndecorateName(
  84 :     std::string&        o_undecorated_name,
  85 :     const std::string&  i_decorated_name
  86 :   );
  87 : 
  88 :   //===========================================================================
  89 :   // メンバ
  90 :   //===========================================================================
  91 : protected:
  92 :   bool                      m_is_opened;          //!< 正しくデータを保持できたか?
  93 :   std::vector<BYTE>         m_obj_data;           //!< obj ファイルデータ。
  94 :   PIMAGE_FILE_HEADER        m_file_header_p;      //!< ファイルヘッダへ。
  95 :   PIMAGE_SECTION_HEADER     m_section_header_p;   //!< セクションヘッダの先頭。
  96 :   PIMAGE_SYMBOL             m_symbols_p;          //!< シンボルテーブル
  97 :   std::vector<std::string>  m_symbol_names;       //!< シンボル名テーブル。
  98 :   std::vector<std::string>  m_undecorated_names;  //!< 修飾を解除したシンボル名。
  99 : };
 100 : 
 101 : 
 102 : #endif

 コンストラクタで obj ファイルの解析を行う Analyze() メソッドを呼び出して、シンボル名などの情報を簡単に取り出せるようにメンバに格納しています。情報を取得するメソッドはメンバから値を取り出して返すだけの簡単なものばかりです。
 各データの取り出し方を見てみましょう。obj_data_p はメモリに読み込んだ obj ファイルの先頭を指すポインタです。

 まず、ファイルヘッダへのポインタは単純に IMAGE_FILE_HEADER* へのキャストです。
ObjFile.cpp
 277 :     // ファイルヘッダ
 278 :     m_file_header_p = reinterpret_cast<IMAGE_FILE_HEADER*>(obj_data_p);

 次はセクションテーブルです。セクションヘッダはファイルヘッダの直後に続くので、ファイルヘッダへのポインタを 1 進めるだけです。また、セクションテーブルはセクションヘッダの配列なので一番最初のセクションヘッダへのポインタということになります。
ObjFile.cpp
 311 :     // 一番最初のセクションヘッダへのポインタを取得する。
 312 :     m_section_header_p = reinterpret_cast<IMAGE_SECTION_HEADER*>(
 313 :                            m_file_header_p + 1
 314 :                          );

 次はシンボルテーブルです。シンボルテーブルはファイルの先頭にシンボルテーブルへのオフセットを加算することで得られます。セクションテーブル同様、IMAGE_SECTION の配列なので一番最初のレコードへのポインタを取得します。
ObjFile.cpp
 321 :     // シンボルテーブルへのポインタを取得する。
 322 :     m_symbols_p = reinterpret_cast<IMAGE_SYMBOL*>(
 323 :                     obj_data_p + m_file_header_p->PointerToSymbolTable
 324 :                   );

 次はロングシンボル名へのポインタです。シンボルテーブルにシンボル情報数を加算すればシンボルテーブルの終端です。
ObjFile.cpp
 331 :     // ロングシンボル名テーブルへのポインタを取得する。
 332 :     // シンボルテーブルの後ろに続く。
 333 :     long_name_table_p = reinterpret_cast<const BYTE*>(
 334 :                           m_symbols_p + m_file_header_p->NumberOfSymbols
 335 :                         );

 最後にシンボル名です。各シンボル名を std::string の vector である m_symbol_names に格納しています。
ObjFile.cpp
 362 :     // シンボル名を取得。
 363 :     for(size_t i = 0; i < m_file_header_p->NumberOfSymbols; i++)
 364 :     {
 365 :       const IMAGE_SYMBOL* symbol_p = m_symbols_p + i;
 366 :       if( IS_VALID_PTR(symbol_p) )
 367 :       {
 368 :         if( symbol_p->N.Name.Short )  // シンボル名は8文字以内
 369 :         {
 370 :           // '\0' で終わっていない場合(8文字)の場合
 371 :           if( symbol_p->N.ShortName[7] != '\0' )
 372 :           {
 373 :             char  name[sizeof(symbol_p->N.ShortName) + 1] = { 0 };
 374 :             memcpy(name, symbol_p->N.ShortName, sizeof(symbol_p->N.ShortName));
 375 :             m_symbol_names[i] = name;
 376 :           }
 377 :           // 8文字未満で '\0' で終わっている場合。
 378 :           else
 379 :           {
 380 :             m_symbol_names[i] = reinterpret_cast<const char*>(
 381 :                                   symbol_p->N.ShortName
 382 :                                 );
 383 :           }
 384 :         }
 385 :         // ロングシンボル名の場合。
 386 :         else
 387 :         {
 388 :           const char* long_name = reinterpret_cast<const char*>(
 389 :                                     long_name_table_p + symbol_p->N.Name.Long
 390 :                                   );
 391 :           if( IsValidPtr(long_name, sizeof(symbol_p->N.ShortName) + 2) == false )
 392 :           {
 393 :             _RPTF0(_CRT_WARN, "シンボルテーブルが壊れています。\n");
 394 :             break;
 395 :           }
 396 :           
 397 :           m_symbol_names[i] = long_name;
 398 :         }
 399 :         
 400 :         // 非修飾名の取得
 401 :         if( m_symbol_names[i][0] == '?' )
 402 :           UndecorateName(m_undecorated_names[i], m_symbol_names[i]);
 403 :         else
 404 :           m_undecorated_names[i] = m_symbol_names[i];
 405 :         
 406 :         i += symbol_p->NumberOfAuxSymbols;
 407 :       }
 408 :     }

関数の検出

 obj ファイルからは関数の開始位置を取り出すことができます。シンボル情報が持つセクション番号からセクションデータへのポインタを取得し、同じくシンボル情報がもつ Value の値を加算すると関数の開始位置となります。
 しかし、関数の終了位置はデバッグ情報出力を有効にしていないと生成されません。逆アセンブルして関数の終了位置を調べるというのもできなくはないですが、話が重くなりすぎてしまいます。
 そこで、関数単位で検出するのではなく、セクション単位で検出することにします。obj ファイルのセクションが実行可能ファイルにリンクされた位置が分かればそこから関数の位置が分かります。

セクションデータの比較

 セクションデータの比較は単純に memcmp() するわけにはいきません。再配置情報が指す場所は、実行可能ファイルにリンクされたときにどのような値になっているか分かりません。そこで、再配置情報が指す場所はスキップして memcmp() します。  これを実装したのが ObjFile::IsEqualSection() メソッドになります。IsEqualSection() メソッドは指定したセクションのセクションデータと、渡されたデータが同じデータかどうか調べるメソッドです。
ObjFile.cpp
 569 : //! 再配置情報を考慮してセクションデータが等しいか調べる。
 570 : bool ObjFile::IsEqualSection(
 571 :   int           i_section_index,
 572 :   const BYTE*   i_data,
 573 :   int           i_size
 574 : ) const
 575 : {
 576 :   bool                          retval           = false;
 577 :   const IMAGE_SECTION_HEADER*   section_header_p = NULL;
 578 :   const BYTE*                   section_data_p   = NULL;
 579 :   const IMAGE_RELOCATION*       relocations      = NULL;
 580 :   int                           reloc_count      = 0;
 581 :   size_t                        cmp_index        = 0;
 582 :   size_t                        cmp_size         = 0;
 583 :   
 584 :   do
 585 :   {
 586 :     // セクションヘッダへのポインタを取得。
 587 :     section_header_p = GetSectionHeaderP(i_section_index);
 588 :     // セクションデータへのポインタを取得。
 589 :     section_data_p = GetSectionDataP(i_section_index);
 590 :     if( section_header_p == NULL || section_data_p == NULL )
 591 :     {
 592 :       _RPTF1(_CRT_WARN,
 593 :              "* ERROR * : セクションインデックス %d は有効ではありません。\n",
 594 :              i_section_index
 595 :             );
 596 :       break;
 597 :     }
 598 :     
 599 :     // 再配置情報を取得。
 600 :     reloc_count = GetRelocationCount(i_section_index);
 601 :     relocations = GetRelocations(i_section_index);
 602 :     if( reloc_count && relocations == NULL )
 603 :     {
 604 :       _RPTF0(_CRT_WARN, "再配置情報が壊れています。\n");
 605 :       break;
 606 :     }
 607 :     
 608 :     // 再配置情報がある場合は最初の再配置情報のオフセットまで比較。
 609 :     // ない場合は引数の i_size 分比較。
 610 :     cmp_size = reloc_count ? relocations[0].VirtualAddress : i_size;
 611 :     if( memcmp(i_data, section_data_p, cmp_size) == 0 )  // 最初のブロックが等しい
 612 :     {
 613 :       retval = true;
 614 :       // 全てのブロックが等しいか調べる。
 615 :       for(int i = 1; i <= reloc_count; i++)
 616 :       {
 617 :         cmp_index = relocations[i - 1].VirtualAddress + sizeof(DWORD);
 618 :         if( i == reloc_count )
 619 :         {
 620 :           cmp_size = section_header_p->SizeOfRawData
 621 :                    - (relocations[i - 1].VirtualAddress +  sizeof(DWORD))
 622 :                    ;
 623 :         }
 624 :         else
 625 :         {
 626 :           cmp_size = relocations[i].VirtualAddress
 627 :                    - (relocations[i - 1].VirtualAddress +  sizeof(DWORD))
 628 :                    ;
 629 :         }
 630 :         
 631 :         if( cmp_size
 632 :         &&  memcmp(i_data + cmp_index, section_data_p + cmp_index, cmp_size) != 0 )
 633 :         {
 634 :           retval = false;
 635 :           break;
 636 :         }
 637 :       }
 638 :     }
 639 :   }
 640 :   while( 0 );
 641 :   
 642 :   return retval;
 643 : }

 FindSymbolData() メソッドは上記の IsEqualSection() をメモリにマップされた実行可能ファイルのサイズ分、繰り返し呼び出すメソッドです。検出したい関数の量が多ければ多いほど時間がかかるので、改良が必要になるかもしれません。
ObjFile.cpp
 649 : //! i_image_p から i_symbol_index 番目のシンボルを探す。
 650 : BYTE* ObjFile::FindSymbolData(
 651 :   int     i_symbol_index,
 652 :   BYTE*   i_image_p,
 653 :   size_t  i_image_size
 654 : ) const
 655 : {
 656 :   BYTE*                         retval            = NULL;
 657 :   const IMAGE_SYMBOL*           symbol_p          = NULL;
 658 :   const IMAGE_SECTION_HEADER*   section_header_p  = NULL;
 659 :   const BYTE*                   section_p         = NULL;
 660 :   
 661 :   do
 662 :   {
 663 :     // シンボル情報へのポインタを取得。
 664 :     symbol_p = GetSymbolP(i_symbol_index);
 665 :     if( symbol_p == NULL )
 666 :     {
 667 :       _RPTF1(_CRT_WARN,
 668 :              "シンボルインデックス %d は有効ではありません。\n", i_symbol_index
 669 :             );
 670 :       break;
 671 :     }
 672 :     
 673 :     // セクションヘッダのポインタを取得。
 674 :     section_header_p = GetSectionHeaderP(symbol_p->SectionNumber - 1);
 675 :     if( section_header_p == NULL || section_header_p->SizeOfRawData == 0 )
 676 :     {
 677 :       _RPTF0(_CRT_WARN, "シンボルデータが壊れています。\n");
 678 :       break;
 679 :     }
 680 : 
 681 :     for(size_t i = 0; i < i_image_size - section_header_p->SizeOfRawData; i++)
 682 :     {
 683 :       if( IsEqualSection(
 684 :             symbol_p->SectionNumber - 1,
 685 :             i_image_p + i,
 686 :             i_image_size - i
 687 :           ) )
 688 :       {
 689 :         retval = i_image_p + i + symbol_p->Value;
 690 :         break;
 691 :       }
 692 :     }
 693 :   }
 694 :   while( 0 );
 695 :   
 696 :   return retval;
 697 : }

printf() をフックしてみる

 printf() をフックして、渡された文字列を MessageBox() で表示するようにしてみましょう。パイプ使ったほうが楽で便利なのはおいといて・・・。
 サンプルプログラムには target, exe, dll の3つのフォルダが含まれています。内訳は以下のようになってます。

target フォルダ printf() をフックする対象プログラムで、伝統的な Hello, world! プログラムのソースが含まれています。
dll フォルダ API フック同様にターゲットプログラムに注入される DLL のソースが入っています。 ロード時に対象プログラムから printf() を検出して置き換える役割を担います。
exe フォルダ target.exe をサスペンド状態で起動して、上記の DLL を注入してから開始させるプログラムのソースです。

 さて、上記3つの exe, dll 以外にもう一つ必要なものがあります。printf() をフックするには printf() の obj ファイルが必要でしたね。  printf() を含む obj は C 言語のランタイムライブラリの中に入っていますので、そこから抽出してきます。ただし、ランタイムライブラリはマルチスレッド対応やデバッグ用によって使用されるファイルが異なりますので注意が必要です(最近ではシングルスレッド用のは廃止されて、リリース用の libcmt.lib とデバッグ用の libcmtd.lib の 2 つになっています)。検出に使う obj を抽出してくるライブラリとフック対象プログラムをリンクする時に使うライブラリは同じものを使ってください。今回、私は libcmt.lib を使いました。
 では、printf() の obj を抽出してみましょう。Windows SDK や .NET のコマンドプロンプトを起動して以下のようにコマンドを入力します。
lib.exe /list libcmt.lib | grep printf
すると
build\intel\mt_obj\wprintf.obj
build\intel\mt_obj\vwprintf.obj
build\intel\mt_obj\vsprintf.obj
build\intel\mt_obj\vfprintf.obj
build\intel\mt_obj\vprintf.obj
build\intel\mt_obj\swprintf.obj
build\intel\mt_obj\sprintf.obj
build\intel\mt_obj\snprintf.obj
build\intel\mt_obj\printf.obj
build\intel\mt_obj\fwprintf.obj
build\intel\mt_obj\fprintf.obj
build\intel\mt_obj\cwprintf.obj
build\intel\mt_obj\cprintf.obj
といったリストが表示されます。この中の build\intel\mt_obj\printf.obj が printf() を含んでいる obj ファイルになります。さらにコマンドで以下のように入力すると printf.obj が抽出されます。
lib.exe /extract:build\intel\mt_obj\printf.obj libcmt.lib
 /list はライブラリに含まれる obj ファイルを一覧表示するオプションで、/extract:membername は指定された obj ファイルを抽出するオプションです。
 これで、必要なものがそろいました。hook.exe から target.exe を起動するとメッセージボックスで Hello, world! と表示されるのが確認できると思います。

hook.exe を実行した結果

まとめ

 冒頭で述べたように、スタティックライブラリ関数の検出はフック以外にもいろいろ用途があります。しかし、今回は、printf() をフックするだけだったので大したことありませんでしたが、本格的にやろうとすると色々と問題が出てくるでしょう。
 例えば、ライブラリに含まれる関数全てを実行可能ファイルの中から検出するとなるとかなり時間がかかってしまいます。また、全く別の関数でも再配置前のセクションデータがまったく同じなどのケースもでてくるでしょう。
 機会があればそこらへんを扱ってみたいと思います。でも次回は lib ファイルについてかな・・・。