1 基本原理

API Hook是什么我就不多說了,直接進(jìn)入正題。API Hook技術(shù)主要有下面的技術(shù)難點(diǎn):

1.     如何將自己的的代碼Inject到其他進(jìn)程

2.     如何HookAPI

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.dllDllMain會(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之間顯然是無(wú)法移植的。

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ù)

Windows9xWindows NT、Windows 2000/XP/2003等操作系統(tǒng)中所使用的可執(zhí)行文件格式是純32PEPortable 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ù)位置使用RVARelative Virtual Address)來表示。RVA指的是相對(duì)虛擬地址,也就是一個(gè)偏移量。當(dāng)PE文件被裝入內(nèi)存中的時(shí)候,WindowsPE文件裝入到某個(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í)行類似下面語(yǔ)句:

 

0040100E      CALL          0040101A

可以看到這是一個(gè)call語(yǔ)句。Call語(yǔ)句則調(diào)用下面的語(yǔ)句:

 

0040101A      JMP           DWORD PTR [00402000]

上面的代碼稱為樁代碼(Stub code),jmp語(yǔ)句中的目標(biāo)地址[00402000]才是API函數(shù)的地址。這段Stub code位于.lib輸入庫(kù)中。如果加以優(yōu)化,那么調(diào)用代碼是下面這樣:

 

XXXXXXXX      CALL          DWORD PTR [XXXXXXXX]

其中[XXXXXXXX]指向IATImport 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):VirtualAllocExWriteProcessMemory,前者可以在指定進(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è)工作是由WindowsPE 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的地址加載,一般沒有特別原因無(wú)需修改。而DLL因?yàn)橐话銦o(wú)法保證預(yù)先設(shè)置好的加載地址總能夠滿足。比如DLL可能指定在0x10000000地址加載,但是有可能此地址已經(jīng)有其他DLL占據(jù)或者被EXE占據(jù),DLL必須得在另外的地址加載,因此一般在DLL中總是保存重定位信息。

一段代碼,一般情況下無(wú)法在任意地址執(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ù)VirtualAllocExWriteProcessMemory,正好我們可以利用這兩個(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è)CreateToolhelp32SnapshotAPI函數(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

    );

ParentIdProcessId用來標(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)求包IRPIO 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é)束,但是無(wú)法通知用戶程序此事件的發(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.codeproject.com/threads/procmon.asp下載到。

5 其他問題

5.1 Unicode

大部分Windows API均有兩個(gè)版本:AnsiUnicode。如GetWindowText API實(shí)際上只是一個(gè)宏,實(shí)際上在不同編譯選項(xiàng)下對(duì)應(yīng)GetWindowTextAGetWindowTextW。在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即可??赡苡信笥岩獑柫耍瑸楹尾恢苯?/span>Hook GetWindowTextW呢?反正GetWindowTextA要調(diào)GetWindowTextW就不用Hook GetWindowTextA了嘛。不過實(shí)際上,因?yàn)?/span>GetWindowTextAGetWindowTextW在同一個(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

由于HookAPI代碼位于某個(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)單,但是性能可能無(wú)法保證。

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類似,無(wú)法同步。

6.     Event/Semaphore/Mutex。這些只能用于同步,無(wú)法傳遞數(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.     全局消息鉤子無(wú)需考慮如何加載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。所使用的APIImageDirectoryEntryToData.

4.     自己編寫的API的代碼放在DLL中以解決重定位問題(如果用全局消息鉤子的話放在DLL是強(qiáng)制要求)

7 相關(guān)參考文獻(xiàn)

我當(dāng)初在寫程序和寫作本文的時(shí)候,參考了下面這些書籍和文章,有興趣的朋友可以參考一下看看:

Windows核心編程,第22
Windows環(huán)境下32位匯編語(yǔ)言程序設(shè)計(jì),第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

  • 文章來源: http://blog.csdn.net/atfield
  • 原文作者: ATField