调试事件的发送流程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了调试事件的发送流程相关的知识,希望对你有一定的参考价值。

调试事件的发送流程
   浏览目录

    

 

-1 调试子系统服务器将消息发给调试器的过程
    - 1.1 调试子系统接收到异常事件消息(在采集的时候)
    - 1.2 调试子系统控制被调试进程(冻结除被调试进程的
          当前线程之外的全部线程), 阻塞被调试进程的调
          用线程进入等待状态
    - 1.3 通知调试器来读取调试信息.并等待调试器回复
    - 1.4 等到回复后,唤醒被调试进程的当前线程,恢复之前挂起
          的线程.
      
      
- 1.1.1 调试子系统在内核函数用于描述和传递调试消息的结构:
{
   

 1 typedef struct _DBGKM_APIMSG
 2 {
 3         PORT_MESSAGE h;                            // LPC端口消息结构,XP之前使用
 4         DBGKM_APINUMBER ApiNumber;                // 消息类型
 5         ULONG ReturnedStatus;                    // 调试器的回复状态
 6         union                                    // 具体描述消息详情的联合结构
 7         {
 8             DBGKM_EXCEPTION Exception;            // 异常
 9             DBGKM_CREATE_THREAD CreateThread;   // 创建线程
10             DBGKM_CREATE_PROCESS CreateProcess;    // 创建进程
11             DBGKM_EXIT_THREAD ExitThread;        // 线程退出
12             DBGKM_EXIT_PROCESS ExitProcess;        // 进程退出
13             DBGKM_LOAD_DLL LoadDll;                // 映射DLl
14             DBGKM_UNLOAD_DLL UnloadDll;            // 反映射Dll
15         };
16 } DBGKM_MSG, *PDBGKM_MSG;

 



    PS:  DBGKM_APINUMBER ApiNumber;// 消息类型
        就是"调试子系统采集调试事件的方法和过程"提到的调试器能够
        采集到的调试事件(消息)
        

    -    调试信息采集函数确认需要向调试子系统报告消息后(确认DebugPort),
         会填写DBGMK_APIMSG结构,然后将其作为参数传给 DbgkpSendApiMessage
         函数.
        
    -    DbgkpSendApiMessage 函数是用来将一个调试消息发送到调试子系统的.
        函数原型:
       

NTSTATUS DbgkpSendApiMessage(PDBGKM_APIMSG ApiMsg,
                                     PVOID Port,
                                     BOOLEAN SuspendProcess
                                    );

 

         形参1 : 用来描述消息的详细信息
        形参2 : 用来指定要发往的端口,大多数时候,就是EPROCESS结构中的
                debugPort字段的值,偶尔是进程中的异常端口.即Exception
                字段
        形参3 : 如果该形参为真,那么该函数会先调用 DbgkpSuspendProcess
                挂起当前进程. 然后发送消息.等收到消息回复后再调用
                DbgkpResumeProcess 函数唤醒当前进程.
                发消息时, 如果系统的版本是NT或win 2000,由于这两个系统的
                调试子系统服务器位于用户态,因此在这些系统上可以使用
                DbgkpSendApiMessage 函数. DbgkpSendApiMessage函数会通过
                LPC机制来发送调试信息,这时,Port 参数指定的是一个LPC端口.
                这个端口的监听者通常是windows环境子系统的服务进程. 即:
                CSRSS, CSRSS 收到消息后会再转发给位于会话管理进程中的调试
                子系统服务器(CSRSS 相当于转发). 调试子系统再通知等候调试事件的
                调试器.
                流程如下:
                    
                DbgkpSendApiMessage
                        ||
                        \\/
                DbgkpSuspendProcess() 挂起当前进程
                        ||
                        \\/                 
                DbgkpSendApiMessage()
                 |--调用 LpcRequestWaitReplyPort 函数完成
                 |    具体的LPC收发任务,该函数是阻塞的,只有
                 |    收到回复,该函数才会返回.
                 |
                 |                               LPC机制
                 |--> LpcRequestWaitReplyPort ---------------> CRSS
                                               Port指定端口
        -----------------------------------------------
        
                CSRSS  -->> 调试子系统 -->> 调试器
        PS :
            DbgkpSendApiMessage 函数只能在NT和win 2000 版本系统中会被调
            用
        
    -    DbgkpQueueMessage 函数    
        如果系统的版本是XP或者是以上版本的系统, 那么将不会再继续使用函数
        DbgkpSendApiMessage, 而是改为 DbgkpQueueMessage 函数.
        函数原型:
     

  NTSTATUS DbgkpQueueMessage(IN PEPROCESS Process,
                                   IN PETHREAD Thread,
                                   IN_OUT PDBGKM_APIMSG ApiMsg,
                                   IN ULONG Flags,
                                   IN PDEBUG_OBJECT TargetDebugObject
                                  );

 

 }
        
