Hook技术:奇妙的调试Hook技术详解(附源码)
为什么叫做奇妙的调试Hook呢?因为在学习《****核心原理》的过程中,发现hook的方式千变万化,越觉得欣喜,相较于其他的Address Hook、Inline Hook,调试Hook让我觉得独特。
什么是调试Hook?
调试Hook是通过调试方式进行的API钩取函数。
在这之前我们先了解一点调试器的知识。
调试器用来确认被调试者是否正确运行,发现未知的错误。调试器能够逐一执行被调试者的指令,拥有对调试器与内存的所有访问权限。
每当被调试者发生调试事件时,OS就会暂停并向调试器报告此事件,调试器做适当处理后,会使被调试进程继续执行。
读上面,调试器能够逐一执行被调试者的指令,拥有对调试器与内存的所有访问权限这一句说明了调试hook的可行性。我们使用调试hook,就相当于实现了一个最简单的调试器,而对被调试进程进行修改内存是很正常的事件,因此被查杀可能性低。
实现原理
伪装成调试器行为进行hook,采用对目标API入口地址下断点,当调用时即断下,截获参数内容后进行修改.这时可以通过修改eip的值让其跳转到任意地址.
这种方式没有文件等其他操作,它就相当于实现了一个最简单的调试器,而对被调试进程进行修改内存是很正常的事件,因此被查杀可能性低
实现流程
基本思路是:
在“调试器—被调试者”的状态下,将被调试者的API起始部分修改为0xCC,控制权转移到调试器后执行指定操作,最后使被调试者重新进入运行状态。
具体的实现流程:
1、对想要钩取的进程进行附加操作(DebugActiveProcess),使之成为被调试者。
2、将要钩取的API的起始地址的第一个字节修改为0xcc(或者使用硬件断点)。
3、当调用目标API的时候,控制权就转移到调试器进程。
4、执行需要的操作。
5、脱钩,将API 函数的第一个字节恢复。
6、运行相应的API。
7、再次修改为0xCC,为了继续钩取
8、控制权返还被调试者
实战练习
我们以钩取Notepad.exe的WriteFile()API为例。
实现功能:在保存文件时操作输入参数,将小写字母全部转换成大写字母。也就是说,在notepad中保存的文件内容时,输入的小写字母全部变成大写字母,然后再保存。
1.首先获得Notepad.exe的PID,附加为被调试进程。
PID为7816.
输入Notepad.exe的PID。
进行附加。
if (!DebugActiveProcess(dwProcessID))
{
printf("DebugActiveProcess(%d) failed!!!\n"
"Error Code = %d\n", dwProcessID, GetLastError());
return 1;
}
2、将要WriteFile()的起始地址的第一个字节修改为0xCC
在进程创建的时候,捕捉到异常,进行处理。
while (WaitForDebugEvent(&DebugEvent, INFINITE))
{
dwContinueStatus = DBG_CONTINUE;
// 调试事件为创建进程
if (CREATE_PROCESS_DEBUG_EVENT == DebugEvent.dwDebugEventCode)
{
OnCreateProcessDebugEvent(&DebugEvent);
}
获得WriteFile的地址,将首字节修改为0xCC,下软件断点。
// WriteFile()函数地址 WriteFileAddress = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");//获得WriteFile的地址
// API Hook - 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,下断。
3、当调用WriteFile()的时候,控制权就转移到调试器进程。
捕捉到断点异常,进入处理分发流程。
// 调试事件
else if (EXCEPTION_DEBUG_EVENT == DebugEvent.dwDebugEventCode)
{
if (OnExceptionDebugEvent(&DebugEvent))
continue;
}
4、执行需要的操作。
这里我们执行的操作就是将小写变大写。
判断异常发生地址是否为我们的地址。
// 发生异常的地方是否为我们要钩取的函数
if (WriteFileAddress == pExceptionRecord->ExceptionAddress)
1.脱钩,将0xCC恢复。
// #1. Unhook
// 先恢复,以免进入死循环
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress,
&OldByte, sizeof(BYTE), NULL);
2. 获得线程上下背景文 为了修改EIp的值,来使进程恢复正常运行
Context.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(CreateProcessDebugInfomation.hThread, &Context);
3. 获取WriteFile() 根据ESP来获得WriteFile 函数的参数,以达到修改数据的目的
Esp + 0x8:获取第二个参数
Esp + 0xC:获取第三个参数
ReadProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)(Context.Esp + 0x8),&dwAddrOfBuffer, sizeof(DWORD), NULL);
ReadProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)(Context.Esp + 0xC),&dwNumOfBytesToWrite, sizeof(DWORD), NULL);
4.把小写字母转换为大写字母后,覆盖WriteFile()缓冲区
获取数据缓冲区的地址和大小
之后将其内容读到调试器进程空间,把小写字母转换成大写字母。
然后将修改后的大写字母覆写到原位置。
#4.
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);
memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);
// #5. WriteFile()
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;
}
printf("\n### converted string ###\n%s\n", lpBuffer);
// #7. 调用原函数
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)dwAddrOfBuffer,lpBuffer, dwNumOfBytesToWrite, NULL);
5、脱钩,将API 函数的第一个字节恢复。
把线程上下文的EIP地址修改为WriteFile()的起始地址,注意EIP当前的值为0xcc的下一条指令的地址。
Context.Eip = (DWORD)WriteFileAddress;
SetThreadContext(CreateProcessDebugInfomation.hThread, &Context);
6、运行相应的API。
7、再次修改为0xCC,为了继续钩取
再次钩取
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress,
&INT3, sizeof(BYTE), NULL);
8、控制权返还被调试者
重启被调试进程,使之继续执行。
// 运行
ContinueDebugEvent(pDebugEvent->dwProcessId, pDebugEvent->dwThreadId, DBG_CONTINUE);
Sleep(0);
Hook 结果
hook前:
hook后:
优缺点
优点:
1.该技术借助“调试”钩取,所以能够进行与用户更具交互性的钩取操作。
2.它就相当于实现了一个最简单的调试器,而对被调试进程进行修改内存是很正常的事件,因此被查杀可能性低
缺点:
1.会导致程序运行速度变慢,不适用于大型程序。
2.也是注意事项,32和32匹配,64和64匹配,本文32位。
源码示例
#include<Windows.h>
#include<iostream>
#include<stdio.h>
using namespace std;
LPVOID WriteFileAddress = NULL; //
CREATE_PROCESS_DEBUG_INFO CreateProcessDebugInfomation;
BYTE INT3 = 0xCC, OldByte = 0;
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pDebugEvent)
{
// WriteFile()函数地址
WriteFileAddress = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");//获得WriteFile的地址
// API Hook - 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;
}
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pDebugEvent)
{
CONTEXT Context;
PBYTE lpBuffer = NULL;
DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
PEXCEPTION_RECORD pExceptionRecord = &pDebugEvent->u.Exception.ExceptionRecord;
// BreakPoint exception
if (EXCEPTION_BREAKPOINT == pExceptionRecord->ExceptionCode)
{
// 发生异常的地方是否为我们要钩取的函数
if (WriteFileAddress == pExceptionRecord->ExceptionAddress)
{
// #1. Unhook
// 先恢复,以免进入死循环
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress,
&OldByte, sizeof(BYTE), NULL);
// #2. 获得线程上下背景文 为了修改EIp的值,来使进程恢复正常运行
Context.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(CreateProcessDebugInfomation.hThread, &Context);
// #3. WriteFile() 根据ESP来获得WriteFile 函数的参数,以达到修改数据的目的
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. WriteFile()
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;
}
printf("\n### converted string ###\n%s\n", lpBuffer);
// #7. 调用原函数
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
free(lpBuffer);
// 设置EIP的值来实现正常运行,注意EIP的值为0xcc的下一条指令的地址。
Context.Eip = (DWORD)WriteFileAddress;
SetThreadContext(CreateProcessDebugInfomation.hThread, &Context);
// 运行
ContinueDebugEvent(pDebugEvent->dwProcessId, pDebugEvent->dwThreadId, DBG_CONTINUE);
Sleep(0);
// 再次钩取
WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress,
&INT3, sizeof(BYTE), NULL);
return TRUE;
}
}
return FALSE;
}
void DebugLoop()
{
DEBUG_EVENT DebugEvent;
DWORD dwContinueStatus;
// 等待调试事件
while (WaitForDebugEvent(&DebugEvent, INFINITE))
{
dwContinueStatus = DBG_CONTINUE;
// 调试事件为创建进程
if (CREATE_PROCESS_DEBUG_EVENT == DebugEvent.dwDebugEventCode)
{
OnCreateProcessDebugEvent(&DebugEvent);
}
// 调试事件
else if (EXCEPTION_DEBUG_EVENT == DebugEvent.dwDebugEventCode)
{
if (OnExceptionDebugEvent(&DebugEvent))
continue;
}
// 调试进程退出
else if (EXIT_PROCESS_DEBUG_EVENT == DebugEvent.dwDebugEventCode)
{
break;
}
ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, dwContinueStatus);
}
}
int main(int argc, char* argv[])
{
DWORD dwProcessID;
cout << "Input ProcessID" << endl;
cin >> dwProcessID;
// Attach Process
if (!DebugActiveProcess(dwProcessID))
{
printf("DebugActiveProcess(%d) failed!!!\n"
"Error Code = %d\n", dwProcessID, GetLastError());
return 1;
}
// 调试事件循环
DebugLoop();
return 0;
}
参考书籍:
《****核心原理》
上一篇: Hook技术:虚表hook及虚函数的调用过程详解(附源码)
下一篇: 专题——Java数据库程序设计