selph
selph
Published on 2020-10-15 / 852 Visits
1
0

Hook-使用调试API实现

在逆向分析过程中,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时间,再挂钩回去再次进入调试器循环

实现效果

保存前:

image-20201014091122059

保存后:

image-20201014091148943

源代码

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

Comment