在windows里面调试跟异常息息相关,如果想要对调试得心应手,异常处理的知识是必不可少的,本文主要介绍的是软件调试方面的有关知识,讲解调试程序和被调试程序之间如何建立联系。
0x00 前言
在windows里面调试跟异常息息相关,如果想要对调试得心应手,异常处理的知识是必不可少的,本文主要介绍的是软件调试方面的有关知识,讲解调试程序和被调试程序之间如何建立联系。
0x01 调试对象
我们知道在windows里面,每个程序的低2G是独立使用的,高2G(内核)区域是共用的。那么我们假设一个场景,我们的调试器要想和被调试程序之间建立通信肯定就需要涉及到进程间的通信以及数据的交换,如果这个过程放在3环完成,不停的进程通信会很繁琐,所以windows选择将这个过程放在0环进行
调试器与被调试程序之间建立起联系的两种方式
- CreateProcess
- DebugActiveProcess
与调试器建立连接
首先看一下DebugActiveProcess
调用ntdll.dll
的DbgUiConnectToDbg
再调用ZwCreateDebugObject
通过调用号进入0环
进入0环创建DEBUG_OBJECT
结构体
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 调试事件的采集
调试事件的种类
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
typedef struct _DEBUG_OBJECT {
KEVENT EventsPresent; //+00 用于指示有调试事件发生
FAST_MUTEX Mutex; //+10 用于同步互斥对象
LIST_ENTRY EventList; //+30 保存调试消息的链表
ULONG Flags; //+38 标志 调试消息是否已读取
} DEBUG_OBJECT, *PDEBUG_OBJECT;
调试事件的处理
因为每种事件的调试信息不一样,所以会有很多种类(7种)的api去采集
编号的值也是对应的
// 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
可以发现这里有一个异常,这里先打印一下异常处理返回的代码
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
断点
那么为什么会有一个异常处理的事件呢?这里首先看一下进程的创建过程
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
相当于编译器为我们生成了一段伪代码
__try
{
}
__except(UnhandledExceptionFilter(GetExceptionInformation())
{
//终止线程
//终止进程
}
只有程序被调试时,才会存在未处理异常
UnhandledExceptionFilter
的执行流程:
1) 通过NtQueryInformationProcess查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH,此时会进入第二轮分发
2) 如果没有被调试:
查询是否通过SetUnhandledExceptionFilter注册处理函数 如果有就调用
如果没有通过SetUnhandledExceptionFilter注册处理函数 弹出窗口 让用户选择终止程序还是启动即时调试器
如果用户没有启用即时调试器,那么该函数返回EXCEPTION_EXECUTE_HANDLER
SetUnhandledExceptionFilter
如果没有通过SetUnhandledExceptionFilter
注册异常处理函数,则程序崩溃
测试代码如下,我自己构造一个异常处理函数callback
并用SetUnhandledExceptionFilter
注册,构造一个除0异常,当没有被调试的时候就会调用callback
处理异常,然后继续正常运行,如果被调试则不会修复异常,因为这是最后一道防线,就会直接退出,起到反调试的效果
// 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打开则直接退出
// 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;
}
正常情况下执行程序
如果是调试程序则直接退出
- 本文作者: szbuffer
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1478
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!