0x13 调试与异常

1. 异常原理


  • 中断和异常
    • 中断由外部因素产生。如键盘,鼠标消息。是异步的
    • 异常是在CPU执行指令时,满足某种条件的时候主动产生,如除零错误,页错误。是同步的
  • 异常的种类
    • 陷阱: 可恢复,恢复后继续在异常发生地址的下一条指令执行(软件断点 int3硬件读写断点)
    • 错误: 可恢复,恢复后继续在异常发生地址处执行(内存断点硬件执行断点内存访问)
    • 终止: 不可恢复,退出进程

image.png

  • Windows中的异常处理机制
    • 异常的执行优先级

image.png

  • VEH: 向量化异常处理
    • 进程相关,可以有多个,最先执行
    • 添加: AddVectoredExceptionHandler
    • 移除: RemoveVectoredExceptionHandler
  • VCH: 向量化异常处理
    • 进程相关,可以有多个,异常被处理的情况下最后执行
    • 添加: AddVectoredContinueHandler
    • 移除: RemoveVectoredContinueHandler
  • UEH: 顶层异常处理函数
    • 进程相关,只存在一个,谁都处理不了就执行,进程被调试时不会被执行
    • 注册:SetUnhandledExceptionFilter
  • SEH: 结构化异常处理程序

    • 线程相关,保存在 FS:[0]的位置,关键字 try, except, finally, leave
    • try的后面必须有一个 except或__ finally块,中间不能有其他代码
    • 应该使用 leave 离开当前的 try 语句块,而不是使用 return go 等

    • 组合方式

      • try{} except(){} 捕获并处理异常

image.png

  1. 1. __try块中的代码发生异常时,__except()中的过滤程序就被调用
  2. 2. 过滤程序可以是一个简单的表达式或一个函数(返回值应为EXCEPTION_CONTINUE_SEARCHEXCEPT_CONTINUE_EXECUTEEXCEPT_EXECUTE_HANDLER之一)
  3. 3. 过滤表达式中可以调用**GetExceptionCode**和GetExceptionInformation函数取得正在处理的异常信息
  4. - _GetExceptionCode 宏用于获取标识发生的异常类型的编码。该函数只能在异常处理程序的**过滤表达式**或**异常处理块**中被使用_
  5. 4. try/finally不同,try/except中可以使用returngotocontinuebreak,它们并不会导致局部展开。
  6. - **__ try{} __ nally{} 无论是否产生异常都执行**

image.png

  1. 1. finally块总是保证,无论__try块中的代码有无异常,finally块总是被调用执行
  2. 2. try块后面只能跟一个finally块或except块,要跟多个时只能用嵌套,但__finally块不可以再嵌套SEH块,except块中可以嵌套SEH块。
  3. 3. 利用try/finally可以使代码的逻辑更清楚,在try块中完成正常的逻辑,finally块中完成清理工作,使代码可读性更强,更容易维护
  4. 4. try块中提前退出(由gotolongjumpcontinuebreakreturn等语句引发)将程序控制流强制转入finally块,这时就会进行局部展开(但ExitProcessExitThreadTerminateProcessTerminateThread等原因导致的提前离开除外,因为这会直接终止线/进程,而不能展开)。说白了,局部展开就是将__finally块的代码提前到上述那几种语句之前执行。
  5. - **__leave**
  6. 1. 该关键字**只能用在try/finally框架**中,它会导致代码执行控制流跳转到try块的结尾,也可以认为是try后的闭花括号处。
  7. 2. ②这种情况下,代码执行是正常从try块进入finally,所以不会进行局部展开。
  8. 3. ③但一般需定义一个布尔变量,指令离开try块时,函数执行的结果是成功还是失败,然后在finally块中可根据这个(或这些)变量以决定资源是否需要释放。
  • 过滤表达式
      1. EXCEPTION_EXCUTE_HANDLER(1) 执行异常处理快 except{ … }
      1. EXCEPTION_CONTINUE_SHEARCH(0) 交由上层处理
      1. 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->用户调试器->程序结束

image.png

  • IDT

image.png

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
      • 根据异常类型处理异常(设置断点、清除断点、恢复断点等)
        1. 反汇编 \ 汇编引擎
        1. Set\GetThreadContext()
        1. VirtualProtectEx()
        1. Write/ReadProcessMemory()
        1. OpenProcess\Thread()
    • 4.恢复程序的执行
      • ContinueDebugEvent() 恢复程序的运行
        • DBG_CONTINUE 它代表调试
        • 事件被调试器处理了
        • DBG_EXCEPTION_NOT_HANDLED 调试事件未被调试器处理,交给其他人处理
  • 断点的实现

    • 单步断点的实现
      • 步入:设置 flag 标志位中的 TF(陷阱标志位) 位为1 0x100
      • 步过:通过计算当前指令的OPCODE长度,找到下一个指令的起始位置,设置一个断点
    • 硬件断点的实现
        1. 在Dr0~Dr3中设置断点地址
        1. 在Dr7中设置断点的类型、长度并且激活断点
        1. 断点命中后在Dr6的低4位获取触发的位置
    • 软件断点的实现
        1. 在目标代码的执行流程中修改指令的首字节为 int3(0xCC) 并保存原有指令
        1. 断点断下后恢复原有的数据
      • 3.将EIP指向的位置 - 1
    • 内存断点的实现
        1. 通过设置内存断点为不可读写来触发异常
          • 由于分页属性都是以分页来设置,所以需要不断的重新设置,效率极低

4.OD 插件的流程