API Hook徹底手冊

  • 文章來源: http://blog.csdn.net/atfield
  • 原文做者: ATField
  • 整理日期: 2008-07-16
  • 發表評論
  • 字體大小:
 

 

注:本文是根據我兩年前寫的一個系統行爲監測程序寫成(參考了一些書籍和文章)。最近在論壇上看到有很多人在問關於API Hook的問題,便寫成此文,但願能對朋友們在寫API Hook代碼的時候可以有所幫助。

1 基本原理

API Hook是什麼我就很少說了,直接進入正題。API Hook技術主要有下面的技術難點:php

  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。windows

顯然使用這種方法要求設置註冊表以後馬上重起系統,不過通常狀況下這不是大問題。這種方法的主要問題在於,只有用到User32.dll的應用程序纔會被Inject。全部的GUI和少部分CUI程序會用到User32.dll,因此若是你的API Hook程序不打算監視CUI程序的話,那麼可能問題並不太大。可是若是你的API Hook程序須要監視系統中全部進程的話,這種方法的限制將是很是致命的。api

 

  1. 調用SetWindowsHookEx(WH_GETMESSAGE, …, 0)

 

可使用SetWindowsHookEx(WH_GETMESSAGE, …, 0) 設置全局的消息鉤子,雖然可能你的程序並不用到消息鉤子,可是鉤子的一個反作用是會將對應的DLL加載到全部的GUI線程之中。相似的,只有用到GUI的進程纔會被掛接。雖然有這種限制,這種方法仍然是最經常使用的掛接進程的方法。網絡

 

  1. 使用CreateRemoteThread函數在目標進程中建立遠程線程

 

這種方法能夠在任意的目標進程中建立一個遠程線程,遠程線程中能夠執行任意代碼,這樣即可以作到把咱們的代碼Inject到目標進程中。這種方法具備最大的靈活性,可是難度也最高:多線程

a) 遠程線程代碼必須能夠自重定位app

b) 要可以監視進程的啓動和結束,這樣才能夠掛接到全部進程函數

這兩個問題都是能夠解決的,在本文中我將重點講述如何建立遠程線程和解決這兩個問題。性能

 

  1. 若是你只是要掛接某個特定進程的而且狀況容許你本身來建立此進程,你能夠調用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便放在了共享段之中。

  1. Memory Mapped File(內存映射文件)。比較簡單,可是缺點和Shared Segment相似,沒法同步。
  2. Event/Semaphore/Mutex。這些只能用於同步,沒法傳遞數據。
  3. …還有不少

能夠根據本身的狀況靈活選用。

6 總結

API Hook的一般作法以下:

  1. 經過全局消息鉤子或者驅動程序監視進程啓動/結束來掛接系統中全部進程
    1. 若是不須要掛接CUI程序則選用全局消息鉤子
    2. 不然則選用驅動程序
  2. 經過全局消息鉤子或者遠程線程來注入代碼到目標進程中
    1. 全局消息鉤子無需考慮如何加載DLL的問題,系統會自動加載
    2. 遠程線程通常直接建立線程執行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

 

 

前天看到kruglinski的一段程序,我看了一下今天花了一下午的時間來本身實現了一個。不過個人沒有用動態地址的方法,而是本身去寫了靜態的地址到裏面靈活性沒有他的大不過功能上面仍是差很少的。下面我簡單講一下個人原理。

跳轉的具體原理
就是用兩句彙編句:

  mov eax , 12345678   (自定義函數的地址,每一個機器各不相同是須要手動調整)
  jmp eax

彙編代碼:

e8 78 56 34 12 //第2,3,4,5是須要手工調整的
ff e0

1。計算出地址「冒名頂替函數」的入口地址。
2。得要HOOK的API函數的地址。
3。保存API函數的入口內容。
4。修改API函數的入口內容。
5。再在「冒名頂替函數」裏面實現完其餘功能後,把原來的API函數的入口內容給回去。

代碼以下:

//
//write by Gxter
//
//經過覆蓋系統函數的地址來實現HOOK API
//
#include "stdio.h"
#include "windows.h"
#include "tchar.h"


BYTE addr_old[8] = {0};
BYTE addr_new[8] = { 0xB8, 0x20, 0x10, 0x40, 0x00, 0xFF, 0xE0, 0x00 }; //第2,3,4,5是須要手工調整的(重要的步驟)
DWORD pfnMsgBox=0; //API函數地址


