API のフックは IAT の書き換えやブレークポイントの設置などで比較的簡単にできますし、やり方に関する情報も結構あるんじゃないでしょうか。API フック用のライブラリなんかもどこかで公開されてたと思います。
が、スタティックライブラリ関数のフックなんてのはなかなか聞いたことないのではないかなと思います。今回は、printf() などスタティックライブラリに含まれる関数を実行可能ファイル内から探し出してきてフックする方法について説明します。
スタティックライブラリ関数の検出はフックのほかにも逆アセンブルやエミュレータの開発に役に立つかと思います。
逆アセンブル時にニーモニックだけでなく関数名を表示すれば解析しやすくなります。この機能をもった逆アセンブラは既にありますね。
エミュレータの開発では、機械語レベルやレジスタレベルでエミュレートせずに機能単位で置き換えることによって開発が容易になります(たぶん)。例えば、ハード A でグラフィック用ライブラリとして OpenGL を提供していました。ハード B はハード A とは互換性がありませんが、同じく OpenGL を提供していました。その場合、ハード A 用に開発された実行可能ファイルから OpenGL の関数を探しだして、ハード B 用の OpenGL 関数呼び出しに置き換えてやればよいわけです。
実行可能ファイルにはリンクした中間ファイルの一部がそのまま含まれています。その一部というのは当然コード部分になりますが、それを実行可能ファイル内から探し出してきて、別の関数への jmp 命令に置き換えるという訳です。
つまり、検出するには対象となる実行可能ファイルにリンクされたライブラリ関数と同じものが必要になります。
また、中間ファイルや実行可能ファイルのフォーマットは OS やコンパイラによって異なります。今回は、VisualC++ の特定のバージョンで 32bit 用にコンパイルされた実行可能ファイルを対象とします。
中間ファイルについて知らないと、探したい関数のデータを取り出せませんので、まずはそちらから。図1 は VC が出力する中間ファイルである obj ファイルのフォーマットで、COFF(Common Object File Format)と呼ばれます。図2 は Hello, world がどのように obj ファイルになるかを端的に示したものです。リンクがどのような手順で行われるか見えてきませんか?
ファイルヘッダの定義は 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 フラグがセット されます。 |
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 | 再配置情報のタイプ。 |
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 | 補助シンボル情報の数。 |
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
ObjFile.cpp
277 : // ファイルヘッダ 278 : m_file_header_p = reinterpret_cast<IMAGE_FILE_HEADER*>(obj_data_p);
ObjFile.cpp
311 : // 一番最初のセクションヘッダへのポインタを取得する。 312 : m_section_header_p = reinterpret_cast<IMAGE_SECTION_HEADER*>( 313 : m_file_header_p + 1 314 : );
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 : );
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 : }
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 : }
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() をフックして、渡された文字列を MessageBox() で表示するようにしてみましょう。パイプ使ったほうが楽で便利なのはおいといて・・・。
サンプルプログラムには target, exe, dll の3つのフォルダが含まれています。内訳は以下のようになってます。
target フォルダ | printf() をフックする対象プログラムで、伝統的な Hello, world! プログラムのソースが含まれています。 | dll フォルダ | API フック同様にターゲットプログラムに注入される DLL のソースが入っています。 ロード時に対象プログラムから printf() を検出して置き換える役割を担います。 |
---|---|
exe フォルダ | target.exe をサスペンド状態で起動して、上記の DLL を注入してから開始させるプログラムのソースです。 |
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 ファイルを抽出するオプションです。