在逆向分析过程中,Hook是一种截取信息、更改程序流向、添加新功能的技术
原理介绍
使用调试API创建调试调试关系,这样就可以在断点接管目标进程,通过获取线程上下文信息并进行修改,从而获取被调试者在某个时刻的信息,并按照我们的想法进行修改
简单来说,通过调试API进行Hook,可以得到目标进程某个点的所有信息,然后进行获取或者修改
相关重点API的介绍
创建与解除调试关系 DebugActiveProcess() DebugActiveProcessStop()
创建调试关系用到两个函数中的一个:
-
CreateProcess
dwCreationFlags参数要设置为 DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS
-
DebugActiveProcess
BOOL
APIENTRY
DebugActiveProcess(
_In_ DWORD dwProcessId
);
接收一个参数,进程PID
功能是附加到进程上,类似CE、OD工具的附加功能
要解除调试关系的话,需要用到另一个函数:DebugActiveProcessStop
BOOL
APIENTRY
DebugActiveProcessStop(
_In_ DWORD dwProcessId
);
调试事件与循环 WaitForDebugEvent()
对于调试器来说,除了对断点和异常做出相应,还会进行其他的一些事件的处理,调试器的工作方式就是依赖于调试过程中不断产生的调试事件
调试器不断对被调试目标进行捕获调试信息,这个过程有点像窗口程序的窗口过程,调试器捕获到调试信息后会进行处理,然后恢复线程继续运行,等待捕获调试进程调试事件的函数是:WaitForDebugEvent
BOOL
APIENTRY
WaitForDebugEvent(
_Out_ LPDEBUG_EVENT lpDebugEvent, //调试事件
_In_ DWORD dwMilliseconds //超时值,不限时INFINITE
);
运行被调试的进程 ContinueDebugEvent()
BOOL
APIENTRY
ContinueDebugEvent(
_In_ DWORD dwProcessId,
_In_ DWORD dwThreadId,
_In_ DWORD dwContinueStatus //DBG_CONTINUE
);
恢复中断的程序的运行
线程环境相关 GetThreadContext() SetThreadContext()
一个进程有多个线程,线程共享进程提供的资源,每个线程都有自己的CPU时间,当线程暂停之后,系统会将线程的一些信息存起来,以便恢复的时候能够正常执行,包括寄存器值,栈信息等,在Windows下,将线程环境定义成CONTEXT结构体
返回值取决于ContextFlag参数
要从中获取和设置线程环境需要用到以下两个函数:
GetThreadContext和SetThreadContext
BOOL
WINAPI
GetThreadContext(
_In_ HANDLE hThread,
_Inout_ LPCONTEXT lpContext
);
BOOL
WINAPI
SetThreadContext(
_In_ HANDLE hThread,
_In_ CONST CONTEXT* lpContext
);
实例练习
这次练习的例子是Hook 记事本的 WriteFile API,让保存的时候,把内容里的小写字母全部转换成大写字母,这个例子来自《逆向工程核心原理》第31章
书上的例子是windows xp 32位的示例,我这里改成,windows 10 64位的了
实现思路
- 附加到目标进程,附加调试关系
- 将目标API的第一个字节设置为INT3(0xcc)中断指令
- 寄存器进行保存的时候,进程被中断,控制器转移到调试器
- 获取目标API的参数中的缓冲区,将其中的小写字母改成大写字母,然后再覆盖回去
- 脱钩,将修改过的0xCC改回去,然后把RIP也改回去(因为执行到0xCC的时候,RIP会+1)
- 恢复目标进程的运行,让出CPU时间,再挂钩回去再次进入调试器循环
实现效果
保存前:
保存后:
源代码
main()函数:
#include<windows.h>
#include<stdio.h>
VOID DebugLoop();
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT);
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT);
LPVOID g_pfWriteFile = NULL; //Hook指针
CREATE_PROCESS_DEBUG_INFO g_cpdi; //调试信息,附加进程进行调试时获取
BYTE g_chINT3 = 0xCC; //int3 字节码
BYTE g_chOrgByte = 0; //原始 字节码
int main(int argc,char* argv[]) {
//寻找记事本窗口句柄
HWND hWnd = FindWindowA("Notepad",NULL);
DWORD dwPid = -1;
GetWindowThreadProcessId(hWnd,&dwPid);
//printf("%d", dwPid);
//附加进程
DebugActiveProcess(dwPid);
//调试器循环
DebugLoop();
return 0;
}
DebugLoop()函数:
调试器循环,不断等待调试信息,收到调试事件,就进入循环,如果是调试器附加或者被调试进程创建的时候,获取进程相关信息,然后触发我们设置的INT3中断事件的时候,进入我们的程序流程
//调试器循环,有点类似Windows窗口程序的窗口过程
VOID DebugLoop() {
DEBUG_EVENT de; //调试事件
DWORD dwContinueStatus;
//等待调试事件的发送
while (WaitForDebugEvent(&de, INFINITE)) {//等待调试信息,收到调试信息立即返回进入循环
dwContinueStatus = DBG_CONTINUE;
// 被调试进程生成或附加事件,被调试进程启动或者附加时执行
if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode) {
OnCreateProcessDebugEvent(&de);
}
//异常事件调试,负责处理INT3中断事件
else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode) {
if (OnExceptionDebugEvent(&de))
continue;
}
//被调试进程终止事件,进程中止后,调试器也结束
else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode) {
break;
}
//再次运行被调试者
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}
OnCreateProcessDebugEvent() 函数
获取目标进程的信息存起来,将目标API的首字节存起来,然后向目标首字节存入0xCC,INT3中断
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde) {
// 获取WriteFile() API地址
g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"),"WriteFile");
// API HOOK
// 设置断点
memcpy(&g_cpdi,&pde->u.CreateProcessInfo,sizeof(CREATE_PROCESS_DEBUG_INFO)); //获取调试目标的信息,来调试事件的结构体
//读取WriteFile第一个字节,存起来
ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
return TRUE;
}
OnExceptionDebugEvent()函数
这里要注意啊,获取目标线程上下文中的栈中的参数,最好先用调试软件中断进去看看在具体哪个位置,以防选错位置导致程序出错
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde) {
CONTEXT ctx;
char * lpBuffer = NULL;
SIZE_T dwNumOfBytesToWrite=0, dwAddrOfBuffer=0, i;
PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;
// 是断点异常INT3时
if (EXCEPTION_BREAKPOINT == per->ExceptionCode) {
//断点异常地址是WriteFile API时
if (g_pfWriteFile == per->ExceptionAddress) {
//1.UnHook
WriteProcessMemory(g_cpdi.hProcess,g_pfWriteFile,&g_chOrgByte,sizeof(BYTE),NULL);
//2.获取线程上下文
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);
//3.获取WriteFile的参数2/3的值
//64位程序第二三个参数在:RSP+0x10 RSP+0x18
//size_t在64位为8字节,在32位为4字节
ReadProcessMemory(g_cpdi.hProcess, (LPCVOID)(ctx.Rsp + 0x18), &dwAddrOfBuffer, sizeof(SIZE_T), NULL);
ReadProcessMemory(g_cpdi.hProcess, (LPCVOID)(ctx.Rsp + 0x20), &dwNumOfBytesToWrite, sizeof(SIZE_T), NULL);
//4.分配临时缓冲区
lpBuffer = (char *)malloc(dwNumOfBytesToWrite*2 + 2);
memset(lpBuffer, 0, dwNumOfBytesToWrite*2 + 2);
//5.复制WriteFile缓冲区到临时缓冲区
ReadProcessMemory(g_cpdi.hProcess, (LPCVOID)dwAddrOfBuffer,(LPVOID)lpBuffer, dwNumOfBytesToWrite*2, NULL);
wprintf(L"Original str:%s\r\n",lpBuffer);
//6.将大写字母改成小写字母
for (i = 0; i < dwNumOfBytesToWrite*2; i++){
if (0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)
lpBuffer[i] -= 0x20;
}
wprintf(L"Converted str:%s\r\n",lpBuffer);
//7.将变换后的缓冲区复制到WriteFile 的缓冲区
WriteProcessMemory(g_cpdi.hProcess,(LPVOID)dwAddrOfBuffer,lpBuffer,dwNumOfBytesToWrite*2,NULL);
//8.释放临时缓冲区
free(lpBuffer);
//9.将线程上下文EIP还原
//INT3中断后,eip会+1到int3的位置上,这里要还原回去
ctx.Rip = (SIZE_T)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);
Sleep(0);//让掉CPU时间
//10.运行被调试的进程
ContinueDebugEvent(pde->dwProcessId,pde->dwThreadId,DBG_CONTINUE);
//11.API Hook
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
return TRUE;
}
}
return FALSE;
}