鱼C论坛

 找回密码
 立即注册
查看: 2208|回复: 0

win32调试器的实现

[复制链接]
发表于 2018-1-26 11:40:56 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 错过会难过 于 2018-1-26 11:14 编辑

调试器的实现

Table of Contents 调试器实现的依赖.1.CPU所支持的调试功能2.操作系统提供的功能3. 调试器实现的原理3.1 以调试创建一个进程或附加到一个进程3.2 2.创建完进程后,需要安装个电话,静静的等待调试子系统找上门来:3.2.1 关于解读DEBUG_EVENT结构体的一些方法和注释3.3 通过DEBUG_EVENT结构体来解读异常信息4. 让程序停下来实现4.1 单步断点的实现(利用TF断点) :4.2 软件断点的实现(利用0xcc指令)

调试器实现的依赖.

1.CPU所支持的调试功能

1.1 CPU中,有标志寄存器EFLAGS的IF,TF标志位用于开启调试功能,如果一个进程是以调试状态被创建,且其线程环境(CONTEXT)中的EFLSGS的TF标志位是1,那么这个进程执行一条指令后,将会产生一个异常,异常被处理后,TF自动被重置为0

1.2 CPU有DRx(DebugRegister)系列的寄存器可用于断点功能.



  • - DR0~DR3这四个寄存器是断点地址存储器,用于保存断点的地址
  • - DR6是调试状态寄存器用于指明DR0~DR3寄存器中哪一个产生了调试异常
  • - DR6寄存器使用比特位B0~B3来指明DR0~DR3中哪个产生了调试异常.
  • - DR7是断点属性控制器
  • - DR7寄存器分别保存着DR0~DR3的断点地址对应的断点的属性,属性有如下几种:
  •   - L0~L3:这个比特位等于1,则断点为本地断点.
  •   - G0~G3:这个比特位等于1,则断点为全局断点.
  •     R/W0-R/W3:
  •     - 00:执行断点
  •     - 01:数据写入断点
  •     - 10:I/0读写断点
  •     - 11:读写断点()读取指令不算)


2.操作系统提供的功能

Windows有一个调试子系统,所有的异常(包括CPU产生的异常)都会中断到调试子系统中,进程产生异常后,调试子系统会捕捉到这个异常,如果这个进程是以被调试状态创建,那么,调试子系统会将这个异常派发到产生异常的进程的父进程. 如果其父进程的代码用有函数WaitForDebugEvent(),那么,函数将会从等待状态中被唤醒,返回到其父进程的调用地点.并将异常信息保存到DEBUG_EVENT结构体中.

3. 调试器实现的原理

程序在运行起来后,其执行指令的速度是光速的.由于人类无法从光速中看清真相,程序出错也是光速的,常会在人无法预知的时候发生了无法预知的错误,让程序变慢,或者停下来是一种能够查明程序出错,或者程序运行机制的重要技能.

程序的运行,是通过CPU对指令流的处理.如果CPU能够听人指挥,一直在指令流的指定地点处执行,那么,程序的流程不会接着往下走,程序就自然而然的停下来了.

前面说的异常,其实就是让CPU告诉操作系统,大佬,我看到了一条让我停下来的指令(0xcc,遇到了DRx寄存器中保存的地址, 除0等),现在该怎么办. 操作系统遇到了这个情况,赶紧回想,自己没有发布这样的指令,于是就让它的马仔调试子系统去解决这个问题,调试子系统会查看进程是否被人调试了,一看,真是被调试了,于是找到了调试这个进程的人的电话(PrentProcessID),打了个电话给那个人. 如果那个人装了电话(WaitForDebugEvent),那么就能从这个电话中得到信息,调试子系统会跟那个人约定,你先去解决吧,我就在这等着,于是那个人就可以针对调试子系统给的信息做出对应的处理.最后就答复调试子系统有没有处理成功. 调试子系统就会拍拍屁股回去,让被调试的进程继续执行或是一直执行那条指令.

调试器在刚才说的作品中扮演的角色就是被调试进程的父进程. 下面就是调试器要完成的流程:



3.1 以调试创建一个进程或附加到一个进程

  1.     /*创建进程*/
  2.     CreateProcess(pszFilePath,//可执行模块路径
  3.                   NULL,//命令行
  4.                   NULL,//安全描述符
  5.                   NULL,//线程属性
  6.                   FALSE,//否从调用进程处继承了句柄
  7.                   DEBUG_ONLY_THIS_PROCESS,//启动方式,这里是以只调试的方式创建一个还没有运行的进程
  8.                   NULL,//新进程的环境块
  9.                   NULL,//新进程的当前工作路径(当前目录)
  10.                   &stcStartupInfo,//指定进程的主窗口特性
  11.                   &stcProcInfo//接收新进程的识别信息
  12.                   );
