API钩取--调试技术

调试技术工作原理

​ 调试进程经过注册后, 每当被调试者发生调试事件 (Debug Event) 时, OS就会暂停其运行, 并向调试器报告相应事件. 调试器对相应事件做适当处理后, 使被调试者继续运行.

​ 调试器必须处理的是 EXCEPTION_BREAKPOINT异常, 汇编指令为 INT3, IA-32指令为0xCC.

​ 调试技术的基本思路: 在 “调试器-被调试者”的状态下, 将被调试者的API起始部分修改为0xCC, 控制权转移到调试器后执行指定操作, 最后使被调试者重新进入运行状态.具体的调试流程如下:

记事本WriteFile() API钩取

实验使用的是 Windows 7 (32位) 中的 notepad.exe 记事本程序.

源代码分析

​ 实验中, 调试器的功能是钩取notepad.exeWriteFile() 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, 如下图所示:

1

​ 2. 使用IDA反汇编hookdbg.exe文件, 经过分析, 发现OnExceptionDebugEvent()中将小写字母转换成大写字母使用的缓冲区分别在0x004010E20x00401119esi寄存器所指向的内存区域, 其后的_printf则分别将内容打印出来:

2

​ 3. 使用OllyDbg打开hookdbg.exe文件, 并输入参数1976 (notepad.exe的PID):

3

​ 4. 在0x004010E20x00401119处分别下断点, 然后F9运行hookdbg.exe, 断点窗口显示如下:

4

​ 5. 在notepad.exe中输入字符串test并保存文件, hookdbg.exe会在0x004010E2处中断, ESI指向的内存区域显示为我们输入的字符串test,如下图所示:

5

​ 6. 继续运行到下一个断点0x00401119处中断, 其中执行了将小写字母转换为大写字母的操作, ESI指向的内存区域显示为转换后的字符串Test, 如下图所示:

6

​ 7. 完整运行整个hookdbg.exe程序, 无论是命令行输出结果还是保存的test.txt文件, 都显示对notepad.exeWriteFile() API钩取成功:

7

​ 因此, 通过实验结果的测试与验证, 使用调试技术成功实现了对记事本WriteFile() API的钩取与利用.