软件调试

Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html

软件调试

1.调试程序如何与被调试程序 
2. 调试事件的采集
3. 调试事件的处理流程
4. 异常的调试流程
5. 软件断点
6. 内存断点
7. 硬件断点
8. 单步异常与单步步过
9. 硬件HOOK

 

1.调试程序如何与被调试程序 

  1)API建立存在两种方式

    ①CreateProcess

    ②DebugActiveProcess

  2) 调试器如何创建调试对象 - Kernel32!DebugActiveProcess函数分析

    

    ① DbgUiConnectToDbg函数分析:

      其函数分析如下:简单的就是调用zw函数进零环,在零环创建一个内核对象然后返回三环。

      其内核模块创建的具体细节,可以搜索nt!CreateDebugObject函数。

      

     ② DebugObject数据结构如下

       typedef struct _DEBUG_OBJECT {
            KEVENT EventsPresent;
            FAST_MUTEX Mutex;
            LIST_ENTRY EventList;
            ULONG Flags;
       } DEBUG_OBJECT, *PDEBUG_OBJECT;

    ③ DebugObject 创建完之后放在那里

      如何我们查看Kernel32!DebugActiveProcess函数时,我们发现虽然调用DbgUiConnectToDbg创建一个DEBUG_OBJECT,但我们并没有看到该结构体。

      其返回值是一个是否调用成功,那存放在哪里了呢?我们查看其zwCreateDebugObject的反汇编,如下:

      我们可以得出,其存放在当前调试器线程的 Teb+0xF24 的位置中。

      

  3)被调试进程与调试进程建立关系 - Kernel32!DebugActiveProcess函数分析

     ①DbgUiConnectToDbg函数分析:

      我们之前分析过该函数通过调用 DbgUiConnectToDbg() 创建调试对象,现在我们分析其是如何与被调试进程建立联系的。

      分析如下,很明显调用一个DbgUiDebugActiveProcess()函数来建立联系的。

      

     ② ntdll!DbgUiDebugActiveProcess函数分析:

      分析如下图所示,这样再结合我们对于调试器的行为,则很好理解。

      

    ③ Nt!NtDebugActiveProcess函数分析:

      如下图,进入内核之后,先做的就是通过三环句柄来获取进程对象与调试对象,然后调用一个函数,在函数内部将其关联起来。

      

     ④nt!DbgkpSetProcessDebugObject函数分析

      该函数内容很多,但很容易看出核心操作就是将进程的DebugPort中传入DEBUG_OBJECT的地址,如下所示:

      

  4总结:

    要牢记大体流程,通过 DebugActiveProcess 来进行具体分析。

    首先,先调用内核创建一个调试对象,返回三环句柄,将句柄存储在Teb+0xF24中;之后将被调试程序的句柄与调试对象句柄一起传入零环中,在零环_EPROCESS.DebugPort中。

    值得注意的是:调试器是通过句柄与被调试对象建立连接的,而被调试程序是在内核中建立连接的,一个_TEB,一个_EPROCESS,这是建立的关键区别。

    

2. 调试事件的采集

  1)调试事件在哪儿

    如下:其是_DEBUG_OBJECT结构体,看存在一个 EventList,其就是一个调试事件链表。

    被调试程序各种事件发往调试对象的EventList中,然后由调试器接收并进行处理,其大体流程就是这个样子。

     typedef struct _DEBUG_OBJECT {
        KEVENT EventsPresent;
        FAST_MUTEX Mutex;
          LIST_ENTRY EventList; +0x30
       ULONG Flags;
     } DEBUG_OBJECT, *PDEBUG_OBJECT;

  2)何为调试事件?

    被调试进程所做的任何一件事,难道都要报告被调试器吗?当然不是,其存在如下七种调试事件:

    typedef enum _DBGKM_APINUMBER
    {
      DbgkmExceptionApi = 0, // 异常
      DbgkmCreateThreadApi = 1, // 创建线程
      DbgkmCreateProcessApi = 2, // 创建进程
      DbgkmExitThreadApi = 3, // 退出线程
      DbgkmExitProcessApi = 4, // 进程退出
      DbgkmLoadDllApi = 5, // 映射DLL
      DbgkmUnloadDllApi = 6, // 反映射DLL
      DbgkmErrorReportApi = 7, // 内部错误(已废弃)
      DbgkmMaxApiNumber = 8, // 这组常量的最大值 (已废弃)
    } DBGKM_APINUMBER;

  3)如何生成调试事件?(调试事件采集函数)

    我们下面介绍一组函数,其是Dbg**函数,其在各个活动事件的必经之路上。

    当被调试程序正在这些事件的一种,其都会经过这些函数,然后其检查是否处于被调试状态,如果处于就发送事件到链表中。

    

     ① Nt!DbgKpSendApiMessage函数分析

      该函数首先要判断该线程是否要暂停,然后调用DbgkpQueueMessage,该函数将生成调试事件,然后加入到调试对象链表中。

      其中我们可以得出一个反调试思路:这里是线程暂停的关键地方,我们可以保护我们的程序,hook该地方,当程序执行到这里,如果发现是我们的进程,则不暂停,

        这样,其就算接收到我们的调试事件了,也不会暂停我们的程序。

      

     ② Nt!DbgKpQueueMessage函数分析

      该函数主要目的是生成调试事件,根据传入过来的有关信息来填写,之后使用KeSetEvent。

      其中的详细过程可以在《等待对象》,那一节查看。

      

        

 

