首页
社区
课程
招聘
[分享]调试事件的采集到和调试事件发送到调试器的过程
发表于: 2016-3-9 14:44 5523

[分享]调试事件的采集到和调试事件发送到调试器的过程

2016-3-9 14:44
5523
最近在读软件调试这本书 , 得到了不少知识.
所以把书中的一些知识点扣了出起来.

调试子系统采集调试事件的方法和过程:
        能够采集到的调试事件(消息)
        typedef enum _DBGKM_APINUMBER
        {
                DbgKmExceptionApi=0, // 异常
                DbgKmCreateThreadApi,//创建线程
                DbgKmCreateProcessApi,//创建进程
                DbgKmExitThreadApi,// 线程退出
                DbgKmExitProcessApi,// 进程退出
                DbgKmLoadDllApi,         // 加载Dll
                DbgKmUnLoadDllApi,        // 卸载Dll
                DbgKmErrorReportApi,// 内部错误
                DbgKmMaxApiNumber,  // 这组常量的最大值
        }
       
        PS:DbgKmErrorReportApi 是用来报告调试子系统内部的错误 ,目前已经不再使用
       
        - 进程创建和线程创建事件的采集过程:
                创建新的进程和线程 ,进程的通信,终止进程和线程,
                资源分配回收这些任务通常称为进程管理,
                完成这些功能的是ntoskrnl.exe中有Ps或Psp开头的系列
                函数. 这写系列函数被泛称为进程管理器.
                进程管理器创建新的用户态Windows线程时,有如下工作
                -        为该线程建立必要的内核对象和数据结构
                -        分配栈空间
                -        挂起该线程
                -        通知环境子系统(子系统会作必要的设置和登记)
                -        调用 PspUserThreadStartup,准备启动线程(函数总是会
                                调用 调试子系统的内核函数 DbgkCreateThread.)
                                - 调试子系统的内核函数 DbgkCreateThread()函数会检查新
                                创建线程所在的进程是否正在被调试(根据 DebugPort 是否为NULL),
                                如果为NULL,便立即返回(返回到 PspUserThreadStartup()函数),
                                如果不是NULL,则会继续检查该进程的用户态时间(UserTime)是否为0,
                                目的是判断该线程是否是进程中的第一个线程 ,如果是第一个线程,则
                                通过DbgkpQueueMessage()函数向调试端口(DebugPort)发送
                                DbgKmCreateProcessApi消息. 如果不是第一个线程,则发送
                                DbgkmCreateTheadApi消息.
                    具体流程如下:
                       
                                  建立内核对象和数据结构
                                                        ||
                                                        \/
                                                分配栈空间
                                                        ||
                                                        \/
                                                 挂起线程
                                                        ||
                                                        \/
                                          通知环境子系统
                                                        ||         |--> 必要的设置和登记
                                                    \/
                        ----->>>PspUserThreadStartup()
                        ||                   |--> DbgkCreateThread()/*在函数内部调用*/
                        ||                               ||
        返回到上层函数                           \/
                        ||<--是<<- [DebugPort==NULL]
                                                          ||
                                                         不是
                                                          ||
                                                          \/                          --------------------------------
                                                 [UserTime==0]--是-->>|通过DbgkpQueueMessage()函数   |
                                                      ||                         |发送DbgKmCreateProcessApi消息 |
                                                         不是                           --------------------------------
                                                          ||          
                                                          \/      
                                -------------------------------
                                | 通过DbgkpQueueMessage()函数 |          
                                | 发送DbgkmCreateTheadApi消息 |
                                -------------------------------       
                                 
        -        进程和线程退出事件的采集过程
                        进程管理器的PspExitThread函数负责线程的退出和清除.
                        在函数销毁线程的结构和资源之前 , 该函数会调用调试
                        子系统的函数让调试器(如果有)得到处理机会.
                        如果退出的是一个进程中的最后一个线程,PspExitThread
                        会调用DbgkExitProcess函数 , 否则调用DbgkExitThread
                        函数.
                        DbgExitThread函数被调用后,会检查进程的DebugPort是否
                        位0,如果不为0,则会先将该进程挂起,然后通过 DbgkpQueueMessage
                        函数向DebugPort发送DbgKmExitThreadApi消息.并且会
                        等待DbgKmExitThreadApi函数返回才将挂起的线程恢复运行.
                        DbgExitProcess函数执行过程和DbgExitThread函数非常类似,
                        只不过发送的是DbgKmExitProcessApi消息.且没必要执行挂起
                        和恢复动作,因为进程管理器已经对该线程做了删除标记(??)
                       
        -        模块映射和反映射事件的采集过程
                        当系统要dll时,会首先判断该dll是否被加载过(判断条件是什么?)
                        如果是,则不会重复加载,只将该dll对应的内存页面映射到目标进程
                        的内存空间(如何得知要已加载的dll在内存中的位置和大小),并把该
                        dll的引用次数加1. 当一个进程退出或调用FreeLibrary函数卸载一个
                        Dll时 , 系统会从该进程的虚拟内存空间中把该Dll的映射删除(如何
                        得知映射到了进程的虚拟内存空间的哪个地址) , 并递减该Dll的引用
                        次数, 如果引用次数为0,那么该Dll会被彻底移出内存(从哪移出)
                       
                        系统内核中的内存管理器(Memory Manager)便是负责DLl的映射和发映
                        射. 内存管理器使用Section对象来表示一块可被多个进程共享的内存
                        区域. 并设计了一系列的内核服务和函数来实现各种映射和反映射任务
                        NtMapViewOfSection函数就是用来映射模块的内核服务,
                        NtUnmapViewOfSection是用来反映射的.
                       
                        当NtMapViewOfSection在把一个模块映像成功映射到指定进程空间中时,
                        (主要是使用MmMapViewOfSection映射),NtMapViewOfSection函数会调用
                        调试子系统的DbgkMapViewOfSection函数通知调试子系统.
                       
                        模块映射过程如下:
                                - 递归遍历模块的输入表(LdrpWalkImportDescriptor)
                                - 加载输入表依赖的模块(LdrpLoadImportModule)
                                - 将模块映射到进程的虚拟空间(用户态函数:ZwMapViewOfSection)
                                   - 将模块映射到进程的虚拟空间(内核态函数:NtMapViewOfSection)
                                - 通知调试子系统(DbgkMapViewOfSection)
                                        - 检查DebugPort字段是否为空
                                                - 如果为空则发送调试信息到调试端口(DbgkpQueueMessage)
                       
                        MnUnmapViewOfSection函数的执行过程也类似 , 该函数会调用调试子系统
                        的DbgkUnmapViewOfSection函数,
                        DbgkUnmapViewOfSection函数内部会检测DebugPort不为空后,会发送
                        DbgKmUnLoadDllAPi消息
       
        -        异常事件的采集
                KiDispatchException函数:异常分发的枢纽,它会给每个异常安排最多两轮被处
                理的机会对于每一轮处理机会, 它都会调用调试子系统的DbgkForwardException
                函数来通知调试子系统.
               
                ,也可以向调试端口发
                消息,具体决定给哪一个发消息 , 是由KiDispatchException函数在调用它时通
                过传递一个布尔类型的形参给这个函数传参决定的.
                如果DbgkForwardException决定了给异常端口发消息, 那么
                DbgkForwardException函数会判断进程的DebugPort字段是否为空,如果不为空,
                则通过DbgkpQueueMessage函数发送DbgKmExceptionApi消息.
               
               
               
       
                                调试事件的发送流程
               
       
