调试进程与被调试进程之间的桥梁
当调试器进程通过CreateProcessW(A)创建进程时,传入的dwCreationFlags为DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS时,表明调试当前创建的进程且不调试这个进程创建的子进程。
而CreateProcessW进程检测到此标志时会创建一个调试对象,具体调用堆栈如下:
KERNELBASE!CreateProcessW
KERNELBASE!CreateProcessInternalW
ntdll!DbgUiConnectToDbg会去读数组中的数据[1]
如果句柄为空调用ntdll!NtCreateDebugObject 作为ntdll!DbgUiConnectToDbg的返回函数,在ntdll!NtCreateDebugObject 函数返回前在内核中 就对teb.DbgSsReserved[1]中写入了句柄值,类型为DebugObject
我们自己写一个进程,这个进程会以调试方式启动一个进程,在创建进程前手动加入断点,运行程序时如果有JIT调试器那么会自动中断下来,然后使用!teb命令获取TEB地址
再查看DbgSsReserved的值
并对DbgSsReserved[1] 即对DbgSsReserved指针+4得到一个地址下一个硬件写入断点,断下来后调用堆栈如下:
可知在call edx的时候就写入完成了,查看句柄值:
如果使用内核调试的话 ,还可以查看此句柄对应的内核调试对象DebugObject的内存地址。
接下来调试器调用WaitForDebugEvent即可获取调试事件,此函数界面原型如下:
WINBASEAPI BOOL APIENTRY WaitForDebugEvent(_Out_ LPDEBUG_EVENT lpDebugEvent,_In_ DWORD dwMilliseconds);
第一个参数为一个指向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;
其中包含一个联合数据类型,当dwDebugEventCode取不同值时,u代表不同的数据结构。
这个函数的调用栈如下:
KERNEL32!WaitForDebugEventStub
KERNELBASE!WaitForDebugEvent
KERNELBASE!WaitForDebugEventWorker
KERNELBASE!BaseFormatTimeOut
DbgUiWaitStateChange 返回c0000008
ntdll!NtWaitForDebugEvent(传入多个参数 包括teb.DbgSsReserved[1]数组共两个数据)
如果当前进程不是调试器进程(即teb.DbgSsReserved[1]为空时),那么DbgUiWaitStateChange 返回0xc0000008,代表STATUS_INVALID_HANDLE句柄错误。
调试器进程的teb.DbgSsReserved[1]应该为一个句柄,才可以获取调试消息。
每个进程在内核中都有一个EPROCESS结构体,而EPRCESS结构体中有一个变量为DebugPort。如下:
对于被调试进程来说,EPROCESS结构体中DebugPort是不为空的,而之前的调试进程的teb.DbgSsReserved[1]存储的是句柄值,句柄在内核中对应的就是这个DebugPort对应的地址。
所以调试进程中有一个句柄指向了DebugObject,被调试进程的DebugPort也指向了同一个DebugObject。
当被调试进程产生了异常事件、进程启动退出事件、线程启动退出事件、模块加载卸载事件、输出调试消息、RIP_INFO报告 RIP 调试事件 (系统调试错误)这些异常消息时,都会将这些消息存储到DebugObject结构体对应的链表中。
当调试器使用WaitForDebugEvent时,内部ntdll的函数会使用teb.DbgSsReserved[1]的句柄寻找到DebugObject,进而获取到调试消息队列获取函数,返回给调试器,调试器处理完成后,再通过continueDebugEvent函数通知系统调试器处理结果:
1、DBG_CONTINUE告诉系统,已经处理了异常,让被调试进程继续运行。
2、DBG_EXCEPTION_NOT_HANDLED告诉系统,调试器不处理该异常,交给程序自己的异常处理机制处理。
3、DBG_REPLY_LATER这个没有使用过,按照MSDN解释为:Windows 10版本 1507 或更高版本中受支持,此标志会导致 dwThreadId 在目标继续后重播现有的中断事件。 通过针对 dwThreadId 调用 SuspendThread API,调试器可以在进程中恢复其他线程,稍后返回到中断状态。下次进行测试。
teb.DbgSsReserved[0]中也保存了数据,当调用WaitForDebugEvent获取到数据后,如下堆栈对teb.DbgSsReserved[0]写入了数据
teb.DbgSsReserved[0]值为0x10a6eb8
通过动态分析看到申请的数据内存为
返回到我们自己写的代码,调用WaitForDebugEvent后
查看event数据
可以看到进程ID与上面堆空间的数据是一样的,其中dwDebugEventCode为3,代表u为CREATE_PROCESS_DEBUG_EVENT结构,如下:
可以看到12c与之前堆中的数据12c是一样的,我们可以看到之前保存DbgSsReserved[0]的函数为ntdll!SaveProcessHandle,保存的是进程的PID以及进程句柄,同时还可以从SaveProcessHandle函数中看出来
总结一下:
1、调试器进程中某一个线程调用了CreateProcess时使用调试标志,那么此线程的teb.DbgSsReserved[1]会存储一个指向DebugObject调试对象的句柄,注意是线程的DbgSsReserved[1],也就是说在同一进程的其他线程中调用WaitForDebugEvent就会出现问题。而被调试进程在内核中EPROCESS结构中DebugPort字段不为空,指向DebugObject调试对象,另外在被调试进程的用户空间PEB中偏移0x2的位置存储一个字段名为BeingDebugged,字段占1个字节,如果当前进程处于调试中,那么此值为不为0。用户态判断当前是否处于调试状态下就是通过此值确定的,通过调用IsDebuggerPresent函数获取这个值。
2、teb.DbgSsReserved[0]指向一块堆内存,保存了当前最新的调试消息对应的进程PID以及进程句柄,并且是一个链表,堆内存起始的内存指针指向了上一条调试消息对应的内存地址,保存的也是进程PID、进程句柄、指针。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端