通过Hook API调用打造进程监控程序

来源:互联网 发布:百度黑帽seo 编辑:程序博客网 时间:2024/05/22 01:55
*(收藏)http://hi.baidu.com/linuxetc/blog/item/1b91c813b017e4035baf53a7.html
Hook(钩子)是Windows消息处理过程中的一个监视点,应用程序可以通过Hook拦截Windows消息,这样就可以捕捉到目标进程的消息,并做相应的处理,从而达到修改系统默认行为的目的,另外,在相对封闭的Windows世界里,Hook技术也为我们了解系统内部机制提供了更好的机会。Hook技术在“黑”与“反黑”的对抗中,也扮演着重要的角色,很多木马,后门,都是通过键盘钩子,在后台记录密码,聊天信息。。。通过Hook系统的;可以通过Hook文件操作函数实现对文件的隐藏,躲避查杀;Hook注册表操作函数,实现对注册表中某些键值的隐藏。 另外,杀毒软件,防火墙也可以通过Hook系统的关键函数,实现对系统行为的监控,防止恶意程序在用户不知情的情况下,恶意破坏系统。
下面,我们就通过利用Hook Explorer 进程的CreateProcess函数打造一个小小的进程监控程序,来给大家讲讲如何利用PE文件的导入表实现Hook API的吧。该程序Hook Explore进程的CreateProcess函数后,在windows资源管理器在要运行其它程序时,会弹出这样一个提示框:

 

点击“是”,则程序运行,点击“否”则,程序被阻止运行。(注意:本例只Hook了Explore进程的CreateProcess,因此,该程序不能监控到系统范围内的程序运行情况,如果要这样,可以通过Hook Windows系统调用来实现,希望以后以机会和大家一起讨论这方面的话题。本例作为一个例子对于说明Hook API 调用的原理已足够)。
我们可以通过SetWindowsHookEx函数安装一个Windows标准钩子,根据你安装的钩子类型,钩子可以是远程的,也可以是局部的。可是,Windows所提供的标准钩子中,并没有直接钩挂API调用的。那么如果我们要钩挂某个API,以达到某些目的,该怎么做呢?
先来复习一下PE文件的导入表的相关知识吧。各种文件都有一定的格试,在windows下,EXE,DLL文件用的就是PE文件格式。如果我们现在要调用的是某个DLL中的一个函数,在代码编译期,使用隐式链接方式时,并不需要有指定的DLL,只要需要有DLL 对应的Lib文件就可以了,很明显,在编译的时候,对应的函数的地址是无法确定下来的。那么,在运行的时候又怎么能够正确调用这个API呢?原因是:PE文件在导入表中记录了它所用到的每一个DLL的名字,以及从相应的DLL导入的函数的名字。(在使用显式链接,通过LoadLibrary/GetProcAddress调用函数时,所需要DLL名,和函数名并不会出现在导入表中)。这样,当运行这个程序的时候,Windows可执行文件装载器,就会搜索并载入出现在该程序的导入表的所有DLL,(如果装载器,找不到导入表中的DLL,就会弹出提示框)然后,根据导入表中的导入的函数名字,在DLL的导出表中找到匹配的函数,取得它的地址,再把这个地址填入可执行文件对应的地方。(如果找到所需要的DLL,但是在该DLL的导出表中找不到可执行文件所导入的函数,就会弹出提示框)这就是动态链接的概念。