- {1.2,1.3,1.4} 调试子系统控制被调试进程详细过程
{
    在调试子系统向调试器发送调试事件之前, 通常会先调用
    DbgkpSuspendProcess()函数, 这个函数内部会调用 KeFreezeAllThreads()
    冻结被调试进程中 除 调用线程之外 的所有线程. 接下来才执行实际的消息
    发送函数, 也就是 DbgkpQueueMessage().
    流程如下:
        DbgkpSuspendProcess()
         |
         |     冻结被调试进程中所有线程(除被调试进程的当前线程)
         |--> KeFreezeAllThreads()
         |
         |        发送消息到调试器
         |---> DbgkpQueueMessage()
         |             ||
         |             \\/
         |-- 阻塞等待调试器回复
         |             ||
         |             \\/
         |-- 唤醒被调试进程的等待线程,
         |             ||
         |             \\/
         |    恢复之前挂起的线程.
         |-- DbgkpResumeProcess()
                |
                |     恢复被调试进程中的所有线程
                |--> KeThawAllThreads()
        
}        


-2 调试子系统和调试器之间用于描述和传递调试消息的结构:
   

typedef struct _DEBUG_OBJECT
    {
        KEVENT        EventsPresent;    // 用于指示有调试事件发生的事件对象
        FAST_MUTEX    Mutex;            // 用于同步的互斥对象
        LIST_ENTRY    StateEventListEntry; // 保存调试事件的链表
        ULONG        Flags;            // 标志
    }DEBUG_OBJECT,*P_DEBUG_OBJECT;
      

    -2.1 StateEventListEntry:
            - 用来存储调试事件的链表
    -2.2 EventPresent
            - 用来同步调试器进程和被调试进程,调试子系统服务器通过设置此事件来
              通知调试器读取消息队列中的调试信息.
              调试器进程通过 WaitFOrDebugEvent()函数来等待调试事件,这个函数对
              应的 NtWaitFOrDebugEvent内核服务内部实际上等待的就是这个事件对象
    -2.3 Mutex
            - 用来锁定对这个数据结构的访问, 以防止多个线程同时读写造成数据错误
    -2.4 Flags
            - 该字段包含多个标志位, 比如 1 代表结束调试会话时是否终止被调试进
              程,DebugSetProcessKillOnExit() 设置的就是这个标志位
    