3. 调试事件的处理流程

  现在,被调试进程的所有调试事件都会发送到DebugEventList中,我们在编写调试器时,使用一个循环来循环遍历是否产生新的消息。

  void main() {

   DebugActiveProcess/CreateProcess;

   while(Alive){

    WaitForDebugEvent(&debugEvent,WaitTime);

    switch(DebugEvent.dwDebugEventCode)

    { .... }

    ContinueDebugEvent();

   }

  }

  1)WaitForDebugEvent、ContinueDebugEvent函数

    循环遍历该事件链表,如果存在事件就会取出来,处理完之后然后等待进一步执行。

    当处理完成之后,调用ContinueDebugEvent让被调试程序恢复执行,这个很好理解。

  2)发送虚假信息

    我们在调试过程时,会看见发送虚假的调试对象,这是为什么呢?

    很简单,我们及时以ActiveDebugProcess附加的方式调试进程,也可以看到其进程的线程以及各种模块加载的情况。

    这样很奇怪,进程模块加载已经完成了,为什么还能接收到加载信息呢,其实就是依赖于这组函数。

    当以附加的形式添加信息时,其会发送虚假的信息给调试器,让其可以显示出调试信息,但其不会中断,这很容易理解。

    

 

4.异常的调试流程

  如下图,用户层出现的异常调试流程,之前我们提到过异常处理流程,可其是如何发送给三环的调试器呢?

  注意③⑤,其调用的函数DbgKForwardException函数,该函数就是异常的收集函数,之后的流程我们上面已经分析过。

  

 

 

5. 软件断点

  硬件断点,众所周知是CC指令,INT 3 中断,0x80000003异常码。

  我们查看 Trap03,很明显看出其执行流程,我们之后学过异常和处理程序,接下来就知道怎么做了。

  

 

 

6. 内存断点

  内存断点本质就是调用 VirtualProtectEx 函数来修改页的属性(PTE),将其属性修改为写,当进行读的时候,会触发异常,然后执行异常的分发流程。

  其内存断点触发时执行 Trap0E 号中断函数,我们分析该中断,发现其也是调用CommonDispatchException函数,0xc0000006来触发异常。

  

 

 

7. 硬件断点

  1)CPU的Dr调试寄存器

    硬件断点本质调用CPU的Dr0~7,8个调试寄存器。

    其相关属性如下图,其中Dr0~Dr3存储断点地址,Dr4~Dr5保存,Dr6~Dr7设置Dr0~Dr4相关寄存器的属性。

    因为断点地址只记录在Dr0~Dr3中,因此硬件断点最多只能记录四个。

    

  2)硬件断点的处理程序

    硬件断点如果被检测,则会走Trap01中断,触发0x80000004中断(软件断点为0x8000003中断),其派发如下图

     

  3)硬件断点的单步异常与TF位产生的单步异常区分

    硬件断点产生的是单步异常,通过TF位也会产生单步异常。(单步异常我们之后会讲)

    那操作系统到底如何区分其单步异常是如何产生的呢?答案是通过Dr6寄存器的有关属性(B0~B4)。

  4)代码写硬件断点的时机

    调用GetThreadContext()和SetThreadContext(),获取线程上下文和设置线程上下文来,然后对Dr寄存器进行有关设置。

    有一点需要注意的,在调用SetThreadContext(),应该暂停其他线程,否则可能出现设置错误。

 

8. 单步异常与单步步过

  1)单步异常:

    ①单步异常的实现:

      我们在使用调试器时肯定使用过单步异常(OD的F7),如果让我们来设置,肯定执行一行代码来设置软件断点或硬件断点。

      但是CPU在设计之初已经考虑过我们这个需求了,其Eflag.TF位就是用来标识的,CPU每运行一次代码,会检查该标志位。

      如果将其设置为1,其就会触发一次单步异常,走Trap01中断(与硬件断点的执行顺序是一样的)。

    ②单步异常的代码实现:

      先设置软件断点,将被调试线程断开,调用GetThreadContext()和SetThreadContext()来修改TF位后继续运行。

  2)单步步过

    ①单步步过的实现:

      单步步过是调试器编写者去实现,并不会像单步步入一样可以通过修改TF位实现。

      根据反汇编引擎动态计算,如果下一条指令是CALL类指令,则在其再下一条指令设置断点,然后运行。

      因此,通过硬件断点或软件断点都可以实现,自己有自己的思路就好。

    ②通过反调试干掉单步步过:

      根据单步步过的反调试思路,我们可以设置大量嵌套无用函数,在里面动态修改返回地址,最后并不会执行call函数的下一个地址,

      而单步步过在call下一个地址下断点,这样该程序就会跑飞,很好理解这种原理。

 

9.硬件HOOK

  其核心原理就是修改线程的Dr寄存器,然后再加入一个VEH异常处理函数。

  其中VEH是全局异常,我们在自己的进程添加,在其他线程也可以获取到。

  我们在VEH异常处理程序中先判断当前EIP是否是要hook的地址,如果是则执行hook代码,之后再还原进去,其实就是这个样子,很好理解。

 

posted @ 2020-04-04 11:05  OneTrainee  阅读(1000)  评论(0编辑  收藏  举报