/
通过PE文件的头的数据目录中的第二个IMAGE_DATA_DIRECTORY项,我们就可以定位到导入表项。也可以直接用ImageDirectoryEntryToData这个API直接定位的导入表项。而导入表由IMAGE_IMPORT_DESCRIPTOR结构组成。
Typedef struct _IMAGE_IMPORT_DESCRIPTOR{
union{
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
}IMAGE_IMPORT_DESCRIPTOR;
其中Name是一个相对虚拟地址(RVA),它指向所导入的DLL名字的一个字符串。如果一个可执行文件导入了多个DLL,则它的导入表中有多个这样的结构,最后有一个全0的IMAGE_IMPORT_DESCRIPTOR结束。OriginalFirstThunk和FirstThunk都是RVA,它们分别指向两个相同的IMAGE_THUNK_DATA。
typedef struct _IMAGE_THUNK_DATA32{
union{
DWORD ForwarderSting;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
}u1;
}IMAGE_THUNK_DATA32;
Typedef IMAGE_THUNK_DATA32 IMAGE_THUNK_DATA;
由此可见,实际上这个联合结构就是一个DWORD,根据规定,当这个DWORD最高位为1时,表示这个导入函数是通过序号形式导入的,否则函数是通过名称导入的,当通过名称导入时,这个DWORD是一个RVA,它指向了一个IMAGE_IMPORT_BY_NAME的结构。
typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint;
BYTE Name[?];
}IMAGE_IMPORT_BY_NAME;
这里面的Name是一个变长数组,它存着所导入的函数名。呵呵,如果没有耐心的话,估计你早就头晕了。现在我们来理一理这些结构的关系吧/。
/
现在,OriginalFirstThunk和FirstThunk都指向两个一样的IMAGE_THUNK_DATA,而这两个IMAGE_THUNK_DATA又都指向了包含了函数名的IMAGE_IMPORT_BY_NAME结构。当Windows装入一个可执行文件的时候,它会找到并载入Name指定的DLL,然后,在该DLL的导出表中找到
IMAGE_IMPORT_BY_NAME所指定的函数名称的地址并填入FirstThunk所指的IMAGE_IMPORT_BY_NAME中。所以当载入完成后.

