跨进程API HOOK

什么是“跨进程 API Hook”?

众所周知Windows应用程序的各种系统功能是通过调用API函数来实现。API Hook就是给系统的API附加上一段小程序,它能监视甚至控制应用程序对API函数的调用。所谓跨进程也就是让自己的程序来控制别人程序的API调用了。

API Hook 理论

通过对Win32 PE文件的分析(如果你还不熟悉PE文件格式,可以看看Iczelion的PE教程或者LUEVELSMEYER的<<The PE File Format>>)。我们知道在PE文件中的IMPORT TABLE内存储着API函数的很多信息。其中包括API的函数名,调用地址等等。而操作系统在执行PE文件时会先将其映射到内存中。在映射的同时还会把当前版本操作系统中API函数的入口地址写入IMPORT TABLE中一组与API调用相关的结构体内,用于该应用程序的API调用。当应用程序调用API时,他会在自己内存映像里寻找API的入口地址,然后执行CALL指令。如此一来,我们通过修改应用程序内存映像的IMPORT TABLE中API函数的入口地址,就可以达到重定向API的目的。将API地址改为我们自己函数的地址,这样我们的函数就可以完成对API的监视和控制了。

API Hook 的实现

/* 1 */HANDLE hCurrent = GetModuleHandle(NULL);/* 2 */IMAGE_DOS_HEADER *pidh;/* 3 */IMAGE_NT_HEADERS *pinh;/* 4 */IMAGE_DATA_DIRECTORY *pSymbolTable;/* 5 */IMAGE_IMPORT_DESCRIPTOR *piid;/* 6 */pidh = (IMAGE_DOS_HEADER *)hCurrent;/* 7 */pinh = (IMAGE_NT_HEADERS *)((DWORD)hCurrent + pidh->e_lfanew);/* 8 */pSymbolTable = &pinh->OptionalHeader.DataDirectory[1];/* 9 */piid =(IMAGE_IMPORT_DESCRIPTOR *)((DWORD)hCurrent +  pSymbolTable->VirtualAddress);/*10 */do {/*11 */    IMAGE_THUNK_DATA *pitd,*pitd2;/*12 */    pitd = (IMAGE_THUNK_DATA *)((DWORD)hCurrent + piid->OriginalFirstThunk);/*13 */    pitd2 = (IMAGE_THUNK_DATA *)((DWORD)hCurrent + piid->FirstThunk);/*14 */    do {/*15 */ IMAGE_IMPORT_BY_NAME *piibn;/*16 */ piibn = (IMAGE_IMPORT_BY_NAME *)((DWORD)hCurrent +  *((DWORD *)pitd));/*17 */ PROC *ppfn = (PROC *)(pitd2->u1.Function);/*18 */ if (!strcmp("MessageBoxW",(char *)piibn->Name)) {/*19 */     oldMsg = (MsgBoxType)(ppfn);/*20 */     DWORD addr = (DWORD)MyMessage;/*21 */     DWORD written = 0;      /* 改变内存读写状态 *//*22 */     DWORD oldAccess;/*23 */     VirtualProtect(&pitd2->u1.Function,sizeof(DWORD),PAGE_WRITECOPY,&oldAccess);/*24 */     APIAddress = (DWORD)&pitd2->u1.Function;      /* 向内存映像写入数据 *//*25 */     WriteProcessMemory(GetCurrentProcess(),&pitd2->u1.Function, &addr,sizeof(DWORD), &written);/*26 */ }/*27 */ pitd++;pitd2++;/*28 */    } while (pitd->u1.Function);/*29 */    piid++;/*30 */} while (piid->FirstThunk + piid->Characteristics  + piid->ForwarderChain + piid->Name + piid->TimeDateStamp);

分析:

寻觅IMPORT TALBE

