调试子系统采集调试事件的方法和过程(1)
软件调试的应用场景一般是:
- 查找程序的BUG
- 逆向破解
本篇文章是本人在阅读张银奎老师的 <软件调试>做的笔记.
主要阐述调试器获取到的调试事件是何时产生,如何收集的.
阅读目录
能够采集到的调试事件(消息)有:
1 typedef enum _DBGKM_APINUMBER 2 { 3 DbgKmExceptionApi=0, // 异常事件 4 DbgKmCreateThreadApi,//创建线程事件 5 DbgKmCreateProcessApi,//创建进程事件 6 DbgKmExitThreadApi,// 线程退出事件 7 DbgKmExitProcessApi,// 进程退出事件 8 DbgKmLoadDllApi, // 加载Dll事件 9 DbgKmUnLoadDllApi, // 卸载Dll事件 10 DbgKmErrorReportApi,// 内部错误事件 11 DbgKmMaxApiNumber, // 这组常量的最大值 12 }
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函数来通知调试子系统. DbgkForwardException函数可以向进程的异常端口发消息,也可以向调试端口发消息,具体决定给哪一个发消息 , 是由KiDispatchException函数在调用它时通过传递一个布尔类型的形参给这个函数传参决定的.如果DbgkForwardException决定了给异常端口发消息, 那么DbgkForwardException函数会判断进程的DebugPort字段是否为空,如果不为空,则通过DbgkpQueueMessage函数发送DbgKmExceptionApi消息.