复制代码


3.2 2.创建完进程后,需要安装个电话,静静的等待调试子系统找上门来:

  1.     DEBUG_EVENTstcDeEvent={0};//这是保存调试子系统发来的信息的结构体
  2.     WaitForDebugEvent(&stcDeEvent,//保存异常信息的结构体
  3.                       INFINIT//等待时间
  4.                       );
复制代码

3.2.1 关于解读DEBUG_EVENT结构体的一些方法和注释

   
  1. typedefstruct_DEBUG_EVENT{
  2.         DWORDdwDebugEventCode;//发生异常的是什么事
  3.         DWORDdwProcessId;//触发异常的进程ID(如果被调试进程有多个进程,这个ID有可能是其子进程的)
  4.         DWORDdwThreadId;//触发异常的线程ID(如果被调试进程有多个线程,这个ID有可能是其中的一个线程的
  5.         union{
  6.             EXCEPTION_DEBUG_INFOException;//异常类型信息
  7.             CREATE_THREAD_DEBUG_INFOCreateThread;//创建线程时得到的信息结构体(有可能会创建多个线程)
  8.             CREATE_PROCESS_DEBUG_INFOCreateProcessInfo;//创建进程时得到的信息结构体,有可能会得到多个
  9.             EXIT_THREAD_DEBUG_INFOExitThread;//线程退出的信息结构体
  10.             EXIT_PROCESS_DEBUG_INFOExitProcess;//进程退出的信息结构体
  11.             LOAD_DLL_DEBUG_INFOLoadDll;//加载模块的信息结构体
  12.             UNLOAD_DLL_DEBUG_INFOUnloadDll;//卸载模块的信息结构体
  13.             OUTPUT_DEBUG_STRING_INFODebugString;//输出调试字串的信息结构体
  14.             RIP_INFORipInfo;//系统调试错误时的信息结构体
  15.         }u;//这是一个联合体,dwDebugEventCode决定联合体中哪个字段是有用的.
  16.     }DEBUG_EVENT,*LPDEBUG_EVENT;
复制代码

3.3 通过DEBUG_EVENT结构体来解读异常信息

首先在这里向大家道歉,我有罪,调试子系统打电话过来时,带各位带来的信息并非仅仅包含着异常信息.调试子系统给的结构体有大量的信息,它把这些信息打包成了上面的所述的结构体,其中,第一个字段DWORD dwDebugEventCode;会告诉大家,此次发来的信息是什么信息(调试子系统可能会给你打很多个电话,所以为了能够接到调试子系统的第二个电话,在这里我偷偷建议你,用一个while循环将电话包起来). 这就是调试子系统所能给你的全部的信息:

  • - CREATE_PROCESS_DEBUG_EVENT 创建进程之后发送此类调试事件,这是调试器收到的第一个调试事件。
  • - CREATE_THREAD_DEBUG_EVENT 创建一个线程之后发送此类调试事件。
  • - EXCEPTION_DEBUG_EVENT 发生异常时发送此类调试事件。
  • - EXIT_PROCESS_DEBUG_EVENT 进程结束后发送此类调试事件。
  • - EXIT_THREAD_DEBUG_EVENT 一个线程结束后发送此类调试事件。
  • - LOAD_DLL_DEBUG_EVENT 装载一个DLL模块之后发送此类调试事件。
  • - OUTPUT_DEBUG_STRING_EVENT 被调试进程调用OutputDebugString之类的函数时发送此类调试事件。
  • - RIP_EVENT 发生系统调试错误时发送此类调试事件。
  • - UNLOAD_DLL_DEBUG_EVENT 卸载一个DLL模块之后发送此类调试事件。

  如果你仅仅想处理异常事件,那么我会这样建议你:

  1.     if(stcDeEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT)
  2.     {
  3.        //搞点事情
  4.     }
复制代码


完事之后,你需要给调试子系统的BB机回复一个信息:

  1.     ContinueDebugEvent(stcDeEvent.dwProcessId,//指明是哪个被调试进程发的消息
  2.                         stcDeEvent.dwThreadId,//指明是哪个被调试进程下的线程发的消息
  3.                         DBG_CONTINUE  // DBG_CONTINUE表示异常已经被处理
  4.                        );
复制代码