int WINAPI MessageBoxProxy(IN HWND hWnd, IN LPCSTR lpText, IN LPCSTR lpCaption, IN UINT uType)
{
int ret = 0;

DWORD dwOldProtect;
MEMORY_BASIC_INFORMATION   mbi;

::VirtualQuery((void *)pfnMsgBox, &mbi, sizeof(mbi));
::VirtualProtect((void *)pfnMsgBox, 8, PAGE_READWRITE, &dwOldProtect);

// 寫入原來的執行代碼
::WriteProcessMemory(::GetCurrentProcess(), 
    (void *)pfnMsgBox, 
    addr_old, 
    sizeof(DWORD)*2, 
    NULL); 

::VirtualProtect((void *)pfnMsgBox, 8, mbi.Protect, 0);

ret=MessageBox(hWnd,"gxter","gxter",uType);

return ret;
}

//----------------------------------------------程序入口
int main()
{
DWORD dwOldProtect;
MEMORY_BASIC_INFORMATION   mbi;

MessageBox(NULL,_T("Hook Demo!"),_T("API Hook"),MB_ICONINFORMATION);
pfnMsgBox=(DWORD)GetProcAddress(GetModuleHandle(_T("user32.dll")),_T("MessageBoxA"));
printf("api入口地址: %x/n",pfnMsgBox);


VirtualQuery( (void *)pfnMsgBox, &mbi, sizeof(mbi) );
//修改咱們要改的地址的頁屬性,爲可讀可寫
VirtualProtect( (void *)pfnMsgBox, 8, PAGE_READWRITE, &dwOldProtect);

// 保存原來的執行代碼
memcpy(addr_old, (void *)pfnMsgBox, 8);

// 寫入新的執行代碼
WriteProcessMemory(   GetCurrentProcess(), 
    (void *)pfnMsgBox, 
        addr_new, 
    sizeof(DWORD)*2, 
    NULL); 
//修改成原來的屬性屬性
VirtualProtect((void *)pfnMsgBox, 8, mbi.Protect, 0);

//當調用這個函數的時候就跳到個人函數上面了
MessageBox(NULL,_T("Hook Demo!"),_T("API Hook"),MB_ICONINFORMATION);


getchar();
return 0;
}

//--------------------------------the end-----------------------------

關於這個文章後來人私下裏問我,那個地址是怎麼回事!
這是我在寫《覆蓋地址HOOK API》這個學習筆記的同時寫的另外一篇東西。
《自定義函數和API函數的跳轉》

//
//write by Gxter
//
//title:自定義函數的跳轉過程,和API函數的跳轉過程
//
//說明:程序裏面的地址是每一個機器都不盡相同的,但道理都是同樣的。

#include "stdio.h"
#include "windows.h"

void fun(); 

int main()
{
fun();

getchar();
return 0;
}

void fun()
{
printf("write by Gxter!");
}

//自定義函數的跳轉是程序中的跳轉指令,而後由跳轉指令跳轉到函數的執行體。
/*

1。主程序中:00401038   E8 C8 FF FF FF   call   @ILT+0(_fun) (00401005)

2。跳轉指令:00401005   E9 B6 00 00 00   jmp   fun (004010c0)

3。函數執行體:004010C0 55           push   ebp 

//自定義函數是根據地址直接跳轉

*/

//-----------------------------------the end------------------------------------------

//
//write by Gxter
//
//title:自定義函數的跳轉過程,和API函數的跳轉過程
//
//說明:程序裏面的地址是每一個機器都不盡相同的,但道理都是同樣的。


#include "stdio.h"
#include "tchar.h"
#include "windows.h"

int main()
{
MessageBox(NULL,_T("Hook Demo!"),_T("API Hook"),MB_ICONINFORMATION);

getchar();
return 0;
}


//API函數調用過程
/*

1。主程序中:00401038 FF 15 94 52 42 00   call   dword ptr [__imp__MessageBoxA@16 (00425294)]
地址004252B4中的內容 "6544 77E1"
(是根據地址的內容裏面的地址跳轉)

2。API函數的執行體:77E16544 55         push     ebp

*/ 

//--------------------------------the end------------------------------------------------

 

http://blog.csdn.net/jiangxinyu/article/details/5385920

相關文章
相關標籤/搜索