调试相关函数
- ContinueDebugEvent:允许调试器恢复先前由于调试事件而挂起的线程
- DebugActiveProcess:允许调试器捆绑到一个正在允许的进程上
- DebugActiveProcessStop:允许调试器从一个正在运行的进程上卸载
- DebugBreak:在当前进程中产生一个断点异常,如果进程未处于调试状态,异常将被系统阶段,多数情况下会直接终止进程
- DebugBreakProcess:在指定的进程中产生一个断点异常
- FatalExit:将使调用进程强制退出,将控制权转交给调试器,与ExitProcess不同是,此函数在退出前会调用一个INT 3断点
- FlushInstructionCache:刷新指令高速缓存
- GetThreadContext:获取指定线程的执行环境
- GetThreadSelectorEntry:获取指定选择器和线程的描述符表的入口地址
- IsDebuggerPresent:判断调用进程是否处于调试环境中
- OutputDebugString:输出一个字符串传递给调试器显示
- ReadProcessMemory:获取指定进程某区域内的数据
- SetThreadContext:设置线程的执行环境
- WaitForDebugEvent:等待被调试进程发生调试事件
- WriteProcessMemory:在指定进程的某区域内写入数据
调试相关事件
被调试进程中发生一个调试事件后,系统将通知调试器来处理这个事件,调试器利用WaitForDebugEvent函数获取目标进程的相关环境信息
常用调试事件如下:
当WaitForDebugEvent收到一个调试事件后,将把调试事件的信息写入DEBUG_EVENT结构中返回,结构定义如下:
- dwDebugEventCode:所发生的调试事件的类型
- dwProcessID:调试事件所发生的进程的标识符
- dwThreadID:调试事件所发生的线程的标识符
- union:联合体结构包含了关于调试事件的更多信息,根据dwDebugEventCode的不同,可以是如下的结构:
一般流程:
假设调试程序调用了WaitForDebugEvnet并得到返回,要做的第一件事就是查看dwDebugEventCode的值,根据调试事件类型来决定union联合体中的结构包含哪些内容
调试器原理
创建一个新进程以供调试
在调用CreateProcess创建进程时,在dwCreationFlag标志字段中指定DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS标志,将创建一个用于调试的新进程。
将调试器捆绑到一个正在运行的进程上
利用DebugActiveProcess函数可以将调试器捆绑到一个正在运行的进程上,如果执行成功,则效果类似于调用CreateProcess时,传入DEBUG_ONLY_THIS_PROCESS标志位创建的进程。
在NT内核中,当试图调用DebugActiveProcess函数将调试器捆绑到一个创建时带有安全描述符的进程上时,将被拒绝。
进入正式调试阶段(调试循环体)
使用一个while循环,不停调用WaitForDebugEvent函数,获取调试事件进行处理,处理完毕后调用和ContinueDebugEvent恢复被调试进程的运行,然后继续调用WaitForDebugEvent等待下一个调试事件的到来。
示例代码:
处理调试事件
当WaitForDebugEvent拿到调试事件后,根据dwDebugEventCode检查union联合体,
union联合体的开头包含一个EXCEPTION_DEBUG_INFO结构
其中,EXCEPTION_RECORD结构包含了异常的很多信息,内容如下:
- ExceptionCode:描述异常类型的代码
- ExceptionFlag:0表示可继续异常,反之,值为EXCEPTION_NONCONTINUABLE
- ExceptionRecord:指向_EXCEPTION_RECORD结构的指针
- ExceptionAddress:异常发生的地址
- NumberParameters:ExceptionInformation队列中定义的32位参数的数目
- ExceptionInformation:额外的32位消息队列,主要在嵌套异常时使用,多数异常情况下没有定义
在发生一个EXCEPTION_NONCONTINUABLE的异常后,如果继续执行,将产生一个EXCEPTION_NONCONTINUABLE_EXCEPTION异常
常见异常情况:
EXCPETION_BREAKPOINT
和EXCPETION_SINGLE_STEP
,当遇到一个INT 3断点时会产生EXCPETION_BREAKPOINT
异常,如果设置了单步执行标志,那么执行完一条指令后会产生一个EXCPETION_SINGLE_STEP
异常。
当调试事件处理完毕后,调用ContinueDebugEvent让线程继续运行。ContinueDebugEvent的dwContinueStatus参数有两个取值,分别是DBG_EXCEPTION_NOT_HANDLED
和DBG_CONTINUE
。对于除了EXCEPTION_DEBUG_EVENT
调试事件以外的事件,两者无区别,都让线程继续运行。
当调试事件是EXCEPTION_DEBUG_EVENT
时,意味着被调试线程中发生了一个异常。
如果指定了DBG_CONTINUE
,线程将忽略它自己的异常处理函数并继续执行,并引发后续由于该异常没有被处理而导致一系列更大的异常,此时调试器的表现为不停的收到EXCEPTION_DEBUG_EVENT
调试事件,直到异常导致程序终止。
如果指定了DBG_EXCEPTION_NOT_HANDLED
就是告诉系统,调试器不对该异常负责,不会处理它。此时系统将使用调试线程的默认异常处理函数来处理异常。
调用ContinueDebugEvent的一般规则:
- 对进程被载入后发生的第一个
EXCEPTION_DEBUG_EVENT
,必须以DBG_CONTINUE
为标志继续 - 如果程序调用了DebugBreakhh函数,或者成功插入了INT 3断点,并将内存恢复,都应该以
DBG_CONTINUE
为标志继续 - 如果在程序中发生了不确定的异常(特别是在调试带壳程序时),问题多半由于外壳的SEH引起的,此时应该以
DBG_EXCEPTION_NOT_HANDLE
为标志继续,让被调试程序本身的异常处理机制来处理。
线程运行环境
当前的线程运行状态保存于一个CONTEXT的结构中,包含线程执行所用的寄存器,系统栈和用户栈,以及线程所使用的描述符等其他状态信息。这样,当该线程再次运行时,Windows就可以恢复最近线程的运行环境,好像中间什么都没有发生一样。
获取指定线程的执行环境:GetThreadContext
设置指定线程的执行环境:SetThreadContext
调试器将代码注入进程
具体步骤如下:
- 利用CreateProcess获得要注入进程的进程句柄
- 建立由WaitForDebugEvent和ContinueDebugEvent函数构成的调试循环体
- 利用SuspendThread函数挂起目标线程
- 利用VirtualProtectEx函数修改目标内存页的读写权限
- 利用ReadProcessMemory函数读取目标页
- 利用GetThreadContext函数保存线程环境
- 利用WaitProcessMemory函数写入新的代码页
- 确认新写入的指令中的最后一个指令是INT 3
- 当INT 3断点被触发后,调试器保存一份CONTEXT结构的临时拷贝
- 在这份临时拷贝中设置新的EIP值
- 恢复原线程的执行,此时线程将执行注入的代码,直到INT 3指令被执行为止,此时将被调试器捕获
- 调试器利用WriteProcessMemory函数恢复原始代码页
- 恢复原始代码页的读写属性
- 利用SetThreadContext函数恢复线程的原始环境
- 恢复线程的执行
将某个区块的相对地址转化为线性虚拟地址,可以使用GetThreadSelectorEntry函数