API Hook是什么我就不多說了,直接進(jìn)入正題。API Hook技術(shù)主要有下面的技術(shù)難點(diǎn):
1. 如何將自己的的代碼Inject到其他進(jìn)程
2. 如何Hook到API
1.1 代碼的Injection
常用的方法有:
1. 使用注冊(cè)表HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
這種方法可以指定多個(gè)DLL,用空格隔開。這些DLL會(huì)被任何用到User32.dll的所有程序自動(dòng)加載。當(dāng)User32.dll加載的時(shí)候,User32.dll的DllMain會(huì)收到一個(gè)DLL_PROCESS_ATTACH通知,User32在這個(gè)時(shí)候讀取注冊(cè)表項(xiàng)中的值,調(diào)用LoadLibrary加載各個(gè)DLL。
顯然使用這種方法要求設(shè)置注冊(cè)表之后立刻重起系統(tǒng),不過一般情況下這不是大問題。這種方法的主要問題在于,只有用到User32.dll的應(yīng)用程序才會(huì)被Inject。所有的GUI和少部分CUI程序會(huì)用到User32.dll,所以如果你的API Hook程序不打算監(jiān)視CUI程序的話,那么可能問題并不太大。但是如果你的API Hook程序需要監(jiān)視系統(tǒng)中所有進(jìn)程的話,這種方法的限制將是非常致命的。
2. 調(diào)用SetWindowsHookEx(WH_GETMESSAGE, …, 0)
可以使用SetWindowsHookEx(WH_GETMESSAGE, …, 0) 設(shè)置全局的消息鉤子,雖然可能你的程序并不用到消息鉤子,但是鉤子的一個(gè)副作用是會(huì)將對(duì)應(yīng)的DLL加載到所有的GUI線程之中。類似的,只有用到GUI的進(jìn)程才會(huì)被掛接。雖然有這種限制,這種方法仍然是最常用的掛接進(jìn)程的方法。
3. 使用CreateRemoteThread函數(shù)在目標(biāo)進(jìn)程中創(chuàng)建遠(yuǎn)程線程
這種方法可以在任意的目標(biāo)進(jìn)程中創(chuàng)建一個(gè)遠(yuǎn)程線程,遠(yuǎn)程線程中可以執(zhí)行任意代碼,這樣便可以做到把我們的代碼Inject到目標(biāo)進(jìn)程中。這種方法具有最大的靈活性,但是難度也最高:
a) 遠(yuǎn)程線程代碼必須可以自重定位
b) 要能夠監(jiān)視進(jìn)程的啟動(dòng)和結(jié)束,這樣才可以掛接到所有進(jìn)程
這兩個(gè)問題都是可以解決的,在本文中我將重點(diǎn)講述如何創(chuàng)建遠(yuǎn)程線程和解決這兩個(gè)問題。
4. 如果你只是要掛接某個(gè)特定進(jìn)程的并且情況允許你自己來創(chuàng)建此進(jìn)程,你可以調(diào)用CreateProcess(…, CREATE_SUSPENDED)創(chuàng)建子進(jìn)程并暫停運(yùn)行,然后修改入口代碼使之調(diào)用LoadLibrary加載自己的DLL。該方法在不同CPU之間顯然是無法移植的。
1.2 Hook API
常用的方法有:
1. 找到API函數(shù)在內(nèi)存中的地址,改寫函數(shù)頭幾個(gè)字節(jié)為JMP指令跳轉(zhuǎn)到自己的代碼,執(zhí)行完畢再執(zhí)行API開頭幾個(gè)字節(jié)的內(nèi)容再跳回原地址。這種方法對(duì)CPU有較大的依賴性,而且在多線程環(huán)境下可能出問題,當(dāng)改寫函數(shù)代碼的時(shí)候有可能此函數(shù)正在被執(zhí)行,這樣做可能導(dǎo)致程序出錯(cuò)。
2. 修改PE文件的IAT (Import Address Table),使之指向自己的代碼,這樣EXE/DLL在調(diào)用系統(tǒng)API的時(shí)候便會(huì)調(diào)用你自己的函數(shù)
2 PE文件結(jié)構(gòu)和輸入函數(shù)
Windows9x、Windows NT、Windows 2000/XP/2003等操作系統(tǒng)中所使用的可執(zhí)行文件格式是純32位PE(Portable Executable)文件格式,大致如下:
文件中數(shù)據(jù)被分為不同的節(jié)(Section)。代碼(.code)、初始化的數(shù)據(jù)(.idata),未初化的數(shù)據(jù)(.bss)等被按照屬性被分類放到不同的節(jié)中,每個(gè)節(jié)的屬性和位置等信息用一個(gè)IMAGE_SECTION_HEADER結(jié)構(gòu)來描述。所有的這些IMAGE_SECTION_HEADER結(jié)構(gòu)組成一個(gè)節(jié)表(Section Table),這個(gè)表被放在所有節(jié)數(shù)據(jù)的前面。由于數(shù)據(jù)按照屬性被放在不同的節(jié)中,那么不同用途但是屬性相同的數(shù)據(jù)可能被放在同一個(gè)節(jié)中,因此PE文件中還使用IMAGE_DATA_DIRECTORY數(shù)據(jù)目錄結(jié)構(gòu)來指明這些數(shù)據(jù)的位置。數(shù)據(jù)目錄和其他描述文件屬性的數(shù)據(jù)和在一起稱為PE文件頭。PE文件頭被放在節(jié)和節(jié)表的前面。PE文件中的數(shù)據(jù)位置使用RVA(Relative Virtual Address)來表示。RVA指的是相對(duì)虛擬地址,也就是一個(gè)偏移量。當(dāng)PE文件被裝入內(nèi)存中的時(shí)候,Windows把PE文件裝入到某個(gè)特定的位置,稱為映像基址(Image Base)。而某個(gè)RVA值表示某個(gè)數(shù)據(jù)在內(nèi)存中相對(duì)于映像基址的偏移量。
輸入表(Import Table)是來放置輸入函數(shù)(Imported functions)的一個(gè)表。輸入函數(shù)就是被程序調(diào)用的位于外部DLL的函數(shù),這些函數(shù)稱為輸入函數(shù)。它們的代碼位于DLL之中,程序通過引用其DLL來訪問這些函數(shù)。輸入表中放置的是這些函數(shù)的名稱(或者序號(hào))以及函數(shù)所在的DLL路徑等有關(guān)信息。程序通過這些信息找到相應(yīng)的DLL,從而調(diào)用這些外部函數(shù)。這個(gè)過程是在運(yùn)行過程中發(fā)生的,因此屬于動(dòng)態(tài)鏈接。由于操作系統(tǒng)的API也是在DLL之中實(shí)現(xiàn)的,因此應(yīng)用程序調(diào)用API也要通過動(dòng)態(tài)連接。在程序的代碼中,當(dāng)需要調(diào)用API的時(shí)候,就執(zhí)行類似下面語句:
0040100E CALL 0040101A
可以看到這是一個(gè)call語句。Call語句則調(diào)用下面的語句:
0040101A JMP DWORD PTR [00402000]
上面的代碼稱為樁代碼(Stub code),jmp語句中的目標(biāo)地址[00402000]才是API函數(shù)的地址。這段Stub code位于.lib輸入庫(kù)中。如果加以優(yōu)化,那么調(diào)用代碼是下面這樣:
XXXXXXXX CALL DWORD PTR [XXXXXXXX]
其中[XXXXXXXX]指向IAT(Import Address Table)即輸入地址表中的表項(xiàng)。表項(xiàng)中指定了API的目標(biāo)地址。這是經(jīng)過編譯器優(yōu)化過的調(diào)用方法,通常速度要比原來的CALL+JMP快一些。
3 掛接API
從上面的PE文件結(jié)構(gòu)可知,當(dāng)我們知道了IAT中的地址所在位置,便可以把原來的API 的地址修改為新的API的地址。這樣,進(jìn)程在調(diào)用API的時(shí)候就會(huì)調(diào)用我們所提供的新的API的地址。修改輸入表可以通過調(diào)用ImageDirectoryEntryToData API函數(shù)得到內(nèi)存中模塊的輸入表的地址:
ULONG ulSize;
PIMAGE_IMPORT_DESCRIPTOR pid = (PIMAGE_IMPORT_DESCRIPTOR)
ImageDirectoryEntryToData(
hModule,
TRUE,
IMAGE_DIRECTORY_ENTRY_IMPORT,
&ulSize );
這個(gè)函數(shù)返回一個(gè)IMAGE_IMPORT_DESCRIPTOR的指針,指向輸入描述符數(shù)據(jù)。然后,遍歷該描述符表通過比較DLL名稱查找到相應(yīng)的DLL所對(duì)應(yīng)的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;
}
找到相應(yīng)的DLL之后,遍歷其IAT表,根據(jù)地址pfnCurrentFuncAddr找到相應(yīng)的表項(xiàng),修改之
// 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++;
}
修改的時(shí)候,需要改變?cè)搲K內(nèi)存的保護(hù)為可讀寫,需要通過VirtualQuery獲得內(nèi)存的信息,然后通過VirtualProtectEx修改為可讀寫。之后可以通過WriteProcessMemory修改內(nèi)存,修改完畢之后還要通過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 遠(yuǎn)程線程
遠(yuǎn)程線程是Win2000以上才支持的技術(shù)。簡(jiǎn)單來講,CreateRemoteThread函數(shù)會(huì)在其他進(jìn)程中創(chuàng)建一個(gè)線程,執(zhí)行指定的代碼。因?yàn)檫@個(gè)線程并非在調(diào)用進(jìn)程之中,而是在其他進(jìn)程,因此稱之為遠(yuǎn)程線程(Remote Thread)。CreateRemoteThread的原型如下:
HANDLE WINAPI CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
雖然概念上非常簡(jiǎn)單,但是使用CreateRemoteThread還會(huì)有一些問題:
1. lpStartAddress必須是其他進(jìn)程的地址,但是我們又如何把代碼放到另外一個(gè)進(jìn)程中呢?幸運(yùn)的是,有兩個(gè)函數(shù)可以做到這一點(diǎn):VirtualAllocEx和WriteProcessMemory,前者可以在指定進(jìn)程中分配一塊內(nèi)存,WriteProcessMemory可以修改指定進(jìn)程的代碼。因此,先調(diào)用VirtualAllocEx在指定進(jìn)程中分配內(nèi)存,再調(diào)用WriteProcessMemory將代碼寫入到分配好的內(nèi)存中,再調(diào)用CreateRemoteThread創(chuàng)建遠(yuǎn)程線程執(zhí)行在事先準(zhǔn)備好的代碼。
2. 此外,這些代碼必須得是自重定位的代碼。在解釋自重定位之前,先解釋一下什么是重定位。在程序訪問數(shù)據(jù)的時(shí)候,必須得訪問某個(gè)絕對(duì)地址,如:
MOV EAX, DWORD PTR [00400120H]
[00400120] 便是一個(gè)絕對(duì)地址。但是,由于程序?qū)嶋H上可以任意地址加載(這句話其實(shí)是不準(zhǔn)確的,后面會(huì)解釋),因此這個(gè)地址不可能是固定的,而是會(huì)在加載的時(shí)候改變的。假如程序在0x00400000地址加載,訪問地址是0x00400120,那么如果程序在0x00800000加載的話,那么地址應(yīng)該會(huì)變成0x00800120,否則便會(huì)訪問到錯(cuò)誤的地址。因此,有必要在程序加載的時(shí)候修正這些地址,這個(gè)工作是由Windows的PE Loader,也就是程序的加載器負(fù)責(zé)的。當(dāng)編譯連接的時(shí)候,在EXE/DLL中會(huì)保存那些地方的數(shù)據(jù)需要重定位,并把這些位置的RVA和數(shù)據(jù)本身的RVA保存在.reloc重定位節(jié)中,從而在加載的時(shí)候,PE Loader會(huì)自動(dòng)檢查重定位節(jié)的內(nèi)容并在程序執(zhí)行之前對(duì)這些數(shù)據(jù)進(jìn)行修正。
實(shí)際上,并非所有EXE/DLL都需要重定位。由于在單個(gè)地址空間中只有一個(gè)EXE,而這個(gè)EXE必然最先加載,因此這個(gè)EXE的加載地址總是不變的。因此,一般情況下EXE并不需要重定位信息,編譯器一般在編譯鏈接的時(shí)候會(huì)將EXE中的重定位信息去掉,以減少程序大小加快加載速度和運(yùn)行速度。EXE一般在0x40000000的地址加載,一般沒有特別原因無需修改。而DLL因?yàn)橐话銦o法保證預(yù)先設(shè)置好的加載地址總能夠滿足。比如DLL可能指定在0x10000000地址加載,但是有可能此地址已經(jīng)有其他DLL占據(jù)或者被EXE占據(jù),DLL必須得在另外的地址加載,因此一般在DLL中總是保存重定位信息。
一段代碼,一般情況下無法在任意地址執(zhí)行。假設(shè)我們有下面的代碼:
00400120 12h, 34h, 56h, 78h
00400124 MOV EAX, DWORD PTR [00400120H]
…
如果我們手動(dòng)把這段代碼copy到另外一個(gè)地方,如00500000,那么顯然00400120H這個(gè)地址需要被修改,我們當(dāng)然可以仿照自重定位的方法來手動(dòng)修改這個(gè)地址值,但是通常較簡(jiǎn)單的方法是寫自重定位代碼,這樣的代碼可以在任意地址執(zhí)行,具體做法如下:
call @F
@@:
pop ebx
sub ebx,offset @B
DATA db 12h, 34h, 56h, 78h
MOV EAX, [EBX + DATA]
可以看到,該段代碼通過使用call指令壓入當(dāng)前地址eip并彈出從而得到當(dāng)前地址。然后,用當(dāng)前地址減去其標(biāo)號(hào)的偏移量就得到重定位修正值,存入ebx之中。之后,就可以使用ebx作為一個(gè)基準(zhǔn)來訪問數(shù)據(jù),以后訪問數(shù)據(jù)可以用EBX + ???來訪問,這樣由于EBX會(huì)根據(jù)當(dāng)前的地址值而變化,所以這段代碼是自重定位的。
下面給出一段代碼,這段代碼中的InjectRemoteCode函數(shù)負(fù)責(zé)將RemoteThread這個(gè)函數(shù)的自重定位代碼Copy到其他進(jìn)程中執(zhí)行:
;=============================================================================
; 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是十分復(fù)雜的。不過,實(shí)際上,我們并不用總是這么做,還有更簡(jiǎn)單的方法:利用Kernel32.dll中的LoadLibrary這個(gè)函數(shù)。由于Kernel32.dll在每個(gè)EXE中都會(huì)被加載,而且由于Kernel32.dll總是第一個(gè)被加載的,因此Kernel32.dll的加載地址總是相同的,換句話說,在我們的主程序中Kernel32.dll中的LoadLibrary函數(shù)的地址同時(shí)也是其他程序中LoadLibrary函數(shù)的地址,而LoadLibrary可以加載任意DLL。此外,LoadLibrary只有一個(gè)參數(shù),正好和普通線程的要求相同!所以我們只要調(diào)用CreateRemoteThread(…, LoadLibrary, DLL_PathName)便可以將Dll Inject到任意進(jìn)程中。唯一需要注意的就是,由于LoadLibrary是在其他進(jìn)程中運(yùn)行,而LoadLibrary的參數(shù)必須保存在另外的進(jìn)程中。怎么做到這一點(diǎn)呢?回憶一下前文提到了兩個(gè)函數(shù)VirtualAllocEx和WriteProcessMemory,正好我們可以利用這兩個(gè)函數(shù)分配一塊內(nèi)存然后把Dll的路徑名Copy到該內(nèi)存中去。
此外,由于DLL中的代碼是可以重定位的,因此實(shí)際上我們會(huì)把API Hook的代碼放在DLL中,這樣寫Hook代碼的時(shí)候便不用考慮重定位問題。
4 監(jiān)視進(jìn)程的啟動(dòng)
綜合上面的內(nèi)容,我們已經(jīng)可以掛接單個(gè)進(jìn)程中的指定API了。不過這還不夠,我們還需要掛接系統(tǒng)中的所有進(jìn)程。如果在程序運(yùn)行之后,不允許新進(jìn)程的創(chuàng)建,那么掛接所有進(jìn)程則是非常容易的。Windows操作系統(tǒng)提供了一個(gè)CreateToolhelp32Snapshot的API函數(shù)。這個(gè)API函數(shù)創(chuàng)建當(dāng)前系統(tǒng)的快照(Snapshot),這個(gè)快照可以是所有進(jìn)程的快照(參數(shù)是TH32CS_SNAPPROCESS),或者是指定某個(gè)進(jìn)程的所有模塊(Module)的快照(參數(shù)是TH32CS_SNAPMODULE),等等。通過調(diào)用CreateToolhelp32Snapshot函數(shù)獲得了所有進(jìn)程之后,便可以依次掛接各個(gè)進(jìn)程。但是事情并非如此簡(jiǎn)單。用戶和操作系統(tǒng)都可以啟動(dòng)新的進(jìn)程,這樣單純的調(diào)用CreateToolhelp32Snapshot函數(shù)并不能解決問題。所以需要一種機(jī)制來通知本系統(tǒng)新進(jìn)程的創(chuàng)建和結(jié)束。經(jīng)過查閱相關(guān)資料(其實(shí)也就是Google啦),發(fā)現(xiàn)監(jiān)視系統(tǒng)進(jìn)程開始和結(jié)束的最好方法是通過DDK中的PsSetCreateProcessNotifyRoutine函數(shù),其原型為:
NTSTATUS PsSetCreateProcessNotifyRoutine(
IN PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
IN BOOLEAN Remove
);
NotifyRoutine指定了當(dāng)進(jìn)程被創(chuàng)建和結(jié)束的時(shí)候所需要調(diào)用的回調(diào)函數(shù)。則Remove是用來告訴該函數(shù)是設(shè)置該回調(diào)還是移除。NotifyRoutine的類型為PCREATE_PROCESS_NOTIFY_ROUTINE,其定義為:
VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create
);
ParentId和ProcessId用來標(biāo)識(shí)進(jìn)程,Create則是用來表示該進(jìn)程是正在被創(chuàng)建還是正在被結(jié)束。這樣,每當(dāng)進(jìn)程被創(chuàng)建或者結(jié)束的時(shí)候,操作系統(tǒng)就會(huì)立刻調(diào)用NotifyRoutine這個(gè)回調(diào)函數(shù)并正確提供參數(shù)。
由于這個(gè)函數(shù)是由ntdll.dll所輸出的,屬于Windows的內(nèi)核空間,因此必須編寫一個(gè)處于內(nèi)核模式的驅(qū)動(dòng)程序才可以。但是,至此問題并沒有完全解決。內(nèi)核模式的驅(qū)動(dòng)程序和用戶模式的主程序如何通訊呢?這里就需要用到IO請(qǐng)求包IRP(IO Request Packet)。這個(gè)IRP的定義為:
typedef struct _CallbackInfo
{
HANDLE hParentId;
HANDLE hProcessId;
BOOLEAN bCreate;
} CALLBACK_INFO, *PCALLBACK_INFO;
其字段的意義就和PCREATE_PROCESS_NOTIFY_ROUTINE一樣,不再贅述。
用戶模式的程序通過DeviceIoControl函數(shù)發(fā)送IO請(qǐng)求包到內(nèi)核模式的驅(qū)動(dòng)。內(nèi)核模式接收到此請(qǐng)求包,并填寫數(shù)據(jù)到用戶程序所提供的CALLBACK_INFO緩沖區(qū)里。這樣通過檢查CALLBACK_INFO的值就可以知道hProcessId所指定的進(jìn)程是正在被創(chuàng)建或者結(jié)束了。
雖然有了數(shù)據(jù)交換的機(jī)制,這還是不夠。這樣只能告訴用戶程序究竟是哪一個(gè)進(jìn)程,是創(chuàng)建還是結(jié)束,但是無法通知用戶程序此事件的發(fā)生。通常,通知某個(gè)程序某個(gè)事件的發(fā)生一般的方法是使用事件(Event)。驅(qū)動(dòng)程序創(chuàng)建一個(gè)內(nèi)核事件(Kernel Event)。用戶程序打開這個(gè)事件用于同步。每當(dāng)事件發(fā)生的時(shí)候驅(qū)動(dòng)程序就首先把該事件設(shè)置為Signaled,然后再Non-signaled。這樣用戶程序就可以接收到通知了。但是為什么需要首先設(shè)置為Signaled,然后再Non-signaled?因?yàn)橛脩舫绦驔]有權(quán)限來設(shè)置其狀態(tài),因此只能由驅(qū)動(dòng)程序來設(shè)置,首先設(shè)置為Signaled,然后再Non-signaled是唯一的辦法。
有了這兩種方法,就可以掛接操作系統(tǒng)中的所有進(jìn)程了。首先,主線程調(diào)用CreateToolhelp32Snapshot函數(shù)創(chuàng)建系統(tǒng)內(nèi)所有進(jìn)程的快照,掛接這些進(jìn)程,然后啟動(dòng)驅(qū)動(dòng)程序,在主程序中啟動(dòng)一個(gè)新線程等待Event來監(jiān)視新的進(jìn)程的創(chuàng)建和舊進(jìn)程的結(jié)束。驅(qū)動(dòng)程序的代碼和監(jiān)聽的代碼可以在http://www./threads/procmon.asp下載到。
5 其他問題
5.1 Unicode
大部分Windows API均有兩個(gè)版本:Ansi和Unicode。如GetWindowText API實(shí)際上只是一個(gè)宏,實(shí)際上在不同編譯選項(xiàng)下對(duì)應(yīng)GetWindowTextA和GetWindowTextW。在NT系統(tǒng)下,GetWindowTextA只是做一個(gè)轉(zhuǎn)換,再調(diào)用GetWindowTextW,實(shí)際的實(shí)現(xiàn)在GetWindowTextW中。因此,掛接API必須要Hook兩個(gè)版本,實(shí)際在Hook的時(shí)候,我們也可以仿照Windows的做法,讓GetWindowTextA做一個(gè)簡(jiǎn)單字符串轉(zhuǎn)換,然后直接調(diào)GetWindowTextW即可。可能有朋友要問了,為何不直接Hook GetWindowTextW呢?反正GetWindowTextA要調(diào)GetWindowTextW就不用Hook GetWindowTextA了嘛。不過實(shí)際上,因?yàn)镚etWindowTextA和GetWindowTextW在同一個(gè)DLL中,他們的調(diào)用很有可能并不是通過IAT來,而是直接調(diào)用的關(guān)系,所以GetWindowTextA會(huì)繞過我們的Hook機(jī)制而直接調(diào)到原始的GetWindowTextW,這不是我們希望看到的,所以兩個(gè)版本保險(xiǎn)起見都應(yīng)該Hook。
5.2 IPC
由于Hook的API代碼位于某個(gè)DLL中,這個(gè)DLL處于不同的進(jìn)程,因此需要用到IPC機(jī)制在主程序和其他被Hook的進(jìn)程進(jìn)行通訊。不同進(jìn)程之間的通訊稱之為IPC(Interprocess Communication),大概的方法有下面幾種:
1. Pipe。管道是比較常用的IPC機(jī)制,可以傳輸大量數(shù)據(jù),代碼寫起來也比較方便。管道也可以用于網(wǎng)絡(luò)間不同計(jì)算機(jī)通訊,但是有一定限制。
2. Socket。雖然Socket一般用于網(wǎng)絡(luò),但是顯然也可以用于本機(jī),優(yōu)點(diǎn)是大家可能對(duì)Socket編程比較熟悉,此外可以很容易擴(kuò)展到網(wǎng)絡(luò)之間的通訊,基本沒有限制,因此也是很不錯(cuò)的選擇。
3. Message。消息一般適用于比較簡(jiǎn)單的通訊,如果要傳遞數(shù)據(jù)必須要使用WM_COPYDATA消息。優(yōu)點(diǎn)是比較簡(jiǎn)單,但是性能可能無法保證。
4. Shared Segment。也就是共享段。簡(jiǎn)單來說,就是把EXE/DLL中的某個(gè)段標(biāo)記為共享,這樣多個(gè)EXE/DLL的實(shí)例之間會(huì)共享同一塊內(nèi)存,通過讀寫此塊內(nèi)存便可以互相傳遞數(shù)據(jù),但是同步比較困難。具體做法是:
#pragma bss_seg("shared_bss")
int a;
#pragma bss_seg()
#pragma comment(linker, "/Section:shared_bss,rws")
這樣,變量a便放在了共享段之中。
5. Memory Mapped File(內(nèi)存映射文件)。比較簡(jiǎn)單,但是缺點(diǎn)和Shared Segment類似,無法同步。
6. Event/Semaphore/Mutex。這些只能用于同步,無法傳遞數(shù)據(jù)。
7. …還有很多
可以根據(jù)自己的情況靈活選用。
6 總結(jié)
API Hook的通常做法如下:
1. 通過全局消息鉤子或者驅(qū)動(dòng)程序監(jiān)視進(jìn)程啟動(dòng)/結(jié)束來掛接系統(tǒng)中所有進(jìn)程
a. 如果不需要掛接CUI程序則選用全局消息鉤子
b. 否則則選用驅(qū)動(dòng)程序
2. 通過全局消息鉤子或者遠(yuǎn)程線程來注入代碼到目標(biāo)進(jìn)程中
a. 全局消息鉤子無需考慮如何加載DLL的問題,系統(tǒng)會(huì)自動(dòng)加載
b. 遠(yuǎn)程線程一般直接創(chuàng)建線程執(zhí)行LoadLibrary代碼加載DLL,當(dāng)然也可以執(zhí)行自己寫的匯編代碼
3. 通過修改IAT (Import Address Table)中的API地址為自己的函數(shù)地址來Hook API。所使用的API是ImageDirectoryEntryToData.
4. 自己編寫的API的代碼放在DLL中以解決重定位問題(如果用全局消息鉤子的話放在DLL是強(qiáng)制要求)
7 相關(guān)參考文獻(xiàn)
我當(dāng)初在寫程序和寫作本文的時(shí)候,參考了下面這些書籍和文章,有興趣的朋友可以參考一下看看:
Windows核心編程,第22章
Windows環(huán)境下32位匯編語言程序設(shè)計(jì),第13章,17章
API Hooking Revealed, 地址:http://www./system/hooksys.asp
Detecting Windows NT/2K process execution, 地址:http://www./threads/procmon