在/*1*/中我们使用GetModuleHandle(NULL)来返回当前进程在内存中映像的基地址。但这个值在文档中仅仅被描述为"a module handle for the specified module",虽然他确实是进程内存映像的基地址。如果你不太放心的话也可以使用,GetModuleInformation函数来获得基地址,只不过你要额外包含psapi.h和psapi.lib了(这个库在VC6里没有,所以我就没有用这个函数了)。在/* 6 */里我们先找到IMAGE_DOS_HEADER结构,他的起始地址就是映像的基地址。/*7*/通过IMAGE_DOS_HEADER给出的PE文件头的偏移量,找到IMAGE_NT_HEADERS结构。顺藤摸瓜,IMAGE_NT_HEADERS里的OptionalHeader中的DataDirectory数组里的第二个元素正是指向我们想要的IMPORT TABLE的地址。在/*9*/中我们将其转化为一个IMAGE_IMPORT_DESCRIPTOR的结构指针存入piid中。

替换的API函数入口地址

在/*12*/和/*13*/中我们分别取得OriginalFirstThunk和FirstThunk结构,用于以后得到API函数的名称和入口地址。/*10*/的do循环让我们遍历每一个IMAGE_IMPORT_DESCRIPTOR结构也就是应用程序引用的每个DLL。在/*14*/的循环中我们遍历DLL中的IMAGE_THUNK_DATA结构来一一查询API的信息。/*16*/中我们将OriginalFirstThunk转换为IMAGE_IMPORT_BY_NAME结构用于获得API函数的名称进行比对。在/*18*/我们找到MessageBoxW函数之后,在/*19*/保存其原始入口地址便于以后恢复时使用。在/*23*/我们需要用VirtualProtect改变一下内存区域的读写性,因为一般应用程序的映像都是只读的,直接写入会造成一个非法访问的异常出现。在/*25*/我们写入自己函数的地址。

这样就基本完成一个API函数的重定向。

其他

恢复函数的API入口地址相对比较简单。只要把保存的值再写回去就可以了。上面的程序中/*24*/我用APIAddress保存了存有MessageBoxW入口地址的地方的地址,便于以后调用WriteProcessMemory恢复时使用。

跨进程理论

我们要用自己的函数来替代别人程序里的API函数,但我们的函数与别人的程序处于不同的进程空间内啊。不同的进程空间是不能相互调用函数的。因此我们要想办法把自己的函数放入别人的进程空间去。这时我们就需要使用DLL injection技术了。如果你对她还不是十分熟悉的话,建议看看Jeffrey Richter大师的<<Programming Applications for Microsoft Windows>>,也可以参考陈宽达先生的<<C++ Builder深度历险>>。

简而言之,DLL injection就是想办法让对方的进程加载我们的一个DLL程序,把需要替换的函数放在我们这个DLL里。如此一来,我们的函数就进入了别人的进程空间了。DLL injection方法很多,Richter大师在书中对各方法利弊有详细解释,陈宽大先生的书中也有深入的分析。我在这里使用SetWindowsHookEx函数来达到目的。主要有这几个原因: 1, 不用重新启动系统,调试方便。2, 可以利用消息循环机制进行两个进程之间的通信,可以较好的掌握Hook的状态。便于安装与卸载。

SetWindowsHookEx之所以能完成DLL injection是因为它要给一个应用程序某个环节加上一个Hook,而Hook就要有Hook Procedure也就是Hook函数。如果这个Hook函数在一个DLL中,那么系统就会把这个DLL加载到SetWindowsHookEx的目标进程上。从而也就达到了我们DLL injection的目的了。当然这里我们会用WH_GETMESSAGE的Hook进行injection,因为这个Hook可以用来监视目标进程的消息循环方便我们的进程与目标进程通信。

跨进程的实现和几点注意