消息的内容,由于是BB机,所以只能发两个消息DBG_EXCEPTION_HANDLED和DBG_CONTINUE, 这个消息一旦发送出去,将产生决定性的效果,如果你不发,调试子系统会在另一头死等你回消息. 一旦你发了,它就会根据你发的消息内容来决定做什么事情:

  • - DBG_CONTINUE 告诉调试子系统已经处理了,不用在找别人处理了.
  • - DBG_EXCEPTION_HANDLED 告诉调试子系统,没有处理,但是可以找别人处理

  流程如下:
  
  由于这是一个影响深远的决定, 故而不能任性放荡地决定:
  在回复之前,我们必须先要看看这个异常时什么情况,能不能处理.那么,异常来临的时候,我们应该需要什么信息才能做出处理呢? 首先,我们要正视自己,摸着自己的良心问自己,我们是谁,然后坚定的回答自己我们是调试器,然后在问我们要异常干什么,我们要用异常来把调试进程停止下来.好了,现在我们知道我们要什么了.
  1 .起码要知道异常发生的地址,这样才能知道这是不是我们自己让程序停下来的地方

1. 其次是异常发生的真相,这样才知道这是不是我们用阴谋让程序停下来的原因

  1.     typedefstruct_EXCEPTION_DEBUG_INFO{
  2.         EXCEPTION_RECORDExceptionRecord;//一个结构体,里面保存的是异常发生的地址和原因
  3.         DWORDdwFirstChance;//
  4.     }EXCEPTION_DEBUG_INFO,*LPEXCEPTION_DEBUG_INFO;
复制代码


查明层层目录结构体下异常的发生地址和原因之结构体:

  1.     typedefstruct_EXCEPTION_RECORD{
  2.         DWORD ExceptionCode;//异常发生的真相
  3.         DWORD ExceptionFlags;
  4.         struct _EXCEPTION_RECORD*ExceptionRecord;
  5.         PVOID ExceptionAddress;//异常发生的地址
  6.         DWORD NumberParameters;
  7.         ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
  8.     }EXCEPTION_RECORD;
复制代码


  • - EXCEPTION_RECORD的第一个字段ExceptionCode,异常发生的真相有如下内容:
  • - EXCEPTION_ACCESS_VIOLATION : 非法访问异常
  • - EXCEPTION_DATATYPE_MISALIGNMENT: 内存对齐异常
  • - EXCEPTION_ILLEGAL_INSTRUCTION : 无效指令
  • - EXCEPTION_INT_DIVIDE_BY_ZERO : 除0错误
  • - EXCEPTION_PRIV_INSTRUCTION : 指令不支持当前模式
  • - EXCEPTION_BREAKPOINT : 断点异常0xcc
  • - EXCEPTION_SINGLE_STEP: 单步或硬件断点异常TF和DR系列寄存器

  这些真相也意味着我们能够用哪些方法让程序听话地停下来.但是一般仅仅使用后两种就足够了.

4. 让程序停下来实现

为了方便断点的管理,在这里约定了一些宏,定义了一个结构体来保存断点的信息

  1.     #define MODE_HARD 1//硬件断点:DR0~DR3
  2.     #define MODE_SOFT 2//软件断点:0xcc
  3.     #define MODE_TF 3//TF断点:TF=1
  4.     #define SOFTTYPE_ONCE 1//一次性断点
  5.     #define SOFTTYPE_NORM 2//正常断点
  6.     #define SOFTTYPE_COND 3//条件断点
  7.     #define CONDITIONTYPE_ADDRE 0//条件断点的地址类型
  8.     #define CONDITIONTYPE_TIMES 1//条件断点的次数类型
  9.     #define TFTYPE_TF 1//正常的TF断点
  10.     #define TFTYPE_RELOAD 2//重新安装软件断点
  11.     #define TFTYPE_TIMES 3//次数断点
  12.     #define UPDOWN_EQU 1//相等
  13.     #define UPDOWN_UP 2//升序
  14.     #define UPDOWN_DOWN 3//降序
  15.     typedef struct BREAKPOINTERTYPE
  16.     {
  17.         //////////////////////////////////////////////////////////////////////////
  18.         //断点方式硬件断点,软件断点,TF断点
  19.         //分表对应不同的表或结构体
  20.         //mode==MODE_HARD==>>保存在vector<BREAKPOINTERTYPE>
  21.         //mode==MODE_SOFT==>>保存在map<SIZE_T,BREAKPOINTERTYPE>
  22.         //mode==MODE_TF==>>保存在BREAKPOINTERTYPE
  23.         DWORD mode:8;
  24.         //------------------------------------------------------------------------
  25.         //////////////////////////////////////////////////////////////////////////
  26.         //不同的mod对应不同的type
  27.         //mod==TF有两种type
  28.         //type1:可继续的TF中断
  29.         //type2:用于恢复软件中断
  30.         //mod==SOFT或mod==HARD
  31.         //type1:一次性断点
  32.         //type2:正常断点
  33.         //type3:条件断点
  34.         //启动条件断点时,下面的结构体被启用
  35.         //times:记录断下的次数
  36.         //address:断点地址
  37.         //upDown:记录是大于/小于/等于条件
  38.         //condition:次数或地址,配合upDown产生作用.
  39.         //
  40.         //SOFT和HARD不一样的地方:
  41.         //SOFT需要还原内存数据,将EIP减1,触发TF断点,重新写入0xcc,
  42.         //HARD需要将对应寄存器的值清零,触发TF断点,重新写入寄存器的值
  43.         DWORD type:8;//断点类型一次性,单步,条件
  44.         //////////////////////////////////////////////////////////////////////////
  45.         DWORD memData:8;//内存中原来的内容.下0xcc断点时替换出来的内容
  46.         DWORD conditionType:4;//条件断点使用哪种条件:地址,次数
  47.         DWORD upDown:4;//>,==,<
  48.         SIZE_T address;//断点地址
  49.         SIZE_T times;//断下的次数(一次性除外)
  50.         SIZE_T condition;//限制条件:地址或者次数
  51.     }BREAKPOINTERTYPE,*PBREAKPOINTERTYPE; lorf'&#1949;]c
