调试事件的发送流程
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
- 遍历调试对象的消息队列(??),将关于这个进程的调试事件清除,但不破坏调试
对象.
以上是关于调试事件的发送流程的主要内容,如果未能解决你的问题,请参考以下文章
Flutter - Sentry如何在调试模式下发送事件和停止发送