问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
软件调试详解
在windows里面调试跟异常息息相关,如果想要对调试得心应手,异常处理的知识是必不可少的,本文主要介绍的是软件调试方面的有关知识,讲解调试程序和被调试程序之间如何建立联系。
0x00 前言 ======= 在windows里面调试跟异常息息相关,如果想要对调试得心应手,异常处理的知识是必不可少的,本文主要介绍的是软件调试方面的有关知识,讲解调试程序和被调试程序之间如何建立联系。 0x01 调试对象 ========= 我们知道在windows里面,每个程序的低2G是独立使用的,高2G(内核)区域是共用的。那么我们假设一个场景,我们的调试器要想和被调试程序之间建立通信肯定就需要涉及到进程间的通信以及数据的交换,如果这个过程放在3环完成,不停的进程通信会很繁琐,所以windows选择将这个过程放在0环进行 调试器与被调试程序之间建立起联系的两种方式 - CreateProcess - DebugActiveProcess 与调试器建立连接 -------- 首先看一下`DebugActiveProcess`  调用`ntdll.dll`的`DbgUiConnectToDbg`   再调用`ZwCreateDebugObject`  通过调用号进入0环  进入0环创建`DEBUG_OBJECT`结构体 ```c++ typedef struct _DEBUG_OBJECT { KEVENT EventsPresent; FAST_MUTEX Mutex; LIST_ENTRY EventList; ULONG Flags; } DEBUG_OBJECT, *PDEBUG_OBJECT; ``` 然后到`ntoskrnl`里面看一下`NtCreateDebugObject`  然后调用了`ObInsertObject`创建`DebugObject`结构返回句柄  再回到`ntdll.dll`,当前线程回0环创建了一个`DebugObject`结构,返回句柄到3环存放在了TEB的`0xF24`偏移处 也就是说,遍历TEB的`0xF24`偏移的地方,如果有值则一定是调试器  与被调试程序建立连接 ---------- 还是回到`kernel32.dll`的`DebugActiveProcess`,获取句柄之后调用了`DbgUiDebugActiveProcess`  调用`ntdll.dll`的`DbgUiDebugActiveProcess`  跟到`ntdll.dll`里面的`DbgUiDebugActiveProcess`,传入两个参数,分别为调试器的句柄和被调试进程的句柄  通过调用号进0环  来到0环的`NtDebugActiveProcess`, 第一个参数为被调试对象的句柄,第二个参数为调试器的句柄  执行`ObReferenceObjectByHandle`,把被调试进程的句柄放到第五个参数里面,这里eax本来存储的是调试器的`EPROCESS`,执行完之后eax存储的就是被调试进程的`EPROCESS`  这里判断调试器打开的进程是否是自己,如果是自己则直接退出  也不能调试系统初始化的进程  然后获取调试对象的地址,之前是句柄,但是句柄在0环里面是无效的,这里就要找真正的地址  获取到调试对象的地址之后还是存到`ebp+Process`的地方,这里之前是被调试对象的地址,现在存储的是调试对象的地址  将调试进程和被调试的`PEPROCESS`传入`_DbgkpSetProcessDebugObject`,将调试对象和被调试进程关联起来  跟进函数,发现有判断`DebugPort`是否为0的操作,ebx为0,edi为被调试进程的EPROCESS,那么`edi+0bc`就是调试端口  然后再把调试对象的句柄放到被调试对象的`DebugPort`里面  0x02 调试事件的采集 ============ 调试事件的种类 ```c++ typedef enum _DBGKM_APINUMBER { DbgKmExceptionApi = 0, //异常 DbgKmCreateThreadApi = 1, //创建线程 DbgKmCreateProcessApi = 2, //创建进程 DbgKmExitThreadApi = 3, //线程退出 DbgKmExitProcessApi = 4, //进程退出 DbgKmLoadDllApi = 5, //加载DLL DbgKmUnloadDllApi = 6, //卸载DLL DbgKmErrorReportApi = 7, //已废弃 DbgKmMaxApiNumber = 8, //最大值 } DBGKM_APINUMBER; ``` 调试事件的采集函数  当创建进程或者线程的时候,一定会调用`PspUserThreadStartup`  判断当前线程是否为当前进程的第一个线程,如果是的话就生成一个编号为1的调试事件  再看一下退出线程必经的函数`PspExitThread`  判断`Debugport`是否为0,如果为0则不搜集信息   进入跳转,判断这个线程是不是当前最后一个线程,如果是则调用`DbgkExitProcess`  如果不是则调用`DbgkExitThread`  DbgkpSendApiMessage ------------------- `DbgkpSendApiMessage`这个api主要就是将各种调试信息封装成一个结构体写到`_DEBUG_OBJECT`结构里面,无论是哪种事件,最后都会调用`DbgkpSendApiMessage`,如果想隐藏进程/线程的创建,就可以给`DbgkCreateThread`挂钩子,如果想隐藏所有的调试事件那么就可以给`DbgkpSendApiMessage`挂钩子  这里跟一下`DbgkExitThread`找`DbgkpSendApiMessage`的过程,跟进函数直接就可以看到`DbgkpSendApiMessage`  所有搜集调试事件的api都会调用`DbgkpSendApiMessage`   `DbgkpSendApiMessage(x, x)`参数说明: 1\) 第一个参数:消息结构 每种消息都有自己的消息结构 共有7种类型 2\) 第二个参数:要不要把本进程内除了自己之外的其他线程挂起。 有些消息需要把其他线程挂起,比如CC 有些消息不需要把线程挂起,比如模块加载。`DbgkSendApiMessage`是调试事件收集的总入口,如果在这里挂钩子,调试器将无法调试。 LoadLibrary ----------- 首先在`kernel32.dll`里面调用`RtlAllocateHeap`  然后跟到`ntdll.dll`调用了`NtQueryPerformanceCounter`  通过调用号进0环   总结来说,`LoadLibrary`首先调用`CreateMapping`创建一块共享内存,再通过`NtMapViewOfSection`映射到线性地址,调用`DbgkMapViewOfSection`将结构体发送给`DbgkpSendApiMessage` \_DEBUG\_OBJECT --------------- ```c++ typedef struct _DEBUG_OBJECT { KEVENT EventsPresent; //+00 用于指示有调试事件发生 FAST_MUTEX Mutex; //+10 用于同步互斥对象 LIST_ENTRY EventList; //+30 保存调试消息的链表 ULONG Flags; //+38 标志 调试消息是否已读取 } DEBUG_OBJECT, *PDEBUG_OBJECT; ``` 调试事件的处理 ------- 因为每种事件的调试信息不一样,所以会有很多种类(7种)的api去采集  编号的值也是对应的   ```c++ // Debug1.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <iostream> #include <Windows.h> #include <stdlib.h> void TestDebugger() { BOOL nIsContinue = NULL; STARTUPINFOA sw = { 0 }; PROCESS_INFORMATION pInfo = { 0 }; auto retCP = CreateProcessA("C:\\Dbgview.exe",NULL, NULL, NULL, TRUE,DEBUG_PROCESS|| DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &sw, &pInfo); if (retCP == 0) { printf("CreateProcess error : %d\n", GetLastError()); return; } while (TRUE) { DEBUG_EVENT debugEvent = { 0 }; auto rDebugEvent = WaitForDebugEvent(&debugEvent, -1); if (rDebugEvent) { switch (debugEvent.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT: printf("EXCEPTION_DEBUG_EVENT\n"); break; case CREATE_THREAD_DEBUG_EVENT: printf("CREATE_THREAD_DEBUG_EVENT\n"); break; case CREATE_PROCESS_DEBUG_EVENT: printf("CREATE_PROCESS_DEBUG_EVENT\n"); break; case EXIT_THREAD_DEBUG_EVENT: printf("EXIT_THREAD_DEBUG_EVENT\n"); break; case EXIT_PROCESS_DEBUG_EVENT: printf("EXIT_PROCESS_DEBUG_EVENT\n"); break; case LOAD_DLL_DEBUG_EVENT: printf("LOAD_DLL_DEBUG_EVENT\n"); break; case UNLOAD_DLL_DEBUG_EVENT: printf("UNLOAD_DLL_DEBUG_EVENT\n"); break; case OUTPUT_DEBUG_STRING_EVENT: printf("OUTPUT_DEBUG_STRING_EVENT\n"); break; } } //在发送事件event给调试器debugger时,被调试进程会被挂起,直到调试器调用了continueDebugEvent函数 ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId,DBG_CONTINUE); } } int main() { TestDebugger(); system("pause"); return 0; } ``` 这里用调试模式启动windbg  可以发现这里有一个异常,这里先打印一下异常处理返回的代码 ```c++ printf("EXCEPTION_DEBUG_EVENT : %x %x %x\n",debugEvent.u.Exception.ExceptionRecord.ExceptionAddress,debugEvent.u.Exception.ExceptionRecord.ExceptionCode,debugEvent.u.Exception.ExceptionRecord.ExceptionFlags); ```  将程序拖入OD看到系统有一个`int3`断点  那么为什么会有一个异常处理的事件呢?这里首先看一下进程的创建过程 ```c++ 1.映射exe文件 2.创建内核对象EPROCESS 3.映射系统dll(ntdll.dll) 4.创建线程内核对象ETHREAD 5.系统启动线程 映射dll(ntdll.LdrInitializeThunk) 线程开始执行 ``` 在映射dll的过程中调用了`LdrInitializeThunk`这个api,`LdrInitializeThunk`会调用`LdrpInitializeProcess`初始化进程 首先找到TEB,然后找TEB的0x30偏移的PEB放入ebx  `DbgBreakPoint`其实就是`int3`的封装  看一下交叉引用,可以看到`LdrpRunInitializeRoutines`引用了`DbgBreakPoint`  这里只有当程序处于调试模式的时候才会启动  在内核文件里面看一下`NtDebugActiveProcess`  会发送线程和模块的加载信息  但是这个信息是不靠谱的,因为这个api是通过遍历PEB链表的方式来寻找模块 在PEB的Ldr结构里面有三个模块,`DbgkpPostFakeProcessCreateMessages`这个api就是通过查询这个结构来判断加载了哪些模块 也就是说当程序加载完成之后,这个api才会去链表里面找模块,但是这个时候可能信息已经被摘除,所以如果要想更准确的获取信息,就可以通过遍历vad树的方式来获取1 0x03 异常的处理流程 ============ 处理流程  正常的异常处理流程  产生异常的时候首先会将异常传递给调试器,如果调试器不处理则继续寻找异常处理函数 这里设置为异常为忽略的话就会执行自己的异常处理函数   如果设置为不忽略的情况下就会一直断在某一行  UnhandledExceptionFilter ------------------------ 相当于编译器为我们生成了一段伪代码 ```c++ __try { } __except(UnhandledExceptionFilter(GetExceptionInformation()) { //终止线程 //终止进程 } ``` 只有程序被调试时,才会存在未处理异常 `UnhandledExceptionFilter`的执行流程: ```c++ 1) 通过NtQueryInformationProcess查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH,此时会进入第二轮分发 2) 如果没有被调试: 查询是否通过SetUnhandledExceptionFilter注册处理函数 如果有就调用 如果没有通过SetUnhandledExceptionFilter注册处理函数 弹出窗口 让用户选择终止程序还是启动即时调试器 如果用户没有启用即时调试器,那么该函数返回EXCEPTION_EXECUTE_HANDLER ``` SetUnhandledExceptionFilter --------------------------- 如果没有通过`SetUnhandledExceptionFilter`注册异常处理函数,则程序崩溃   测试代码如下,我自己构造一个异常处理函数`callback`并用`SetUnhandledExceptionFilter`注册,构造一个除0异常,当没有被调试的时候就会调用`callback`处理异常,然后继续正常运行,如果被调试则不会修复异常,因为这是最后一道防线,就会直接退出,起到反调试的效果 ```c++ // SEH7.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> long _stdcall callback(_EXCEPTION_POINTERS* excp) { excp->ContextRecord->Ecx = 1; return EXCEPTION_CONTINUE_EXECUTION; } int main(int argc, char* argv[]) { SetUnhandledExceptionFilter(callback); _asm { xor edx,edx xor ecx,ecx mov eax,0x10 idiv ecx } printf("Run again!"); getchar(); return 0; } ``` 直接启动可以正常运行  使用od打开则直接退出   ```c++ // Debug3.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> DWORD g_Test = 0; LONG NTAPI TopLevelExceptFilter(PEXCEPTION_POINTERS pExcepinfo) { printf("The top_function fix the exception!\n"); g_Test = 1; return EXCEPTION_CONTINUE_EXECUTION; } int main(int argc, char* argv[]) { int x = 0; int y = 100; SetUnhandledExceptionFilter(&TopLevelExceptFilter); x = y/g_Test; printf("正常逻辑开始执行\n"); for (int i=0;i<10;i++) { ::Sleep(1000); printf("%d\n", i); } getchar(); return 0; } ``` 正常情况下执行程序  如果是调试程序则直接退出 
发表于 2022-04-15 09:44:27
阅读 ( 6927 )
分类:
漏洞分析
2 推荐
收藏
0 条评论
请先
登录
后评论
szbuffer
30 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!