使用 Windows API 实现软件断点调试器

前言

在软件开发和逆向工程领域,调试是一项至关重要的任务。为了深入理解程序的执行过程,我们经常需要检查程序在特定位置的状态,或者跟踪程序在执行时的行为。在 Windows 平台上,我们可以使用 Windows API 提供的调试功能来实现这一目的。

在本文中,我们将介绍如何使用 Windows API 实现一个简单的软件断点调试器。软件断点是一种调试技术,通过在程序代码中插入中断指令来中断程序的执行,以便我们可以检查程序的状态或跟踪程序的执行流程。

1 理论基础

在介绍如何实现软件断点调试器之前,我们先来了解一些理论基础。软件断点调试器是一种调试工具,可以让我们暂停目标程序的执行,并在特定位置观察程序的状态。为了理解软件断点调试器的实现原理,我们需要了解以下几个关键概念:

1.1 调试器

调试器是一种用于监视、控制和分析程序执行的工具。它通常用于调试和分析软件程序,帮助开发人员找到和修复程序中的错误。调试器可以暂停程序的执行,观察程序的内部状态(如寄存器值、内存内容等),以及在需要时修改程序的行为。

1.2 调试事件

调试事件是指调试过程中发生的各种事件,例如异常、断点触发、线程创建和退出等。调试器可以通过监视调试事件来控制程序的执行,例如在程序触发断点时暂停程序的执行并显示相关信息。

1.3 断点

断点是调试中常用的一种技术,用于在程序执行到特定位置时暂停程序的执行。软件断点是一种特殊的断点,它通过在程序代码中插入中断指令来实现。当程序执行到这个中断指令时,会触发一个调试事件,调试器就可以在这个时机暂停程序的执行并进行相关处理。

1.4 寄存器

寄存器是计算机内部用于存储和处理数据的一种特殊的存储单元。在调试过程中,我们通常会关注程序的寄存器状态,以便了解程序的执行情况。常见的通用寄存器包括通用目的寄存器(如 RAX、RBX、RCX 等)和特殊寄存器(如 RIP、RSP 等)。

1.5 标志位

标志位是寄存器中的特殊位,用于标识和记录程序执行过程中的各种状态。例如,EFLAGS 寄存器中的 CF(进位标志位)、ZF(零标志位)、SF(符号标志位)等就是常见的标志位。通过分析标志位的值,我们可以了解程序的控制流和执行结果。

2 实现目的和原理

本文通过 Windows 提供的附加调试 API 和线程上下文查询实现获取受调试进程的寄存器信息。利用内存读写,对指定位置插入软件断点,实现在目标函数触发断点时,获取寄存器和标志位信息。在本文未提供的部分,可以继续实现通过寄存器访问内存地址,从而获取在堆栈上的目标数据。

2.1 实现步骤

了解了以上理论基础后,我们可以开始实现软件断点调试器。下面是实现软件断点调试器的基本步骤:

  1. 创建目标进程并挂起:使用 CreateProcess 函数创建目标进程,并将其挂起,以便后续设置断点。

  2. 设置软件断点:在目标进程的指定位置插入中断指令,以实现软件断点。

  3. 附加调试器到目标进程并恢复执行:使用 DebugActiveProcess 函数将调试器附加到目标进程,并恢复目标进程的执行。

  4. 监听调试事件并处理异常:在调试循环中,通过 WaitForDebugEvent 函数监听调试事件,并根据事件类型进行相应处理。对于异常调试事件,检查是否触发了断点,并打印相关信息。

  5. 打印寄存器信息和标志位:在断点触发时,打印目标线程的寄存器信息和标志位值,以便分析程序状态。

  6. 恢复原始字节并继续执行:如果断点是我们设置的软件断点,恢复断点位置的原始字节,并继续执行目标程序。

通过以上步骤,我们可以实现一个简单的软件断点调试器,用于监视和分析目标程序的执行过程。

2.2 通过 x64Dbg 初步确定参数

首先,用 x64Dbg 调试器附加进程,查看测试程序的符号,找到目标函数:

使用 Windows API 实现软件断点调试器_第1张图片

在目标函数中确定插入断点的地址:

使用 Windows API 实现软件断点调试器_第2张图片

关闭调试进程和 x64Dbg 调试器。

3 测试和结果分析

3.1 编译测试

测试代码如下:

#include 
#include 
#include 

BYTE g_OriginalByte;