这样的话,当调用CreateProcess,就会到FirstThunk中找到对应的CreateProcess的入口地址。现在,如果在装载后,把CreateProcess的入口地址改成我们指定的某个地址,那么对CreateProcess的调用会不会转到调用我们的代码上来呢?答案是肯定的。但是在Windows中各个进程都有各自独立的虚拟地址空间,如果我们要Hook一个远程进程,就一定得保证把代码映射到目标进程的地址空间中去。这个,可以使用VirtualAllocEx为目标进程分配空间,然后用WriteProcessMemory把代码写入到目标进程的地址空间中去,再把FirstThunk中的CreateProcess的入口地址改成写入代码的首地址。该方法要自己解决地址重定位的问题。另一种方法,就是把代码放到一个DLL中,然后,让目标进程加载这个DLL,这样就把地址重定位的事情交给Windows来做了。再把FirstThunk中的CreateProcess的入口地址改写成这个DLL中的某段代码的地址。但是,又怎么才能让目标进程加载这个DLL呢?这个我们可以通过为目标进程安装一个远程的钩子(SetWindowsHookEx),这样,就可以把一个DLL映射到目标进程的地址空间中去,然后,修改目标进程相关函数的入口地址就可以了。好了,说到这里,相关的基础知识,相信你也清楚了。我们就来看看程序的实现过程吧。程序分为两部分,一部分是主程序,一部分是一个DLL。启动Hook的过程如下:
// 找到Explorer的窗口句柄
hWnd = FindWindow("Progman", "Program Manager");
DWORD dwProcessId; //获取Explorer进程及窗口线程的ID。
DWORD dwThreadId = GetWindowThreadProcessId(hWnd, &dwProcessId);
if (!dwThreadId){
MessageBox(GetActiveWindow(), "Explorer 没有启动", "Explorer 没有启动", MB_OK);
return;
}
HMODULE hModule = LoadLibrary("HookApi.dll"); // HookApi.dll是要注入的DLL
HOOKPROC GetMsgProc = (HOOKPROC)GetProcAddress(hModule, "MsgHook");
hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, hModule, dwThreadId);
PostMessage(hWnd, WM_HOOK, 0, 0);
FreeLibrary(hModule);
上面的代码关键处是用SetWindowsHookEx为Explorer的窗口线程安装了一个WH_GETMESSAGE类型的钩子,这样,每当Explorer的窗口线程从它的消息队列中取到消息的时候,就会先调用HookApi.dll中的GetMsgProc回调函数。注意这里,不能用SendMessage,因为SendMessage并不把消息放入目标窗口的消息队列中,而是直接调用目标窗口的消息回调函数。
接下来,我们看看HookApi.dll中的GetMsgProc回调函数,
__declspec(dllexport) LRESULT CALLBACK MsgHook(int nCode, WPARAM wParam, LPARAM lParam){
PMSG pmsg = (PMSG)lParam;
switch(pmsg->message){
case WM_HOOK:
GetDebugPrivilege(); // 提高进程权限,为写导入表做准备
// 用于同步,我将在后面详细解释为什么。
hEvent = CreateEvent(NULL, TRUE, FALSE, "MyApiHookEvent");
Hook("Kernel32.dll", "CreateProcessA", (PROC)NewCreateProcessA);
Hook("Kernel32.dll", "CreateProcessW", (PROC)NewCreateProcessW);
break;
case WM_UNHOOK:
UnHook();
SetEvent(hEvent);
CloseHandle(hEvent);
break;
default:
CallNextHookEx(NULL, nCode, wParam, lParam);
break;
}
return 0;
}
每当Explorer从它的窗口消息队列中,取到消息的时候,就会先调用MsgHook回调函数,我们在这个函数中,先判断消息是不是我们自己定义的消息WM_HOOK或WM_UNHOOK,如果是,则处理,如果不是,则不是我们关心的消息,调用CallNextHookEx把消息往下传。
void Hook(LPCTSTR lpModule, LPCTSTR lpFunction, PROC pfnNewFunc){
HMODULE hThisModule = GetThisModule(); // 获取本DLL的模块句柄
// 这里取得Explorer进程中的所有模块。
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
MODULEENTRY32 me = {sizeof(me)};
for (BOOL fFlag = Module32First(hSnapShot, &me); fFlag; fFlag = Module32Next(hSnapShot, &me)){
if (me.hModule != hThisModule){
try{
ReplaceFunc(me.hModule, pfnNewFunc, lpModule, lpFunction);
}catch(...){
continue;
}
}
}
CloseHandle(hSnapShot);
}
代码首先获取Explorer进程中的每一个模块,然后调用ReplaceFunc尝试修改每个模块的导入表。
void ReplaceFunc(HMODULE hModule, PROC pfnNew, LPCTSTR lpModule, LPCTSTR lpFunction){
ULONG ulSize;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
// 获取导入表的地址。
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData
(hModule, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
if (pImportDesc == NULL)
return;
// 由于导入表中,可能导入了很多DLL,我们先找到,要Hook的DLL,此例中,为kernel32.dll
while (pImportDesc->Name){
PSTR pszName = (PSTR) ((PBYTE) hModule + pImportDesc->Name);
if (lstrcmpi(pszName, lpModule) == 0)
break; // 找到目标DLL
else
pImportDesc++;
}
if (pImportDesc == 0 || pImportDesc->Name == 0)
return;
// 定位FirstThunk和OriginalThunk,前面说过,在内存中,FirstThunk间接指向导入表中,真实的函数
//地址的结构,而OriginalThunk间接指向真实的函数名。
PIMAGE_THUNK_DATA pFirstThunk = (PIMAGE_THUNK_DATA)((PBYTE)hModule + pImportDesc->FirstThunk);
PIMAGE_THUNK_DATA pOriginalThunk = (PIMAGE_THUNK_DATA)((PBYTE)hModule + pImportDesc->OriginalFirstThunk);
// 确保函数不是以序号导入的。
if ((pOriginalThunk->u1.ForwarderString & IMAGE_ORDINAL_FLAG32) != 0)
return;
while (pOriginalThunk->u1.Function){ // 找到函数名。
PIMAGE_IMPORT_BY_NAME pImageByName = (PIMAGE_IMPORT_BY_NAME)((PBYTE) hModule + pOriginalThunk->u1.ForwarderString);
// 比较该项的函数名是不是我们要Hook的函数,如果是,则修改对应的导入地址。
if (lstrcmpi(((LPCSTR)&(pImageByName->Name)), lpFunction) == 0){
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE, GetCurrentProcessId());
// 将修改的信息保存起来,以简化UnHook的工作。
PHooker p;
p = new Hooker;
p->pNext = Head;
p->lpFunction = lpFunction;
p->lpModule = lpModule;
p->pfnNew = pfnNew;
p->pfnOld = (PROC)pFirstThunk->u1.Function;
p->lpAddr = &(pFirstThunk->u1.Function);
Head = p;
WriteProcessMemory(hProcess, (LPVOID)(&(pFirstThunk->u1.Function)), (LPVOID)(&pfnNew), sizeof(pfnNew), NULL);
}
pFirstThunk++;
pOriginalThunk++;
}
}
主程序,能过下列方式通知HookApi.dll进行UnHook动作。
PostMessage(hWnd, WM_UNHOOK, 0, 0);
HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, "MyApiHookEvent");
WaitForSingleObject(hEvent, INFINITE);
CloseHandle(hEvent);
UnhookWindowsHookEx(hHook);

