调试
调试对象: 用于调试器与被调试程序之间建立起联系。
调试的本质:被调试程序触发异常–调试器接管异常的过程。
调试器与被调试程序之间建立起联系
当使用调试器调试未启动的程序时,调用:
CreateProcess
当使用调试器调试已启动程序时,调用:
DebugActiveProcess
注意:这两个函数都是调试器的进程在调用。
_DEBUG_OJECT 结构体
typedef struct _DEBUG_OBJECT
{
KEVENT EventsPrasent; //用于指示有调试事件发生
FAST_MUTEX Mutex; //用于同步互斥对象
LIST_ENTRY EventList; //保存调试事件的链表
union
{
ULOG Flage; //标志调试信息是否已读
struct
{
UCHAR DebuggetInactive:1;
UCHAR KillProcessOnExit:1;
};
};
}DEBUG_OBJECT, *PDEBUG_OBJECT
_DEBUG_OJECT的本质–桥:
调试器进程 被调试进程
----- -----
----- ----- 存储位置:EPROCESS的DebugPort
、、、、、 存储位置 TEB + 0xF24 ----- 存储内容:_DEBUG_OJECT对象的地址(0环)
----- 保存一个_DEBUG_OJECT对象句柄 -----
----- -----
-----
_DEBUG_OJECT
------
------ 调试事件 -> 调试事件
所以,调试器进程通过_DEBUG_OJECT这个对象和被调试进程建立链接,被调试进程保存的是 _DEBUG_OJECT对象的地址因为是在0环。
DebugActiveProcess执行流程
DebugActiveProcess调用方式(调试器的进程调用):
//先创建对象DebugOject,然后将调试进程与DebugOject对象关联起来 (3环关联)
<1> Kernel32!DbgUiConnectToDbg() //主要是创建内核对象
ntdll!DbgUiConnectToDbg
ntdll!ZwcreateObject() -- 从这进0环
nt!NtCreateDebugObject() -- 创建一个 DebugOject对象 (在这挂钩可以反调试)
//将被调试进程与DebugOject对象关联起来 (0环关联)
<2> Kernet32!DbgUiDebugActiveProcess(被调试进程句柄)
ntdll!DbgUiDebugActiveProcess(被调试进程句柄)
ntdll!NtDebugActiveProcess(被调试进程句柄,调试器进程TEB + 0xF24) -- 进0环
nt!NtDebugActiveProcess(HANDLE ProcessHandele, HANDLE DebugObjectHandle)
DebugActiveProcess分析:
; BOOL __stdcall DebugActiveProcess(DWORD dwProcessId)
public DebugActiveProcess
DebugActiveProcess proc near ; DATA XREF: .text:off_7C802654↑o
dwProcessId = dword ptr 8
mov edi, edi
push ebp
mov ebp, esp
call 7C88089Fh //ds:__imp_DbgUiConnectToDbg 调用这个函数
test eax, eax
//上面先调用DbgUiConnectToDbg函数一直到内核后,创建一个DEBUG_OBJECT内核对象(结构体)
//后返回一个内核对象的句柄,与调试器进程建立链接(即保存这个对象的句柄)
jge short loc_7C85B043 //判断对象是否创建成功
push eax ; Status
call sub_7C8093FD //报错
xor eax, eax
jmp short loc_7C85B075
; ---------------------------------------------------------------------------
loc_7C85B043: ; CODE XREF: DebugActiveProcess+C↑j
push esi
push [ebp+dwProcessId] ; ProcessHandle
call sub_7C85AFC5 //调用 processIdtoHandle, 根据进程ID获取进程句柄(获取被调试进程的)
mov esi, eax
test esi, esi
jz short loc_7C85B074 //获取失败
push edi
push esi ; Process句柄--被调试进程句柄
call 7C880894h //ds:__imp_DbgUiDebugActiveProcess 这个地方会调用这个函数
//__imp_DbgUiDebugActiveProcess 将被调试进程与DebugOject对象关联起来 (0环关联)
push esi ; Handle
mov edi, eax
call ds:NtClose
test edi, edi
jge short loc_7C85B070
push edi ; Status
call sub_7C8093FD
xor eax, eax
jmp short loc_7C85B073
; ---------------------------------------------------------------------------
loc_7C85B070: ; CODE XREF: DebugActiveProcess+39↑j
xor eax, eax
inc eax
loc_7C85B073: ; CODE XREF: DebugActiveProcess+43↑j
pop edi
loc_7C85B074: ; CODE XREF: DebugActiveProcess+25↑j
pop esi
loc_7C85B075: ; CODE XREF: DebugActiveProcess+16↑j
pop ebp
retn 4
DebugActiveProcess endp
DbgUiConnectToDbg - ntdll.dll 分析
DbgUiConnectToDbg解析:
DbgUiConnectToDbg函数调用 ZwCreateDebugObject 进入0环创建一个 DebugObjeck内核对象,内核函数返回到3环一个句柄,当前句柄保存到 [TEB + 0F24h] 的位置即 TEB.DbgSsReserved[2]。(调试器调用)
当反调试时,可以通过判断 [TEB + 0F24h] 的值是否为空来确定当前调试器是否在调试,DbgUiConnectToDbg函数的调用者是调试器,所以我们取的TEB是调试器进程的。
DbgUiConnectToDbg分析:
; _DWORD __stdcall DbgUiConnectToDbg()
public _DbgUiConnectToDbg@0
_DbgUiConnectToDbg@0 proc near ; DATA XREF: .text:off_7C923428↑o
var_18 = dword ptr -18h
var_14 = dword ptr -14h
var_10 = dword ptr -10h
var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
mov edi, edi
push ebp
mov ebp, esp
sub esp, 18h
xor ecx, ecx
mov eax, large fs:18h //eax所指向的位置为:TEB.self,指向自己的指针,eax = TEB
cmp [eax+0F24h], ecx //判断 TEB.DbgSsReserved[2] 是否为0
jnz short loc_7C96FF3D
mov [ebp+var_18], 18h
mov [ebp+var_14], ecx
mov [ebp+var_C], ecx
mov [ebp+var_10], ecx
mov [ebp+var_8], ecx
mov [ebp+var_4], ecx
mov eax, large fs:18h //eax所指向的位置为:TEB.self,指向自己的指针 eax = TEB
push 1
lea ecx, [ebp+var_18]
push ecx
push 1F000Fh
add eax, 0F24h // DebugObject对像的句柄保存到的位置:TEB.DbgSsReserved[2]
push eax
call _ZwCreateDebugObject@16 ; ZwCreateDebugObject(x,x,x,x) //当前函数进入内核创建一个DebugObject对像
mov ecx, eax //通过eax 返回一个 DebugObject对像 的句柄
loc_7C96FF3D: ; CODE XREF: DbgUiConnectToDbg()+16↑j
mov eax, ecx //如果调用了 ZwCreateDebugObject后,
//将返回的句柄保存到
//large fs:0F24h = TEB.DbgSsReserved[2] 的位置
//否则 eax = 0
leave
retn
_DbgUiConnectToDbg@0 endp
因为DbgUiConnectToDbg函数在3环,操作系统是不可能返回一个内核对象的地址给3环的,所以 ZwCreateDebugObject 函数进入内核创建内核对象后返回一个DebugObject对像的句柄。
DbgUiDebugActiveProcess - ntdll.dll 分析
DbgUiDebugActiveProcess函数接受2个参数(被调试进程句柄 和 DebugObject对象句柄)后,通过调用 NtDebugActiveProcess函数(系统调用)进入内核。
; int __stdcall DbgUiDebugActiveProcess(HANDLE Handle)
public _DbgUiDebugActiveProcess@4
_DbgUiDebugActiveProcess@4 proc near ; DATA XREF: .text:off_7C923428↑o
Handle = dword ptr 8
mov edi, edi
push ebp
mov ebp, esp
push esi
mov eax, large fs:18h //获取TEB
push dword ptr [eax+0F24h] //TEB + 0xF24h 保存DebugObject对象句柄
push [ebp+Handle] //被调试进程句柄
call _NtDebugActiveProcess@8 ; NtDebugActiveProcess(x,x)
//NtDebugActiveProcess 函数接受2个参数: 被调试进程句柄 和 DebugObject对象句柄
//通过 NtDebugActiveProcess 进入内核
mov esi, eax
test esi, esi
jl short loc_7C9700B8
push [ebp+Handle] ; Handle
call _DbgUiIssueRemoteBreakin@4 ; DbgUiIssueRemoteBreakin(x)
mov esi, eax
test esi, esi
jge short loc_7C9700B8
push [ebp+Handle]
call _DbgUiStopDebugging@4 ; DbgUiStopDebugging(x)
loc_7C9700B8: ; CODE XREF: DbgUiDebugActiveProcess(x)+1E↑j
; DbgUiDebugActiveProcess(x)+2C↑j
mov eax, esi
pop esi
pop ebp
retn 4
_DbgUiDebugActiveProcess@4 endp
NtDebugActiveProcess - Ntoskrnl.dll 分析
NtDebugActiveProcess流程:
1)根据被调试进程句柄获取被调试进程地址_EPROCESS
2)根据DebugObject对象句柄获取DebugObject对象的地址
3)调用函数ExAcquireRundownProtection发送一个假的进程创建消息
4)调用函数DbgkpSetProcessDebugObject将调试对象与被调试进程关联起来,即将DebugObject对象地址保存到被调试进程的_EPROCESS.DebugPort
NtDebugActiveProcess分析:
; NTSTATUS __stdcall NtDebugActiveProcess(HANDLE Process, HANDLE DebugObject) //被调试进程句柄 和 DebugObject对象句柄
_NtDebugActiveProcess@8 proc near ; DATA XREF: .text:0040B78C↑o
AccessMode = byte ptr -4
Process = dword ptr 8
DebugObject = dword ptr 0Ch
mov edi, edi
push ebp
mov ebp, esp
push ecx
mov eax, large fs:124h //KPRCB.CurrentThread获取当前线程的_EThread,即调试器线程_EThread
mov al, [eax+140h] //_EThread.PreviousMode 获取先前模式
push 0 ; HandleInformation
mov [ebp+AccessMode], al //保存到历史变量
lea eax, [ebp+Process] //参数一:被调试进程句柄
push eax ; Object // 用于保存返回的内核对象
push dword ptr [ebp+AccessMode] ; AccessMode //先前模式
push _PsProcessType ; ObjectType //转换成内核对象类型(EPROCESS)
push 800h ; DesiredAccess
push [ebp+Process] ; Handle //被调试进程句柄
call _ObReferenceObjectByHandle@24 ; ObReferenceObjectByHandle(x,x,x,x,x,x) //跟据句柄获得内核对象
test eax, eax
jl locret_584294 //转化失败
//以上函数将被调试进程的句柄转化成内核对象即 _EPROCESS = eax, 保存到 ebp+Process的位置
push ebx
push esi
mov eax, large fs:124h //KPRCB.CurrentThread获取当前线程的_EThread,即调试器线程_EThread
mov esi, [ebp+Process] //被调试进程对象
cmp esi, [eax+44h] //eax + 44 = _EThread._KAPC_STATE.Process(提供CR3的进程) 和 被调试进程对象比较
jz short loc_584284 //相同跳转结束(即调试的是当前进程)
cmp esi, _PsInitialSystemProcess //判断是否是系统初始化进程
jz short loc_584284 //相同跳转结束
push 0 ; HandleInformation
lea eax, [ebp+Process] //被占用 ebp+Process = _DEBUG_OBJECT(DebugObject对象)
push eax ; Object // 用于保存返回的内核对象, ebp+Process = _EPROCESS
push dword ptr [ebp+AccessMode] ; AccessMode //先前模式
push _DbgkDebugObjectType ; ObjectType //转换成内核对象类型(_DEBUG_OBJECT)
push 2 ; DesiredAccess
push [ebp+DebugObject] ; Handle //DebugObject对象句柄
call _ObReferenceObjectByHandle@24 ; ObReferenceObjectByHandle(x,x,x,x,x,x) //跟据句柄获得内核对象
mov ebx, eax
test ebx, ebx //转化失败
jl short loc_584289
//以上函数将被调试进程的句柄转化成内核对象即 _DEBUG_OBJECT = eax
push edi
lea edi, [esi+80h] //_EThread
mov ecx, edi ; RunRef
call @ExAcquireRundownProtection@4 ; ExAcquireRundownProtection(x) //请求停运保护,锁住共享对象避免在访问时被删除
test al, al
jz short loc_584274
//ExAcquireRundownProtection 例程试图获取共享对象上的运行停止保护,以便调用者可以安全地访问该对象。
lea eax, [ebp+DebugObject]
push eax ; int //DebugObject对象句柄
push [ebp+Process] ; FastMutex //_DEBUG_OBJECT对象
push esi ; PROCESS //被调试进程对象 - _EPROCESS
call _DbgkpPostFakeProcessCreateMessages@12 ; DbgkpPostFakeProcessCreateMessages(x,x,x)
//发送一个假的进程创建消息
push [ebp+DebugObject] ; Object //DebugObject对象句柄
push eax ; int //DebugObject对象
push [ebp+Process] ; PVOID //_DEBUG_OBJECT对象
push esi ; PVOID //被调试进程对象 - _EPROCESS
call _DbgkpSetProcessDebugObject@16 ; DbgkpSetProcessDebugObject(x,x,x,x)
//将调试对象与被调试进程关联起来
mov ecx, edi ; RunRef
mov ebx, eax
call @ExReleaseRundownProtection@4 ; ExReleaseRundownProtection(x) //取消停运保护
jmp short loc_584279
; ---------------------------------------------------------------------------
loc_584274: ; CODE XREF: NtDebugActiveProcess(x,x)+80↑j
mov ebx, 0C000010Ah
loc_584279: ; CODE XREF: NtDebugActiveProcess(x,x)+A5↑j
mov ecx, [ebp+Process] ; Object
call @ObfDereferenceObject@4 ; ObfDereferenceObject(x)
pop edi
jmp short loc_584289
; ---------------------------------------------------------------------------
loc_584284: ; CODE XREF: NtDebugActiveProcess(x,x)+47↑j
; NtDebugActiveProcess(x,x)+4F↑j
mov ebx, 0C0000022h
loc_584289: ; CODE XREF: NtDebugActiveProcess(x,x)+6E↑j
; NtDebugActiveProcess(x,x)+B5↑j
mov ecx, esi ; Object
call @ObfDereferenceObject@4 ; ObfDereferenceObject(x)
pop esi
mov eax, ebx
pop ebx
locret_584294: ; CODE XREF: NtDebugActiveProcess(x,x)+33↑j
leave
retn 8
_NtDebugActiveProcess@8 endp
DbgkpSetProcessDebugObject函数部分分析:
//edi保存被调试进程的_EPROCESS, ebx = 0
cmp [edi+0BCh], ebx //判断 _EPROCESS.DebugPort 是否为空
jnz short loc_584068 //不为空则结束
loc_583FF5: ; CODE XREF: DbgkpSetProcessDebugObject(x,x,x,x)+CF↓j
mov eax, [ebp+arg_4] //eax = _DEBUG_OBJECT对象地址
mov ecx, [ebp+Object] ; Object
mov [edi+0BCh], eax //_EPROCESS.DebugPort = _DEBUG_OBJECT对象地址, 关联起来
其中,反调试 DebugPort 清零就是用一个线程无限制清零自身的_EPROCESS.DebugPort时,调试进程与被调试进程无法建立链接是现实的。
采集与发送调试消息
当调试进程与被调试进程关联后,每当被调试进程产生一个调试事件时会被附加到调试对象(_DEBUG_OBJECT)中发送到调试事件的链表(_DEBUG_OBJECT.EventList)中,然后调试器通过读取调试对象中调试链表的值处理调试事件。
采集调试消息
调试事件会根据种类进行记录:
trpedef enum _DBGKM_APINUMBER
{
DbgKmExceptionApi = 0; //异常 -- 断点也是
DbgKmCreateThreadApi = 1; //创建线程
DbgKmCreateProcessApi = 2; //创建进程
DbgKmExitThreadApi = 3; //线程退出
DbgKmExitProcessApi = 4; //进程退出
DbgKmLoadDllApi = 5; //加载DLL
DbgKmUnloadDllApi = 6; //卸载DLL
DbgKmErrorReportApi = 7; //已废弃
DbgKmMaxApiNumber = 8; //最大值
} DEGKM_APINUMBER;
调试事件采集函数:
用 DbgK 开头的大部分是调试事件采集函数,采集函数在每个调用的必经之路上加一个调用,用来产生调试事件。这些采集函数调用后,都会先判断 DebugPort 是否为空,只有不为空才会调用 DbgKpSendApiMessage 函数发送消息。
<1> 创建进程、线程必经之路:
PsUserThreadStartup
DbgKCreateThread (采集函数) -> 判断当前线程是进程的第一个线程时,为创建进程,否则为创建线程
DbgKpSendApiMessage(x, x)
<2> 退出线程、进程必经之路:
PspExitThread
DbgKExitThread/DbgKExitProcess (采集函数)
DbgKpSendApiMessage(x, x)
<3> 加载模块的必经之路:
NtMapViewOfSection: (LoadLib -> CraeteMapping -> NtMapViewOfSection)
DbgkMapViewOfSection (采集函数)
DbgKpSendApiMessage(x, x)
<4> 卸载模块的必经之路:
NtUnMapViewOfSection
DbgKUnMapViewOfSection (采集函数)
DbgKpSendApiMessage(x, x)
<5> 异常的必经之路:
KiDispatchException
DbgKForwardException(x, x, x) (采集函数)
DbgKpSendApiMessage(x, x)
DbgKForwardException函数:
DbgKForwardException 函数既可以向进程的异常端口发送消息,也可以向调试端口发送消息,KiDispatchException 函数在调用它是会通过一个布尔类型的参数来指定。如果要向调试端口发送消息,那么 DbgKForwardException 函数会判断进程的 DebugPort 字段是否为空,如果不为空,便通过 DbgKpSendApiMessage 函数发送 DbgKmExceptionApi消息。
发送调试消息
调试子系统的内核函数使用一个结构体俩描述和传递调试消息(主要是向DbgKpSendApiMessage函数传递数据):
typedef struct _DEGKM_APIMSG
{
PORT_MESSAGE h; //LPC 端口消息结构,Windows Xp 之前使用
DBGKM_APINUMBER ApiNumber; //消息类型
NTSTATUS ReturnedStatus; //调试器恢复状态
union
{
DBGKM_EXCEPTION Exception; //异常
DBGKM_CREATE_THREAD CreateThread; //创建线程
DBGKM_CREATE_PROCESS CreateProcess; //创建进程
DBGKM_EXIT_THREAD ExitThread; //线程退出
DBGKM_EXIT_PROCESS ExitProcess; //进程退出
DBGKM_LOAD_DLL LoadDll; //映射DLL
DBGKM_UNLOAD_DLL UnloadDll; //反映射DLL -- 卸载
} u;
} DBGKM_APIMSG, *PDBGKM_APIMSG;
DbgKpSendApiMessage(x, x)说明:
1)第一个参数:消息结构,每种消息都有自己的消息结构(7种类型) – _DEGKM_APIMSG
2)第二个参数:要不要把本进程内除了自己之外的其他线程挂起。有些消息需要把其他线程挂起,比如CC,有些消息不需要把其他线程挂起,比如模块加载。
3)在后续版本中还会有个参数为 端口。 即 DebugProt 或者 ExecpptionProt
DbgKpSendApiMessage是调试事件收集的总入口,如果在这里挂钩子,调试器将无法调试。
; __stdcall DbgkpSendApiMessage(x, x)
_DbgkpSendApiMessage@8 proc near ; CODE XREF: DbgkCreateThread(x)+461E8↑p
; DbgkCreateThread(x)+46302↑p ...
arg_0 = dword ptr 8
arg_4 = byte ptr 0Ch
mov edi, edi
push ebp
mov ebp, esp
push ebx
xor ebx, ebx
cmp [ebp+arg_4], bl
push esi
jz short loc_584496
call _DbgkpSuspendProcess@0 ; DbgkpSuspendProcess()
mov [ebp+arg_4], al
loc_584496: ; CODE XREF: DbgkpSendApiMessage(x,x)+C↑j
mov edx, [ebp+arg_0]
mov dword ptr [edx+1Ch], 103h
mov eax, large fs:124h
mov ecx, [eax+44h]
xor eax, eax
inc eax
lea esi, [ecx+248h]
lock or [esi], eax
mov eax, large fs:124h
push ebx ; FastMutex
push ebx ; Event
push edx ; int
push eax ; PVOID
push ecx ; Object
call _DbgkpQueueMessage@20 ; DbgkpQueueMessage(x,x,x,x,x) //当前函数发送消息
push ebx ; NumberOfBytesToFlush
push ebx ; BaseAddress
push 0FFFFFFFFh ; ProcessHandle
mov esi, eax
call _ZwFlushInstructionCache@12 ; ZwFlushInstructionCache(x,x,x)
cmp [ebp+arg_4], bl
jz short loc_5844DA
call _KeThawAllThreads@0 ; KeThawAllThreads()
loc_5844DA: ; CODE XREF: DbgkpSendApiMessage(x,x)+53↑j
mov eax, esi
pop esi
pop ebx
pop ebp
retn 8
_DbgkpSendApiMessage@8 endp
DbgkpSuspendProcess 与 DbgkpResumeProcess(控制被调试进程):
调试子系统设计了两个内核函数来控制被调试进程。
调试事件处理
当被调试进程采集到对应的调试信息后,会发送一个消息到 _DEBUG_OBJECT结构体的EventList中,发送的消息是一个结构体:
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; //加载DLL
UNLOAD_DLL_DEBUG_INFO UnloadDll; //卸载DLL
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT;
即 DEBUG_EVENT。
调试器的处理代码:
1)关联被调试进程
2)调试进程循环调试链表处理调试事件
#include <iostream>
#include <windows.h>
#include <winbase.h.>
#include <string>
using namespace std;
int main()
{
BOOL bRet = FALSE;
BOOL bIsContinue = TRUE;
wstring wstrFilePath = L"C:\\Users\\WGH\\Desktop\\Dbgview.exe";
DEBUG_EVENT debugEvent;
memset(&debugEvent, 0, sizeof(debugEvent));
//创建调试进程
STARTUPINFO StartUpInfo;
memset(&StartUpInfo, 0, sizeof(StartUpInfo));
PROCESS_INFORMATION pInfo;
memset(&pInfo, 0, sizeof(PROCESS_INFORMATION));
GetStartupInfo(&StartUpInfo); //取得进程在启动时被指定的 STARTUPINFO 结构。
bRet = CreateProcessW(wstrFilePath.c_str(), NULL, NULL, NULL, TRUE, DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &StartUpInfo, &pInfo);
if (bRet == FALSE)
{
printf("CreateProcess Error: %d \n", GetLastError());
}
//附加形式
/*
if (!DebugActiveProcess(21872)) //接受一个进程ID
{
return 0;
}
*/
//调试循环
while (bIsContinue)
{
bRet = WaitForDebugEvent(&debugEvent, INFINITE); //等待异常消息
if (bRet == FALSE)
{
printf("WaitForDebugEvent Error: %d \n", GetLastError());
return 0;
}
// 可以在下面的代码处理调试信息
switch (debugEvent.dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT : //异常
printf("EXCEPTION_DEBUG_EVENT, %x, %x, %x \n", debugEvent.u.Exception.ExceptionRecord.ExceptionAddress, debugEvent.u.Exception.ExceptionRecord.ExceptionRecord, debugEvent.u.Exception.ExceptionRecord.ExceptionFlags);
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: //加载DLL
printf("LOAD_DLL_DEBUG_EVENT \n");
break;
case UNLOAD_DLL_DEBUG_EVENT: //卸载DLL
printf("UNLOAD_DLL_DEBUG_EVENT \n");
break;
}
//DBG_CONTINUE, 表示编译器已经处理了改异常
//DBG_EXCPTION_NOT_HANDLED, 即表示调试器没有处理该异常,转回到用户态中执行,寻找可以处理该异常的异常处理器
bRet = ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); //调试事件处理完毕
if (bRet == FALSE)
{
printf("ContinueDebugEvent Error: %d \n", GetLastError());
}
}
return 0;
}
通过CreateProcess方式运行输出:
CREATE_PROCESS_DEBUG_EVENT //开始调试时,先创建一个进程
LOAD_DLL_DEBUG_EVENT //下面加载一堆DLL
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
EXCEPTION_DEBUG_EVENT, 774e1ba2, 0, 0 //这里出现了一个异常,在 774e1ba2 的位置
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
CREATE_THREAD_DEBUG_EVENT //创建一个线程
上面代码出现的异常:
当通过 CreateProcess() 打开一个进程进行调试的时候,CreateProcess会调用 LdrInitializeThunk(ntdll.dll) 初始化进程函数,最后会调用LdrpInitializeProcess(ntdll.dll)函数(如果创建的线程是这个进程的第一个线程,就说明这是个创建进程的函数):
loc_7C9410B2: ; CODE XREF: LdrpInitializeProcess(x,x,x,x,x)+BCC↓j
//ebx = PEB
cmp byte ptr [ebx+2], 0 //判断 PEB.BeingDebugged 是否为0,不为零则开了调试模式
jnz loc_7C95E60D
loc_7C95E60D: ; CODE XREF: LdrpInitializeProcess(x,x,x,x,x)-4BB↑j
call _DbgBreakPoint@0 ; DbgBreakPoint() //调用 int 3 中断
mov eax, [ebx+68h]
shr eax, 1
and al, 1
mov _ShowSnaps, al
jmp loc_7C9410BC
在 LdrpInitializeProcess 函数中调用了一个 _DbgBreakPoint函数,这个函数:
; _DWORD __stdcall DbgBreakPoint()
public _DbgBreakPoint@0
_DbgBreakPoint@0 proc near ; CODE XREF: LdrpRunInitializeRoutines(x):loc_7C95A534↓p
; LdrpInitializeProcess(x,x,x,x,x):loc_7C95E60D↓p ...
int 3 ; Trap to Debugger
retn
_DbgBreakPoint@0 endp
可以看出这个函数就是一个int 3 中断。
当在调用CreateProcess函数的时候,会进行判断,如果当前进程以调试模式打开时函数产生一个 int 3 中断。当在OllyDbg中加载一个进程并设置第一次暂停于系统断点而不是断在WinMain函数的时候,他触发的中断就是这个中断。
774E1BA2 CC int3
774E1BA3 EB 07 jmp short 774E1BAC
774E1BA5 33C0 xor eax, eax
774E1BA7 40 inc eax
774E1BA8 C3 retn
由此可见OllyDbg会中断到 774E1BA2 的位置,和调试器代码收到的一样。
通过DebugActiveProcess方式运行输出:
CREATE_PROCESS_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
CREATE_THREAD_DEBUG_EVENT
CREATE_THREAD_DEBUG_EVENT
CREATE_THREAD_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
CREATE_THREAD_DEBUG_EVENT
EXCEPTION_DEBUG_EVENT, 774a4d30, 0, 0 //通过这个可以看出假消息依旧走了和上面打开进程一样的调试路线
EXIT_THREAD_DEBUG_EVENT
EXIT_THREAD_DEBUG_EVENT
EXIT_THREAD_DEBUG_EVENT
附加情况下应该不会出现 CREATE_PROCESS_DEBUG_EVENT 等消息,通过 NtDebugActiveProcess分析 可以看到:
call _DbgkpPostFakeThreadCreateMessages@12 ; DbgkpPostFakeThreadCreateMessages(x,x,x)
call _DbgkpPostFakeProcessCreateMessages@12 ; DbgkpPostFakeProcessCreateMessages(x,x,x) (在DbgkpPostFakeThreadCreateMessages函数中)
call _DbgkpPostFakeModuleMessages@12 ; DbgkpPostFakeModuleMessages(x,x,x) (在DbgkpPostFakeThreadCreateMessages函数中)
在NtDebugActiveProcess里,通过这两个函数发送假的线程、进程创建信息等。之所以要发送假的消息,是因为如果附加的时候进程已经执行完加载DLL、启动进程、启动线程等操作了,但为了保证调试仍然有这些信息,所以通过发送假消息的形式将这些信息发送过去。但是这个模拟的流程并不可靠,如加载DLL的消息实际上是查找了PEB.Ldr(_PEB_LDR_DATA) 这个字段:
kd> dt _PEB_LDR_DATA
ntdll!_PEB_LDR_DATA
+0x000 Length : Uint4B //长度
+0x004 Initialized : UChar //初始化
+0x008 SsHandle : Ptr32 Void //句柄
+0x00c InLoadOrderModuleList : _LIST_ENTRY (_LDR_DATA_TABLE_ENTRY) //模块加载顺序
+0x014 InMemoryOrderModuleList : _LIST_ENTRY (_LDR_DATA_TABLE_ENTRY) //模块在内存中的顺序
+0x01c InInitializationOrderModuleList : _LIST_ENTRY (_LDR_DATA_TABLE_ENTRY) //模块初始化装载顺序
+0x024 EntryInProgress : Ptr32 Void
即使函数以附加形式调用即用函数DebugActiveProcess,也会去修改 PEB.BeingDebugged 的这个值。
软件断点
当我们给一个指令添加一个软件点时,实际上就是在被调试进程加断点的位置将其修改为 int 3 中断(0xcc)。
软件断点本质是改变了被调试程序数据,CRC可以检测到软件断点。
int 3 指令流程:
int 3是一个异常指令所以他的流程前半段与异常相同,在之前异常流程中异常处理都会经过内核函数 KiDispatchException 进行分发,不论是内核异常还是用户异常在 KiDispatchException 中都会判断是否存在调试器,如果存在调试器则交给调试器处理,通过 DbgKForwardException 函数收集并发送调试信息.
被调试进程:
1.CPU检测到INT 3指令
2.查IDT表找到对应的中断处理函数
3.CommonDispatchException
4.KiDispatchException
5.DbgKForwardException 收集并发送调试事件(只有在处理用户异常时才会调用),会阻塞在这等待结果
DbgkpSengApiMessage(x, x) 发送消息
调试进程:
1.循环判断
2.取出调试事件
3.列出信息
寄存器
内存
4.用户处理
调试处理的流程:
1)判断是否为我们需要的 INT 3指令:前面说过当加载了调试器后,在线程启动的时候会有一个断点,可以通过判断是否需要启用系统断点来判断是否开启。
2)将INT 3修复为原来的数据:因为之前加断点要将加断点位置的代码修改为 0Xcc,当中断后要将代码修复为原来的数据进行显示和后续的执行。
3)显示断点的位置
4)获取线程上线文:显示寄存器的信息
5)修复EIP:当线程中断在我们断点的位置时,此时的EIP实际上已经指向下一条指令了。如果我们没有修复EIP的话,那我们上一条被我们修改为0xcc的指令就没有正常执行,即使我们前面将其修复了,CPU也会从中断的下条指令继续执行。所以要将EIP修改为断点指令的位置让其重新执行一下我们将0Xcc修复后的指令。(不等的断点修正的EIP方法不同)
6)将我们的指令以反汇编形式显示出来:可以拿到这条指令的地址(EIP),通过其硬编码转换为汇编代码
7)等待用户指令:下一步,继续执行等
注意:软件断点可以多个,所以真正写调试器的时候要用链表来保存多个软件断点的地址。
内存断点
内存断点的本质是修改内存的属性,PTE。所以只要保证所有用到的物理页都是可访问的,断点就不会被触发。
内存断点调用的函数:(VirtualProtectEx函数可以改变在特定进程中内存区域的保护属性。)
BOOL VirtualProtectEx(
HANDLE hPeocess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);
当给某个内存(被调试进程)添加内存断点时(访问断点或写断点)实际上是修改内存的页属性:
PAGE_NOACCESS //访问断点:将页属性设置为不可访问 (PTE P位 = 0,即物理页无效会引发异常)
PAGE_EXECUTE_READ //读断点:将属性页设置为可读、可执行但不能写 (PTE R/W位 = 0 表示只读, p = 1)
即修改要加断点位置内存的物理页的属性PTE,修改PTE修改的是一个页的属性,也就是说当你给一个内存地址加上断点后,当前内存地址所在的那一个页都会产生异常并触发这个断点,不仅限于你要加断点的内存,所以在我们处理异常的函数中要判断是否是我们加断点的内存。
被调试进程:
1.CPU访问错误内存地址,如地址不能访问、不能读等,触发页异常
2.查IDT表找到对应的中断处理函数 (nt!_KiTrapOE)
3.CommonDispatchException
4.KiDispatchException
5.DbgKForwardException 收集并发送调试事件(只有在处理用户异常时才会调用),会阻塞在这等待结果
DbgkpSengApiMessage(x, x) 发送消息
调试进程:
1.循环判断
2.取出调试事件
3.列出信息
寄存器
内存
4.用户处理
调试处理的流程:
1)获取异常信息,异常原因,异常地址
<1> _EXCEPTION_RECORD.ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]数组 描述异常的附加参数数组。 RaiseException函数可以指定这个参数数组。 对于大多数异常代码,数组元素是未定义的。 下表描述了定义数组元素的异常代码。
**\_EXCEPTION\_RECORD.ExceptionCode = EXCEPTION\_ACCESS\_VIOLATION:**
数组的第一个元素包含一个读写标志,指示导致访问冲突的操作类型。 如果该值为0,则线程试图读取不可访问的数据。 如果该值为1,则表示线程试图写入不可访问的地址。
如果该值为8,则线程将导致用户模式数据执行预防(DEP)违规。
第二个数组元素指定不可访问数据的虚拟地址。
_EXCEPTION_RECORD.ExceptionCode = EXCEPTION_IN_PAGE_ERROR:
数组的第一个元素包含一个读写标志,指示导致访问冲突的操作类型。 如果该值为0,则线程试图读取不可访问的数据。 如果该值为1,则表示线程试图写入不可访问的地址。
如果该值为8,则线程将导致用户模式数据执行预防(DEP)违规。
第二个数组元素指定不可访问数据的虚拟地址。
第三个数组元素指定导致异常的底层NTSTATUS代码。
2)判断断下的是否是我们下断点的地址,不是则放过,是则处理
3)修改内存页的属性为原来的属性
4)获取线程上线文:显示寄存器的信息
5)修复EIP:在内存异常中EIP不需要修复,因为在CPU读取指令的时候就已经发生异常了,所以不需要修复
6)将我们的指令以反汇编形式显示出来:可以拿到这条指令的地址(EIP),通过其硬编码转换为汇编代码
7)等待用户指令:下一步,继续执行等
注意:
1)内存断点和软件断点一样都可以添加多个,所以真正写调试器的时候要用链表来保存多个内断点的地址。
2)内存断点设置时可以设置内存大小各不相同,比如设置1个字节或者4个字节,当设置多个字节时,有可能会出现这多个字节不在同一个物理页中,所以可能修改了多个物理页PTE的标志位。
硬件断点
硬件断点不依赖被调试程序,即硬件断点不修改被调试进程的数据,但修改寄存器。
硬件断点产生的异常为单步异常。
设置硬件断点:
1)Dr0 ~ Dr3用于设置硬件断点,由于只有4个断点寄存器,所以最多只能设置4个硬件调试断点。
2)Dr7最重要的寄存器(32位):
<1> 0位 L0 和 1位 G0 用于Dr0是全局断点还是局部断点,G0位为1则是全局断点,L0位为1则是局部断点。从第2位到第8位为G1L1 ~ G3L3 用于控制 CR1 ~ CR0功能同上。每次异常后,Lx都会被清零,Gx不清零。
注: G3 L3 G2 L2 G1 L1 G0 L0(0位), 占0到8位
<2> LEN0 ~ LEN3从第16位开始,每个LENX占2位,用于控制 Dr0 ~ Dr3的断点长度:
00 1字节
01 2字节
10 保留
11 4字节
<3> RWE0 ~ RWE3从第18位开始,每个RWEX占2位,控制 Dr0 ~ Dr3 的断点是读、写、执行还是I/O 端口断点:
00 执行断点
01 写入数据断点
10 I/O端口断点
11 访问断点
注: LEN3 RWE3 LEN2 RWE2 LEN1 RWE1 LEN0(18位) RWE0 (16位) , 占16到31位
<4> GD位:用于保护DRx,如果GD位位1,则对DRx的任何访问都会导致进入1号调试陷阱。即IDT的对应入口,这样可以保证调试器在必要的时候完全控制DRx。
处理硬件断点:
1)硬件调试断点产生的异常是 STATUS_SINGLE_STEP(单步异常),其中 EFlage 的 TF位为1也会引发单步异常。
2)DR6 中 B0 ~ B3(0位 ~ 3位):哪个调试器触发的异常。
3)当 B0 ~ B3 中都为空时,触发的就是 EFlage 引起的单步异常。
被调试进程:
1.CPU执行时检测到调试寄存器(DR0 ~ DR3)
2.查IDT表找到对应的中断处理函数 (nt!_KiTrap01)
3.CommonDispatchException
4.KiDispatchException
5.DbgKForwardException 收集并发送调试事件(只有在处理用户异常时才会调用),会阻塞在这等待结果
DbgkpSengApiMessage(x, x) 发送消息
调试进程:
1.循环判断
2.取出调试事件
3.列出信息
寄存器
内存
4.用户处理
调试处理的流程:
不能在进程的入口设置断点,因为此时线程还没被创建,我们的硬件断点是基于线程的(context)。
所以测试时可以通过在软件断点的处理中设置硬件断点,确保硬件断点设在在当前线程。
1)获取线程上下文
2)通过DR6判断是否是硬件断点导致的异常
3)通过DR6判断哪个寄存器触发的异常
4)显示寄存器信息
5)显示反汇编
6)去掉断点
7)等待用户指令
注意:寄存器是一个CPU一份的当我们给线程寄存器如DR1中设置中断地址时,并不会影响其他线程的DR1,因为我们修改的DR1的值并不是直接修改DR1寄存器的值,修改的是线程CONTEXT中的值,再线程执行时才会将值写入到DR1中,其他寄存器同理。
单步异常(单步步入)
在EFLAGE中第8个字节的位置为单步异常标识位:
TF = 1 //设置单步运行
处理异常:
STATUS\_SINGLE\_STEP(单步异常)
单步异常的处理流程与硬件中断的流程相同没有区别,只有在我们写的调试器中通过判断DR6来确定是硬件中断还是单步异常:
context.Dr6 & 0xF = TRUE 说明为硬件中断,否则单步异常
主要是判断Dr6中,B0 ~ B3 是否为空,不为空则是硬件中断,否则单步异常。
在用户操作事件设置TF = 1:
CONTEXT context;
memset(&context, 0, sizeof(CONTEXT));
//获取线程上下文,显示寄存器信息
context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &context);
context.EFlags |= 0x100; //设置 TF = 1
//设置线程上下文
SetThreadContext(hThread, &context);
在单步中断后将TF位还原:
//单步断点
printf("SingleStep: Eip = 0x%p", context.Eip);
//还原
context.EFlags |= 0xfffffeff; //还原TF位
之后再次等待用户操作。
当我们执行单步异常即单步不入时,碰到CALL指令他是会跟随CALL指令等跳转指令一起跳转的。但是单步步过不会。
单步步过
单步步过与单步步入不同,当碰到CALL等跳转指令时,单步步过会跳过。
实现方式(只有碰到跳转指令时单步步过和单步步入才会不同):
1)硬件断点
当我们判断当前指令为CALL时,在通过当前指令的长度计算出下一条指令的地址,然后在上面下一个硬件断点。若硬件断点的4个寄存器都被占用,则使用软件断点。
2)软件断点
当我们判断当前指令为CALL时,在通过当前指令的长度计算出下一条指令的地址,然后在上面下一个软件断点。
因为单步步过是通过当前指令的长度计算出下一条指令的地址后,在下个指令上加一个断点,当我们在一个CALL里面将返回地址修改掉,那通过单步步过的方式可能会找不到,无法继续即跟丢了无法继续往下跑。
调试器框架
硬件HOOK过检测
一些软件会通过一个线程来循环检测代码的正确性,当你HOOK去修改dll或exe中的代码时会触发这个检测导致被HOOK软件失败。
通过硬件HOOK过检测:
1)不修改检测线程
2)不修改挂钩子函数的代码
在要HOOK的函数地址(函数地址可以用LoadLiberal系列函数获取)处下一个硬件断点,通过我们上面的异常可以获取到这个异常和CONTEXT,可以修改Esp、Eip等寄存器,获取了Esp就是拿到了堆栈,即参数、返回地址等。Eip可以跳到我们想要执行的位置。
可以通过设置顶层异常处理过滤函数来捕获这个异常,最好也在dll注入后设置。至于硬件断点我们只需要在DLL注入时在我们想要HOOK的函数地址通过DR7与空闲的DR0~3中下上断点就好。