调试进程与被调试进程之间的桥梁

当调试器进程通过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、进程句柄、指针。

posted @ 2023-02-17 16:31  psj00  阅读(114)  评论(0编辑  收藏  举报