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;
}

参考书籍:
《逆向工程核心原理》

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值