0x13 调试与异常
1. 异常原理
- 中断和异常
- 中断由外部因素产生。如键盘,鼠标消息。是异步的
- 异常是在CPU执行指令时,满足某种条件的时候主动产生,如除零错误,页错误。是同步的
- 异常的种类
- 陷阱: 可恢复,恢复后继续在异常发生地址的下一条指令执行(软件断点 int3,硬件读写断点)
- 错误: 可恢复,恢复后继续在异常发生地址处执行(内存断点,硬件执行断点,内存访问)
- 终止: 不可恢复,退出进程
- Windows中的异常处理机制
- 异常的执行优先级
- VEH: 向量化异常处理
- 进程相关,可以有多个,最先执行
- 添加: AddVectoredExceptionHandler
- 移除: RemoveVectoredExceptionHandler
- VCH: 向量化异常处理
- 进程相关,可以有多个,异常被处理的情况下最后执行
- 添加: AddVectoredContinueHandler
- 移除: RemoveVectoredContinueHandler
- UEH: 顶层异常处理函数
- 进程相关,只存在一个,谁都处理不了就执行,进程被调试时不会被执行
- 注册:SetUnhandledExceptionFilter
SEH: 结构化异常处理程序
- 线程相关,保存在 FS:[0]的位置,关键字 try, except, finally, leave
- try的后面必须有一个 except或__ finally块,中间不能有其他代码
应该使用 leave 离开当前的 try 语句块,而不是使用 return go 等
组合方式
- try{} except(){} 捕获并处理异常
1. 当__try块中的代码发生异常时,__except()中的过滤程序就被调用
2. 过滤程序可以是一个简单的表达式或一个函数(返回值应为EXCEPTION_CONTINUE_SEARCH、EXCEPT_CONTINUE_EXECUTE或EXCEPT_EXECUTE_HANDLER之一)
3. 过滤表达式中可以调用**GetExceptionCode**和GetExceptionInformation函数取得正在处理的异常信息
- _GetExceptionCode 宏用于获取标识发生的异常类型的编码。该函数只能在异常处理程序的**过滤表达式**或**异常处理块**中被使用_
4. 与try/finally不同,try/except中可以使用return、goto、continue和break,它们并不会导致局部展开。
- **__ try{} __ finally{} 无论是否产生异常都执行**
1. finally块总是保证,无论__try块中的代码有无异常,finally块总是被调用执行
2. try块后面只能跟一个finally块或except块,要跟多个时只能用嵌套,但__finally块不可以再嵌套SEH块,except块中可以嵌套SEH块。
3. 利用try/finally可以使代码的逻辑更清楚,在try块中完成正常的逻辑,finally块中完成清理工作,使代码可读性更强,更容易维护
4. 从try块中提前退出(由goto、longjump、continue、break、return等语句引发)将程序控制流强制转入finally块,这时就会进行局部展开(但ExitProcess、ExitThread、TerminateProcess、TerminateThread等原因导致的提前离开除外,因为这会直接终止线/进程,而不能展开)。说白了,局部展开就是将__finally块的代码提前到上述那几种语句之前执行。
- **__leave**
1. 该关键字**只能用在try/finally框架**中,它会导致代码执行控制流跳转到try块的结尾,也可以认为是try后的闭花括号处。
2. ②这种情况下,代码执行是正常从try块进入finally,所以不会进行局部展开。
3. ③但一般需定义一个布尔变量,指令离开try块时,函数执行的结果是成功还是失败,然后在finally块中可根据这个(或这些)变量以决定资源是否需要释放。
- 过滤表达式
- EXCEPTION_EXCUTE_HANDLER(1) 执行异常处理快 except{ … }
- EXCEPTION_CONTINUE_SHEARCH(0) 交由上层处理
- EXCEPTION_CONTINUE_EXCUTION(-1) 重新执行一次,通常修复之后才会返回
- 过滤表达式可以是一个函数调用,但是必须返回以上三个值
- 可以使用 GetExceptionCode 和 GetExceptionInformation 获取异常信息
- GetExceptionCode 能在过滤表达式和异常处理块中使用
- GetExceptionInformation 只能在过滤表达式中使用
- SEH链
- SEH链,是个链表,保存这SEH的函数地址,链表的首地址保存在TEB的第一个字段中,通过FS寄存器来获取(FS:[0])。链表中的每个元素都是这样一个结构体
- 通过FS:[0]找到 异常链,遍历异常链中的 Handler 挨个调用一次。FS:[0] 是TEB首地址—>NT_TIB(TEB的第一个字段)—>ExceptionList指针(TiB的第一个字段) ```cpp / SEH 结构体 typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPTION_ROUTINE Handler; } EXCEPTION_REGISTRATION_RECORD;
// SEH函数原型 EXCEPTIONDISPOSITION NTAPI EXCEPTION_ROUTINE ( _Inout struct EXCEPTION_RECORD *ExceptionRecord, _In PVOID EstablisherFrame, Inout struct CONTEXT *ContextRecord, _In PVOID DispatcherContext );
push fs:[0] push new_handler mov fs:[0], esp ```
- 异常的分发流程
- IDT:中断和异常是统一管理的,系统为每一种中断或异常都提供了处理函数,这些函数的地址就保存在IDT中,使用 !idt /a 查看IDT中的所有内容
- 异常分发的源头 : CPU -> IDT -> KiTrapN(填充陷阱帧)->CommonDispatchException(填充异常结构) -> KiDispatchException(分发异常)
- 用户态异常分发的源头: KeUserExceptionDispatcher
- 用户 RtlDispatchException -> VEH -> SEH(UEH) -> VCH
- 内核 RtlDispatchException -> SEH
- DbgkFowardException 将异常传递给三环的调试器
- 用户态和内核态分发流程
- 内核态->内核调试器->内核SEH处理->第二次->内核调试器->蓝屏
- 用户态->内核调试器->用户调试器->回到用户层->VEH->SEH->UEH->VCH->RtlRaiseException->第二次->KiDispathchException->用户调试器->程序结束
- IDT
2. 反调试与反反调试
静态反调试:一般在调试开始时阻拦调试者,调试者只需找到原因后可一次性突破
- PEB: BeingDebuged、Ldr、Heap、NtGlobalFlag
- TEB: StaticUnicodeString
- 攻击调试器: NtSetInformationThread(ThreadHideFormDebugger(0x11))
- 查询调试信息:NtQueryInformationProcess()
- 在TLS回调函数内:检查父进程、窗口名、进程名、文件和路径、注册表
动态反调试:可在调试过程中被频繁触发,因此需要调试者时时关注
- 使用异常: 主动产生异常并在处理函数内反调试,SEH UEH等
- 补丁检查(0xCC的检测): 检查执行流程中的0xCC,API是否被Hook,代码HASH值 CRC 校验
- 反反汇编: 指令截断、混淆、膨胀、代码乱序
- 使用壳技术:加密函数(IAT加密,偷取代码),API虚拟机,指令虚拟机
- 时间戳检查
- 虚拟机技术
3. 调试器的实现
调试器的实现流程
- 1. 创建调试会话
- CreateProcess(DEBUG_ONLY_THIS_PROCESS) 调试方式创建进程
- 附加进程:DebugActiveProcess
- 2.等待调试事件
- WaitForDebugEvent()
- 接到调试事件之前,调试器是卡住的
- 街道调试事件之后,目标进程是暂停
- 3.处理调试事件 DEBUG_EVENT
- 根据异常类型处理异常(设置断点、清除断点、恢复断点等)
- 反汇编 \ 汇编引擎
- Set\GetThreadContext()
- VirtualProtectEx()
- Write/ReadProcessMemory()
- OpenProcess\Thread()
- 4.恢复程序的执行
- ContinueDebugEvent() 恢复程序的运行
- DBG_CONTINUE 它代表调试
- 事件被调试器处理了
- DBG_EXCEPTION_NOT_HANDLED 调试事件未被调试器处理,交给其他人处理
- ContinueDebugEvent() 恢复程序的运行
- 1. 创建调试会话
断点的实现
- 单步断点的实现
- 步入:设置 flag 标志位中的 TF(陷阱标志位) 位为1 0x100
- 步过:通过计算当前指令的OPCODE长度,找到下一个指令的起始位置,设置一个断点
- 硬件断点的实现
- 在Dr0~Dr3中设置断点地址
- 在Dr7中设置断点的类型、长度并且激活断点
- 断点命中后在Dr6的低4位获取触发的位置
- 软件断点的实现
- 在目标代码的执行流程中修改指令的首字节为 int3(0xCC) 并保存原有指令
- 断点断下后恢复原有的数据
- 3.将EIP指向的位置 - 1
- 内存断点的实现
- 通过设置内存断点为不可读写来触发异常
- 由于分页属性都是以分页来设置,所以需要不断的重新设置,效率极低
- 通过设置内存断点为不可读写来触发异常
- 单步断点的实现