HookApi.dll中的MsgHook回调函数对WM_UNHOOK消息的处理:
case WM_UNHOOK:
UnHook();
SetEvent(hEvent);
CloseHandle(hEvent);
UnHook过程就非常简单了,前面每修改了一项导入表中的函数地址,就把它的相关信息保存在一个链表中,现在,只要根据这个链表,把每一个函数地址恢复,就完成了UnHook过程。这里我要说的是,当调用PostMessage把消息丢入Explorer的消息队列中,但不能保证Explorer什么时候来处理这条消息,而我们是通过SetWindowsHookEx把HookApi.dll映射到Explorer的进程地址空间中去的,当调用UnhookWindowsHookEx的时候,HookApi.dll会从Explorer进程中卸载。现在,考虑这样一种情况,当用PostMessage发送消息后,由于Explorer因为某种原因没来得及处理这条消息,而主程序调用了UnhookWindowsHookEx,这时候,HookApi.dll在Explorer进程地址空间中的映射被解除,后来,Explorer从它的消息队列中取出WM_UNHOOK,由于消息钩子也已经被解除,所以WM_UNHOOK消息不会再有机会被处理,因而也没有机会把导入表中的函数地址恢复回样,如果在个时候,再调用之前被HOOK了的API,那个地址还是指向HookApi.dll中的某个函数中的地址,但是,这个DLL已经被解除,很明显,由于一个对无效地址的CALL,将直接导致Explorer进程的崩溃。所以,本例中,在WM_HOOK中,建立了一个未通知状态的事件对象,然后,主程序UnHook时,用PostMessage发送消息后,用WaitForSingleObject无限期等待这个事件对象,只有当Explorer的消息钩子处理了WM_UNHOOK消息时,才用SetEvent把这个事件对象设为通知状态,这样,主程序中的UnhookWindowsHookEx才有机会被执行,从而保证了同步。
后记:本文结合消息钩子以及对PE文件导入表的操作,实现了对API调用的拦截,由于篇幅的原因,本文只对关键代码进行了分析,详细的代码请参阅附带的源代码。注意本文的代码,只做说明之用,略去了一些错误处理,用VS 2003编译通过,可执行程序,在作者的Windows XP SP2以及Windows 2K中测试通过,对于其它机器上作者不能保证其正确运行,对此引起的任何问题,作者不承担任何责任。对本文的纰漏之处,望诸位大侠斧正。