ScareCrowL's blog

To maintain world peace

【项目】写一个简单的调试器

一、项目简介

  之前学习调试与异常写的,时间有限,完成了一个简易版的调试器,对异常分发、调试事件以及各类断点有了一些认识,项目是使用QT+VS2015写的,说明一下,VS2015可以安装QT的扩展插件, 调试起来比QT CREATOR方便很多,建议使用。由于时间过了很久了,很多知识就不写细啦,最后发源码,有需要的朋友可参考~

二、实现功能

1.建立调试机制√
2.软件断点√
3.硬件断点√
4.内存访问断点√
5.寄存器信息√
6.堆栈信息√
7.模块信息√
8.条件断点√
9.调试器保护√
扩展:
1.导出表
2.导入表
3.DUMP
4.解析符号
5.单步\API等

来张动图体会一下~
《【项目】写一个简单的调试器》

三、基础理论

一、异常分发

  首先我们应该明确,Windows将一些处理异常的函数地址存放在IDT(中断描述表)中,CPU产生中断后会调用不同的IDT函数,Windows得以接管异常处理,如果目标进程是调试状态的,操作系统需要找到目标进程的调试器进程,并发送异常信息,如果调试器进程使用WaitForDebugEvent函数等待调试事件,就能够获取到调试信息。如果异常进程没有被调试,操作系统会找到目标程序已经注册过的异常处理程序(SEH,VEH或VCH)并调用这些异常处理程序,这样的流程称之为异常分发。

二、调试子系统

  简单的说,当调用创建进程的函数,并传入调试标志时,调试子系统就会准备调试会话,并为进程内核对象中的调试端口赋值,然后在调试进程的线程内核对象中加入一个调试对象的句柄,当被调试进程被创建时,内核的进程创建函数,线程创建函数,模块加载,模块卸载函数等都会被一一调用,而这些,统统会被调试子系统监控并发送给我们的调试器进程。

  之后通过调试器与调试子系统的交互即可实现基本的流程,在实现调试器我使用了两个线程,一个是用于等待调试事件的线程(可以理解为控制台版),另一个是用户交互的线程,即与QT UI完成交互。

  处理调试事件的大致流程如下:(以F11单步为例)
TF置1->执行代码->CPU产生中断->IDT函数被调用->操作系统进行异常分发->调试子系统发送调试事件->调试器得到EXCEPTION_DEBUG_EVENT异常事件->调试器显示反汇编信息

其中会使用到一些重要结构体如下:

    1.typedef struct _DEBUG_EVENT {
      DWORD dwDebugEventCode;
      DWORD dwProcessId;
      DWORD dwThreadId;
      union {
        EXCEPTION_DEBUG_INFO      Exception;
        CREATE_THREAD_DEBUG_INFO      CreateThread;
        CREATE_PROCESS_DEBUG_INFO      CreateProcessInfo;
        EXIT_THREAD_DEBUG_INFO      ExitThread;
        EXIT_PROCESS_DEBUG_INFO      ExitProcess;
        LOAD_DLL_DEBUG_INFO       LoadDll;
        UNLOAD_DLL_DEBUG_INFO      UnloadDll;
        OUTPUT_DEBUG_STRING_INFO      DebugString;
        RIP_INFO            RipInfo;
      } u;
    } DEBUG_EVENT, *LPDEBUG_EVENT;

    2.typedef struct _EXCEPTION_DEBUG_INFO {
      EXCEPTION_RECORD   ExceptionRecord;
      DWORD   dwFirstChance;
    } EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;

    3.typedef struct _EXCEPTION_RECORD {
      DWORD                    ExceptionCode;
      DWORD                    ExceptionFlags;
      struct _EXCEPTION_RECORD  *ExceptionRecord;
      PVOID                    ExceptionAddress;
      DWORD                    NumberParameters;
      ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    } EXCEPTION_RECORD, *PEXCEPTION_RECORD;

MSDN上对于异常的具体值有详细的说明~
《【项目】写一个简单的调试器》

四、设计分享

项目时间很短,写QT界面又很费时间,友好度什么的就都没管了,重在学习调试技术~
基本功能,各类断点的原理不难,大家看代码吧,当时在写代码时候感觉最难实现的是DUMP,在这就着重点写一下DUMP吧~
DUMP: 主要是PE的知识,本质也就是把内存中的程序写入到文件中,主要难在各个区段的拷贝,贴代码见注释:

