コンピュータプログラミングにおいて、フッキングとは、ソフトウェアコンポーネント間で渡される関数呼び出し、メッセージ、イベントを傍受することによって、オペレーティングシステム、アプリケーション、またはその他のソフトウェアコンポーネントの動作を変更または拡張するために使用される一連の手法です。傍受された関数呼び出し、イベント、またはメッセージを処理するコードはフックと呼ばれます。
フックメソッドは、抽象クラスの共通コードをサブクラスのカスタムコードで拡張できるテンプレートメソッドパターンにおいて特に重要です。この場合、各フックメソッドは抽象クラスで空の実装とともに定義され、各具象サブクラスで異なる実装を提供できるようになります。
フックは、デバッグや機能拡張など、さまざまな目的で使用されます。例えば、アプリケーションに到達する前にキーボードやマウスのイベントメッセージを傍受したり、オペレーティングシステムの呼び出しを傍受して動作を監視したり、アプリケーションやその他のコンポーネントの機能を変更したりすることが挙げられます。また、ベンチマークプログラムでも広く使用されており、例えば3Dゲームのフレームレート測定では、入出力はフックを介して行われます。
フックは悪意のあるコードにも利用されることがあります。例えば、ルートキットは、本来であれば自身の存在を明らかにするはずのAPI呼び出しの出力を偽装することで、自身を隠蔽しようとするソフトウェアですが、こうしたルートキットはしばしばフック技術を利用します。
通常、フックはソフトウェアの実行中に挿入されますが、アプリケーションの起動前にフックを実装することも可能です。これらの手法については、以下で詳しく説明します。
フックは、リバースエンジニアリングの手法を用いて、アプリケーションの実行前に実行ファイルまたはライブラリのソースコードを変更することで実現できます。これは通常、関数呼び出しを傍受し、監視したり、完全に置き換えたりするために使用されます。
例えば、逆アセンブラを使用することで、モジュール内の関数のエントリポイントを特定できます。その後、そのエントリポイントを変更して、別のライブラリモジュールを動的にロードし、ロードされたライブラリ内の目的のメソッドを実行するようにすることができます。また、関連する別の方法として、実行可能ファイルのインポートテーブルを変更する方法もあります。このテーブルを変更することで、追加のライブラリモジュールをロードしたり、アプリケーションが関数を呼び出したときに呼び出される外部コードを変更したりできます。
関数フックを実現する別の方法として、ラッパーライブラリを介して関数呼び出しを傍受する方法があります。ラッパーとは、アプリケーションがロードするライブラリのバージョンであり、置き換える元のライブラリと全く同じ機能を備えています。つまり、アクセス可能な関数は、元のライブラリと置き換え後のライブラリで基本的に同じです。このラッパーライブラリは、元のライブラリの任意の機能を呼び出すように設計することも、まったく新しいロジックで置き換えるように設計することもできます。
GNUリンカーはCLI オプションをサポートしています--wrap=foo。このオプションを使用すると、リンカーは へのすべての呼び出しをfooへの呼び出しに置き換えます__wrap_foo。 の元の定義は、fooシンボル で参照できるようになります__real_foo。[ 1 ]この方法は動的リンカー を使用しないため、プログラムのソース コードが利用可能で、ビルドのリンク手順を変更できる場合にのみ使用できます。
オペレーティングシステムやソフトウェアは、実行時にイベントフックを簡単に挿入する手段を提供する場合があります。ただし、フックを挿入するプロセスに十分な権限が付与されていることが前提となります。たとえば、Microsoft Windowsでは、ダイアログ、スクロールバー、メニューなどのシステムイベントやアプリケーションイベントを処理または変更するために使用できるフックを挿入できます。また、キーボードイベントやマウスイベントを挿入、削除、処理、変更するためのフックも使用できます。Linuxでは、 NetFilterを介してカーネル内のネットワークイベントを処理するために、同様の方法でフックを使用できる別の例が提供されています。
このような機能が提供されていない場合、特別なフック手法として、プロセスが行うライブラリ関数呼び出しを傍受する方法が用いられます。関数フックは、対象関数の最初の数個のコード命令を変更して、挿入されたコードにジャンプするようにすることで実現されます。あるいは、共有ライブラリの概念を採用しているシステムでは、メモリ内の割り込みベクタテーブルまたはインポート記述子テーブルを変更することも可能です。これらの手法は基本的にソースコードの変更と同じ考え方に基づいていますが、プロセスが既に実行されている状態で、そのメモリ内の命令や構造体を変更する点が異なります。
クラスが仮想関数(またはメソッド)を定義/継承するたびに、コンパイラは仮想メソッドテーブル(VMT または Vtable)を指す隠しメンバ変数をクラスに追加します。ほとんどのコンパイラは、クラスのすべてのインスタンスの最初の 4 バイトに隠し VMT ポインタを配置します。VMT は基本的に、クラスのインスタンスが呼び出す可能性のあるすべての仮想関数へのポインタの配列です。コンパイル時には、基本関数が呼び出されるのか、派生クラスのオーバーライドされたバージョンの関数が呼び出されるのかがまだわからないため(これによりポリモーフィズムが可能になります)、実行時にこれらのポインタは正しい関数を指すように設定されます。したがって、仮想関数は、それらが現れる VMT 内のそれらへのポインタを置き換えることによってフックできます。以下のコードは、C++ で記述された Microsoft Windows の典型的な VMT フックの例を示しています。[ 2 ]
#include <iostream> #include "windows.h" using namespace std ; class VirtualClass { public : int number ; virtual void VirtualFn1 () //これはフックされる仮想関数です。{ cout << "VirtualFn1 called " << number ++ << " \n\n " ; } }; using VirtualFn1_t = void ( __thiscall * )( void * thisptr ); VirtualFn1_t orig_VirtualFn1 ;void __fastcall hkVirtualFn1 ( void * thisptr , int edx ) //これはフック関数です。フック処理が完了した後、元の VirtualFn1 関数の代わりにプログラムが呼び出すようにします。{ cout << "フック関数が呼び出されました" << " \n " ; orig_VirtualFn1 ( thisptr ); //元の関数を呼び出します。} int main () { VirtualClass * myClass = new VirtualClass (); //VirtualClass の動的に割り当てられたインスタンスへのポインタを作成します。void ** vTablePtr = * reinterpret_cast < void ***> ( myClass ); //VirtualClass の VMT の基底を指すアドレス (VirtualFn1 を指すアドレス) を見つけて、vTablePtr に格納します。DWORD oldProtection ; VirtualProtect ( vTablePtr , 4 , PAGE_EXECUTE_READWRITE , & oldProtection ); // VMT の開始時にページ保護を解除し、最初のポインタを上書きできるようにします。orig_VirtualFn1 = reinterpret_cast < VirtualFn1_t > ( * vTablePtr ); // VMT の VirtualFn1 へのポインタをグローバル変数に格納し、VMT のエントリがフック関数で上書きされた後、後で再度アクセスできるようにします。* vTablePtr = & hkVirtualFn1 ; // 仮想テーブル内の VirtualFn1 へのポインタを、フック関数 (hkVirtualFn1) へのポインタに上書きします。VirtualProtect ( vTablePtr , 4 , oldProtection , 0 ); // 古いページ保護を復元します。myClass -> VirtualFn1 (); // クラスインスタンスから仮想関数を呼び出します。フックされているため、実際にはフック関数 (hkVirtualFn1) が呼び出されます。myClass -> VirtualFn1 (); myClass -> VirtualFn1 (); delete myClass ; return 0 ; }すべての仮想関数はクラスメンバ関数でなければならず、すべての(非静的)クラスメンバ関数は__thiscall呼び出し規約で呼び出されます(メンバ関数が可変個の引数を取る場合は__cdeclで呼び出されます)。__thiscall呼び出し規約は、呼び出し元のクラスインスタンスへのポインタ(一般に「this」ポインタと呼ばれます)をECXレジスタ(x86アーキテクチャの場合)経由で渡します。したがって、フック関数が渡された「this」ポインタを適切にインターセプトして引数として受け取るには、ECXレジスタを参照する必要があります。上記の例では、フック関数(hkVirtualFn1)を__fastcall呼び出し規約を使用するように設定することでこれを実現しています。これにより、フック関数は引数の1つをECXレジスタから取得します。
また、上記の例では、フック関数(hkVirtualFn1)自体はメンバ関数ではないため、__thiscall呼び出し規約を使用できません。代わりに__fastcallを使用する必要があります。これは、引数をECXレジスタから取得する唯一の他の呼び出し規約だからです。
次の例では、Microsoft .NET Frameworkを使用して Microsoft Windows のキーボード イベントにフックします。
System.Runtime.InteropServicesを使用します。名前空間Hooks ;public class KeyHook { /* メンバ変数 */ protected static int Hook ; protected static LowLevelKeyboardDelegate Delegate ; protected static readonly object Lock = new object (); protected static bool IsRegistered = false ;/* DLL インポート */ [DllImport("user32")] private static extern int SetWindowsHookEx ( int idHook , LowLevelKeyboardDelegate lpfn , int hmod , int dwThreadId );[DllImport("user32")] private static extern int CallNextHookEx ( int hHook , int nCode , int wParam , KBDLLHOOKSTRUCT lParam );[DllImport("user32")] private static extern int UnhookWindowsHookEx ( int hHook );/* 型と定数 */ protected delegate int LowLevelKeyboardDelegate ( int nCode , int wParam , ref KBDLLHOOKSTRUCT lParam ); private const int HC_ACTION = 0 ; private const int WM_KEYDOWN = 0x0100 ; private const int WM_KEYUP = 0x0101 ; private const int WH_KEYBOARD_LL = 13 ;[StructLayout(LayoutKind.Sequential)] public struct KBDLLHOOKSTRUCT { public int vkCode ; public int scanCode ; public int flags ; public int time ; public int dwExtraInfo ; }/* メソッド */ static private int LowLevelKeyboardHandler ( int nCode , int wParam , ref KBDLLHOOKSTRUCT lParam ) { if ( nCode == HC_ACTION ) { if ( wParam == WM_KEYDOWN ) System . Console . Out . WriteLine ( "Key Down: " + lParam . vkCode ); else if ( wParam == WM_KEYUP ) System . Console . Out . WriteLine ( "Key Up: " + lParam . vkCode ); } return CallNextHookEx ( Hook , nCode , wParam , lParam ); }public static bool RegisterHook () { lock ( Lock ) { if ( IsRegistered ) return true ; Delegate = LowLevelKeyboardHandler ; Hook = SetWindowsHookEx ( WH_KEYBOARD_LL , Delegate , Marshal . GetHINSTANCE ( System . Reflection . Assembly . GetExecutingAssembly (). GetModules ()[ 0 ] ). ToInt32 (), 0 );if ( Hook != 0 ) return IsRegistered = true ; Delegate = null ; return false ; } }public static bool UnregisterHook () { lock ( Lock ) { return IsRegistered = ( UnhookWindowsHookEx ( Hook ) != 0 ); } } }以下のソースコードは、宛先関数の最初の 6 バイトを新しい関数へのJMP命令で上書きすることによってフックする API/関数フック方法の例です。このコードはDLLファイルにコンパイルされ、任意のDLL インジェクション方法を使用してターゲット プロセスにロードされます。元の関数のバックアップを使用すると、最初の 6 バイトを復元して呼び出しが中断されないようにすることができます。この例では、win32 API関数 MessageBoxW がフックされています。[ 3 ]
/*このアイデアは chrom-lib のアプローチに基づいています。GNU LGPL ライセンスの下で配布されています。chrom-lib のソース: https://github.com/linuxexp/chrom-lib Copyright (C) 2011 Raja Jamwal */ #include <windows.h> #define SIZE 6typedef int ( WINAPI * pMessageBoxW )( HWND , LPCWSTR , LPCWSTR , UINT ); // メッセージボックスのプロトタイプint WINAPI MyMessageBoxW ( HWND , LPCWSTR , LPCWSTR , UINT ); // 迂回路void BeginRedirect ( LPVOID ); pMessageBoxW pOrigMBAddress = NULL ; // 元のバイトのアドレスoldBytes [ SIZE ] = { 0 }; // バックアップバイトJMP [ SIZE ] = { 0 }; // 6 バイトの JMP 命令DWORD oldProtect , myProtect = PAGE_EXECUTE_READWRITE ;INT APIENTRY DllMain ( HMODULE hDLL , DWORD Reason , LPVOID Reserved ) { switch ( Reason ) { case DLL_PROCESS_ATTACH : // アタッチされている場合pOrigMBAddress = ( pMessageBoxW ) GetProcAddress ( GetModuleHandleA ( "user32.dll" ), // 元の"MessageBoxW"のアドレスを取得); if ( pOrigMBAddress != NULL ) BeginRedirect ( MyMessageBoxW ); // 迂回を開始break ;case DLL_PROCESS_DETACH : VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , myProtect , & oldProtect ); // 読み書き保護を割り当てるmemcpy ( pOrigMBAddress , oldBytes , SIZE ); // バックアップを復元するVirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , oldProtect , & myProtect ); // 保護をリセットするcase DLL_THREAD_ATTACH : case DLL_THREAD_DETACH : break ; } return TRUE ; }void BeginRedirect ( LPVOID newFunction ) { BYTE tempJMP [ SIZE ] = { 0xE9 , 0x90 , 0x90 , 0x90 , 0x90 , 0xC3 }; // 0xE9 = JMP 0x90 = NOP 0xC3 = RET memcpy ( JMP , tempJMP , SIZE ); // jmp 命令を JMP に格納DWORD JMPSize = (( DWORD ) newFunction - ( DWORD ) pOrigMBAddress - 5 ); // ジャンプ距離を計算VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , // 読み書き保護を割り当てPAGE_EXECUTE_READWRITE , & oldProtect ); memcpy ( oldBytes , pOrigMBAddress , SIZE ); // バックアップを作成memcpy ( & JMP [ 1 ], & JMPSize , 4 ); // ジャンプ距離でNOPを埋める (JMP、distance(4バイト)、RET) memcpy ( pOrigMBAddress , JMP , SIZE ); // 元の関数の先頭にジャンプ命令を設定VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , oldProtect , & myProtect ); // 保護をリセット}int WINAPI MyMessageBoxW ( HWND hWnd , LPCWSTR lpText , LPCWSTR lpCaption , UINT uiType ) { VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , myProtect , & oldProtect ); // 読み書き保護を設定memcpy ( pOrigMBAddress , oldBytes , SIZE ); // バックアップを復元int retValue = MessageBoxW ( hWnd , lpText , lpCaption , uiType ); // 元の関数の戻り値を取得memcpy ( pOrigMBAddress , JMP , SIZE ); // ジャンプ命令を再度設定VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , oldProtect , & myProtect ); // 保護をリセットreturn retValue ; // 元の戻り値を返す}この例では、 Netfilterを使用して Linux カーネルのネットワークトラフィックを変更するためにフックを使用する方法を示します。
#include <linux/module.h> #include <linux/kernel.h> #include <linux/skbuff.h>#include <linux/ip.h> #include <linux/tcp.h> #include <linux/in.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h>/* パケットをドロップしたいポート */ static const uint16_t port = 25 ;/* これはフック関数自体です */ static unsigned int hook_func ( unsigned int hooknum , struct sk_buff ** pskb , const struct net_device * in , const struct net_device * out , int ( * okfn )( struct sk_buff * )) { struct iphdr * iph = ip_hdr ( * pskb ); struct tcphdr * tcph , tcpbuf ;if ( iph -> protocol != IPPROTO_TCP ) return NF_ACCEPT ;tcph = skb_header_pointer ( * pskb , ip_hdrlen ( * pskb ), sizeof ( * tcph ), & tcpbuf ); if ( tcph == NULL ) return NF_ACCEPT ;return ( tcph -> dest == port ) ? NF_DROP : NF_ACCEPT ; }/* フック関数を登録するために使用します */ static struct nf_hook_ops nfho = { . hook = hook_func , . hooknum = NF_IP_PRE_ROUTING , . pf = NFPROTO_IPV4 , . priority = NF_IP_PRI_FIRST , };static __init int my_init ( void ) { return nf_register_hook ( & nfho ); }static __exit void my_exit ( void ) { nf_unregister_hook ( & nfho ); }module_init ( my_init ); module_exit ( my_exit );以下のコードは、別のモジュールからインポートされた関数をフックする方法を示しています。これは、呼び出し元のプロセスとは異なるプロセスの関数をフックするために使用できます。そのためには、コードをDLLファイルにコンパイルし、 DLLインジェクションの任意の方法を使用してターゲットプロセスにロードする必要があります。この方法の利点は、アンチウイルスソフトウェアやアンチチートソフトウェアによる検出が困難であることです。悪意のある呼び出しを使用しない外部フックとして作成することもできます。ポータブル実行可能ヘッダーにはインポートアドレステーブル(IAT)が含まれており、以下のソースに示すように操作できます。以下のソースはMicrosoft Windows上で動作します。
#include <windows.h>typedef int ( __stdcall * pMessageBoxA ) ( HWND hWnd , LPCSTR lpText , LPCSTR lpCaption , UINT uType ); //これは MessageBoxA 呼び出しの「型」です。pMessageBoxA RealMessageBoxA ; //これは元の関数へのポインタを格納します。void DetourIATptr ( const char * function , void * newfunction , HMODULE module );int __stdcall NewMessageBoxA ( HWND hWnd , LPCSTR lpText , LPCSTR lpCaption , UINT uType ) { // 偽の関数printf ( "MessageBoxA に送信された文字列は次のとおりです: %s \n " , lpText ); return RealMessageBoxA ( hWnd , lpText , lpCaption , uType ); // 実際の関数を呼び出す}int main ( int argc , CHAR * argv []) { DetourIATptr ( "MessageBoxA" ,( void * ) NewMessageBoxA , 0 ); // 関数をフックしますMessageBoxA ( NULL , "Just A MessageBox" , "Just A MessageBox" , 0 ); // 関数を呼び出します -- これにより、偽のフックが呼び出されます。return 0 ; }void ** IATfind ( const char * function , HMODULE module ) { // 指定された関数に固有の IAT (インポート アドレス テーブル) エントリを検索します。int ip = 0 ; if ( module == 0 ) module = GetModuleHandle ( 0 ); PIMAGE_DOS_HEADER pImgDosHeaders = ( PIMAGE_DOS_HEADER ) module ; PIMAGE_NT_HEADERS pImgNTHeaders = ( PIMAGE_NT_HEADERS )(( LPBYTE ) pImgDosHeaders + pImgDosHeaders -> e_lfanew ); PIMAGE_IMPORT_DESCRIPTOR pImgImportDesc = ( PIMAGE_IMPORT_DESCRIPTOR )(( LPBYTE ) pImgDosHeaders + pImgNTHeaders -> OptionalHeader . DataDirectory [ IMAGE_DIRECTORY_ENTRY_IMPORT ]. VirtualAddress );if ( pImgDosHeaders -> e_magic != IMAGE_DOS_SIGNATURE ) printf ( "libPE エラー: e_magic は有効な DOS 署名ではありません\n " );for ( IMAGE_IMPORT_DESCRIPTOR * iid = pImgImportDesc ; iid -> Name != NULL ; iid ++ ) { for ( int funcIdx = 0 ; * ( funcIdx + ( LPVOID * )( iid -> FirstThunk + ( SIZE_T ) module )) != NULL ; funcIdx ++ ) { char * modFuncName = ( char * )( * ( funcIdx + ( SIZE_T * )( iid -> OriginalFirstThunk + ( SIZE_T ) module )) + ( SIZE_T ) module + 2 ); const uintptr_t nModFuncName = ( uintptr_t ) modFuncName ; bool isString = ! ( nModFuncName & ( sizeof ( nModFuncName ) == 4 ? 0x80000000 : 0x8000000000000000 )); if ( isString ) { if ( ! _stricmp ( function , modFuncName )) return funcIdx + ( LPVOID * )( iid -> FirstThunk + ( SIZE_T ) module ); } } } return 0 ; }void DetourIATptr ( const char * function , void * newfunction , HMODULE module ) { void ** funcptr = IATfind ( function , module ); if ( * funcptr == newfunction ) return ;DWORD oldrights , newrights = PAGE_READWRITE ; //保護を READWRITE に更新しますVirtualProtect ( funcptr , sizeof ( LPVOID ), newrights , & oldrights );RealMessageBoxA = ( pMessageBoxA ) * funcptr ; //一部のコンパイラではキャストが必要です (MinGW など)。ただし、MSVC についてはわかりません。* funcptr = newfunction ;//古いメモリ保護フラグを復元します。VirtualProtect (funcptr , sizeof ( LPVOID ) , oldrights , & newrights ) ; }Hook
{{cite web}}:|author=一般的な名前を持っています (ヘルプ)