API Hook完全手册
注:本文是根據我兩年前寫的一個系統行為監測程序寫成(參考了一些書籍和文章)。最近在論壇上看到有不少人在問關于API Hook的問題,便寫成此文,希望能對朋友們在寫API Hook代碼的時候能夠有所幫助。
1?基本原理
API Hook是什么我就不多說了,直接進入正題。API Hook技術主要有下面的技術難點:
1.?????如何將自己的的代碼Inject到其他進程
2.?????如何Hook到API
1.1?代碼的Injection
常用的方法有:
1.?????使用注冊表HKLM/Software/Microsoft/Windows NT/CurrentVersion/Windows/AppInit_DLLs
?
這種方法可以指定多個DLL,用空格隔開。這些DLL會被任何用到User32.dll的所有程序自動加載。當User32.dll加載的時候,User32.dll的DllMain會收到一個DLL_PROCESS_ATTACH通知,User32在這個時候讀取注冊表項中的值,調用LoadLibrary加載各個DLL。
顯然使用這種方法要求設置注冊表之后立刻重起系統,不過一般情況下這不是大問題。這種方法的主要問題在于,只有用到User32.dll的應用程序才會被Inject。所有的GUI和少部分CUI程序會用到User32.dll,所以如果你的API Hook程序不打算監視CUI程序的話,那么可能問題并不太大。但是如果你的API Hook程序需要監視系統中所有進程的話,這種方法的限制將是非常致命的。
?
2.?????調用SetWindowsHookEx(WH_GETMESSAGE, …, 0)
?
可以使用SetWindowsHookEx(WH_GETMESSAGE, …, 0)?設置全局的消息鉤子,雖然可能你的程序并不用到消息鉤子,但是鉤子的一個副作用是會將對應的DLL加載到所有的GUI線程之中。類似的,只有用到GUI的進程才會被掛接。雖然有這種限制,這種方法仍然是最常用的掛接進程的方法。
?
3.?????使用CreateRemoteThread函數在目標進程中創建遠程線程
?
這種方法可以在任意的目標進程中創建一個遠程線程,遠程線程中可以執行任意代碼,這樣便可以做到把我們的代碼Inject到目標進程中。這種方法具有最大的靈活性,但是難度也最高:
a)?遠程線程代碼必須可以自重定位
b)?要能夠監視進程的啟動和結束,這樣才可以掛接到所有進程
這兩個問題都是可以解決的,在本文中我將重點講述如何創建遠程線程和解決這兩個問題。
?
4.?????如果你只是要掛接某個特定進程的并且情況允許你自己來創建此進程,你可以調用CreateProcess(…, CREATE_SUSPENDED)創建子進程并暫停運行,然后修改入口代碼使之調用LoadLibrary加載自己的DLL。該方法在不同CPU之間顯然是無法移植的。
1.2 Hook API
常用的方法有:
1.?????找到API函數在內存中的地址,改寫函數頭幾個字節為JMP指令跳轉到自己的代碼,執行完畢再執行API開頭幾個字節的內容再跳回原地址。這種方法對CPU有較大的依賴性,而且在多線程環境下可能出問題,當改寫函數代碼的時候有可能此函數正在被執行,這樣做可能導致程序出錯。
2.?????修改PE文件的IAT (Import Address Table),使之指向自己的代碼,這樣EXE/DLL在調用系統API的時候便會調用你自己的函數
?
2 PE文件結構和輸入函數
Windows9x、Windows NT、Windows 2000/XP/2003等操作系統中所使用的可執行文件格式是純32位PE(Portable Executable)文件格式,大致如下:
文件中數據被分為不同的節(Section)。代碼(.code)、初始化的數據(.idata),未初化的數據(.bss)等被按照屬性被分類放到不同的節中,每個節的屬性和位置等信息用一個IMAGE_SECTION_HEADER結構來描述。所有的這些IMAGE_SECTION_HEADER結構組成一個節表(Section Table),這個表被放在所有節數據的前面。由于數據按照屬性被放在不同的節中,那么不同用途但是屬性相同的數據可能被放在同一個節中,因此PE文件中還使用IMAGE_DATA_DIRECTORY數據目錄結構來指明這些數據的位置。數據目錄和其他描述文件屬性的數據和在一起稱為PE文件頭。PE文件頭被放在節和節表的前面。PE文件中的數據位置使用RVA(Relative Virtual Address)來表示。RVA指的是相對虛擬地址,也就是一個偏移量。當PE文件被裝入內存中的時候,Windows把PE文件裝入到某個特定的位置,稱為映像基址(Image Base)。而某個RVA值表示某個數據在內存中相對于映像基址的偏移量。
輸入表(Import Table)是來放置輸入函數(Imported functions)的一個表。輸入函數就是被程序調用的位于外部DLL的函數,這些函數稱為輸入函數。它們的代碼位于DLL之中,程序通過引用其DLL來訪問這些函數。輸入表中放置的是這些函數的名稱(或者序號)以及函數所在的DLL路徑等有關信息。程序通過這些信息找到相應的DLL,從而調用這些外部函數。這個過程是在運行過程中發生的,因此屬于動態鏈接。由于操作系統的API也是在DLL之中實現的,因此應用程序調用API也要通過動態連接。在程序的代碼中,當需要調用API的時候,就執行類似下面語句:
| ? 0040100E??????CALL??????????0040101A |
可以看到這是一個call語句。Call語句則調用下面的語句:
| ? 0040101A??????JMP???????????DWORD PTR [00402000] |
上面的代碼稱為樁代碼(Stub code),jmp語句中的目標地址[00402000]才是API函數的地址。這段Stub code位于.lib輸入庫中。如果加以優化,那么調用代碼是下面這樣:
| ? XXXXXXXX??????CALL??????????DWORD PTR [XXXXXXXX] |
其中[XXXXXXXX]指向IAT(Import Address Table)即輸入地址表中的表項。表項中指定了API的目標地址。這是經過編譯器優化過的調用方法,通常速度要比原來的CALL+JMP快一些。
3?掛接API
從上面的PE文件結構可知,當我們知道了IAT中的地址所在位置,便可以把原來的API?的地址修改為新的API的地址。這樣,進程在調用API的時候就會調用我們所提供的新的API的地址。修改輸入表可以通過調用ImageDirectoryEntryToData?API函數得到內存中模塊的輸入表的地址:
| ????ULONG ulSize; ????PIMAGE_IMPORT_DESCRIPTOR pid = (PIMAGE_IMPORT_DESCRIPTOR) ????????ImageDirectoryEntryToData( ????????hModule, ????????TRUE, ????????IMAGE_DIRECTORY_ENTRY_IMPORT, ????????&ulSize ); |
這個函數返回一個IMAGE_IMPORT_DESCRIPTOR的指針,指向輸入描述符數據。然后,遍歷該描述符表通過比較DLL名稱查找到相應的DLL所對應的IMAGE_IMPORT_DESCRIPTOR:
| ????// if this image has no import section, just simply return and do nothing ????if( pid == NULL ) ????????return; ? ????// find the corresponding item ????while( pid->Name ) ????{ ????????// pid->Name contains the RVA addr of the module name string ????????PSTR pszModName = (PSTR) ( (PBYTE)hModule + pid->Name ); ????????if( lstrcmpiA( pszModuleName, pszModName ) == 0 ) ????????{ ????????????// found ????????????break;??? ????????} ? ????????pid++; ????} ? ????if( pid->Name == 0 ) ????{ ????????// not found, just return ????????return; ????} |
找到相應的DLL之后,遍歷其IAT表,根據地址pfnCurrentFuncAddr找到相應的表項,修改之
| ????// get caller's import address table(IAT) for the callee's functions ????PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA) ( (PBYTE)hModule + pid->FirstThunk ); ? ????while( pThunk->u1.Function ) ????{ ????????PROC *ppfnEntry = (PROC*) &(pThunk->u1.Function); ? ????????if( *ppfnEntry == pfnCurrentFuncAddr ) ????????{ ????????????// … ????????????// Modify IAT ????????????// … ????????} ? ????????pThunk++; ????} |
修改的時候,需要改變該塊內存的保護為可讀寫,需要通過VirtualQuery獲得內存的信息,然后通過VirtualProtectEx修改為可讀寫。之后可以通過WriteProcessMemory修改內存,修改完畢之后還要通過VirtualProtectEx再改回來。
| ????????????SIZE_T sBytesWritten; ????????????BOOL bProtectResult = FALSE; ????????????DWORD dwOldProtect = 0; ??????????????? ????????????MEMORY_BASIC_INFORMATION memInfo; ??????????? ????????????if( ::VirtualQuery( ppfnEntry, &memInfo, sizeof( memInfo ) ) > 0 ) ????????????{ ??????????????? ????????????????// change the pages to read/write ????????????????bProtectResult = ????????????????????::VirtualProtect( ????????????????????????memInfo.BaseAddress, ????????????????????????memInfo.RegionSize, ????????????????????????PAGE_READWRITE, ????????????????????????&dwOldProtect ); ??????????? ? ????????????????// then write it ????????????????::WriteProcessMemory( ::GetCurrentProcess(), ????????????????????ppfnEntry, &pfnReplacementFuncAddr, sizeof( PROC * ), &sBytesWritten ????????????????????); ??????????????? ????????????????// restore the page to its old protect status ????????????????bProtectResult = ????????????????????::VirtualProtect( ????????????????????????memInfo.BaseAddress, ????????????????????????memInfo.RegionSize, ????????????????????????PAGE_READONLY, ????????????????????????&dwOldProtect ); ????????????} |
?
3?遠程線程
遠程線程是Win2000以上才支持的技術。簡單來講,CreateRemoteThread函數會在其他進程中創建一個線程,執行指定的代碼。因為這個線程并非在調用進程之中,而是在其他進程,因此稱之為遠程線程(Remote Thread)。CreateRemoteThread的原型如下:
| HANDLE WINAPI CreateRemoteThread( ??HANDLE hProcess, ??LPSECURITY_ATTRIBUTES lpThreadAttributes, ??SIZE_T dwStackSize, ??LPTHREAD_START_ROUTINE lpStartAddress, ??LPVOID lpParameter, ??DWORD dwCreationFlags, ??LPDWORD lpThreadId ); |
雖然概念上非常簡單,但是使用CreateRemoteThread還會有一些問題:
1.??lpStartAddress必須是其他進程的地址,但是我們又如何把代碼放到另外一個進程中呢?幸運的是,有兩個函數可以做到這一點:VirtualAllocEx和WriteProcessMemory,前者可以在指定進程中分配一塊內存,WriteProcessMemory可以修改指定進程的代碼。因此,先調用VirtualAllocEx在指定進程中分配內存,再調用WriteProcessMemory將代碼寫入到分配好的內存中,再調用CreateRemoteThread創建遠程線程執行在事先準備好的代碼。
2.??此外,這些代碼必須得是自重定位的代碼。在解釋自重定位之前,先解釋一下什么是重定位。在程序訪問數據的時候,必須得訪問某個絕對地址,如:
| MOV EAX, DWORD PTR [00400120H] |
[00400120]?便是一個絕對地址。但是,由于程序實際上可以任意地址加載(這句話其實是不準確的,后面會解釋),因此這個地址不可能是固定的,而是會在加載的時候改變的。假如程序在0x00400000地址加載,訪問地址是0x00400120,那么如果程序在0x00800000加載的話,那么地址應該會變成0x00800120,否則便會訪問到錯誤的地址。因此,有必要在程序加載的時候修正這些地址,這個工作是由Windows的PE Loader,也就是程序的加載器負責的。當編譯連接的時候,在EXE/DLL中會保存那些地方的數據需要重定位,并把這些位置的RVA和數據本身的RVA保存在.reloc重定位節中,從而在加載的時候,PE Loader會自動檢查重定位節的內容并在程序執行之前對這些數據進行修正。
實際上,并非所有EXE/DLL都需要重定位。由于在單個地址空間中只有一個EXE,而這個EXE必然最先加載,因此這個EXE的加載地址總是不變的。因此,一般情況下EXE并不需要重定位信息,編譯器一般在編譯鏈接的時候會將EXE中的重定位信息去掉,以減少程序大小加快加載速度和運行速度。EXE一般在0x40000000的地址加載,一般沒有特別原因無需修改。而DLL因為一般無法保證預先設置好的加載地址總能夠滿足。比如DLL可能指定在0x10000000地址加載,但是有可能此地址已經有其他DLL占據或者被EXE占據,DLL必須得在另外的地址加載,因此一般在DLL中總是保存重定位信息。
一段代碼,一般情況下無法在任意地址執行。假設我們有下面的代碼:
| 00400120 12h, 34h, 56h, 78h 00400124 MOV EAX, DWORD PTR [00400120H] … |
如果我們手動把這段代碼copy到另外一個地方,如00500000,那么顯然00400120H這個地址需要被修改,我們當然可以仿照自重定位的方法來手動修改這個地址值,但是通常較簡單的方法是寫自重定位代碼,這樣的代碼可以在任意地址執行,具體做法如下:
| call????@F @@: ???????pop ebx ???????sub ebx,offset @B DATA???db 12h, 34h, 56h, 78h ???????MOV????EAX, [EBX + DATA] |
可以看到,該段代碼通過使用call指令壓入當前地址eip并彈出從而得到當前地址。然后,用當前地址減去其標號的偏移量就得到重定位修正值,存入ebx之中。之后,就可以使用ebx作為一個基準來訪問數據,以后訪問數據可以用EBX + ???來訪問,這樣由于EBX會根據當前的地址值而變化,所以這段代碼是自重定位的。
下面給出一段代碼,這段代碼中的InjectRemoteCode函數負責將RemoteThread這個函數的自重定位代碼Copy到其他進程中執行:
| ;============================================================================= ; RemoteThread.ASM ; Author : ATField ; Description : ;???????This assembly file contains a InjectRemoteCode function ;???????which injects remote code into a process ; History : ;???????2004-3-8????????Start ;???????2004-3-9????????Completed and tested.???? ;???????2004-3-26???????bug fix: ;???????????????????????not all clients connected ;???????????????????????????Wait for completion of the remote thread????????????????????????????????????????????????????????????????????? ;============================================================================= ? ????????.386 ????????.MODEL FLAT, STDCALL????????????; must be stdcall here, ????????????????????????????????????????; or link error will occur ????????OPTION CASEMAP:NONE ??????? ??????? ????????INCLUDE?????WINDOWS.INC ????????INCLUDE?????USER32.INC ????????INCLUDELIB??USER32.LIB ????????INCLUDE?????KERNEL32.INC ????????INCLUDELIB??KERNEL32.LIB ????????;INCLUDE????MACRO.INC ? ? ????????.DATA??????????? hRemoteThread???????dd 0 szKernel32??????????db 'Kernel32.dll',0 hmodKernel32????????dd 0 szGetProcAddress????db 'GetProcAddress',0 szLoadLibraryA??????db 'LoadLibraryA',0 lpRemoteCode????????dd 0 lpGetProcAddress????dd 0 lpLoadLibraryA??????dd 0????????? ????????.CODE?? ? ;============================================================================= ; remote code starts here ;============================================================================= REMOTE_CODE_START???equ this byte ? ;============================================================================= ; data ;============================================================================= lpRemoteGetProcAddress????dd 0 lpRemoteLoadLibraryA??????dd 0????????? szRemoteDllPathName???????db 255 dup(0) lpRemoteDllHandle?????????dd 0 lpRemoteInitDll???????????dd 0 szRemoteInitDllFuncName???db 'InitializeDll',0 ;============================================================================= ? RemoteThread????PROC uses ebx lParam ??????? ????????;===================================================================== ????????; relocation ????????;===================================================================== ????????; just for debug ????????;int 3 ??????? ????????call????@F ????????@@: ????????pop ebx ????????sub ebx,offset @B ??????? ????????; LoadLibraryA szRemoteDllPathName ????????lea?????ecx, [ebx + offset szRemoteDllPathName] ????????push????ecx ????????call????[ebx + offset lpRemoteLoadLibraryA] ??????? ????????test????eax, eax ????????jz??????error ??????? ????????mov?????[ebx + offset lpRemoteDllHandle], eax ????????; GetProcAddress hModule InitializeDll ????????lea?????ecx, [ebx + offset szRemoteInitDllFuncName] ????????push????ecx?????????????????????????????????????; 'InitializeDll' ????????push????[ebx + offset lpRemoteDllHandle]????????; hmodule ????????call????[ebx + offset lpRemoteGetProcAddress] ??????? ????????test????eax, eax ????????jz?????error ??????????? ????????; InitializeDll() ????????call????eax ????????ret error: ????????mov eax, -1 ????????ret RemoteThread????endp ? REMOTE_CODE_END?????equ this byte REMOTE_CODE_LENGTH??equ offset REMOTE_CODE_END - offset REMOTE_CODE_START ;============================================================================= ; remote code ends ;============================================================================= ? ; BUG FIX: do not use FAR here! InjectRemoteCode????PROC C, hProcess : HANDLE, szDllPathName : DWORD ??????? ????????INVOKE GetModuleHandleA, offset szKernel32 ????????.IF eax ????????????mov hmodKernel32, eax ????????.ELSE ????????????mov eax, 0 ????????????ret ????????.ENDIF ??????? ????????INVOKE GetProcAddress, hmodKernel32, addr szGetProcAddress ????????mov????lpGetProcAddress, eax ??????? ????????INVOKE GetProcAddress, hmodKernel32, addr szLoadLibraryA ????????mov????lpLoadLibraryA, eax ??????? ????????INVOKE VirtualAllocEx,hProcess,NULL,REMOTE_CODE_LENGTH,MEM_COMMIT,PAGE_EXECUTE_READWRITE ??????? ????????.IF eax ????????????; memory allocation success ??????????? ????????????mov lpRemoteCode,eax ??????????? ????????????; copy the code ????????????INVOKE??WriteProcessMemory,hProcess,lpRemoteCode,/ ????????????????offset REMOTE_CODE_START,REMOTE_CODE_LENGTH,NULL ??????????? ????????????; write function start addresses to the remote memory?? ????????????INVOKE??WriteProcessMemory,hProcess,lpRemoteCode,/ ????????????????offset lpGetProcAddress,sizeof dword * 2,NULL ??????????? ????????????; write dll path name to the remote memory ????????????INVOKE??lstrlen, szDllPathName ????????????mov ecx, eax ????????????inc ecx ??????????? ????????????mov ebx, lpRemoteCode ????????????add ebx, 8 ????????????INVOKE??WriteProcessMemory,hProcess,ebx,szDllPathName,ecx,NULL ?????????????????????????????????? ????????????mov eax,lpRemoteCode ????????????add eax,offset RemoteThread - offset REMOTE_CODE_START ????????????INVOKE??CreateRemoteThread,hProcess,NULL,0,eax,0,0,NULL ??????????? ????????????mov hRemoteThread, eax ????????????.IF hRemoteThread ????????????????INVOKE WaitForSingleObject, hRemoteThread, INFINITE?????????? ????????????????INVOKE CloseHandle, hRemoteThread?????????????? ????????????.ELSE ????????????????jmp errorHere ????????????.ENDIF ??????????? ????????.ELSE ????????????jmp errorHere?? ????????.ENDIF ??????? ????????mov eax, 0 ?????? ????????ret errorHere: ????????mov eax, -1 ????????ret InjectRemoteCode????ENDP ? ????????END |
上面講到了CreateremoteThread的做法,可以看到使用CreateRemoteThread是十分復雜的。不過,實際上,我們并不用總是這么做,還有更簡單的方法:利用Kernel32.dll中的LoadLibrary這個函數。由于Kernel32.dll在每個EXE中都會被加載,而且由于Kernel32.dll總是第一個被加載的,因此Kernel32.dll的加載地址總是相同的,換句話說,在我們的主程序中Kernel32.dll中的LoadLibrary函數的地址同時也是其他程序中LoadLibrary函數的地址,而LoadLibrary可以加載任意DLL。此外,LoadLibrary只有一個參數,正好和普通線程的要求相同!所以我們只要調用CreateRemoteThread(…, LoadLibrary, DLL_PathName)便可以將Dll Inject到任意進程中。唯一需要注意的就是,由于LoadLibrary是在其他進程中運行,而LoadLibrary的參數必須保存在另外的進程中。怎么做到這一點呢?回憶一下前文提到了兩個函數VirtualAllocEx和WriteProcessMemory,正好我們可以利用這兩個函數分配一塊內存然后把Dll的路徑名Copy到該內存中去。
此外,由于DLL中的代碼是可以重定位的,因此實際上我們會把API Hook的代碼放在DLL中,這樣寫Hook代碼的時候便不用考慮重定位問題。
4?監視進程的啟動
綜合上面的內容,我們已經可以掛接單個進程中的指定API了。不過這還不夠,我們還需要掛接系統中的所有進程。如果在程序運行之后,不允許新進程的創建,那么掛接所有進程則是非常容易的。Windows操作系統提供了一個CreateToolhelp32Snapshot的API函數。這個API函數創建當前系統的快照(Snapshot),這個快照可以是所有進程的快照(參數是TH32CS_SNAPPROCESS),或者是指定某個進程的所有模塊(Module)的快照(參數是TH32CS_SNAPMODULE),等等。通過調用CreateToolhelp32Snapshot函數獲得了所有進程之后,便可以依次掛接各個進程。但是事情并非如此簡單。用戶和操作系統都可以啟動新的進程,這樣單純的調用CreateToolhelp32Snapshot函數并不能解決問題。所以需要一種機制來通知本系統新進程的創建和結束。經過查閱相關資料(其實也就是Google啦),發現監視系統進程開始和結束的最好方法是通過DDK中的PsSetCreateProcessNotifyRoutine函數,其原型為:
| NTSTATUS??PsSetCreateProcessNotifyRoutine( ????IN PCREATE_PROCESS_NOTIFY_ROUTINE??NotifyRoutine, ????IN BOOLEAN??Remove ????); |
NotifyRoutine指定了當進程被創建和結束的時候所需要調用的回調函數。則Remove是用來告訴該函數是設置該回調還是移除。NotifyRoutine的類型為PCREATE_PROCESS_NOTIFY_ROUTINE,其定義為:
| VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE) ( ????IN HANDLE??ParentId, ????IN HANDLE??ProcessId, ????IN BOOLEAN??Create ????); |
ParentId和ProcessId用來標識進程,Create則是用來表示該進程是正在被創建還是正在被結束。這樣,每當進程被創建或者結束的時候,操作系統就會立刻調用NotifyRoutine這個回調函數并正確提供參數。
由于這個函數是由ntdll.dll所輸出的,屬于Windows的內核空間,因此必須編寫一個處于內核模式的驅動程序才可以。但是,至此問題并沒有完全解決。內核模式的驅動程序和用戶模式的主程序如何通訊呢?這里就需要用到IO請求包IRP(IO Request Packet)。這個IRP的定義為:
| typedef struct _CallbackInfo { ????HANDLE??hParentId; ????HANDLE??hProcessId; ????BOOLEAN bCreate; } CALLBACK_INFO, *PCALLBACK_INFO; |
其字段的意義就和PCREATE_PROCESS_NOTIFY_ROUTINE一樣,不再贅述。
用戶模式的程序通過DeviceIoControl函數發送IO請求包到內核模式的驅動。內核模式接收到此請求包,并填寫數據到用戶程序所提供的CALLBACK_INFO緩沖區里。這樣通過檢查CALLBACK_INFO的值就可以知道hProcessId所指定的進程是正在被創建或者結束了。
雖然有了數據交換的機制,這還是不夠。這樣只能告訴用戶程序究竟是哪一個進程,是創建還是結束,但是無法通知用戶程序此事件的發生。通常,通知某個程序某個事件的發生一般的方法是使用事件(Event)。驅動程序創建一個內核事件(Kernel Event)。用戶程序打開這個事件用于同步。每當事件發生的時候驅動程序就首先把該事件設置為Signaled,然后再Non-signaled。這樣用戶程序就可以接收到通知了。但是為什么需要首先設置為Signaled,然后再Non-signaled?因為用戶程序沒有權限來設置其狀態,因此只能由驅動程序來設置,首先設置為Signaled,然后再Non-signaled是唯一的辦法。
有了這兩種方法,就可以掛接操作系統中的所有進程了。首先,主線程調用CreateToolhelp32Snapshot函數創建系統內所有進程的快照,掛接這些進程,然后啟動驅動程序,在主程序中啟動一個新線程等待Event來監視新的進程的創建和舊進程的結束。驅動程序的代碼和監聽的代碼可以在http://www.codeproject.com/threads/procmon.asp下載到。
5?其他問題
5.1 Unicode
大部分Windows API均有兩個版本:Ansi和Unicode。如GetWindowText API實際上只是一個宏,實際上在不同編譯選項下對應GetWindowTextA和GetWindowTextW。在NT系統下,GetWindowTextA只是做一個轉換,再調用GetWindowTextW,實際的實現在GetWindowTextW中。因此,掛接API必須要Hook兩個版本,實際在Hook的時候,我們也可以仿照Windows的做法,讓GetWindowTextA做一個簡單字符串轉換,然后直接調GetWindowTextW即可。可能有朋友要問了,為何不直接Hook GetWindowTextW呢?反正GetWindowTextA要調GetWindowTextW就不用Hook GetWindowTextA了嘛。不過實際上,因為GetWindowTextA和GetWindowTextW在同一個DLL中,他們的調用很有可能并不是通過IAT來,而是直接調用的關系,所以GetWindowTextA會繞過我們的Hook機制而直接調到原始的GetWindowTextW,這不是我們希望看到的,所以兩個版本保險起見都應該Hook。
5.2 IPC
由于Hook的API代碼位于某個DLL中,這個DLL處于不同的進程,因此需要用到IPC機制在主程序和其他被Hook的進程進行通訊。不同進程之間的通訊稱之為IPC(Interprocess Communication),大概的方法有下面幾種:
1.?????Pipe。管道是比較常用的IPC機制,可以傳輸大量數據,代碼寫起來也比較方便。管道也可以用于網絡間不同計算機通訊,但是有一定限制。
2.?????Socket。雖然Socket一般用于網絡,但是顯然也可以用于本機,優點是大家可能對Socket編程比較熟悉,此外可以很容易擴展到網絡之間的通訊,基本沒有限制,因此也是很不錯的選擇。
3.?????Message。消息一般適用于比較簡單的通訊,如果要傳遞數據必須要使用WM_COPYDATA消息。優點是比較簡單,但是性能可能無法保證。
4.?????Shared Segment。也就是共享段。簡單來說,就是把EXE/DLL中的某個段標記為共享,這樣多個EXE/DLL的實例之間會共享同一塊內存,通過讀寫此塊內存便可以互相傳遞數據,但是同步比較困難。具體做法是:
| #pragma bss_seg("shared_bss") int a; #pragma bss_seg() #pragma comment(linker, "/Section:shared_bss,rws") |
這樣,變量a便放在了共享段之中。
5.?????Memory Mapped File(內存映射文件)。比較簡單,但是缺點和Shared Segment類似,無法同步。
6.?????Event/Semaphore/Mutex。這些只能用于同步,無法傳遞數據。
7.?????…還有很多
可以根據自己的情況靈活選用。
6?總結
API Hook的通常做法如下:
1.?????通過全局消息鉤子或者驅動程序監視進程啟動/結束來掛接系統中所有進程
a.?????如果不需要掛接CUI程序則選用全局消息鉤子
b.?????否則則選用驅動程序
2.?????通過全局消息鉤子或者遠程線程來注入代碼到目標進程中
a.?????全局消息鉤子無需考慮如何加載DLL的問題,系統會自動加載
b.?????遠程線程一般直接創建線程執行LoadLibrary代碼加載DLL,當然也可以執行自己寫的匯編代碼
3.?????通過修改IAT (Import Address Table)中的API地址為自己的函數地址來Hook API。所使用的API是ImageDirectoryEntryToData.
4.?????自己編寫的API的代碼放在DLL中以解決重定位問題(如果用全局消息鉤子的話放在DLL是強制要求)
7?相關參考文獻
我當初在寫程序和寫作本文的時候,參考了下面這些書籍和文章,有興趣的朋友可以參考一下看看:
Windows核心編程,第22章
Windows環境下32位匯編語言程序設計,第13章,17章
API Hooking Revealed,?地址:http://www.codeproject.com/system/hooksys.asp
Detecting Windows NT/2K process execution,?地址:http://www.codeproject.com/threads/procmon.asp?
?
作者:??????ATField
Blog:??????http://blog.csdn.net/atfield
總結
以上是生活随笔為你收集整理的API Hook完全手册的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 转贴 CxImage类库使用说明
- 下一篇: Windows Mobile的高效贴图