/* DllPart.Dll */#include <windows.h>#include <stdio.h>#include <stdlib.h>#include <string.h>typedef (WINAPI *MsgBoxType)(HWND,LPCWSTR,LPCWSTR,UINT);MsgBoxType oldMsg;  /*API原入口地址*/DWORD APIAddress; /*存储API入口地址的地方的地址*/int WINAPI  MyMessage(HWND hWnd ,LPCWSTR M1,LPCWSTR M2, UINT M3) { /* 这是用来替换的函数 */ return oldMsg(hWnd,buf,M2,MB_OK);}const char szApp[] = "DllPart.dll";HHOOK hHook; /*Hook的句柄*/HMODULE hInst; /*DLL 模块句柄,用于SetWindowsHookEx函数*/HWND hTarget; /*目标窗口句柄*//*DLL 入口*/BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID lpvReserved){    hInst = inst;    switch (reason) { case DLL_PROCESS_ATTACH:     /*调试信息,表示DLL已经加载*/     MessageBox(NULL,"DLL_PROCESS_ATTACH",szApp,MB_OK);     break; case DLL_PROCESS_DETACH:     /*调试信息,表示DLL已经卸载*/     MessageBox(NULL,"DLL_PROCESS_DETACH",szApp,MB_OK);     break;    }    return true;}/*显示GetLastError的信息*/void showerr(const char *m) {    char message[255];    FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,0,GetLastError() ,MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),message,255, 0);    MessageBox(NULL,message,m,MB_OK);}//-----------------------void UnHookApi() { /*卸载API Hook用*/}void HookApi() { /*加载API Hook同上面介绍的函数一样*/}//-----------------------/*用于WH_GETMESSAGE的Hook Procedure*/LRESULT CALLBACK GetMsgProc(int nCode,WPARAM wParam, LPARAM lParam) { if (nCode == HC_ACTION) {     MSG *msg = (MSG *)lParam;     if (msg->message == WM_CHAR) {      if (msg->wParam == 'h') HookApi();      if (msg->wParam == 'u') UnHookApi();     } }    return CallNextHookEx(hHook,nCode,wParam,lParam);}extern "C" __declspec(dllexport) SetAPIHook(HWND handle) {    DWORD ThreadId = GetWindowThreadProcessId(handle, NULL);    hTarget = handle;    MessageBox(NULL, "Enabling CallWndProc Hook", szApp, MB_OK);    hHook = SetWindowsHookEx(WH_GETMESSAGE,GetMsgProc,hInst,ThreadId);    if (hHook) { MessageBox(NULL,"Hook OK!", szApp, MB_OK);    } else { showerr("SetWindowsHookEx");    }}extern "C" __declspec(dllexport) UnHookAPIHook() {    MessageBox(NULL, "Disenabling CallWndProc Hook", szApp, MB_OK);    if (UnhookWindowsHookEx(hHook)) {        MessageBox(NULL,"UnHook OK!", szApp, MB_OK);    } else { showerr("UnHookWindowsHookEx");    }}

分析

几个需要注意的问题

SetAPIHook和UnHookAPIHook是我们自己进程调用的用来加载WH_GETMESSAGE Hook的函数。由于我们的程序要用LoadLibrary加载这个Dll因此这两个函数要用__declspec(dllexport)修饰,使其成为导出函数,才能被GetAddressProc函数找到。加上 extern "C"是让编译器使用C语言编码方式。因为C++编译器会进行Dynamic Binding(C++函数重载的实现),将函数的参数类型附加到名称上。是函数的导出名看起来像SetAPIHook@XYTZX之类的,不利于GetAddressProc进行引用。因此使用extern "C"让编译器不使用Dynamic Binding,自然使用extern"C"的函数也就不能被重载了。

不要忘记在GetMsgProc最后要调用CallNextHookEx函数,保证Hook链的完整性。

一定要在Hook Procedure中调用HookApi和UnHookApi函数,因为保存API入口地址的地方在目标进程中,你必须在目标进程的进程空间内完成卸载操作,不能在UnHookAPIHook或是SetAPIHook函数中调用,因为UnHookAPIHook是我们的进程调用的,因此在我们的进程空间中。在这里使用UnHookApi会造成一个非法访问的错误。而使用HookApi会给自己的DLL加上API Hook。

SetWindowsHookEx的最后参数是ThreadId不是Handle,因此要通过调用GetWindowThreadProcessId转换一下。

在跨进程API HOOK时可能用到的其他技术

主进程与目标进程的信息交互和共享

由于使用了WH_GETMESSAGE钩子我们可以利用Windows消息机制实现进程间通讯。需要注意的是应该使用PostThreadMessage来发送让WH_GETMESSAGE得到的消息而不是SendMessage或者PostMessage,因为后两个是用来给窗口发送消息的。而我们的WH_GETMESSAGE是Hook在线程上面的,因此需使用PostThreadMessage.

