最近看见不少关于APIHook的帖子,但是大部分都只做了一个简单的描述,即使有些提供了例子的帖子,也多多少少有些执行上问题。
因此,本人根据以前写的半成品,并参照《Windows核心技术开发》的22章节,对原有代码进行重新设计,在进行详细的测试之后,基本的框架已经可以出炉了。
首先,介绍一下APIHook。进行API挂接时,所需的基本函数有【LoadLibraryA,LoadLibraryW,LoadLibraryExA,LoadLibraryExW,GetProcAddress】。
这5个API是不可缺少的,其余的API挂接都可以算作扩展,因此构架上将其分成两个以上的模块是最合适,也是最容易扩展的。
基于上述考虑,我们将实现部分分成APIHook.DLL和UserAPIHook.DLL两个部分。当然用户扩展模块还可以按照类别分成User1,User2,...UserN等多个模块,这样的话,可以在增加新的API挂接时,可以不用修改原有的DLL。
好了我们首先来介绍一下APIHook.DLL的构架。
其中包含接口和实现两个部分。
1)接口
对于这次实现的DLL,只要当前EXE的进程加载该DLL,DLL的初始化处理中,会自动扫描进程中所有的DLL模块,并且将上述5个基本函数挂接上。
原理也很简单,就是将所有的DLL的输入节都进行修改。
基于这一点,如果只是在本进程内使用的话,也不需要什么接口。因此,此处实现的接口是用来挂接其他进程的,技术上将DLL注入其他进程有很多方法,这里只实现两种,HOOK和CreateRemoteThread。
1.1)HOOK
使用系统的SetWindowsHookEx方法来实现,接口上提供以下接口:
BOOL SetupHook(DWORD dwThreadID, HMODULE hModule); BOOL SetupHookWithHWnd(HWND hWnd, HMODULE hModule); BOOL SetupUnhook();
最后一个用来取消Hook。
前面两个调用,最终是通过内部函数SetupHook来实现。
BOOL CAPIHookImpl::SetupHook(BOOL bInstall, DWORD dwThreadID, HMODULE hModule)
{
BOOL fOK = FALSE;
HANDLE hMutex = ::CreateMutex(NULL, TRUE, TEXT("APIHook_SetWindowsHookEx_Mutex"));
if(bInstall)
{
if(g_hHook == NULL)
{
if(hModule == NULL)
{
hModule = ModuleFromAddress(GetMessageProc);
}
g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, GetMessageProc, hModule, dwThreadID);
fOK = g_hHook != NULL;
if(fOK)
{
if(dwThreadID != 0)
{
::PostThreadMessage(dwThreadID, WM_NULL, 0 ,0);
}
}
}
}
else
{
if(g_hHook != NULL)
{
fOK = UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
}
::ReleaseMutex(hMutex);
::CloseHandle(hMutex);
return fOK;
}
1.1.1)g_hHook需要用共享段来处理,这样可以让使用该DLL的所有进程都能访问该句柄。
1.1.2)因为上述g_hHook句柄是一个全局变量,该内部函数的处理过程中,需要加上同步控制。
1.1.3)ModuleFromAddress函数是参照《Windows核心技术开发》来写的,也就是得到参数所在的模块的句柄,这里一般为本DLL的句柄。
1.1.4)hModule参数,和上面一样,先卖个关子。
1.1.5)这次使用的Hook是GetMessage钩子,因此在挂上之后,需要给目的线程发一个通知。因为不知道目的地的窗口句柄,因此使用PostThreadMessage是最方便的,消息存在即可,因此选择空消息WM_NULL。
1.2)CreateRemoteThread
使用创建远程线程方法来实现,接口上提供以下接口:
HMODULE RemoteInject(DWORD dwProcessID, PCSTR pszLibName); HMODULE RemoteInjectWithHWnd(HWND hWnd, PCSTR pszLibName); BOOL RemoteEject(DWORD dwProcessID, HMODULE hModule); BOOL RemoteEjectWithHWnd(HWND hWnd, HMODULE hModule);
如果目标进程是一个系统进程时,由于其没有退出,该DLL始终处于占用状态,不能被改写。这样调试的时候,想重新编译都很麻烦。
有些人会问,RemoteInject是如何得到远程加载的DLL句柄呢?卸载只能用FreeLibray函数,参数为加载时得到的句柄。幸好FreeLibrary的函数原型,和LoadLibrary一样,与线程函数一样,因此有了可行的必要条件。
我们知道,当一个线程结束时,可以通过GetExitCodeThread来得到线程的结束代码,由于LoadLibrary的返回值为模块句柄,也为远程线程的返回码,真是完事具备。
只要我们在RemoteInject函数中调用GetExitCodeThread来取得线程的结束代码,也就得到了远程DLL的加载时的句柄。
这里需要注意一下,这个句柄只有远程进程中有效,对于当前进程是无效的。不能在本进程中调用FreeLibray来卸载远程DLL。
好了,一切前提条件都OK了,我们也通过创建远程线程方法来实现RemoteEject功能。
创建远程线程的代码,这里偷偷懒,也不网上查了,就地取材参照熊猫正正的代码,稍微按照自己的风格修改了一下。
HMODULE RemoteInject(DWORD dwProcessID, PCSTR pszLibName)
{
HMODULE hRemoteModule = NULL;
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
PSTR pszLibFileRemote = NULL;
DWORD cch;
PTHREAD_START_ROUTINE pfnThreadRoutine;
CHAR szModuleName[MAX_PATH];
if(pszLibName == NULL)
{
::GetModuleFileNameA(g_hModule, szModuleName, sizeof(szModuleName));
pszLibName = szModuleName;
}
do
{
//获得想要注入代码的进程的句柄
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
if (hProcess == NULL)
break;
//计算DLL路径名需要的字节数
cch = 1 + strlen(szModuleName);
//在远程线程中为路径名分配空间
pszLibFileRemote = (PSTR)::VirtualAllocEx(hProcess, NULL, cch, MEM_COMMIT, PAGE_READWRITE);
if (pszLibFileRemote == NULL)
break;
//将DLL的路径名复制到远程进程的内存空间
if (!::WriteProcessMemory(hProcess, (PVOID)pszLibFileRemote, (PVOID)pszLibName, cch, NULL))
break;
//获得LoadLibraryA在Kernel32.dll中的真正地址
pfnThreadRoutine = (PTHREAD_START_ROUTINE)::GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
if (pfnThreadRoutine == NULL)
break;
//创建远程线程,并通过远程线程调用用户的DLL文件
hThread = ::CreateRemoteThread(hProcess, NULL, 0, pfnThreadRoutine, (LPVOID)pszLibFileRemote, 0, NULL);
if (hThread == NULL)
break;
//等待远程线程终止
::WaitForSingleObject(hThread, INFINITE);
}while(FALSE);
//关闭句柄
if (pszLibFileRemote != NULL)
{
::VirtualFreeEx(hProcess,(PVOID)pszLibFileRemote, 0, MEM_RELEASE);
}
if (hThread != NULL)
{
DWORD dwExitCode;
::GetExitCodeThread(hThread, &dwExitCode);
hRemoteModule = (HMODULE)dwExitCode;
::CloseHandle(hThread);
}
if (hProcess != NULL)
{
::CloseHandle(hProcess);
}
return hRemoteModule;
}
BOOL RemoteEject(DWORD dwProcessID, HMODULE hModule)
{
BOOL bRetCode = FALSE;
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
PTHREAD_START_ROUTINE pfnThreadRoutine;
do
{
//获得想要注入代码的进程的句柄
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
if (hProcess == NULL)
break;
//获得LoadLibraryA在Kernel32.dll中的真正地址
pfnThreadRoutine = (PTHREAD_START_ROUTINE)::GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "FreeLibraryA");
if (pfnThreadRoutine == NULL)
break;
//创建远程线程,并通过远程线程调用用户的DLL文件
hThread = ::CreateRemoteThread(hProcess, NULL, 0, pfnThreadRoutine, (LPVOID)hModule, 0, NULL);
if (hThread == NULL)
break;
//等待远程线程终止
::WaitForSingleObject(hThread, INFINITE);
}while(FALSE);
if (hThread != NULL)
{
DWORD dwExitCode;
::GetExitCodeThread(hThread, &dwExitCode);
bRetCode = (BOOL)dwExitCode;
::CloseHandle(hThread);
}
if (hProcess != NULL)
{
::CloseHandle(hProcess);
}
return bRetCode;
}
1.3)还有一类也可以算作接口的接口,主要是为了给扩展APIHook.DLL的使用的。
我们定义导出类,来简化扩展APIHook.DLL的实现。
class CAPIHookImpl;
class APIHOOK_API CAPIHook
{
public:
CAPIHook(PCSTR pszCalleeModName, PCSTR pszFuncName, PROC pfnHook);
~CAPIHook();
operator PROC();
private:
CAPIHookImpl *m_APIHookImpl;
};
CAPIHook构造函数进行挂接,析构函数进行取消,内部实现也是在构造函数创建CAPIHookImpl对象,析构函数删除CAPIHookImpl对象。因此所有实现细节都在CAPIHookImpl类中。
操作函数operator PROC是用来返回原始API函数指针的。
为了更好的使用该导出类,我们同时定义了一套操作宏定义。如下:
#define DEFINE_USERAPIHOOK1(lib, ret, x, t1, p1) \
ret WINAPI User_##x(t1 p1);\
CAPIHook ghook_##x(lib, #x, (PROC)User_##x);\
ret WINAPI CallBack_##x(t1 p1){return ((ret (WINAPI *)(t1 p1))(PROC)ghook_##x)(p1);}\
ret WINAPI User_##x(t1 p1)
#define DEFINE_USERAPIHOOK2(lib, ret, x, t1, p1, t2, p2) \
ret WINAPI User_##x(t1 p1, t2 p2);\
CAPIHook ghook_##x(lib, #x, (PROC)User_##x);\
ret WINAPI CallBack_##x(t1 p1, t2 p2){return ((ret (WINAPI *)(t1 p1, t2 p2))(PROC)ghook_##x)(p1, p2);}\
ret WINAPI User_##x(t1 p1, t2 p2)
#define DEFINE_USERAPIHOOK3(lib, ret, x, t1, p1, t2, p2, t3, p3) \
ret WINAPI User_##x(t1 p1, t2 p2, t3 p3);\
CAPIHook ghook_##x(lib, #x, (PROC)User_##x);\
ret WINAPI CallBack_##x(t1 p1, t2 p2, t3 p3){return ((ret (WINAPI *)(t1 p1, t2 p2, t3 p3))(PROC)ghook_##x)(p1, p2, p3);}\
ret WINAPI User_##x(t1 p1, t2 p2, t3 p3)
#define DEFINE_USERAPIHOOK4(lib, ret, x, t1, p1, t2, p2, t3, p3, t4, p4) \
ret WINAPI User_##x(t1 p1, t2 p2, t3 p3, t4 p4);\
CAPIHook ghook_##x(lib, #x, (PROC)User_##x);\
ret WINAPI CallBack_##x(t1 p1, t2 p2, t3 p3, t4 p4){return ((ret (WINAPI *)(t1 p1, t2 p2, t3 p3, t4 p4))(PROC)ghook_##x)(p1, p2, p3, p4);}\
ret WINAPI User_##x(t1 p1, t2 p2, t3 p3, t4 p4)
实际代码中,我们最多提供了9个参数的操作宏定义,并且还可以根据需要进行扩展。
这样在扩展APIHook.DLL中就可以使用如下的代码来挂接一个API,简单吧。如下:
DEFINE_USERAPIHOOK4("user32.dll", int, MessageBoxA, HWND, hWnd, LPCSTR, lpText, LPCSTR, lpCaption, UINT, uType)
{
return CallBack_MessageBoxA(hWnd, lpText, lpCaption, uType);
}
DEFINE_USERAPIHOOK4("user32.dll", int, MessageBoxW, HWND, hWnd, LPCWSTR, lpText, LPCWSTR, lpCaption, UINT, uType)
{
return CallBack_MessageBoxW(hWnd, lpText, lpCaption, uType);
}
2)实现部分
帖子很长了,可能不方便看了,因此实现部分,在回复中继续写。