注:本文是根据我两年前写的一个系统行为监测程序写成(参考了一些书籍和文章)。最近在论坛上看到有很多人在问关于API Hook的问题,便写成此文,但愿能对朋友们在写API Hook代码的时候可以有所帮助。
1 基本原理
API Hook是什么我就很少说了,直接进入正题。API Hook技术主要有下面的技术难点:php
- 如何将本身的的代码Inject到其余进程
- 如何Hook到API
1.1 代码的Injection
经常使用的方法有:编程
- 使用注册表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
- 调用SetWindowsHookEx(WH_GETMESSAGE, …, 0)
可使用SetWindowsHookEx(WH_GETMESSAGE, …, 0) 设置全局的消息钩子,虽然可能你的程序并不用到消息钩子,可是钩子的一个反作用是会将对应的DLL加载到全部的GUI线程之中。相似的,只有用到GUI的进程才会被挂接。虽然有这种限制,这种方法仍然是最经常使用的挂接进程的方法。网络
- 使用CreateRemoteThread函数在目标进程中建立远程线程
这种方法能够在任意的目标进程中建立一个远程线程,远程线程中能够执行任意代码,这样即可以作到把咱们的代码Inject到目标进程中。这种方法具备最大的灵活性,可是难度也最高:多线程
a) 远程线程代码必须能够自重定位app
b) 要可以监视进程的启动和结束,这样才能够挂接到全部进程函数
这两个问题都是能够解决的,在本文中我将重点讲述如何建立远程线程和解决这两个问题。性能
- 若是你只是要挂接某个特定进程的而且状况容许你本身来建立此进程,你能够调用CreateProcess(…, CREATE_SUSPENDED)建立子进程并暂停运行,而后修改入口代码使之调用LoadLibrary加载本身的DLL。该方法在不一样CPU之间显然是没法移植的。
1.2 Hook API
经常使用的方法有:学习
- 找到API函数在内存中的地址,改写函数头几个字节为JMP指令跳转到本身的代码,执行完毕再执行API开头几个字节的内容再跳回原地址。这种方法对CPU有较大的依赖性,并且在多线程环境下可能出问题,当改写函数代码的时候有可能此函数正在被执行,这样作可能致使程序出错。
- 修改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),大概的方法有下面几种:
- Pipe。管道是比较经常使用的IPC机制,能够传输大量数据,代码写起来也比较方便。管道也能够用于网络间不一样计算机通信,可是有必定限制。
- Socket。虽然Socket通常用于网络,可是显然也能够用于本机,优势是你们可能对Socket编程比较熟悉,此外能够很容易扩展到网络之间的通信,基本没有限制,所以也是很不错的选择。
- Message。消息通常适用于比较简单的通信,若是要传递数据必需要使用WM_COPYDATA消息。优势是比较简单,可是性能可能没法保证。
- Shared Segment。也就是共享段。简单来讲,就是把EXE/DLL中的某个段标记为共享,这样多个EXE/DLL的实例之间会共享同一块内存,经过读写此块内存即可以互相传递数据,可是同步比较困难。具体作法是:
#pragma bss_seg("shared_bss") int a; #pragma bss_seg() #pragma comment(linker, "/Section:shared_bss,rws") |
这样,变量a便放在了共享段之中。
- Memory Mapped File(内存映射文件)。比较简单,可是缺点和Shared Segment相似,没法同步。
- Event/Semaphore/Mutex。这些只能用于同步,没法传递数据。
- …还有不少
能够根据本身的状况灵活选用。
6 总结
API Hook的一般作法以下:
- 经过全局消息钩子或者驱动程序监视进程启动/结束来挂接系统中全部进程
- 若是不须要挂接CUI程序则选用全局消息钩子
- 不然则选用驱动程序
- 经过全局消息钩子或者远程线程来注入代码到目标进程中
- 全局消息钩子无需考虑如何加载DLL的问题,系统会自动加载
- 远程线程通常直接建立线程执行LoadLibrary代码加载DLL,固然也能够执行本身写的汇编代码
- 经过修改IAT (Import Address Table)中的API地址为本身的函数地址来Hook API。所使用的API是ImageDirectoryEntryToData.
- 本身编写的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------------------------------------------------