void ScDebugger_Service::DumpMemory(LPVOID lpAddr)
{
    IMAGE_DOS_HEADER * pDosHdr = (IMAGE_DOS_HEADER*)lpAddr;
    IMAGE_NT_HEADERS * pNtHdr = (IMAGE_NT_HEADERS*)(pDosHdr->e_lfanew+(SIZE_T)lpAddr);
    //IMAGE_OPTIONAL_HEADER * pOpHdr=(IMAGE_OPTIONAL_HEADER*)(pNtHdr->)
}

unsigned char* ScDebugger_Service::DumpPe(HANDLE hProcess, unsigned char *szAddress, int *nLen)
{
    PIMAGE_DOS_HEADER pDos; // MS-DOS头
    PIMAGE_NT_HEADERS pNt; // NT映象头
    PIMAGE_SECTION_HEADER pSection; // 区块头

    unsigned char szHeader[8192], *szSection;
    DWORD dwRet;
    // 从指定的地址szAddress读进程内存内容到szHeader
    if (!ReadProcessMemory(hProcess, (LPVOID)szAddress, (LPVOID)szHeader, sizeof(szHeader), NULL))
        return FALSE;
    // 通过文件头两个字节是否等于“MZ”来判断是否为PE文件
    if (memcmp(szHeader, "MZ", 2))
    {
        return FALSE;
    }

    pDos = (PIMAGE_DOS_HEADER)szHeader;
    // MS-DOS头的最后一个成员e_lfanew指向NT映象头
    pNt = (PIMAGE_NT_HEADERS)(szHeader + pDos->e_lfanew);
    // 获得所需分配内存空间的大小
    *nLen = pNt->OptionalHeader.SizeOfImage +
        pNt->OptionalHeader.SizeOfHeaders ;

    // 分配虚拟内存空间, 注意后面的保护属性为PAGE_READWRITE
    if (!(dwRet = (DWORD)VirtualAlloc(NULL,
        pNt->OptionalHeader.SizeOfImage +
        pNt->OptionalHeader.SizeOfHeaders ,
        MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)))
    {
        printf("%d/n", GetLastError());
        MessageBox(0, 0, L"不能分配有效内存", 0);
        return FALSE;
    }

    //将头拷进申请的空间中
    memcpy((char*)dwRet, szHeader, pNt->OptionalHeader.SizeOfHeaders);

    pSection = IMAGE_FIRST_SECTION(pNt);
    for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
    { 

        if (!(szSection = (unsigned char*)VirtualAlloc(NULL,
            pSection[i].Misc.VirtualSize,
            MEM_RESERVE | MEM_COMMIT,
            PAGE_READWRITE)))
        {
            MessageBox(0, 0, L"不能分配有效内存", 0);
            return FALSE;
        }
        // 从进程内存里读映像块
        if (!ReadProcessMemory(hProcess, szAddress + pSection[i].VirtualAddress,
            szSection, pSection[i].Misc.VirtualSize, NULL))
        {
            MessageBox(0, 0, L"不能读取映象块", 0);
            return FALSE;
        }
        //PointerToRawData是区段的文件偏移
        //VirtualSize是内存中的此区段起始的相对虚拟地址RVA
        //用Sizeofrawdata才是区段在文件中的大小(对齐后)
        memcpy((char*)dwRet + pSection[i].PointerToRawData, szSection, pSection[i].SizeOfRawData);

        // 释放申请到的内存
        VirtualFree(szSection, 0, MEM_RELEASE);
    }

    return (unsigned char *)dwRet;
}

void ScDebugger_Service::WritePeDump(char * outfile, unsigned char* pe, int nlen)
{
    //C:\Users\superltx\Desktop\1.exe
    HANDLE hFile = NULL;
    DWORD cbWritten;

    hFile = CreateFileA(outfile, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL, NULL); // 创建文件,具有写属性
    if (hFile == INVALID_HANDLE_VALUE)
    {
        MessageBox(0, 0, L"创建文件错误", 0);
        return;
    }
    // 写文件操作
    WriteFile(hFile, pe, nlen, &cbWritten, NULL);

    if (nlen != cbWritten)
    { 
        // 校验写文件操作是否正确
        MessageBox(0, 0, L"写入文件时错误", 0);
    }
    else {
        MessageBox(0, L"文件DUMP完毕!",L"提醒" , 0);
    }
    CloseHandle(hFile);
}

DWORD ScDebugger_Service::PageSize()
{
    SYSTEM_INFO systemInfo; // 系统信息结构
    GetSystemInfo(&systemInfo); // 获得系统信息结构
    return systemInfo.dwPageSize; // 返回内存页大小
}

使用
《【项目】写一个简单的调试器》

由于代码太多了,大家参考源码看哈,不足请留言指教~

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注