void SetSoftwareBreakpoint(HANDLE hProcess, LPVOID lpAddress) {
    DWORD oldProtect;
    VirtualProtectEx(hProcess, lpAddress, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
    ReadProcessMemory(hProcess, lpAddress, &g_OriginalByte, 1, NULL); // 保存原始字节
    WriteProcessMemory(hProcess, lpAddress, "\xCC", 1, NULL); // \xCC 是 INT 3 指令,用于产生软件断点
    VirtualProtectEx(hProcess, lpAddress, 1, oldProtect, &oldProtect);
}

void RestoreOriginalByte(HANDLE hProcess, LPVOID lpAddress) {
    DWORD oldProtect;
    VirtualProtectEx(hProcess, lpAddress, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
    WriteProcessMemory(hProcess, lpAddress, &g_OriginalByte, 1, NULL); // 恢复原始字节
    VirtualProtectEx(hProcess, lpAddress, 1, oldProtect, &oldProtect);
}

void DebugLoop(HANDLE hProcess, LPVOID lpAddress) {
    DEBUG_EVENT debugEvent;
    CONTEXT context = { 0 };
    DWORD continueStatus = DBG_CONTINUE;
    HANDLE hEventThread = NULL;
    // 设置调试事件过滤器
    DebugSetProcessKillOnExit(FALSE);

    // 等待调试事件
    while (true) {
        WaitForDebugEvent(&debugEvent, INFINITE);

        switch (debugEvent.dwDebugEventCode) {
        case EXCEPTION_DEBUG_EVENT:
            context.ContextFlags = CONTEXT_FULL;
            hEventThread = OpenThread(THREAD_ALL_ACCESS, FALSE, debugEvent.dwThreadId);
            if (hEventThread == NULL) {
                std::cerr << "Failed to open thread" << std::endl;
                return;
            }
            GetThreadContext(hEventThread, &context);
            CloseHandle(hEventThread);

            // 在这里处理异常事件
            if (debugEvent.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT) {
                // 计算并打印断点发生的位置
                std::cout << "Software breakpoint hit at address: 0x" << std::hex << context.Rip - 1 << std::endl;

                // 打印通用寄存器和调试寄存器信息

                const char* registerNames[] = {
                    "RAX", "RBP", "RBX", "RCX", "RDI", "RDX", "RIP", "RSI", "RSP",
                    "R8 ", "R9 ", "R10", "R11", "R12", "R13", "R14", "R15", "DR0",
                    "DR1", "DR2", "DR3", "DR6", "DR7"
                };
                
                DWORD64* registers[] = {
                    &context.Rax, &context.Rbp, &context.Rbx, &context.Rcx, &context.Rdi, &context.Rdx, &context.Rip,
                    &context.Rsi, &context.Rsp, &context.R8, &context.R9, &context.R10, &context.R11, &context.R12,
                    &context.R13, &context.R14, &context.R15, &context.Dr0, &context.Dr1, &context.Dr2, &context.Dr3,
                    &context.Dr6, &context.Dr7
                };
                for (int i = 0; i < sizeof(registerNames) / sizeof(registerNames[0]); ++i) {
                    std::cout << registerNames[i] << ": 0x" << std::hex << *registers[i] << std::endl;
                }

                // 打印 XMM 寄存器
                const char* xmmRegisterNames[] = {
                    "XMM0", "XMM1", "XMM2", "XMM3", "XMM4", "XMM5", "XMM6", "XMM7",
                    "XMM8", "XMM9", "XMM10", "XMM11", "XMM12", "XMM13", "XMM14", "XMM15"
                };
                M128A* xmmRegisters = reinterpret_cast(&context.Xmm0);
                for (int i = 0; i < sizeof(xmmRegisterNames) / sizeof(xmmRegisterNames[0]); ++i) {
                    std::cout << xmmRegisterNames[i] << ": ";
                    std::cout << std::hex << xmmRegisters[i].Low << xmmRegisters[i].High << std::endl;
                }

                // 解析 EFLAGS 标志位
                const char* eFlagsRegisterNames[] = {
                    "CF", "PF", "AF", "ZF", "SF", "TF", "IF", "DF", "OF"
                };
                int eFlagsOffest[] = { 0, 2, 4, 6, 7, 8, 9, 10, 11 };

                std::bitset<32> eflags(context.EFlags);
                std::cout << "EFLAGS: " << eflags << std::endl;

                for (int i = 0; i < 8; ++i) {
                    std::cout << eFlagsRegisterNames[i] << ": ";
                    if (!eFlagsOffest[i])
                    {
                        std::cout << (context.EFlags & 0x1) << " | ";
                    }
                    else {
                        std::cout << ((context.EFlags >> eFlagsOffest[i]) & 0x1) << " | ";
                    }
                    if (i == 3 || i == 7) std::cout << std::endl;
                }

                //判断是不是我们设置的断点
                if (context.Rip == reinterpret_cast(lpAddress))
                    RestoreOriginalByte(hProcess, lpAddress);

                // 继续执行
                continueStatus = DBG_CONTINUE;
            }
            break;
        case EXIT_PROCESS_DEBUG_EVENT:
            // 被调试进程退出事件
            std::cout << "Process exited" << std::endl;
            CloseHandle(hProcess);
            return;

        case CREATE_PROCESS_DEBUG_EVENT:
            // 创建进程调试事件,可以在这里进行一些初始化工作
            break;

        default:
            // 其他调试事件,可以根据需要处理
            break;
        }

        ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, continueStatus);
    }

    CloseHandle(hProcess);
}

int main() {
    // DWORD dwProcessId = 1234; // 替换为你想要调试的进程的进程 ID
    STARTUPINFO si = { 0 };
    si.cb = sizeof(si);

    PROCESS_INFORMATION pi = { 0 };

    if (!CreateProcess(
        L"测试受调试程序路径.exe",  // 填写被调试程序文件的路径
        NULL,
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
        NULL,
        NULL,
        &si,
        &pi)) {

        std::wcerr << L"CreateProcess failed: " << GetLastError() << std::endl;
        return 0;
    }

    // 在指定进程的指定位置设置软件断点
    HANDLE hProcess = pi.hProcess;
    if (hProcess == NULL) {
        std::cerr << "Failed to open process" << std::endl;
        return 1;
    }

    LPVOID lpAddress = (LPVOID)0x7FF7733611D9; // 替换为你想要设置断点的地址
    SetSoftwareBreakpoint(hProcess, lpAddress);
    DebugActiveProcess(pi.dwProcessId);
    // 释放线程
    ResumeThread(pi.hThread);
    // 开始调试循环
    DebugLoop(hProcess, lpAddress);

    return 0;
}

运行结果如下:

使用 Windows API 实现软件断点调试器_第3张图片 结果截图 1
使用 Windows API 实现软件断点调试器_第4张图片 结果截图 2

3.2 结果分析

调试器可以捕获到三次断点,最后一次是用户断点。由于我们测试时硬编码的地址恰好是 LoadLibrary 函数的返回地址,所以 rax 寄存器的值是 LoadLibrary 返回值,也就是模块加载基址。

这和测试程序显示的地址对比一致:

使用 Windows API 实现软件断点调试器_第5张图片 结果截图 3

说明结果正确。

总结&补充说明

通过使用 Windows API,我们成功实现了一个简单的软件断点调试器,该调试器能够在目标进程中设置断点、监听调试事件并打印寄存器信息。这个调试器虽然简单,但为理解调试器的基本工作原理提供了一个很好的起点。在实际场景中,可以根据需求对其进行扩展和优化,以满足更复杂的调试需求。

补充说明:这里仅仅是为了测试附加进程而写的主函数代码。实际上需要这样写:如果是创建调试进程,直接在 CreateProcess 的 fdwCreate 参数里面加上 DEBUG_ONLY_THIS_PROCESS | DEBUG_PROCESS 即可,附加进程则需要 OpenProcess 打开进程访问句柄(需要足够权限)。

部分示例代码:

// 启动被调试进程。
void StartDebugSession(LPCTSTR path) {

	if (g_debuggeeStatus != STATUS_NONE) {
		std::wcout << TEXT("Debuggee is running.") << std::endl;
		return;
	}

	STARTUPINFO si = { 0 };
	si.cb = sizeof(si);

	PROCESS_INFORMATION pi = { 0 };

	if (CreateProcess(
		path,
		NULL,
		NULL,
		NULL,
		FALSE,
		DEBUG_ONLY_THIS_PROCESS | DEBUG_PROCESS | CREATE_NEW_CONSOLE | CREATE_SUSPENDED,
		NULL,
		NULL,
		&si,
		&pi) == FALSE) {

		std::wcout << TEXT("CreateProcess failed: ") << GetLastError() << std::endl;
		return;
	}

	g_hProcess = pi.hProcess;
	g_hThread = pi.hThread;
	g_processID = pi.dwProcessId;
	g_threadID = pi.dwThreadId;

	g_debuggeeStatus = STATUS_SUSPENDED;

	std::wcout << TEXT("Debugee has started and was suspended.") << std::endl;
}

代码参考文献:一个调试器的实现(四)读取寄存器和内存 - Zplutor - 博客园 


发布于:2024.01.28,更新于:2024.01.28

你可能感兴趣的:(调试和汇编技术,windows,学习方法,软件工程,汇编)