概述
介绍:HOOK API是指截获特定进程或系统对某个API函数的调用,使得API的执行流程转向指定的代码。Windows下的应用程序都建立在API函数至上,所以截获API是一项相当有用的技术,它使得用户有机会干预其它应用程序的程序流程。
最常用的一种挂钩API的方法是改变目标进程中调用API函数的代码,使得它们对API的调用变为对用户自定义函数的调用。
HOOK API和HOOK技术完全不同。尽管它们都是钩子。HOOK钩的是消息,它在系统将消息传递给应用程序之前截获它,然后进行操作、或修改消息、或停止消息的传递;而HOOK API截获的是应用程序对系统API的调用,它在应用程序对系统API的调用之前截获此调用动作,让其转而调用我们所定义的函数(内容可能是进行一些操作后再调用原系统API)。
关于HOOK技术,微软为我们提供了现成的API,有固定的使用步骤。
而对于HOOK API技术,微软并没有向我们提供类似的API,没有那么简洁的步骤可供我们参考,也许是因为微软并不希望我们用这样的手段编程,所以相对要麻烦一些。
以下为《windows核心编程》学习笔记:
windows编程中,有些时候必须要打破进程的界限。访问另一个进程的地址空间。这些情况包括:
1.当你想要为另一个进程创建的窗口建立子类时,即为此窗口指定新的窗口过程函数。
2.当你需要调试帮助时。
3.当你想要挂接其它进程时(HOOK API?)。
一旦你的DLL插入到另一个进程的地址空间,就可以对另一个进程为所欲为。
一、为另一个进程创建的窗口建立一个子类。
建立子类就能改变窗口的行为特性。若要建立子类,只需要调用SetWindowLongPtr函数,改变窗口的内存块中的窗口过程地址,指向一个新的(你自己的)WndProc。
当调用下面所示的SetWindowLongPtr函数,建立一个窗口的子类时,你告诉系统,发送到或者显示在hwnd设定的窗口中的所有消息都应该送往MySubclassProc,而不是送往窗口正常的窗口过程:
SetWindowLongPtr(hwnd,GWLP_WNDPROC,MySubclassProc);
换句话说,当系统需要将消息发送到指定窗口的WndProc时,要查看它的地址,然后直接调用WndProc。在这里,系统发现MySubclassProc函数的地址与窗口相关联,因此就直接调用MySubclassProc函数。
窗口函数被调用的过程是这样的:如,进程A正在运行,并且已经创建了一个窗口。文件User32.dll被映射到进程A的地址空间中。对User32.dll文件的映射是为了接收和发送在进程A中运行的任何线程创建的任何窗口中发送和显示的消息(USER32.DLL里含有DispatchMessage函数,函数为:
LONG DispatchMessage (CONST MSG *msg){
LONG lResult;
WNDPROC lpfnWndProc =
(WNDPROC)GetWindowLongPtr(msg.hwnd, GWLP_WNDPROC);
lResult = lpfnWndProc(msg.hwnd, msg.message, msg.wParam, msg.lParam);
return (lResult);
}
)。
当User32.dll的映像发现一个消息时,它首先要确定窗口的WndProc的地址,然后调用该地址,传递窗口的句柄、消息和wParam和lParam值。当WndProc处理该消息后,User32.dll便循环运行,并等待另一个窗口消息被处理。
若想在进程B中为进程A的线程所创建的窗口建立子类(即指定窗口过程函数),在进程B中就需要得到目标窗口的句柄,方法很多,如FindWindow。
但是,由于进程的边界问题,Microsoft决定不让SetWindowLongPtr改变另一个进程创建的窗口过程。不是SetWindowLongPtr不能为窗口指定窗口过程函数,而是由于进程的边界问题,使得这样不具有可操作性。如果能将子类过程(MySubclassProc)的代码放入进程A的地址空间,就可以方便地调用SetWindowLongPtr函数,将进程A的地址传递给MySubclassProc函数。将这个方法成为将DLL(含有MySubclassProc的代码)“插入”进程的地址空间。有若干种方法可以用来进行这项操作。
1.使用注册表来插入DLL
2.使用Windows挂钩来插入DLL
3.使用远程线程来插入DLL
分别来学习一下:
1.使用注册表来插入DLL
这种方式是在注册表中的 HKEY_LOCAL_MACHINE/Software/Microsoft/Windows NT/CurrentVersion/Windows/AppInit_DLLs关键字里保存要加载的dll文件名(或包含路径)。这样再重新启动系统(系统初始化)后,dll就得到了加载。而要使其失效,同样需要重新启动电脑。这种方法dll只能被加载到使用User32.dll的进程中。
2.使用Windows挂钩来插入DLL
使用HOOK的方式(而非HOOK API)来插入DLL到另一个进程的地址空间。
HOW?
我们都知道,可以用进程A为进程B挂钩(HOOK),比如A按调用SetWindowsHookEx:
HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE,GetMsgProc,hinstDll,0);
安装了一个WH_GETMESSAGE类型的钩子,安装到系统内的所有线程(0),钩子函数(钩子过程)为hinstDll所指定的DLL里的GetMsgProc函数。当一个进程,如进程B中的一个线程准备将一条消息发送到一个窗口时,系统查看发现该线程已经被安装了WH_GETMESSAGE类型的钩子,于是系统知道应该将这个消息先送给钩子函数GetMsgProc。既然如此,系统就会去查看包含GetMsgProc的DLL是否被映射到进程B中,如果没有则强制映射。然后调用GetMsgProc对消息进行处理。
这就是HOOK的大概过程。
如果我们在GetMsgProc函数体里用SetWindowLongPtr函数为消息即将发送给的窗口设置窗口过程函数,就可以完成为窗口设置子类(即指定新的窗口过程函数)的工作。当然新的窗口过程函数的代码必须与GetMsgProc位于同一个DLL里。
即:
目标窗口被挂接WH_GETMESSAGE类型钩子,当有消息发送到此窗口时,系统调用钩子过程GetMsgProc,在此函数中,调用SetWindowLongPtr设置窗口的新窗口过程函数。
王艳萍版《windows程序设计》中提到的利用HOOK注入DLL的方法与此类似,只不过那里没有强调设置子类这一点,而只是为了注入DLL:
使用Windows钩子注入特定DLL到其它进程时一般都在目标进程上安装WH_GETMESSAGE钩子,因为Windows下的应用程序大部分都需要调用GetMessage或PeekMessage来从消息队列中获取消息,所以WH_GETMESSAGE型钩子函数一定会被调用,而各个进程要执行钩子函数,必须要将钩子函数所在的DLL加载到目标进程汇总,这个工作是系统为我们完成的,系统会自动强迫所有能产生激活SH_GETMESSAGE钩子的消息的进程调用LoadLibrary来加载钩子函数所在的dll,然后执行其中的钩子函数。由于此时的钩子的目的只是为了加载钩子函数所在的DLL(DLL中其它部分才是加载它的原因),所以此时钩子函数可以什么都不做,而只是return ::CallNextHookEx(g_hHook,code,wParam,lParam);。
3.使用远程线程来插入DLL
Windows的大多数函数都只允许调用函数的进程对自己进行操作。但是,有些函数却允许一个进程对另一个进程进行操作。
这个方法的思路是,让目标进程中的线程调用LoadLibrary来加载必要的DLL(DLL里可以包括我们希望目标进程用来调用以覆盖其本应调用的API的我们自己定义的API实现)。但是如何才能控制另一个进程中的线程去调用LoadLibrary呢?方法是我们在目标进程中创建一个新线程,由于这个线程是我们自己创建的,当然我们让它干什么都可以(通过代码实现)。Windows提供了一个函数,使我们能够非常容易地在另一个进程中创建线程,这就是CreateRemoteThread函数:
HANDLE CreateRemoteThread(
HANDLE hProcess,
PSECURITY_ATTRIBUTES psa,
DWORD dwStackSize,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadId);
对比一下CreateThread函数:
HANDLE CreateRemoteThread(
PSECURITY_ATTRIBUTES psa,
DWORD dwStackSize,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadId);
两者非常相似,只是前者多了一个参数hProcess,这个线程就指明了此函数是为哪个远程进程创建新线程。pfnStartAddr指明了线程函数的内存地址。当然,该内存地址与远程进程是相关的。线程函数的代码不能位于你自己的进程的地址空间中。
值得一提的是,在Windows2000里,CreateThread是用对CreateRemoteThread进行封装来实现的:
HANDLE CreateThread(PSECURITY_ATTRIBUTES psa,DWORD dwStackSize,PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam,DWORD fdwCreate,PDWORD pdwThreadID)
{
return (CreateRemoteThread(GetCurrentProcess(),
psa,dwStackSize,pfnStartAddr,pvParam,fdwCreate,pdwThreadID));
}
远程注入DLL(以及利用此方法进行API挂接)的思想和步骤概括如下:
思想:在我们自己的进程中为目标进程创建一个新线程,让此线程执行LoadLibraryA(or W)加载我们希望注入的DLL(里面包含我们希望挂接的API的我们实现的版本)。在DLL被注入时,DLL的主函数DllMain得到机会执行,在DllMain里完成API的替换工作。
步骤:
1.使用VirtualAllocEx函数,在远程进程(目标进程)的空间里分配内存。
2.使用WriteProcessMemory函数,将要注入的DLL路径名拷贝到第一步骤中分配的内存中。
3.使用GetProcAddress函数,获取LoadLibraryA或LoadLibraryW函数的实地址(在Kernel32.dll中),作为之后创建远程线程时传递给线程函数(LoadLibraryA或LoadLibraryW)的参数。
4.使用CreateRemoteThread函数,在远程进程中创建线程,用于加载DLL。它的线程函数为LoadLibraryA/LoadLibraryW,参数为步骤3的返回值。
这时,DLL已经被插入远程进程的地址空间中,同时DLL的DllMain函数接收到一个DLL_PROCESS_ATTACH通知,并且能够执行需要的代码(你注入此DLL的目的所在)。当DllMain返回时,远程线程从它对LoadLibrary的调用返回到BaseThreadStart。然后BaseThreadStart调用ExitThread,使远程线程终止运行,它已经完成了注入DLL的任务,并且DLL也得到机会完成我们希望它的任务(在DllMain中)。
如果在DllMain中完成对API的挂接,那么通过远程注入实现的API挂接就完成了。当然,不要忘了执行下面的步骤从目标进程的地址空间中删除DLL。
5.使用VirtualFreeEx函数,释放第一个步骤中分配的内存。
6.使用GetProcAddress函数,获得FreeLibrary函数的实地址(在Kernel32.dll里)。
7.使用CreateRemoteThread函数,在远程进程中创建一个线程,它调用FreeLibrary函数,传递远程DLL的HINSTANCE。
下面研究一下在DllMain中如何完成API的挂接。
首先了解一下导入表。
对于一个模块来说,如果它调用的函数的实现代码在其它模块中,那么这个函数就叫做导入函数。对于一个模块来说,他的所有导入函数的函数名和函数所驻留的DLL名等信息都保留在该模块的导入表(Import Table)中。导入表是一个IMAGE_IMPORT_DESCRIPTOR结构的数组,每一个IMAGE_IMPORT_DESCRIPTOR结构表述了一个导入模块。
结构体的定义详见王艳萍版《Windows程序设计》320页。
此结构体中的两个关键信息是1.函数序号/名称表的偏移量,2.IAT(导入地址表Import Address Table)的偏移量,根据这两个信息可以分别找到这两个表。
应用程序启动时,载入器根据PE文件(如DLL)的导入表记录的DLL名加载相应DLL模块,再根据导入表的hint/name表(OriginalFirstThunk指向的数组)记录的函数名取得函数的地址,将这些地址保存到导入表的IAT中。
应用程序在调用导入函数时,要先到导入表的IAT中找到这个函数的地址,然后再调用。例如,调用User32.dll模块中的MessageBoxA函数的代码最终会被汇编成如下代码:
call dword ptr [__imp__MessageBoxA@16(0042428c)]
函数的IAT仅仅是一个DWORD数组,数组的每个成员记录着一个导入函数的地址。地址0042428c是导入地址表中MessageBoxA函数对应成员的地址,这个地址处的内容是MessageBoxA在User32模块的真实地址。可见,调用API函数时,程序先要转向PE文件的导入地址表取得API函数的真实地址,然后再转向API函数的执行代码。
于是我们可以用修改IAT表的方法来完成HOOK API的实现。比如,如果将0042428c地指出的内容用一个自定义函数的地址覆盖掉,那么以后这个模块对MessageBoxA的调用实际上就成了对改自定义函数的调用,应用程序以为自己调用的是系统API,但是我们将IAT表修改后,使得在它试图寻找系统API的时候,却找到了我们定义的用来代替该API的自定义函数。但是,为了保持堆栈的平衡,自定义函数使用的调用规则和参数的个数必须与它所替代的API函数完全相同。
这种用修改IAT表进行HOOK API的方法是最稳定的一种。
下面将详细介绍。
首先要在PE文件(如DLL)中定位导入表。这主要是对PE文件结构的分析。
PE文件格式是任何可执行文件或DLL的文件格式,PE文件以64字节的DOS头开始(IMAGE_DOS_HEADER结构),接着是一小段DOS程序,然后是248字节的NT文件头(IMAGE_NT_HEADERS结构)。NT文件头相对文件开始位置的偏移量(因为中间的DOS程序长短不定)可以有IMAGE_DOS_HEADER结构的e_lfanew给出。
下面的代码取得了一个指向IMAGE_OPTIONAL_HEADER结构的指针。
HMODULE hMod = ::GetModuleHandle(NULL);
IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hMod;
IMAGE_OPTIONAL_HEADER* pOptHeader =
(IMAGE_OPTIONAL_HEADER*)((BYTE*)hMod+pDosHeader->e_lfanes+24);
IMAGE_OPTIONAL_HEADER结构体里保存着许多重要的信息,包括我们最感兴趣的数据目录表指针(即我们想要操作的表的地址)。PE文件保存了16个数据目录,最常见的有导入表、导出表、资源和重定位表。我们感兴趣的是导入表。它是一个IMAGE_IMPORT_DESCRIPTOR结构的数组,每个结构对应着一个导入模块。下面的代码取得导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的指针(导入表首地址)。
IMAGE_IMPORT_DESCRIPTOR* pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)hMod+pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
除了通过分析PE文件的结构来定位模块的导入表外,还可以使用ImageDirectoryEntryToData函数。这个函数知道模块基地址后,直接返回指定数据目录表的首地址,用法如下:
PVOID ImageDirectoryEntryToData(
PVOID Base, //模块及地址
BOOLEAN MappedAsImage, //如果此参数是TRUE,文件被系统当做镜像映射,否则,
//将当做数据文件映射
USHORT DirectoryEntry, //指定要取得哪个表的表首地址 传入
//IMAGE_DIRECTORY_ENTRY_IMPORT,取得导入表
//首地址
PULONG Size //返回表象的大小
);//为了调用此API,请添加代码“#include<ImageHlp.h>”和“#progma comment(lib."ImageHil")”
IMAGE_IMPORT_DESCRIPTOR结构包含了hint/name(函数序号/名称)表和IAT表的偏移量。这两个表的大小相同,一个成员对应一个导入函数,分别记录了导入函数的名称和地址。下面的代码打印出了此模块从其它模块导入的所有函数的名称和地址。
此模块从很多其它模块(DLL)中导入函数,这里分别打印出它们。每一个IMAGE_IMPORT_DESCRIPTOR结构描述了一个模块(DLL)向此模块导入函数的情况,该结构提供了很多表的地址,如hint/name表和IAT表,这两个表分别记录了了从此IMAGE_IMPORT_DESCEIPTOR结构所描述的模块导入的函数的名称和地址。
while(pImportDesc->FirstThunk)
{
char* pszDllName=(char*)((BYTE*)hMod+pImportDesc->Name);
printf("/n 模块名称:%s/n",pszDllName);
//一个IMAGE_THUNK_DATA就是一个双字,它指定了一个导入函数
IMAGE_THUNK_DATA *pThunk =
(IMAGE_THUNK_DATA*)((BYTE*)hMod+pImportDesc->OriginalFirstThunk);
int n = 0;
while(pThunk->u1.Function)
{//取得函数名称。hint/name表项前两个字节是函数序号,后面才是函数名称字符串
char* pszFunName=(char*)((BYTE*)hMod+(DWORD)pThunk->
u1.AddressOfData+2);
//取得函数地址。IAT表就是一个DWORD类型的数组,每个成员记录一个函数的地址
PDWORD lpAddr = (DWORD*)((BYTE*)hMod+pImportDesc->FirstThunk)+n;
//打印出函数名称和地址
printf("从此模块导入的函数:%-25s, ",pszFunName);
printf("函数地址:%X/n",lpAddr);
n++;pThunk++;
}
pImportDesc++;
}
由上述可知,定位导入表之后即可定位导入地址表。为了截获API调用,只要用自定义函数的地址覆盖掉导入地址表中真实的API函数地址即可。这就是用DLL远程注入和修改IAT表的方式实现HOOK API的过程。
具体挂钩过程为:在我们注入的DLL里,因为DLL第一次被注入时系统会自动调用DLL的DllMain函数,在DllMain里调用我们同在此DLL里定义的SetHookApi函数,在SetHookApi函数里完成对目标进程(亦即当前进程,因为现在调用DLL内函数的就是我们的目标进程)IAT表的修改,具体修改方法为:
1.保存原有真实API地址,因为此时该API已经被映射到该进程的空间中,所以该API的函数名就代表了其在进程空间里的地址;这个应在SetHookApi外完成,why?
2.定位当前进程的导入表-IMAGE_IMPORT_DESCRIPTOR数组;
3.在导入表中查找要替换的API所在的DLL模块对应的IMAGE_IMPORT_DESCRIPTOR结构;
4.找到后,再定位导入地址表IAT;
5.找到该DLL的IAT后,通过之前保存的真实API地址与IAT表中的u1.Function成员进行匹配,找到存放此真实API地址的内存;
6.取得我们定义的用以替代此API的函数的地址;
7.WriteProcessMemory进行对IAT表的改写。
搞定。
============================================
============================================
最后总结一下利用远程注入DLL、修改IAT表的方式进行HOOK API的思路:
1.要想让目标进程的API被HOOK掉,需要让目标进程执行我们为其定义的SetHookApi函数,如何能让目标进程执行我们定义的函数呢?方法是让目标函数加载我们定义的DLL,SetHookApi的定义也在此DLL里,利用DLL第一次加载时会执行DllMain的机会,执行SetHookApi。
2.新问题,如何让目标进程调用LoadLibrary加载我们定义的DLL呢?方法是为目标进程创建一个远程线程,为其指定线程函数为LoadLibrary,参数为"Mydll.dll"。这样目标进程的新线程创建后就会加载我们定义的DLL到目标进程的空间。创建远程线程的方法是CreateRemoteThread函数。
3.至此,完成了DLL的加载,DLL加载后目标进程会自动执行DllMain,DllMain中执行SetHookApi函数,完成对API的挂接。如何完成API的挂接?只需要修改目标进程的IAT表。目标进程又一个导入表,记录了各个模块导入到此进程中的导入函数的信息,其中又有一个IAT表记录导入函数地址的信息,只需要修改IAT表中欲覆盖的API的函数地址为我们定义的函数的地址即可完成API的挂接。
最后
以上就是有魅力小熊猫为你收集整理的HOOK API——Windows核心编程 第22章 插入DLL和挂接API学习笔记的全部内容,希望文章能够帮你解决HOOK API——Windows核心编程 第22章 插入DLL和挂接API学习笔记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复