传递不太大的数据可以使用WM_COPYDATA消息来进行。同样也应该注意,如果使用MFC的窗口过程获得消息就需要用SendMessage发送了。WM_COPYDATA的使用相对简单可以参考MSDN的文档。也可以参考附录里的程序Hook.cpp的showerr函数部分。

如果传递较大的数据或者希望数据共享比较方便可以开辟共享内存来进行数据共享。这里简单分析一下使用共享内存的代码

HANDLE hMap;switch (reason) {    case DLL_PROCESS_ATTACH:    /*创建/打开共享内存区域*/    hMap = CreateFileMapping((HFILE *)0xFFFFFFFF,NULL,PAGE_READWRITE,0,sizeof(GLOBALDATA),ID_MAP);    pg_data = (GLOBALDATA*)MapViewOfFile(hMap,FILE_MAP_ALL_ACCESS,0 ,0 ,0);    if (!pg_data) { MessageBox(NULL,"无法建立共享内存,程序终止!",szApp,MB_OK); if (hMap) {     CloseHandle(hMap);     hMap = NULL;     return 0;       }    }    pg_data->hInst = hInst;    showerr("共享内存映像文件");    showerr("DLL装载中...",FALSE);    break;    case DLL_PROCESS_DETACH:    if (pg_data) {        UnmapViewOfFile(pg_data); pg_data = NULL;    }    if (hMap) { CloseHandle(hMap); hMap = NULL;     }    break;}

上面的代码通过CreateFileMapping建立共享区域。将其第一个参数设置为0xFFFFFFFF使其能创建一个内存共享区域而不是文件。并标记为可读写的(PAGE_READWRITE).其大小为我们定义的结构体GLOBALDATA的大小。最后的ID_MAP是一个用来标示这个区域的字符串。打开或者创建完共享区域后,我们用MapViewOfFile来获得这个区域的地址。之后就可以直接使用pg_data来操作共享区域了。不要忘记在DLL退出的时候安全的删除共享区域释放内存。

消息等待与安全卸载

在我们卸载WH_GETMESSAGE钩子之前必须先把目标程序的API调用恢复正常。我们不能再调用UnHookApi之后就立刻调用UnhookWindowsHookEx,因为很有可能UnHookApi还没来得急完成API入口地址的恢复操作,WH_GETMESSAGE钩子就已经被卸载了。因此需要等待一段时间,等UnHookApi完成了恢复操作在调用UnhookWindowsHookEx。以防错误发生。

extern "C" __declspec(dllexport) void UnHookAPIHook() {    /*向目标线程发送消息进行API UNHOOK*/    PostThreadMessage(pg_data->idTarget,WM_DISABLEAPIHOOK,(WPARAM)GetCurrentThreadId(),0);    showerr("WM_DISABLEAPIHOOK");    /*等待目标进程返回WM_UNHOOKOK消息,确认可以将WH_GETMESSAGE的HOOK去掉*/    MSG Msg;    do {        GetMessage(&Msg,NULL,0,0);    }while(Msg.message !=  WM_UNHOOKOK);    UnhookWindowsHookEx(pg_data->hHook);    PostThreadMessage(pg_data->idTarget,WM_DISABLEAPIHOOKOK,(WPARAM)GetCurrentThreadId(),0);    showerr("UnHookWindowsHookEx");}

上面的代码中我们使用一个含有GetMessage的循环来等待消息的到达,一旦UnHookApi完成他就会发送WM_UNHOOKOK消息。等我们接收到消息确认一切安全了在来卸载WH_GETMESSAGE钩子。

弄清消息对象

我们一定要清楚代码是在主程序进程空间中执行的还是在目标程序进程空间中执行的。像上面的UnHookAPIHook函数就是通过主程序调用的,因此在主程序进程空间中执行。这样一来用来恢复目标程序API信息的UnHookApi完成后就应该向主程序发送消息,而不是目标程序。

目标进程加载了其他DLL

如果目标进程动态加载了其他的DLL文件,我们必须监视LoadLibrary函数。保证DLL中的API入口地址也被正确修改。防止出现混乱的情况。我从LoadLibrary获得DLL的路径用于GetModuleHandle来取得他的ImageBase的地址。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值