on
API钩取--调试技术
调试技术工作原理
调试进程经过注册后, 每当被调试者发生调试事件 (Debug Event) 时, OS就会暂停其运行, 并向调试器报告相应事件. 调试器对相应事件做适当处理后, 使被调试者继续运行.
- 一般的异常 (Exception) 也属于调试器.
- 若相应进程处于非调试, 调试事件会在其自身的异常处理或OS的异常处理机制中被处理.
- 调试器无法处理或不关心的调试事件最终由OS处理.
调试器必须处理的是 EXCEPTION_BREAKPOINT
异常, 汇编指令为 INT3
, IA-32
指令为0xCC
.
调试技术的基本思路: 在 “调试器-被调试者”的状态下, 将被调试者的API起始部分修改为0xCC
, 控制权转移到调试器后执行指定操作, 最后使被调试者重新进入运行状态.具体的调试流程如下:
- 对想钩取的进程进行附加操作, 使之成为被调试者;
- “钩子”: 将API起始地址的第一个字节修改为
0xCC
; - 调试相应API时, 控制权转移到调试器;
- 执行需要的操作 (操作参数, 返回值等);
- 脱钩: 将
0xCC
恢复原值 (为了正常运行API); - 运行相应API (无
0xCC
的正常状态); - “钩子”: 再次修改为
0xCC
(为了继续钩取); - 控制权返回给被调试者.
记事本WriteFile() API钩取
实验使用的是 Windows 7 (32位) 中的 notepad.exe 记事本程序.
源代码分析
实验中, 调试器的功能是钩取notepad.exe
的WriteFile() API
, 保存文件时操作输入输出参数, 将小写字母全部转换为大写字母. 其各部分源代码如下:
1. main()
:
#include "windows.h"
#include "stdio.h"
LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;
int main(int argc, char* argv[])
{
DWORD dwPID;
if (argc != 2)
{
printf("\nUSAGE : hookdbg.exe <pid>\n");
return 1;
}
// Attach Process
dwPID = atoi(argv[1]);
if (!DebugActiveProcess(dwPID))
{
printf("DebugActiveProcess(%d) failed!!!\n"
"Error Code = %d\n", dwPID, GetLastError());
return 1;
}
//调试器循环
DebugLoop();
return 0;
}
main()
函数通过DebugActiveProcess
将调试器附加到该运行的进程上, 开始调试, 然后进入DebugLoop()
函数, 处理来自被调试者的调试事件.
2. DebugLoop()
:
void DebugLoop()
{
DEBUG_EVENT de;
DWORD dwContinueStatus;
// 等待被调试者发生事件
while( WaitForDebugEvent(&de, INFINITE) )
{
dwContinueStatus = DBG_CONTINUE;
// 被调试进程生成或者附加事件
if( CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
OnCreateProcessDebugEvent(&de);
}
// 异常事件
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);
}
DebugLoop()
函数从被调试者处接收事件并处理, 然后使被调试者继续运行.
3. OnCreateProcessDebugEvent()
:
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
// 获取 WriteFile() API 地址
g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");
// API Hook - WriteFile()
// 更改第一个字节为 0xCC
// orginal byte 是 g_ch0rgByte 备份
memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chOrgByte, sizeof(BYTE), NULL);
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);
return TRUE;
}
OnCreateProcessDebugEvent()
是CREATE_PROCESS_DEBUG_EVENT
事件句柄, 被调试进程启动(或附加)时即调用该函数. 首先获取WriteFile() API
的起始地址; 由于调试器拥有被调试器进程的句柄, 所以可以使用ReadProcessMemory()
和WriteProcessMemory()
对被调试进程的内存空间自由进行读写操作.
4. OnExceptionDebugEvent()
:
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
CONTEXT ctx;
PBYTE lpBuffer = NULL;
DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;
// 异常是断点异常 (INT 3) 时
if( EXCEPTION_BREAKPOINT == per->ExceptionCode )
{
// BP 地址为 WriteFile() API地址时
if( g_pfWriteFile == per->ExceptionAddress )
{
// #1. Unhook
// 将0xCC恢复为 original byte
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chOrgByte, sizeof(BYTE), NULL);
// #2. 获取线程上下文
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);
// #3. 获取 WriteFile() 的 param 2,3 值 (缓冲区地址和缓冲区大小)
// 函数参数存在于相应进程的栈
// param 2 : ESP + 0x8
// param 3 : ESP + 0xC
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),
&dwAddrOfBuffer, sizeof(DWORD), NULL);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),
&dwNumOfBytesToWrite, sizeof(DWORD), NULL);
// #4. 分配临时缓冲区
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
memset(lpBuffer, 0, dwNumOfBytesToWrite+1);
// #5. 复制 WriteFile() 缓冲区到临时缓冲区
ReadProcessMemory(g_cpdi.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. 将变换后的缓冲区复制到 WriteFile() 缓冲区
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
// #8. 释放临时缓冲区
free(lpBuffer);
// #9. 将线程上下文的EIP更改为 WriteFile() 首地址
// (当前为 WriteFile() + 1 位置, INT3命令后)
ctx.Eip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);
// #10. 运行被调试进程
ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
Sleep(0);
// #11. API Hook
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);
return TRUE;
}
}
return FALSE;
}
OnExceptionDebugEvent()
处理被调试者的INT3
指令, 用于实现调试器的主要功能.
测试
编译链接上述源代码, 得到hookdbg.exe
可执行调试器文件.
1. 首先运行notepad.exe
, 运行 Process Explorer
获取其PID, 如下图所示:
2. 使用IDA
反汇编hookdbg.exe
文件, 经过分析, 发现OnExceptionDebugEvent()
中将小写字母转换成大写字母使用的缓冲区分别在0x004010E2
和0x00401119
处esi
寄存器所指向的内存区域, 其后的_printf
则分别将内容打印出来:
3. 使用OllyDbg
打开hookdbg.exe
文件, 并输入参数1976 (notepad.exe的PID):
4. 在0x004010E2
和0x00401119
处分别下断点, 然后F9运行hookdbg.exe
, 断点窗口显示如下:
5. 在notepad.exe
中输入字符串test
并保存文件, hookdbg.exe
会在0x004010E2
处中断, ESI
指向的内存区域显示为我们输入的字符串test
,如下图所示:
6. 继续运行到下一个断点0x00401119
处中断, 其中执行了将小写字母转换为大写字母的操作, ESI
指向的内存区域显示为转换后的字符串Test
, 如下图所示:
7. 完整运行整个hookdbg.exe
程序, 无论是命令行输出结果还是保存的test.txt
文件, 都显示对notepad.exe
的WriteFile() API
钩取成功:
因此, 通过实验结果的测试与验证, 使用调试技术成功实现了对记事本WriteFile() API
的钩取与利用.