-1 调试子系统服务器将消息发给调试器的过程
        - 1.1 调试子系统接收到异常事件消息(在采集的时候)
        - 1.2 调试子系统控制被调试进程(冻结除被调试进程的
                  当前线程之外的全部线程), 阻塞被调试进程的调
                  用线程进入等待状态
        - 1.3 通知调试器来读取调试信息.并等待调试器回复
        - 1.4 等到回复后,唤醒被调试进程的当前线程,恢复之前挂起
              的线程.
          
          
- 1.1.1 调试子系统在内核函数用于描述和传递调试消息的结构:
{
        typedef struct _DBGKM_APIMSG
        {
                PORT_MESSAGE h;                                                        // LPC端口消息结构,XP之前使用
                DBGKM_APINUMBER ApiNumber;                                // 消息类型
                ULONG ReturnedStatus;                                        // 调试器的回复状态
                union                                                                        // 具体描述消息详情的联合结构
                {
                        DBGKM_EXCEPTION Exception;                        // 异常
                        DBGKM_CREATE_THREAD CreateThread;   // 创建线程
                        DBGKM_CREATE_PROCESS CreateProcess;        // 创建进程
                        DBGKM_EXIT_THREAD ExitThread;                // 线程退出
                        DBGKM_EXIT_PROCESS ExitProcess;                // 进程退出
                        DBGKM_LOAD_DLL LoadDll;                                // 映射DLl
                        DBGKM_UNLOAD_DLL UnloadDll;                        // 反映射Dll
                };
        } 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
                - 遍历调试对象的消息队列(??),将关于这个进程的调试事件清除,但不破坏调试
                  对象.

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 0
支持
分享
最新回复 (5)
雪    币: 152
活跃值: (92)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
2
这排版, 我也是醉了....
2016-3-9 14:57
0
雪    币: 207
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
我也想说。。。
2016-3-9 15:15
0
雪    币: 152
活跃值: (92)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
4
2016-3-9 15:17
0
雪    币: 6
活跃值: (19)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
好文章学习一下。
2016-3-9 19:13
0
雪    币: 152
活跃值: (92)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
6
没有没有,只是摘录书上的内容而已
2016-3-9 21:05
0
游客
登录 | 注册 方可回帖
返回
//