工作关系,想HOOK并修改一些API,使得不支持某些设备的第三方工具可以正常运行,因此花时间写了这么个工具。比如ReadFile时,某些设备不支持指定的缓存大小(如512KB),可以HOOK ReadFile,把缓存大小修改为更小,可能ReadFile就能正常工作,第三方工具也能正常使用。其实,只是想借工作这个契机,学习远程线程注入和HOOK API。工作上测试的设备和第三方工具运行在64位机上,还没有时间在64位机上修改并编译。
运行DEMO说明:
首先进入TestExe目录,打开MFCDialogApplication.exe,8个按钮分别简单的调用8个API,可一一点击查看效果,标题栏显示进程ID:
打开DllImport.exe,弹出Console,以下显示的是我输入并HOOK完的界面:
首先输入需要HOOK的进程ID,这里输入对话框进程ID12600。然后提示选择需HOOK的API,每个API有FLAG值,占一位,可以用位组合,这里输入511,即9个API都需HOOK。然后选择是HOOK还是UNHOOK,这里当然输入1。然后这个程序向对话框程序注入线程,调用DllExport.dll,DllExport.dll中HOOK这9个API。结果全部成功,见上图。HOOK对话框后,会在对话框进程中弹出一个Console窗口,用以显示相关信息,见下图:
然后在对话框上一一点击各个按钮,并随时查看弹出的Console窗口中内容,单击完后,见下图:
每点击一个按钮后,在Console中会显示相关信息。这些信息是HOOK时打印的,我只设置了打印简单的信息。还要注意,HOOK MessageBoxA和MessageBoxW时,改变了弹出消息框的标题、文字等。
然后再打开DllImport.exe,把HOOK的API还原,并从对话框进程中卸载DllExport.dll,输入顺序与HOOK一致,只是第三步时需指定0,见下图:
UNHOOK API时我选择的全部还原。完后,9个API不再被HOOK,正常执行,之前显示信息的Console关闭,对话框正常运行。所有API UNHOOK后,DllExport.dll被卸载,可改名、删除等。此时,再点击对话框按钮,就不再有任何显示显示。注意点击两个MessageBox后,消息框的标题与文字。
编译说明:
MFCDialogApplication是生成被注入的对话框工程;DllWorkspace中,DllExport生成需注入的DllExport.dll,DllImport生成执行注入操作的DllImport.exe。
DllWorkspace工作空间中,两个项目都是用的安全字符串操作函数,如果出现找不到头文件,需要更新SDK,我的VC6 Include路径包括“C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include”和“C:\Program Files\Microsoft Visual Studio 9.0\VC\include”,即可编译通过。
DllWorkspace下的两个项目,ANSI、UNICODE编译均可,既四种组合的编译,都可正常注入并HOOK。
注入及HOOK代码简要说明:
注入主要代码如下:
// create a remote thread, and start LoadLibrary to load dll that we make, in the dll, we can // hook apis and can do everything we want BOOL CreateRemoteThread(HANDLE hProcess, LPTHREAD_START_ROUTINE pfnStartAddr, LPVOID pRemoteMem) { BOOL bRet = FALSE; HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnStartAddr, pRemoteMem, 0, NULL); do { if(!hThread) { PrintError(_T("CreateRemoteThread"), GetLastError(), __MYFILE__, __LINE__); break; } TCOUT << _T("Waiting for the end of the remote thread...") << endl; WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); hThread = NULL; bRet = TRUE; } while (FALSE); return bRet; }用CreateRemoteThread创建一个远程进程下的线程,开始执行pfnStartAddr(为LoadLibrary的地址)地址处的代码,传递的参数为pRemoteMem(远程进程下的一段内存空间,保存的是DllExport.dll的路径),创建的远程线程由LoadLibrary开始执行,然后加载DllExport.dll,在DllExport.dll中可执行我们的处理。注意:DllExport.dll的路径是相对于被注入进程的,因此被注入进程需要能找到这个Dll。程序中szDllPath保存路径。
// write infomation to file-mapping, the infomation includes witch apis need to be (un)hooked, // and includes the (un)hook results and so on LPVOID WriteFileMapping(HANDLE hMap, CONTENT_FILE_MAPPING content) { LPVOID pContent = NULL; do { if(!hMap) { PrintMsg(_T("WriteFileMapping fail : hMap is null, file : %s, line : %d\n"), __FILE__, __LINE__); break; } pContent = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(content)); if(!pContent) { PrintError(_T("MapViewOfFile"), GetLastError(), __MYFILE__, __LINE__); break; } memcpy(pContent, &content, sizeof(content)); } while (FALSE); return pContent; }hMap是名字为宏NAME_FILE_MAPPING定义的一个file-mapping,然后向其中写入数据。dll执行时,再打开这个file-mapping,从中读取数据,再把结果写回。
// if no apis be hooked, we must free the library BOOL UnLoadModule(DWORD dwProcesssId, LPCTSTR lpModuleName) { BOOL bRet = FALSE; HANDLE hModuleSnap = INVALID_HANDLE_VALUE; MODULEENTRY32 me32; HANDLE hProcess = NULL; HMODULE hModule = NULL; me32.dwSize = sizeof(me32); hProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, dwProcesssId); do { if(!hProcess) { PrintError(_T("OpenProcess"), GetLastError(), __MYFILE__, __LINE__); break; } hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcesssId); if(INVALID_HANDLE_VALUE == hModuleSnap) { PrintError(_T("CreateToolhelp32Snapshot"), GetLastError(), __MYFILE__, __LINE__); break; } if(!Module32First(hModuleSnap, &me32)) { PrintError(_T("Module32First"), GetLastError(), __MYFILE__, __LINE__); break; } int nRefCount = 0; do { if(!StrCmpI(me32.szModule, lpModuleName) || !StrCmpI(me32.szExePath, lpModuleName)) { hModule = me32.hModule; nRefCount = me32.ProccntUsage; break; } } while(Module32Next(hModuleSnap, &me32)); LPTHREAD_START_ROUTINE pfnStartAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(_T("Kernel32")), "FreeLibrary"); if (!pfnStartAddr) { PrintError(_T("GetProcAddress"), GetLastError(), __MYFILE__, __LINE__); break; } for (int i = 0; i < nRefCount; i++) { HANDLE hThread = ::CreateRemoteThread(hProcess, NULL, 0, pfnStartAddr, hModule, 0, NULL); WaitForSingleObject(hThread, INFINITE); CLOSE_HANDLE(hThread); } PrintMsg(_T("FreeLibrary %s in the process %d finished!\n"), lpModuleName, dwProcesssId); bRet = TRUE; } while (FALSE); CLOSE_HANDLE(hModuleSnap); CLOSE_HANDLE(hProcess); return bRet; }遍历被注入进程加载的模块,如果找到了DllExport.dll,得到被这个进程引用的次数,然后再循环创建远程线程使用FreeLibrary来卸载DllExport.dll。注意,DllExport.dll被加载几次,就必须执行几次FreeLibrary才能完全卸载。至此,DllExport.dll就可以任意操作了。
// hook the specify api // pRecallApiInfo : infomation of the api BOOL HookSpecifyApi(PRECALL_API_INFO pRecallApiInfo) { BOOL bRet = FALSE; do { if (!pRecallApiInfo) { break; } if (pRecallApiInfo->pOrgfnMem) { bRet = TRUE; break; } HMODULE hModule = LoadLibrary(pRecallApiInfo->lpDllName); if (!hModule) { PrintError(_T("LoadLibrary"), GetLastError(), __MYFILE__, __LINE__); break; } USES_CONVERSION; FARPROC pfnStartAddr = (FARPROC)GetProcAddress(hModule, T2CA(pRecallApiInfo->lpFunctionName)); pRecallApiInfo->lpApiAddr = pfnStartAddr; if (!pfnStartAddr) { PrintError(_T("GetProcAddress"), GetLastError(), __MYFILE__, __LINE__); break ; } // we must save the first few bytes of the api(at least five, and these few bytes must complete // the assembly codes), then make the 5 bytes in front of api to jump to our function, and our // function must execute the few bytes saved before, and then jump to the api to execute // the rest code in the api int nSize = 0; int nDisassemblerLen = 0; while(nSize < 5) { // GetOpCodeSize can get the assembly code size nDisassemblerLen = GetOpCodeSize((BYTE*)(pfnStartAddr) + nSize); nSize = nDisassemblerLen + nSize; } DWORD dwProtect = 0; if (!VirtualProtect(pfnStartAddr, nSize, PAGE_EXECUTE_READWRITE, &dwProtect)) { PrintError(_T("VirtualProtect"), GetLastError(), __MYFILE__, __LINE__); break ; } // be sure that we must change pOrgfnMem's protect, because the code in pOrgfnMem // also need to execute pRecallApiInfo->pOrgfnMem = new BYTE[5 + nSize]; DWORD dwMemProtect = 0; if (!VirtualProtect(pRecallApiInfo->pOrgfnMem, 5 + nSize, PAGE_EXECUTE_READWRITE, &dwMemProtect)) { delete [] pRecallApiInfo->pOrgfnMem; pRecallApiInfo->pOrgfnMem = NULL; PrintError(_T("VirtualProtect"), GetLastError(), __MYFILE__, __LINE__); break ; } pRecallApiInfo->nOrgfnMemSize = 5 + nSize; memcpy(pRecallApiInfo->pOrgfnMem, pfnStartAddr, nSize); *(BYTE*)(pRecallApiInfo->pOrgfnMem + nSize) = 0xE9; *(DWORD*)(pRecallApiInfo->pOrgfnMem + nSize + 1) = (DWORD)pfnStartAddr + nSize - (DWORD)(pRecallApiInfo->pOrgfnMem + 5 + nSize); *(BYTE*)(pfnStartAddr) = 0xE9; *(DWORD*)((BYTE*)pfnStartAddr + 1) = (DWORD)pRecallApiInfo->lpRecallfn - ((DWORD)pfnStartAddr + 5); memset((BYTE*)pfnStartAddr + 5, 0x90, nSize - 5); // be sure that we must set the rest to 0x90(assembly code for nop, do nothing, // and occupy one byte), because we should't change the assembly code VirtualProtect(pfnStartAddr, nSize, dwProtect, &dwProtect); bRet = TRUE; } while (FALSE); return bRet; }需要注意的是: 保存的可能并不是API的前5个字节,因为前5个字节可能不是一个或几个完整的汇编指令,比如第5、6个字节合起来才是一个指令,我们就不能只保存前5个字节,最后执行这5个字节,再跳转到第6个字节处执行。这样破坏了指令,必然造成崩溃。这时需要保存前6个字节才行。程序中,我使用了从网上找到的一段代码GetOpCodeSize,GetOpCodeSize可以得到当前地址处的汇编指令长度。然后保存API前至少5个字节,并且这些字节可以组成完整的汇编指令。实际也可以不用这样,可以用另一方式,我们函数中先恢复API的前5个字节,然后再调用API,调用完后再改API前5个字节为跳转到我们函数的指令。但是,这种方式并不好,如果调用API时,API的前5个字节正常,如果再有进程中其他线程调用API,这时流程完全正常,没有被HOOK。
int WINAPI MyMessageBoxA(IN HWND hWnd, IN LPCSTR lpText, IN LPCSTR lpCaption, IN UINT uType) { int nOrderHookApi = ORDER_MESSAGEBOXA; int nRet = 0; static int i = 1; if (g_arHookAPIs[nOrderHookApi].pOrgfnMem) { USES_CONVERSION; PrintMsgA("%-18s %08d : 0x%08x \"%s\" \"%s\" 0x%08x\n", T2CA(g_arHookAPIs[nOrderHookApi].lpFunctionName), i++, hWnd, VALID_CHAR(lpText), VALID_CHAR(lpCaption), uType); nRet = ((pfnMessageBoxA)(LPVOID)g_arHookAPIs[nOrderHookApi].pOrgfnMem)( hWnd, "HelloWorld", "Caption", MB_OKCANCEL); } return nRet; }
最后一定要从保存的API的地址处开始执行。
代码中已实现同时HOOK9个API(MessageBoxA、MessageBoxW、DeviceIoControl、CreateFileA、CreateFileW、ReadFile、ReadFileEx、WriteFile、WriteFileEx),稍加修改,即可实现HOOK更多的API。
编译环境:Windows XP SP3、VC++ 6.0 SP6
源码及DEMO下载地址: 线程注入、HOOK APIs(附VC6源码)