-3 调试事件的产生和传递
    -3.1 创建调试对象
            - 当调试器与调试子系统建立连接时,调试子系统会调用内核API
              NtCreateDebugObject()创建一个调试对象.
            - 并将这个内核对象保存在调试器当前线程的线程环境块的
              DbgSsReserved[1]字段.
            - 一个线程的线程环境块的DbgSsReserved[1]字段保存的调试对象是这个调
              试线程区别于其他普通线程的重要标志.
              
    -3.2 设置调试对象
        - 当调试器建立应用程序调试会话时, 会有两种情况:
            - 被调试进程是在调试器中打开的
                - 系统在创建被调试的进程时, 会把调试器线程TEB结构的
                  DbgSsReserved[1]字段中保存的调试对象句柄传递给创建进程的内核
                  服务.内核中的进程创建函数会将这个句柄所对应的调试对象指针赋
                  给新创建进程的 EPROCESS结构中的DebugPort字段.
            - 调试器进程附加到被调试进程
                - 系统会调用内核中的 DbgkpSetProcessDebugObject() 函数来将一个
                  创建好的调试对象附加到其参数所指定的进程中(被调试进程)
                  DbgkpSetProcessDebugObject() 函数内部除了将调试对象赋给
                  EPROCESS结构的DebugPort字段外, 还会调用
                  DbgkpMarkProcessPeb() 函数设置进程环境块的 BeingDebugged字段
                  
    -3.3 传递调试对象
            - DbgkpQueueMessage() 函数用于向一个调试对象的消息队列追加调试事件.
            
            - 指定 DbgkpQueueMessage()函数的调试对象的方法有两个:
                -    直接在参数中指定调试对象
                -    指定 EPROCESS 结构, DbgkpQueueMessage 函数会使用这个结
                    构中的 DebugPort 字段代替调试对象
            - 调试对象的消息队列的每一个节点的结构: DEBUG_EVENT, 这个结构与调
              试API的 DEBUG_EVENT 同名,但是内容不相同,为了避免混淆, 这里将内核
              中的 DEBUG_EVENT 结构称为 DBGKM_DEBUG_EVENT,其结构定义如下:
              

 typedef struct _DBGKM_DEBUG_EVENT
                {
                    LIST_ENTRY EventList;      // 与兄弟节点相互链接的节点结构
                    KEVENT        ContinueEvent;// 用于等待调试器回复的事件对象
                    CLIENT_ID  ClientId;      // 调试事件所在线程的线程ID和进程ID
                    PEPROCESS  Process;        // 被调试进程的EPROCESS结构地址
                    PETHREAD   Thread;         // 被调试进程中触发调试事件的线程
                                            // ETHREAD地址
                    NTSTATUS   Statuc;        // 对调试事件的处理结果
                    ULONG       Flags;        // 标志
                    PETHREAD   BackoutThread;//产生假信息的线程
                    DBGKM_MSG  ApiMsg;        // 调试事件的详细信息
                }DBGKM_DEBUG_EVENT,*P_DBGKM_DEBUG_EVENT

 

 


                
                CLIENT_ID是一个包含两个DWORD字段的结构体,这两个DWORD字段分别表
                示:
                    -    进程ID
                    -    线程ID
                    
              在把 DBGKM_DEBUG_EVENT 结构赋值之后, DbgkpQueueMessage() 函数会
              把它插入到调试子系统的调试对象(DEBUG_OBJECT)中的消息链表中
              (StateEventListEntry).
              之后 DbgkpQueueMessage() 函数会根据参数Flag是否有NOWAIT标记,来选
              择是否通知调试器来读取调试消息.
              当Flag设置了NOWAIT标记,函数会返回.
              如果没有设置,  函数会设置形参TargetDebugObject(调试对象)的
              EventPresent字段(KEVENT),通知调试器来读取调试信息().
              然后调试器会将 ContinueEvent(插入到调试对象链表的调试对象结构体
              中的)传入 KeWaitForSingleObject()函数,等待调试器的回复.
              
              调试器方面:
              调试器中的一个线程使用了函数WaitforDebugEvent()函数,这个函数最终
              会转到内核API: NtWaitForDebugEvent()。
              当调试子系统设置了EventPresent字段(KEVENT), NtWaitForDebugEvent()
              函数就会被唤醒, 然后就去读取一个调试事件(使用CLIENT_ID遍历匹配调
              试事件链表的调试对象),读取到调试事件之后,先是在这个事件
              DBGKM_DEBUG_EVENT结构的Flags字段中设置一个已读标志, 再调用函数
              DbgkpConvertKernelToUserStateChange()将DBGKM_DEBUG_EVENT结构转换
              成用户态使用的DBGUI_WAIT_STATE_CHANGE结构.
              最后会通过 ContinueDebugEvent() 函数间接或直接调用
              nt!NtDebugContinue 内核API. 而 NtDebugContinue()会根据参数中指定
              的 CLIENT_ID结构找到要恢复的调试事件结构(可能是遍历调试事件链表),
              找到之后, 设置它的 ContinueEvent事件对象, 使处于等待的被调试器的
              等待线程唤醒而继续执行.
              
             技术分享       
    -3.4 清除调试对象
        - 系统会调用 DbgkCLearProcessDebugObject()将被调试进程的DebugPort字段
          恢复为NULL
        - 遍历调试对象的消息队列(??),将关于这个进程的调试事件清除,但不破坏调试
          对象.
       

以上是关于调试事件的发送流程的主要内容,如果未能解决你的问题,请参考以下文章

03 调试事件的处理

Flutter - Sentry如何在调试模式下发送事件和停止发送

谷歌浏览器调试jsp 引入代码片段,如何调试代码片段中的js

从活动中调用片段事件

PHP代码-psysh调试代码片段工具

方便调试使用的代码片段