Win32调试API
Win32自带了一些API函数。提供了相当于一般调试器的大部分功能,这些函数统称为Win32调试API(Win32 Debug API)。利用这些API,可以加载一个程序或捆绑到一个正在运行的程序上以供调试,可以获取被调试程序的底层信息(例如进程ID,进入地址,映像基址等),甚至可以对被调试程序进行任意的修改。
0x01 调试相关函数的简要说明
记录了一些Win32调试API函数的用法,参数,返回值。
0x02 调试事件
作为调试器,监视目标进程的执行,对目标进程发生的每一个调试事件作出应有的反应是它的主要工作。当目标进程中发生一个调试事件后,系统将通知调试器来处理这个事件,调试器将利用WaitForDebugEvent函数来获取进程的相关环境信息。调试事件如下:
WaitForDebugEvent收到一个调试事件的时候,会将调试事件的信息填写入DEBUG_EVENT结构中并返回。
dwDebugEventCode标记了发生的调试事件的类型,dwProcessId的值是调试事件所发生的进程的标识符。dwThreadId的值是调试事件所发生的线程的标识符。u结构包含了关于调试事件的更多信息,根据dwDebugEventCode值的不同,它可以是如下结构:
假设程序调用了WaitForDebugEvent函数并返回,首先检查dwDebugEventCode字段中的值,根据它来判断debugger进程发生了哪种类型的调试事件。
0x03 创建并跟踪进程
3.1 如何创建一个新进程以供调试
在通过CreateProcess创建进程时,如果在dwCreationFlags标志字段中设置了DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS标志,将创建一个用于调试的新进程。如果以DEBUG_PROCESS标志创建新进程,调试器会收到目标进程及由目标进程创建的所有子进程中发生的所有调试事件的信息。
如果设置了DEBUG_ONLY_THIS_PROCESS标志,调试器将只会收到目标进程的调试事件,而对其子进程的调试事件不予理睬。进程被创建后,可以查看PROCESS_INFORMATION结构获取被创建进程及其主线程的进程标识符和线程标识符。
3.2 如何将调试器捆绑到一个正在运行的进程上
利用DebugActiveProcess函数可以将调试器捆绑到一个正在运行的进程上,如果执行成功,则类似于利用DEBUG_ONLY_THIS_PROCESS标志创建的新进程。
在NT内核中,当试图通过DebugActiveProcess函数将调试器捆绑到一个创建时带有安全描述符的进程上时,将被拒绝。
0x04 调试循环体
利用调试API建立一个简单的调试程序
while(TRUE):
if WaitForDebugEvent(&devent, xxxsencond):
swtich devent.dwDebugEventCode:
case xxx:
do something
break
case xxx:
do something
break
continueDebugEvent()
伪代码如上
0x05 处理调试事件
EXCEPTION_DEBUG_EVENT结构如下:
EXCEPTION_RECORD可以看异常处理那篇文章。
最常见的异常情况是EXCEPTION_BREAKPOINT和EXCEPTION_SINGLESTEP。当遇到一个INT3断点时产生前者,如果设置了单步执行标志,执行一条指令产生后者异常。
当以调试的方式创建一个进程时,在进入进程之前,系统会执行依次DebugBreak函数,这样会产生一个EXCEPTION_BREAKPOINT异常;如果一切正常,这是第一个遇到的也是必定遇到的异常。
调用ContinueDebugEvent函数可以让线程继续运行。ContinueDebugEvent函数的dwContinueStatus参数由两个取值,分别是DBG_EXCEPTION_NOT_HANDLED和DBG_CONTINUE。对大多数调试事件来说,这两个值没有区别都是恢复线程。唯一的例外是EXCEPTION_DEBUG_EVENT,如果线程报告发生了一个异常事件而指定了DBG_CONTINUE,线程将忽略它自己的异常处理部分并继续执行。在这种情况下,程序必须在DBG_CONTINUE恢复线程之前检查并处理异常,否则异常将不断发生,直到程序被系统终止为止(按照之前的异常处理机制应该时就分发两次)。如果指定了DBG_EXCEPTION_NOT_HANDLED,就是告知Windows,程序不处理异常。Windows将使用被调试线程的默认异常处理函数来处理异常。对进程被载入后发生的第一个EXCEPTION_DEBUG_EVENT,必须以DBG_CONTINUE为标志继续,如果程序调用了DebugBreak函数,或者是插入了INT3断点并将内存回复,都应该以DBG_CONTINUE继续。如果程序中发生了不确定的异常,就以DBG_EXCEPTION_NOT_HANDLED继续,以便让被调试程序线程调用自己的异常处理机制。
0x06 线程环境
在Win32系统中,进程的概念包含了它的私有地址空间,代码,数据和一个主线程。每个进程都有一个最初的主线程,通过这个主线程可以创建在同一地址空间中运行其他的其他线程。所有线程分享相同的地址空间和相同的系统资源,但是它们各自由不同的执行环境
Windows是一个多任务,多线程的操作系统。Windows是分时操作系统,给每一个线程分配一小段时间片,这段时间后,冻结当前线程并将线程的执行状态保存在一个名叫CONTEXT的结构中。线程环境包含线程执行所使用的寄存器,系统栈和用户栈,以及线程所用的描述符表等其他信息。当线程再次运行时,Windows恢复最近一次线程运行的环境。
CONTEXT结构可看第八章异常处理。
其中的FloatSave是指向FLOATING_SAVE_AREA结构的指针,其结构定义如下:
typedef struct _FLOATING_SAVE_AREA {
DWORD ControlWord;
DWORD StatusWord;
DWORD TagWord;
DWORD ErrorOffset;
DWORD ErrorSelector;
DWORD DataOffset;
DWORD DataSelector;
BYTE RegisterArea[SIZE_OF_80387_REGISTERS];
DWORD Cr0NpxState;
} FLOATING_SAVE_AREA;
- ControlWord:控制字,包含浮点控制寄存器的控制位。
- StatusWord:状态字,包含浮点状态寄存器的状态位。
- TagWord:标记字,包含浮点标记寄存器的标记位。
- ErrorOffset:错误偏移量,指示引发浮点异常的指令偏移量。
- ErrorSelector:错误选择器,指示引发浮点异常的代码段选择器。
- DataOffset:数据偏移量,指示浮点操作数的地址偏移量。
- DataSelector:数据选择器,指示浮点操作数所在的数据段选择器。
- RegisterArea:寄存器区域,用于保存浮点寄存器的内容。
- Cr0NpxState:CR0 NPX 状态,保存控制寄存器 CR0 的值。
ContextFlags用于控制GetThreadContext和SetThreadContext函数去处理哪些环境信息,它的定义如下。
- CONTEXT_CONTROL:表示 Eip,Esp,Ebp,Eflags,SegCs,SegSs 字段是有效的。
- CONTEXT_INTEGER:表示整数寄存器字段(如 Eax,Ebx,Edx 等)是有效的。
- CONTEXT_SEGMENTS:表示段寄存器字段(如 SegDs,SegEs 等)是有效的。
- CONTEXT_FLOATING_POINT:表示浮点寄存器字段(如 FloatSave)是有效的。
- CONTEXT_DEBUG_REGISTERS:表示调试寄存器字段(如 Dr0,Dr1 等)是有效的。
- CONTEXT_FULL:(CONTEXT_CONTROL|CONTEXT_INTEGER|CONTEXT_SEGMENTS)
Windows实际上允许查看线程内核对象的内部情况,以便抓取它的当前一组CPU寄存器。若要进行这项操作,可以调用GetThreadContext和SetThreadContext函数。
- GetThreadContext
- BOOL GetThreadContext(
HANDLE hThread,
LPCONTEXT lpContext
); - hThread:线程的句柄,表示要获取上下文的目标线程。
- lpContext:指向 CONTEXT 结构的指针,用于接收获取的上下文信息。
- 要使用该函数,需要在代码中包含 <Windows.h> 头文件。
- 在调用此函数之前,需要先初始化 CONTEXT 结构中的 ContextFlags 字段,以指示需要获取的上下文信息的类型。
- SetThreadContext
- BOOL SetThreadContext(
HANDLE hThread,
const CONTEXT *lpContext
); - hThread:线程的句柄,表示要设置上下文的目标线程。
- lpContext:指向 CONTEXT 结构的指针,其中包含要设置的上下文信息。
- 要使用该函数,需要在代码中包含 <Windows.h> 头文件。
- 在调用此函数之前,需要先初始化 CONTEXT 结构中的 ContextFlags 字段,并根据需要设置其他上下文信息。
- 此函数通常用于调试和线程注入等特定的操作,需要小心使用,并确保了解操作的影响和风险。
在调用GetThreadContext函数之前,应该调用SuspendThread函数,否则线程可能会被调度,而且线程的环境可能与收回的不同。一个线程实际上有两个环境,一个是用户方式,一个是内核方式。GetThreadContext只能返回线程的用户方式环境。即使SuspendThread函数暂停线程的时候线程是以内核方式允许,那么也会让其用户方式处于稳定状态,无法执行更多的用户方式代码。
每一个线程会有一个线程暂停计数器,其他线程对此线程使用SuspendThread函数,计数器加一,使用ResumeThread函数,计数器减一。计数器变为0时,才能继续运行。
0x07 将代码注入进程
如果需要将一段代码注入到某个进程的地址空间。可利用原进程各个区块之间间隙,如原进程文件头的DOSstub部分(执行之前要更改目标进程文件头的读写属性)。如果要注入的代码比较长,可以先将目标进程中的某个代码页保存,然后注入新的代码,执行后再将原始代码写回,具体步骤如下。
- 利用CreateProcess函数创建一个可供调试的进程。
- 建立WaitForDebugEvent和ContinueDebugEvent函数构成的调试循环体。
- 利用SuspendThread函数挂起目标线程。
- 利用VirtualProtectEx函数修改目标页的读写权限。
- 利用ReadProcessMemory函数读取目标页。
- 利用GetThraedContext函数保存线程环境。
- 利用WriteProcessMemory函数写入新的代码也。
- 确定新指令的最后一个指令时INT3,我们需要利用它在指令执行后获得系统控制权。
- 保存一份CONTEXT结构的临时拷贝。
- 在这份临时拷贝中设置新的EIP的值。
- 恢复原线程的执行,它将执行我们的代码,直到INT3指令被执行为止。当它被我们的程序捕获,目标线程再次被挂起。
- 利用WriteProcessMemory函数恢复原始代码页。
- 恢复原始代码页的读写属性。
如果需要将某个区块的相对地址转换为线性虚拟地址,可以使用GetThreadSelectorEntry函数。
文章评论