一、调试概述
调试相当于我们看医生,医生会看出我们身体上的问题
windows提供DebugActiveProcess函数来附加目标进程调试进程,参数是进程ID,调试成功返回true,失败返回false。
返回false可以使用GetLastError获取错误
由于不知道什么时候会传来调试信息,所以用一个死循环一直等待目标进程发送调试信息给调试器
用循环中WaitForDebugEvent来等待目标进程传来调试信息
调试信息包括创建进程,创建线程,加载模块等等。。
WaitForDebugEvent第一个参数为反馈的调试信息结构体,第二个参数为等待的时间
DEBUG_EVENT中的union根据不同的调试信息选择不同的结构体
WaitForDebugEvent捕获了一个调试事件后,目标进程会暂停运行,调试器处理信息,处理完后要恢复目标进程,让它继续运行,需要使用ContinueDebugEvent
当调试器附加进程时,调试器会把进程所有的创建进程,创建线程,加载模块等收集起来告诉调试器
二、用户层调试执行流程分析
ZwDebugActiveProcess函数存在于ntdll.dll模块中,参数是进程ID,传入-1时调试的是csrss.exe进程
DebugActiveProcess中调用DbgUiConnectToDbg函数
DbgUiConnectToDbg函数先初始化一个OBJECT_ATTRIBUTES结构
获取_TEB结构体中0xF20中的DbgSsReserved,这个成员是个数组,判断这个数组中的第二个元素值是否为0,这个元素保存的是调试对象的句柄,一个线程只能保存一个调试对象句柄。如果这个元素为空调用ZwCreateDebugObject函数创建调试对象
然后调用GetTargProcessHandle函数获得目标进程句柄,GetTargProcessHandle函数中使用ZwOpenProcess获取目标进程句柄
然后调用DbgUiDebugActiveProcess函数,这个函数中先获取当前线程_teb+0xf20数组中的第二个元素,也就是调试对象句柄。然后调用ZwDebugActiveProcess函数进入内核,附加要被调试的进程。成功以后调用DbgUiIssueRemoteBreakin函数创建远程断点,像OD附加成功进程后,会中断进程
调试器下断点使进程中断,目标进程产生调试事件,调试事件被调试器接收到
很多壳在DbgUiRemoteBreakin函数做hook防止反调试
WaitForDebugEvent调用DbgUiWaitStateChange,DbgUiWaitStateChange调用ZwWaitForDebugEvent进入内核等待调试事件到来。ZwWaitForDebugEvent函数第一个参数为当前线程_teb+0xf20中的第二个元素调试对象句柄,第四个参数为PDBGUI_WAIT_STATE_CHANGE,这个结构体用来保存调试事件的信息
WaitForDebugEvent函数原型第一个参数为LPDEBUG_EVENT。用户层和内核层调试事件的信息结构体是不一样的。内核层使用的是DBGUI_WAIT_STATE_CHANGE,用户层使用的是LPDEBUG_EVENT
所以要使用DbgUiConvertStateChangeStructure函数将两个结构体转换
然后根据DEBUG_EVENT调试事件中的调试事件代码选择不同的处理方法。将所有的进程和线程信息全都挂载到_teb+0xf20中的第一个元素链表中。调试器中查找进程和线程的方法就是查找_teb+0xf20的第一个元素
最后是继续调试,根据HandleMarked移除已经退出的线程
三、内核层调试机理的窥探
ZwCreateDebugObject调用内核层函数NtCreateDebugObject创建调试对象
在NtCreateDebugObject中判断当前模式,如果不是内核模式,判断调试句柄是否可写
然后调用ObCreateObject函数创建调试对象
将句柄插入到句柄表,没一个进程都会有一张句柄表,所有打开的句柄都会放在句柄表里
ZwDebugActiveProcess函数调用内核层函数NtDebugActiveProcess函数调试目标进程
在NtDebugActiveProcess函数中先获取当前模式,然后调用ObReferenceObjectByHandle根据进程句柄获得进程对象EPROCESS
判断调试进程是否是自己或者是不是系统进程,如果是的话返回失败
然后判断当前模式是不是用户模式、当前进程和目标进程是不是受保护模式
进程的保护模式是在CreateProcess函数中的dwCreationFlags决定的
根据调试句柄获得调试对象,使用ExAcquireRundownProtection函数获得进程锁,在进程锁释放前,进程不会退出
使用DbgkpPostFakeProcessCreateMessage函数发送一个虚假的创建进程消息给调试器
然后使用DbgkpSetProcessDebugObject函数设置进程的调试对象
在DbgkpSetProcessDebugObject中先判断进程的DebugPort是否为空,如果没有设置DebugPort,将调试对象赋值到DebugPort