读书笔记|Windows 调试原理学习|持续更新
关于调试方面的学习笔记,主要来源于《软件调试》的读书笔记和梦织未来论坛的视频教程
1.调试器使用一个死循环监听调试信息。
DebugActiveProcess(PID);
while(TRUE) { DEBUG_EVENT MyDebugInfo; WaitForDebugEvent(MyDebugInfo,INFINITE);//阻塞 switch (MyDebugInfo.dwDebugEventCode) { case CREATE_THREAD_DEBUG_EVENT: break; } }
2.什么是调试信息,进程创建、终止,加载模块都是调试信息。dwDebugEventCode说明了调试信息的种类。
- dwDebugEventCode
-
Type: DWORD
-
The code that identifies the type of debugging event. This member can be one of the following values.
Value Meaning - CREATE_PROCESS_DEBUG_EVENT
- 3
Reports a create-process debugging event. The value of u.CreateProcessInfo specifies a CREATE_PROCESS_DEBUG_INFO structure.
- CREATE_THREAD_DEBUG_EVENT
- 2
Reports a create-thread debugging event. The value of u.CreateThread specifies a CREATE_THREAD_DEBUG_INFO structure.
- EXCEPTION_DEBUG_EVENT
- 1
Reports an exception debugging event. The value of u.Exception specifies an EXCEPTION_DEBUG_INFO structure.
- EXIT_PROCESS_DEBUG_EVENT
- 5
Reports an exit-process debugging event. The value of u.ExitProcess specifies an EXIT_PROCESS_DEBUG_INFO structure.
- EXIT_THREAD_DEBUG_EVENT
- 4
Reports an exit-thread debugging event. The value of u.ExitThread specifies an EXIT_THREAD_DEBUG_INFO structure.
- LOAD_DLL_DEBUG_EVENT
- 6
Reports a load-dynamic-link-library (DLL) debugging event. The value of u.LoadDll specifies a LOAD_DLL_DEBUG_INFO structure.
- OUTPUT_DEBUG_STRING_EVENT
- 8
Reports an output-debugging-string debugging event. The value of u.DebugString specifies an OUTPUT_DEBUG_STRING_INFO structure.
- RIP_EVENT
- 9
Reports a RIP-debugging event (system debugging error). The value of u.RipInfo specifies a RIP_INFO structure.
- UNLOAD_DLL_DEBUG_EVENT
- 7
Reports an unload-DLL debugging event. The value of u.UnloadDll specifies an UNLOAD_DLL_DEBUG_INFO structure.
3.DEBUG_EVENT中使用共用体来储存具体数据
typedef struct _DEBUG_EVENT { DWORD dwDebugEventCode; DWORD dwProcessId; DWORD dwThreadId; union { EXCEPTION_DEBUG_INFO Exception; CREATE_THREAD_DEBUG_INFO CreateThread; CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; EXIT_THREAD_DEBUG_INFO ExitThread; EXIT_PROCESS_DEBUG_INFO ExitProcess; LOAD_DLL_DEBUG_INFO LoadDll; UNLOAD_DLL_DEBUG_INFO UnloadDll; OUTPUT_DEBUG_STRING_INFO DebugString; RIP_INFO RipInfo; } u; } DEBUG_EVENT, *LPDEBUG_EVENT;
4.一但附加进程,Windows系统会发送调试信息。包括已经创建过的进程和加载过的模块信息也会发送
5.用户层调试函数的实现
6.调试原理概述
Windows调试系统使用事件驱动,这一点与窗体是很相似的。
WaitForDebugEvent是用来等待调试事件的,调试器处理调试事件时,被调试进程会挂起,所以调试器处理完毕后要调用ContinueDebugEvent来使挂起的被调试进程继续运行。
在系统内核中调试事件的数据结构是DBGKM_APIMSG,而NTDLL也就是原生应用层使用的是DBGUI_WAIT_STATE_CHANGE,而应用层的调试器是使用DEBUG_EVENT。所以要进行一些结构的转换。
在内核创建线程的函数执行过程中会调用一个DbgkCreateThread函数,这个DbgkCreateThread函数会检查新建线程的进程是否正在被调试(通过检查DebugPort的值是否为空)然后会发送调试消息。
注意windows内核函数会根据DebugPort是否为空来判断进程是否处于被调试情况。
内核态下的调试结构DBGKM_APIMSG
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; 17 18 复制代码
NTSTATUS
DbgkpSendApiMessage(
IN OUT PDBGKM_APIMSG ApiMsg,
IN BOOLEAN SuspendProcess
);
调试系统使用DbgkpSuspendProcess和DbgkpResumeProcess这两个函数来控制被调试进程。
DbgkpSuspendProcess会冻结被调试进程中除了调用线程之外的所有线程,执行这个函数后被调试进程中就只有这个发生调试信息的线程还活动着。接着会执行实际的发送消息的函数,
即DbgkpQueueMessage。
windows调试子系统处于CSRSS会话管理器中,调试子系统是以内核对象DebugObject为核心的。
调试对象
来自wrk1.2 typedef struct _DEBUG_OBJECT { KEVENT EventsPresent; FAST_MUTEX Mutex; LIST_ENTRY EventList; ULONG Flags; } DEBUG_OBJECT, *PDEBUG_OBJECT
EventPresent事件对象是用来同步调试器进程和被调试进程的。函数WaitForDebugEvent等待的其实就这个事件。
快速互斥体Mutex用来处理并发访问,相当于一个锁的作用。
调试器与调试子系统连接时,调试子系统会创建一个调试对象(NtCreateDebugObject),并且将其保存在调试器当前线程的TEB的DbgSsReserved[1]中,而这个线程就是调试器线程。
要建立调试器与被调试进程之间的联系,需要把这个调试对象设置到被调试进程的EPROCESS的DebugPort中。
DbgkpQueueMessage函数用于向一个调试对象的消息队列中增加调试事件
这里EventList链表中每一项都是如下结构
1 typedef struct _DEBUG_EVENT { 2 LIST_ENTRY EventList; // Queued to event object through this 3 KEVENT ContinueEvent; //用于等待调试器回复的事件对象 4 CLIENT_ID ClientId; //调试事件所在的线程ID和进程ID 5 PEPROCESS Process; // 被调试进程的EPROCESS 6 PETHREAD Thread; // 被调试进程中触发调试事件的线程的ETHREAD 7 NTSTATUS Status; //调试事件处理结果 8 ULONG Flags; 9 PETHREAD BackoutThread; // 产生假消息(Faked)的线程ETHREAD 10 DBGKM_APIMSG ApiMsg; // 调试事件的真正内容 11 } DEBUG_EVENT, *PDEBUG_EVENT;
以上来自WRK1.2,注意这个是与用户层同名都是DEBUG_EVENT但是内容完全不同,这是个内核结构。是内核中的调试事件结构。
DbgkpQueueMessage把这个结构插入到调试对象的调试事件链表中。
DbgkpQueueMessage有等待和不等待两种方式,如果指定不等待(异步处理)则函数直接返回。
如果没有指定不等待则设置调试对象的EventPresent,然后再等待(KeWaitForSingleObject)DEBUG_EVENT结构中的ContinueEvent对象用来等待调试器回复。
调试器调用ContinueDebugEvent实际上就是设置这个ContinueEvent对象。
在调试器进程执行的NtDebugActiveProcess中会调用一个函数DbgkpSetProcessDebugObject将一个调试对象设置到要调试的进程中(即EPROCESS的DebugPort)。
这样被调试进程就与调试器进程产生了联系(通过调试对象)
而由于
- 这个函数是代表附加进程方式调试(只有这种方式才会调用这个函数)
- 这个函数代表刚刚启动对进程的调试
- 附加进程方式调试代表目标进程已经运行了一段时间
所以就需要进行虚假调试信息发送。
会通过遍历被调试进程的所有线程,然后在调试内核对象中放置这些线程的虚假调试事件消息。再防止虚假模块加载调试事件消息。
当取消对进程的调试时,会将DebugPort端口清零。
调试器的调试线程的TEB中有特殊结构,这个是区别于普通线程的地方。DbgSsReserved[0]指向一个被调试进程的所有线程的链表,用来描述被调试进程中的每一个线程。
DbgSsReserved[1]指向调试对象。
WaitForDebugEvent和ContinueDebugEvent这两个函数会维护那个线程链表。
一个进程被调试会造成
- 进程的EPROCESS的DebugPort值不为0
- 进程的PEB的BeingDebugged值不为0
- 可能会有调试器建立在被调试进程中的远程线程——RemoteBreakin线程
大名鼎鼎的IsDebuggerPresent就是通过判断BeingDebugged来实现的。
调试器与被调试进程之间的交互被称做“调试会话”
两种调试方式
- 启动被调试进程
- 附加到已经运行的被调试进程
1.启动被调试进程
首先调试器线程会调用一个DbgUiConnectToDbg来是调试器线程与调试子系统建立连接(初始化调试器线程),具体做法是新建一个内核调试对象,然后把这个内核调试对象放入调试器线程的DbgSsReserved[1]中,这样调试器线程就初始化好了。
当调用CreateProcess创建进程时指定DEBUG_PROCESS标志即可。系统会把调用这个函数的进程当作调试器进程,把新创建的进程当作被调试的进程。
建立起调试关系
当进程的初始线程创立时会查看自己是否是被调试中(BeingDebugged标志),如果是被调试中会调用DbgBreakPoint来触发一个断点
2.附加到已经运行的被调试进程
通过DebugActiveProcess就可以附加到一个已经运行的进程中。
首先是DbgUiConnectToDbg,这一步与上面是一样的。
打开被调试的进程,因为不打开被调试的进程也就没办法对其进行操作。
调用内核函数(向下分发调用)NtDebugActiveProcess
这个内核函数主要是
1.发送伪造的线程创建、进程创建和模块加载调试消息。
2.设置被调试进程的调试端口,调试对象在DbgUiConnectToDbg调用后就已经创建好了直接拿来用就可以,同时设置被调试进程BeingDebugged字段。
为什么会先发送调试消息,后设置调试端口呢?
这个不是很奇怪吗?发送之后才去设置调试端口?
其实是因为,所谓的发送调试消息实质上指的是创建并设置好一些调试事件,然后把这些调试事件放入调试对象的调试事件链表中,这样一来是否设置好了调试端口也就无关紧要了。
因为这些数据只是储存在调试对象中,还没有人去等待。
WaitForDebugEvent
调试器在用户层的操作接下来就会调用WaitForDebugEvent函数来实现,用来取出一个DEBUG_EVENT结构,这个函数是阻塞的,因而可以设置一个等待时间来防止无限等待。
这个函数会调用底层的等待调试事件函数,然后把等待到的结构转化为用户态的DEBUG_EVENT结构。因为我们前面说过,一个调试事件在不同的层次下的表示的数据结构是不同的。
调试器是如何实现让运行中的被调试进程立刻中断到调试器中的呢?这个功能叫做异步阻停,一种实现方法是使用CreateRemoteThread函数来新建一个触发int 3断点的线程。
系统中已经提供一个用来触发断点的函数,NTDLL的DbgUiRemoteBreakin函数。
windows xp之后系统提供了一个现成的API DebugBreakProcess
1 BOOL WINAPI DebugBreakProcess( 2 _In_ HANDLE Process 3 );
这个函数会自动创建远程线程