复制代码

4.1 单步断点的实现(利用TF断点) :

首先单步断点利用的是将EFLAGS寄存器的TF标志位置1来实现 !

获取EFLAGS寄存器的方法是利用如下函数:

  1.     GetThreadContext(HANDLE hThread,//线程句柄,被调试进程的线程句柄,保存在进程被创建时得到CREATE_PROCESS_DEBUG_INFO结构体中
  2.                     CONTEXT* pContext//保存着线程上下文(就是寄存器组)
  3.                     )
复制代码

这个线程环境里面有很多内容,有通用寄存器,段寄存器,当然也有我们想要的标志寄存器 得到这个寄存器后,将其EFlags的TF比特位置为1 :  pContext->EFlags|=0x100

更新线程环境

  1.     SetThreadContext(HANDLE hThread,//线程句柄,被调试进程的线程句柄,保存在进程被创建时得到的CREATE_PROCESS_DEBUG_INFO结构体中
  2.                     CONTEXT* pContext//线程环境(也叫线程上下文)
  3.                     )
复制代码


值得注意的是,TF标志为只能用一次,使用过后TF会被自动置零,这意味你需要再次将TF标志位置1

4.2 软件断点的实现(利用0xcc指令)

软件断点的实现就是将0xcc写入到调试进程的指定内存地址中,0xcc在汇编中是int3指令,我们需要将原来的内容读取出来,保存,然后将0xcc写入.

  1.     ReadProcessMemory(HANDLEhProcess,//被读取内存信息的进程句柄,也就是被调试的进程的句柄
  2.                      LPVOID lpAddress,//读取地址
  3.                      LPVOID lpBuff,//函数将读取内容保存的缓冲区
  4.                      SIZE_T nSize,//缓冲区大小,
  5.                      SIZE_T* lpNumberOfBuffRead//函数读取了多少
  6.                      );
  7.     WriteProcessMemory(HANDLEhProcess,//被读取内存信息的进程句柄,也就是被调试的进程的句柄
  8.                        LPCVOID lpAddress,//读取地址
  9.                        LPCVOID lpBuff,//写入的内存
  10.                        SIZE_T nSize,//缓冲区大小,
  11.                        SIZE_T* lpNumberOfBuffRead//函数写入了多少
  12.                        );
复制代码

值得注意的是,内存断点被触发后需要将EIP减1,还原内存数据,这样做是为了什么呢?

E9 12345678这是一条跳转指令,下了0xcc断点后变成了这样:

CC 12345678,EIP不减1,CPU会从12345678处继续执行,这样下去,指令全部乱套.所以要将EIP减1 将EIP减1后,CPU重CC处执行,会继续触发异常,陷入死循环,所以要将内存数据还原 但是还原内存数据之后,这个断点下一次就不能使用了. 解决的办法就是恢复数据后触发TF断点,在TF断点触发时将CC重新写入.

原理大概就是这些了,有很多内容没讲太细,留给大伙思考吧(其实是我太懒了).

参考源码: https://github.com/enoorez/dbg3
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-4-20 05:47

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表