DWORD nLen = UserFuncAddr – SysFuncAddr - 指令大小;
Jmp nLen;
1 SetWindowsHookEx(
2 WH_KEYBOARD, // 键盘消息
3 KeyboardProc, // 钩子函数(处理键盘输入的函数)
4 hInstance, // 钩子函数所在DLL的Handle
5 0 // 该参数用于设定要Hook的线程ID,为0时表示监视所有线程
6 )
\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Des ktop Item Position Saver
// 获取类别(class)为ProgMan的窗口并校验
HWND hWnd = GetFirstChild(GetFirstChild(FindWindow(TEXT("ProgMan"), NULL)));
chASSERT(IsWindow(hWnd));
// 设置将DLL注入资源管理器地址空间的钩子
chVERIFY(SetMsgHook(GetWindowThreadProcessId(hWnd, NULL)));
// 这个线程ID是ListView的父线程的线程ID, 也就是Explorer进程的子线程
BOOL WINAPI SetMsgHook(DWORD dwThreadId) {
BOOL bOk = FALSE;
if (dwThreadId != 0) {
// 校验是否已经注入
chASSERT(g_hHook == NULL);
// 保存当前DLL线程ID, 当server窗口创建完成GetMsgProc函数会post消息到这个线程
g_dwThreadIdHook = GetCurrentThreadId();
// 给指定线程安装消息钩子
g_hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_hInstDll, dwThreadId);
bOk = (g_hHook != NULL);
if (bOk) {
// 此时, hook已经安装成功; 强行Post Msg到Explorer
// 进程的子线程的消息队列,触发间接调用Hook函数
bOk = PostThreadMessage(dwThreadId, WM_NULL, 0, 0);
}
} else {
chASSERT(g_hHook != NULL);
bOk = UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
return(bOk);
}
// 注意:这个线程属于Explorer进程
LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam) {
static BOOL bFirstTime = TRUE;
if (bFirstTime) {
bFirstTime = FALSE;
// 创建Hook服务窗口处理客户端请求
CreateDialog(g_hInstDll, MAKEINTRESOURCE(IDD_HOOK), NULL, Dlg_Proc);
// 唤醒MsgHook进程
PostThreadMessage(g_dwThreadIdHook, WM_NULL, 0, 0);
}
return(CallNextHookEx(g_hHook, nCode, wParam, lParam));
}
// 等待Hook服务窗口创建
MSG msg;
GetMessage(&msg, NULL, 0, 0);
// 找到隐藏的服务窗口句柄
HWND hWndHook = FindWindow(NULL, TEXT("Wintellect Hook"));
// 确定窗口是否创建
chASSERT(IsWindow(hWndHook));
// 告诉服务窗口 ListView 窗口的元素需要Save或者Restore
BOOL bSave = (cWhatToDo == TEXT('S'));
SendMessage(hWndHook, WM_APP, (WPARAM) hWnd, bSave);
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
chHANDLE_DLGMSG(hWnd, WM_CLOSE, Dlg_OnClose);
case WM_APP:
if (lParam)
SaveListViewItemPositions((HWND) wParam);
else
RestoreListViewItemPositions((HWND) wParam);
break;
}
return(FALSE);
}
// 通知Hook窗口关闭, 必须先销毁对话框,再清除挂钩
SendMessage(hWndHook, WM_CLOSE, 0, 0);
chASSERT(!IsWindow(hWnd));
// 卸载钩子, 从Explorer进程的地址空间移除Hook对话框
SetMsgHook(0);
// 附加进程, 将目标进程附加在当前进程准备进行调试
// DebugActiveProcess是一个函数程序,使调试器附加到一个活动进程并且调试它。
if (!DebugActiveProcess(dwProcessID)) {
printf("DebugActiveProcess(%d) failed!!!\n"
"Error Code = %d\n", dwProcessID, GetLastError());
return 1;
}
DEBUG_EVENT DebugEvent;
DWORD dwContinueStatus; // 等待调试事件
while (WaitForDebugEvent(&DebugEvent, INFINITE)) {
dwContinueStatus = DBG_CONTINUE; // 调试事件为创建进程, 首次
if (CREATE_PROCESS_DEBUG_EVENT == DebugEvent.dwDebugEventCode) {
OnCreateProcessDebugEvent(&DebugEvent);
}
}
LPVOID WriteFileAddress = NULL;
CREATE_PROCESS_DEBUG_INFO CreateProcessDebugInfomation;
BYTE INT3 = 0xCC, OldByte = 0;
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pDebugEvent)
{
// WriteFile()函数地址
WriteFileAddress = GetProcAddress(GetModuleHandleA("kernelbase.dll"), "WriteFile"); // 获得WriteFile()的地址
// 将WriteFile()函数的首个字节改为0xCC
memcpy(&CreateProcessDebugInfomation, &pDebugEvent->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
ReadProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress, &OldByte, sizeof(BYTE), NULL); // 保存原函数首地址的首字节
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress, &INT3, sizeof(BYTE), NULL); // 写入0xCC(调试中断指令),下软件断点。
return TRUE;
}
只要pcs_subject.exe有写log操作,就会触发调用WriteFile函数,从而触发软中断指令。
// 调试事件入口, 需要被调试进程触发 只要触发WriteFile()函数地址就会进来
else if (EXCEPTION_DEBUG_EVENT == DebugEvent.dwDebugEventCode) {
if (OnExceptionDebugEvent(&DebugEvent))
continue;
}
接着就是具体实现了。先恢复,以免进入死循环 主要是为了避免多次进入。
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pDebugEvent)
{
CONTEXT Context;
PBYTE lpBuffer = NULL;
DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
PEXCEPTION_RECORD pExceptionRecord = &pDebugEvent->u.Exception.ExceptionRecord;
// 软件终端异常
if (EXCEPTION_BREAKPOINT == pExceptionRecord->ExceptionCode)
{
// 确认发生异常的地方是否为我们要钩取的WriteFile()函数
if (WriteFileAddress == pExceptionRecord->ExceptionAddress)
{
// 1. Unhook 先恢复,以免进入死循环 主要是为了避免多次进入
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress,
&OldByte, sizeof(BYTE), NULL);
获得线程上下背景文 为了修改Eip的值,来使进程恢复正常运行。
// 2. 获得线程上下背景文 为了修改Eip的值,来使进程恢复正常运行
Context.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(CreateProcessDebugInfomation.hThread, &Context);
根据ESP寄存器来获得WriteFile()函数的参数,以达到修改数据的目的。
// 3. 根据ESP寄存器来获得WriteFile()函数的参数,以达到修改数据的目的
/*
BOOL WriteFile(
HANDLE hFile,//文件句柄
LPCVOID lpBuffer,//数据缓存区指针
DWORD nNumberOfBytesToWrite,//你要写的字节数
LPDWORD lpNumberOfBytesWritten,//用于保存实际写入字节数的存储区域的指针
LPOVERLAPPED lpOverlapped//OVERLAPPED结构体指针
);
*/
ReadProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)(Context.Esp + 0x8), // 此参数是存缓冲区的起始地址
&dwAddrOfBuffer, sizeof(DWORD), NULL);
ReadProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)(Context.Esp + 0xC), // 此参数是存缓冲区的大小
&dwNumOfBytesToWrite, sizeof(DWORD), NULL);
获取数据缓冲区的地址和大小。
// 4. 获取数据缓冲区的地址和大小
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);
memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);
将其内容读到调试器进程空间,控制台打印。
// 5. 将其内容读到调试器进程空间
ReadProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
printf("\n### original string ###\n%s\n", lpBuffer);
修改数据:把小写字母改为'@'字符,控制台打印。
//6. 修改数据:把所有小写字母改为'@'字符
for (i = 0; i < dwNumOfBytesToWrite; i++) {
if (0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)
//lpBuffer[i] -= 0x20;
lpBuffer[i] = '@';
}
printf("\n### converted string ###\n%s\n", lpBuffer);
将修改后的数据写回进程的地址空间,如图8所示。
// 7. 然后将修改后的大写字母覆写到原位置。
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
free(lpBuffer);
两次打印对比如下图,发先log已经被修改了。
脱钩,将API 函数的第一个字节恢复。把线程上下文的EIP地址修改为WriteFile()的起始地址,注意EIP当前的值为0xcc的下一条指令的地址,运行相应的API。
// 设置EIP的值来实现正常运行,注意EIP的值为0xCC的下一条指令的地址。
Context.Eip = (DWORD)WriteFileAddress;
SetThreadContext(CreateProcessDebugInfomation.hThread, &Context);
// 运行
ContinueDebugEvent(pDebugEvent->dwProcessId, pDebugEvent->dwThreadId, DBG_CONTINUE);
Sleep(0);
再次修改为0xCC,为了继续钩取
// 再次钩取
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress,
&INT3, sizeof(BYTE), NULL);
至此,我们成功实现了基于调试技术的API Hook。当然,通过这种方式可以Hook的进程很多,在这里只讲一个基础的例子,有时间大家可以自己去尝试。
以下三种Hook形式本质上都是通过改写函数的入口地址,使得执行流切换到自定义函数。
1)InLine Hook
(1)原理。
内联Hook直接修改内存中的任意函数的代码,将其劫持至Hook API。它的适用范围更广,比较简单,因为只要是内存中有的函数它都能Hook。
(2)案例。
效果为以下将用一个demo简单说明Inline Hook的基本原理。很简单,没有DLL注入,仅仅是Hook了我自己的一个模块的API,修改接口计算结果,这里先看下demo效果图,下面将会贴上代码以及详细解析,如图9所示。
测试机器:Win10 x64,测试软件x86
(3)实现。
add.dll实现add函数,返回两个int值相加后的结果; Hook.dll实现了具体Hook细节,含安装卸载钩子以及Hook函数的实现;CallAdd进程实现了加载dll UI入口。
首先,我们先要找到需要Hook的函数原型(不同的调用约定下的函数修饰后的符号有区别) Windows下可以用这个命令获取Dll所有导出符号,找到自己想要的就行:dumpbin /exports 目录/文件.dll,结果如图10所示。
接下来看下add.dll的导出接口,这个就是我们后面即将Hook的接口,导出符号如上图。
#ifdef ADD_EXPORTS
#define ADD_API __declspec(dllexport)
#else
#define ADD_API __declspec(dllimport)
#endif
#ifdef __cplusplus //如果是c++文件,就将endif内的代码用c编译器编译
extern "C" {
#endif
__declspec(dllexport) int WINAPI add(int a, int b) //__declspec(dllexport) 声明此函数为导出函数
{
return a + b;
}
#ifdef __cplusplus
}
#endif
接着,点击"开启钩子"按钮,开始加载Hook.dll
HINSTANCE hinst = NULL;
void CCallAddDlg::OnBnClickedButtonStartHook()
{
typedef BOOL(CALLBACK* inshook)(); // 函数原型定义
inshook insthook;
hinst = LoadLibrary(_T("Hook.dll")); // 加载dll文件
if (hinst == NULL)
{
AfxMessageBox(_T("no Hook.dll!"));
return;
}
dll初始化开始安装钩子。
// CHookApp 初始化
BOOL CHookApp::InitInstance()
{
CWinApp::InitInstance();
// 获得dll 实例,进程句柄
hinst = ::AfxGetInstanceHandle();
DWORD dwPid = ::GetCurrentProcessId();
hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, dwPid);
// 调用注射函数
Inject();
return TRUE;
}
接下来就是比较核心的组织汇编代码、替换函数地址的逻辑了。 保证只注射一次,获取_add@8符号对应的地址,先保存这个地址,将JMP指令0xE9存入NewCode的首地址,然后将MyAdd的地址拼接进去。然后就可以开启钩子了。
void Inject()
{
if (m_bInjected == false)
{ // 保证只调用1次
m_bInjected = true;
// 获取add.dll中的add()函数
HMODULE hmod = ::LoadLibrary(_T("add.dll"));
if (hmod == NULL) {
return;
}
add = (AddProc)::GetProcAddress(hmod, "_add@8");
pfadd = (FARPROC)add;
if (pfadd == NULL)
{
AfxMessageBox(L"cannot locate add()");
}
// 将add()中的入口代码保存入OldCode[]
_asm
{
lea edi, OldCode
mov esi, pfadd
cld
/*
movsd(dword==>四个字节)
movsw(word==>两个字节)
movsb(byte==>一个字节)
*/
movsd
movsb
}
NewCode[0] = 0xe9; // 实际上0xe9就相当于jmp指令
// 获取Myadd()的相对地址
_asm
{
lea eax, Myadd
mov ebx, pfadd
sub eax, ebx
sub eax, 5
mov dword ptr[NewCode + 1], eax
}
// 填充完毕,现在NewCode[]里的指令相当于Jmp Myadd
HookOn(); // 可以开启钩子了
}
}
下面是开启钩子的代码如下:
// 开启钩子的函数
void HookOn()
{
ASSERT(hProcess != NULL);
DWORD dwTemp = 0;
DWORD dwOldProtect;
// 将内存保护模式改为可写,老模式保存入dwOldProtect
VirtualProtectEx(hProcess, pfadd, 5, PAGE_READWRITE, &dwOldProtect);
// 将所属进程中add()的前5个字节改为Jmp Myadd
WriteProcessMemory(hProcess, pfadd, NewCode, 5, 0);
// 将内存保护模式改回为dwOldProtect
VirtualProtectEx(hProcess, pfadd, 5, dwOldProtect, &dwTemp);
bHook = true;
}
钩子开启完成之后,回来继续点击"执行函数"按钮,此时add的地址已经被修改了。
void CCallAddDlg::OnAddBnClickedButton()
{
HINSTANCE hAddDll = NULL;
typedef int (WINAPI* AddProc)(int a, int b); // 函数原型定义
AddProc add;
if (hAddDll == NULL)
{
hAddDll = ::LoadLibrary(_T("add.dll")); // 加载dll
}
if (hAddDll == NULL) {
return;
}
add = (AddProc)::GetProcAddress(hAddDll, "_add@8"); // 获取函数add地址
int a = 1;
int b = 2;
int c = add(a, b); // 调用函数
CString tem;
tem.Format(_T("%d+%d=%d"), a, b, c);
AfxMessageBox(tem);
}
所以调用会直接跳转到下面这个函数中来,注意这里需要先HookOff卸载钩子。不然会自己调自己,造成死循环,拿到计算结果后再次开启钩子。
// 然后,写我们自己的Myadd()函数
int WINAPI Myadd(int a, int b)
{
// 截获了对add()的调用,我们给a加10
a = a - 10;
HookOff(); // 关掉Myadd()钩子防止死循环
int ret;
ret = add(a, b);
HookOn(); // 开启Myadd()钩子
return ret;
}
然后,点击"卸载钩子"按钮,卸载钩子。
void CCallAddDlg::OnBnClickedButtonStopHook()
{
if (hinst == NULL)
{
return;
}
typedef BOOL(CALLBACK* UnhookProc)(); // 函数原型定义
UnhookProc UninstallHook;
UninstallHook = ::GetProcAddress(hinst, "UninstallHook");// 获取函数地址
if (UninstallHook != NULL)
{
UninstallHook();
}
if (hinst != NULL)
{
::FreeLibrary(hinst);
}
}
// 卸载鼠标钩子函数
void UninstallHook()
{
if (hhk != NULL)
{
::UnhookWindowsHookEx(hhk);
}
HookOff(); // 记得恢复原函数入口
}
将之前保存的add函数的地址恢复,记得修改内存属性,否则会失败。
// 关闭钩子的函数
void HookOff() // 将所属进程中add()的入口代码恢复
{
ASSERT(hProcess != NULL);
DWORD dwTemp = 0;
DWORD dwOldProtect;
VirtualProtectEx(hProcess, pfadd, 5, PAGE_READWRITE, &dwOldProtect);
WriteProcessMemory(hProcess, pfadd, OldCode, 5, 0);
VirtualProtectEx(hProcess, pfadd, 5, dwOldProtect, &dwTemp);
bHook = false;
}
再次点击"执行函数"按钮,发现调用原始接口,数据恢复为原始结果。
另外,对于c++虚函数Hook,虚函数调用是从虚函数表里面获得的函数地址进行调用的。因此对于Hook这类函数,就需要改写它的虚函数表了。一般来说,对于某个含有虚函数表的C++类,this指针指向的地址,取值就是虚函数表指针。虚函数表指针指向了虚函数表,里面的每一个元素都指向了实际要调用的函数的地址。因此,可以按照这样的方式访问虚函数表指针:
int** pVTable = (int**)this;
也就是将指向对象的指针强制转化成指针的指针,这样就可以通过取值就可以访问虚函数表:
(*pVTable)[0] = address of virtual function 1;
(*pVTable)[1] = address of virtual function 2;
...
因此,我们就可以改写虚函数的地址了,从而达到Hook的目的。
2)Hotfix Hook
从上节对Inline Hook方法的讲解中,我们会发现Inline Hook存在一个效率的问题,因为每次Inline Hook都要进行“挂钩+脱钩”的操作,也就是要对API的前5字节修改两次,这样,当我们要进行全局Hook的时候,系统运行效率会受影响。而且,当一个线程尝试运行某段代码时,若另一个线程正在对该段代码进行“写”操作,这时就会程序冲突,最终引发一些错误。
因此,使用HotFix Hook("热补丁")方法。如我们的大班云学生端app的热修复,原理是一样的。
测试机器:Win10 x64,测试dll: XP系统的
(1)原理为API的起始代码上都有这样的特色,5个NOP(空)指令,1个“MOV EDI,EDI”(占2字节),这7字节的指令实际没有任何意义,所以能够经过修改这7字节来实现HOOK操做,这种方法可使得进程处于运行状态时临时更改进程内存中的库文件,所以被称为打“热补丁”。在上述5字节代码修改技术中,脱钩是为了调用原函数,但使用HotFix Hook API时,在API代码被修改的状态下仍然可以正常的调用原API(从[原API起始地址+2]开始,仍能正常调用原API,且执行动作一致)。这种方法因为可以在进程处于运行状态时临时更改进程内存中的库文件,所以微软也常用这种方法来打“热补丁”。
该技术难的地方在于计算偏移地址。由于HotFix Hook需要修改7个字节的代码,所以并不是所有API都适用这种方法,若不适用,请使用5字节代码修改技术。
(2)现状为下图是用OllyDbg打开user32.dll,这个dll用的是网上下载的Xp的。因为我自己本机是Win10系统,这个dll可能是被更新掉了,没有找到可用的Hook点。网上下载的Xp的看起来是有的,估计是后面windows版本对dll安全性升级了。看来在Windows10系统上面这种Hook方式很难实现了。Win7/Win8没试过,有兴趣可以自行尝试,如图11所示。
下面两张图是XP的跟Win10的文件信息的对比,如图12与图13所示。
那么,这个Hook类型就不再展示代码案例了。了解一下有这么个方式就行,不过Win10肯定有可以Hook的API,没有找到而已。不过,重在了解原理,代码实现跟之前的大同小异。
3)SSDT(内核) Hook
SSDT Hook属于内核层Hook,也是最底层的Hook。由于用户层的API最后实质也是调用内核API,所以该Hook方法最为强大。不过值得注意的是内核通SSDT(System Service Descriptor Table)调用各种内核函数,SSDT就是一个函数表,只要得到一个索引值,就能根据这个索引值在该表中得到想要的函数地址。本质上其实内核层Hook并没想象中的那么高大上,Hook的原理相同,只不过Hook的对象不一样罢了。
当前安全软件很多也用到了 SSDT Hook技术来实现对系统的安全防护。例如图14所示,是360主动防御进程对 SSDT 的一个 Hook,Hook的目的是“取得系统R0权限,当有进程要结束自己的时候进行拦截,然后给出提示,拒绝访问”。比如上图:结束进程是由NtTerminateProcess 来完成的,Hook这个内核函数,那么在进程结束前,就有机会更改结果了,可以拒绝被结束。
4.1 项目背景
大班云直播课堂在线安装程序,是一个独立的应用程序,提供学生端的安装功能,为了减少安装包体积,避免引入第三方网络库,使用的是操作系统的WinInet网络库。为了更好的优化网络,提高网络连接的成功率,避免Local DNS造成的域名劫持等问题,采用HttpDNS方式实现域名解析。
4.2 为什么使用HttpDNS
相比于传统的DNS,HttpDNS主要有以下优势:
(1)域名防劫持。使用Http(Https)协议进行域名解析,域名解析请求直接发送至HttpDNS服务器,绕过运营商Local DNS,避免域名劫持问题。
(2)调度精准。由于运营商策略的多样性,其 Local DNS 的解析结果可能不是最近、最优的节点,HttpDNS能直接获取客户端 IP ,基于客户端 IP 获得最精准的解析结果,让客户端就近接入业务节点。
(3)实时生效。配合端上策略(热点域名预解析、缓存DNS解析结果、解析结果懒更新)实现毫秒级低解析延迟的域名解析效果。
4.2.1 HttpDNS实现方案
使用HttpDNS的通常方法有两个方案:
1)方案一
发起网络请求之前把域名使用HttpDNS解析为IP地址,然后请求的时候把域名替换为IP进行请求,但是这种方案存在两个问题需要解决:
(1)虚拟主机问题
从http/1.1开始,header中支持Host字段,用来实现访问虚拟主机的目的。http请求header中必须配置适当的Host才能正确访问想要的服务,默认情况下Host字段是请求地址中的域名。如果直接把请求的域名替换成IP地址则无法正确访问对应服务,所以需要所使用的网络库支持自定义Host字段。而WinInet是Windows系统库,不支持修改Host字段。所以不能简单的把域名替换为解析后的IP发起请求。另外在https协议中,虚拟主机同时带来SNI问题,即在TLS握手阶段就需要指定适当的Host信息,以保证服务端可以返回正确的证书,否则会导致SSL握手失败。
(2)Https证书验证问题
把域名直接替换为IP地址带来的另一个问题是SSL/TLS握手时候的证书验证问题。主要原因是服务端证书和客户端的peer name不一致导致的。一个简单的解决方案是忽略SSL证书验证失败这个问题,但是这样会导致https请求成了不安全的请求。
2)方案二
如果第三方网络库提供域名解析的回调,可以自定义域名解析也可以实现HttpDNS。本文采用的就是这个方案,利用Windows的API Hook机制,对域名解析GetAddrInfoEx接口进行Hook,以实现自定义DNS解析,失败 情况下走默认DNS解析。
常用网络库提供的解决方案如下:
(1)Qt5Network库:比如在qt 5.15版本中,connectToHostEncrypted这个接口,他提供了peer name参数来实现SSL握手阶段需要验证的peer name以解决证书验证域名不匹配的问题;
(2)libcurl库:用curl_easy_setopt CURLOPT_RESOLVE提供自定义主机名到IP地址的解析,即可以自定义域名解析。
本文的解决方案由于我们项目需要只能使用Windows系统的WinInet网络库,该库不支持修改Host头,也不提供域名解析的回调。但是Windows的域名解析一般使用的gethostbyname,GetAddrInfo,GetAddrInfoEx这些API来实现的,如果我们Hook这些API,来实现HttpDNS解析过程,如果失败了,再走默认的域名解析过程,这样就可以实现了HttpDNS功能了。
4.2.2 使用detours库实现Hook
detours库是微软提供的被广泛使用的用于API Hook的库,它封装了Hook的实现细节,使用起来非常方便。例如:GetAddrInfoEx是我们需要Hook的API,声明Old_GetAddrInfoEx保留Hook之前的函数指针,New_GetAddrInfoEx为Hook后的函数指针,应用程序在适当的时机调用StartHook/StopHook以Hook对应的API。
INT (WSAAPI* Old_GetAddrInfoEx)( __in_opt PCWSTR pName, __in_opt PCWSTR pServiceName, __in DWORD dwNameSpace, __in_opt LPGUID lpNspId, __in_opt const ADDRINFOEX* hints, __deref_out PADDRINFOEXW* ppResult, __in_opt struct timeval* timeout, __in_opt LPOVERLAPPED lpOverlapped, __in_opt LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine, __out_opt LPHANDLE lpHandle) = GetAddrInfoEx; INT WSAAPI New_GetAddrInfoEx( __in_opt PCWSTR pName, __in_opt PCWSTR pServiceName, __in DWORD dwNameSpace, __in_opt LPGUID lpNspId, __in_opt const ADDRINFOEX* hints, __deref_out PADDRINFOEXW* ppResult, __in_opt struct timeval* timeout, __in_opt LPOVERLAPPED lpOverlapped, __in_opt LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine, __out_opt LPHANDLE lpHandle ) { // 这里可以实现自己的dns解析逻辑 // ... // 自定义解析失败后,调用默认解析以兜底 return Old_GetAddrInfoEx(pName, pServiceName, dwNameSpace, lpNspId, hints, ppResult, timeout, lpOverlapped, lpCompletionRoutine, lpHandle); } bool StartHook() { DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)Old_GetAddrInfoEx, New_GetAddrInfoEx); LONG ret = DetourTransactionCommit(); return ret == NO_ERROR; } bool StopHook() { DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)Old_GetAddrInfoEx, New_GetAddrInfoEx); LONG ret = DetourTransactionCommit(); return ret == NO_ERROR; }
4.2.3 Hook过程
WinInet网络请求的一般过程如下图所示,在发送HttpSendRequest请求的时候会调用域名解析函数GetAddrInfoEx函数完成域名的解析。在域名解析的时候Hook GetAddrInfoEx函数。Hook后的WinInet网络请求过程如右下图所示,在Hook域名解析函数GetAddrInfoEx的时候成功以后,就不再调用原有的域名解析函数GetAddrInfoEx,而是调用自定义的域名解析函数。在调用自定义的域名解析函数失败的时候,有个兜底的策略,还调回原来的域名解析函数GetAddrInfoEx。下面是自定义的域名解析函数New_GetAddrInfoEx,如图15、16所示。
图15 原始网络请求流程
图16 Hook后网络请求流程
自定义域名解析函数如下所示:
// 从私有堆上分配ADDRINFOEX空间 static void my_addressinfo_alloc( __in_opt PCWSTR pServiceName, __in DWORD dwNameSpace, __in_opt LPGUID lpNspId, __in_opt const ADDRINFOEX* hints, __deref_out PADDRINFOEXW* ppResult, __in_opt struct timeval* timeout, __in_opt LPOVERLAPPED lpOverlapped, __in_opt LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine, __out_opt LPHANDLE lpHandle) { ADDRINFOEX my_hints = *hints; my_hints.ai_family = AF_INET; my_hints.ai_flags ^= (AI_CANONNAME | AI_FQDN); Old_GetAddrInfoEx(L"localhost", pServiceName, dwNameSpace, lpNspId, &my_hints, ppResult, timeout, lpOverlapped, lpCompletionRoutine, lpHandle); } INT WSAAPI New_GetAddrInfoEx( __in_opt PCWSTR pName, __in_opt PCWSTR pServiceName, __in DWORD dwNameSpace, __in_opt LPGUID lpNspId, __in_opt const ADDRINFOEX* hints, __deref_out PADDRINFOEXW* ppResult, __in_opt struct timeval* timeout, __in_opt LPOVERLAPPED lpOverlapped, __in_opt LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine, __out_opt LPHANDLE lpHandle ) { do { struct in_addr addr; // ip和localhost不需要httpdns if (pName == nullptr || hints == nullptr || InetPtonW(AF_INET, pName, (void*)&addr) || wcscmp(pName, L"localhost") == 0) { break; } // 从缓存或者云服务商获取该域名对应的ip列表 HttpDNS::IpList ipList = HttpDNS::instance()->getHostByName(pName); if (ipList.size() == 0) { break; } // 由于GetAddrInfoEx调用时候在私有堆上分配的内存,自己new的对象无法正常释放,会导致崩溃 // blog: http://www.youngroe.com/2018/12/01/Windows/windows_client_dns_over_https/ ADDRINFOEX* pTarget = nullptr; for (auto& ip : ipList) { // 私有堆上分配ADDRINFOEX空间 ADDRINFOEX* pTemp = nullptr; my_addressinfo_alloc(pServiceName, dwNameSpace, lpNspId, hints, &pTemp, timeout, lpOverlapped, lpCompletionRoutine, lpHandle); if (pTemp == nullptr) { continue; } if (*ppResult == nullptr) { *ppResult = pTemp; pTarget = *ppResult; } else { assert(pTarget); pTarget->ai_next = pTemp; pTarget = pTarget->ai_next; } std::string ipa = CStringUtil::wstring2string(ip); struct sockaddr_in* mysock = (struct sockaddr_in*)pTemp->ai_addr; mysock->sin_addr.S_un.S_addr = inet_addr(ipa.c_str()); } if (*ppResult == nullptr) { break; } return NO_ERROR; } while (false); return Old_GetAddrInfoEx(pName, pServiceName, dwNameSpace, lpNspId, hints, ppResult, timeout, lpOverlapped, lpCompletionRoutine, lpHandle); }
在Hook GetAddrInfoEx函数的实现过程中遇到了一个小问题,GetAddrInfoEx返回结果中的addrinfoexW内存分配问题。正常情况下返回结果中的addrinfoexW由GetAddrInfoEx函数在其私有堆上分配,然后调用者使用完结果后使用FreeAddrInfoEx 释放,但是当我们自己实现的时候很难获取到私有堆的句柄,这样就没办法为addrinfoexW分配内存,如果使用new分配内存会在FreeAddrInfoEx 释放时错误产生问题。我实现的时候通过一个简单粗暴的方式是通过调用原始的GetAddrInfoEx解析localhost然后直接使用结果中的addrinfoexW,因为是GetAddrInfoEx分配,所以最后使用FreeAddrInfoEx 释放也没问题。
Hook技术也被广泛应用于安全的多个领域,Windows xp及其之前的安全机制,除了靠定期向病毒木马样本库中添加新样本外,还需要辅之以Hook关键的系统函数,以方便在用户down文件或者打开exe时查杀木马。此外,早期的杀软,除了被动扫描之外,也还需要主动对一些敏感API进行Hook监控;有时Windows系统本身及一些相关应用程序,在打补丁时也需要使用Hook技术,还有游戏外挂以及一些监控软件,可以说这是一把双刃剑。
微软在Win 10里设立了Secure ETW通道,安全软件不再需要像以前那样Hook系统内核来完成对系统内进程的监视。而Hook作为一个比较老的技术,也已经越来越少被提起到了,但这并不妨碍它曾经的光辉岁月,作为一类经典的流行方法,很值得我们去了解。
[1] 参考书籍1 《Windows 核心编程》
[2] 参考书籍2 《逆向工程核心原理》
[3] 参考书籍3 《程序员的自我修养》