avatar

Catalog
调试与异常

前言

通过前几篇对调试的学习,现在可以对调试的过程有个整体的认识,本篇介绍调试与异常之间的关系,尽管在之前一个篇章中,已经比较详细的概括了异常相关的各个知识点,但是调试本身就相当于给调试器发送一个异常类型的调试事件。这里就再回顾一下。

调试器下的异常分发

在之前学习用户异常的分发内核异常的分发时,由于我们直接分析了KiDispatchException的执行流程,所以我们知道在异常分发时,会先判断调试器是否存在,尽管在当时的实验中并没有加入调试器的实验。本篇就要验证一下有无调试器时,异常分发的流程:

  1. 编写运行如下代码:(环境:Windows XP,编译器:VC++6.0)

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include "stdafx.h"

    int main(int argc, char* argv[])
    {
    int x = 100;
    int y = 0;
    int z;

    _try{
    z = x/y;
    printf("无法执行的代码!\n");
    getchar();
    }
    _except(1){
    printf("SEH异常处理代码\n");
    }
    getchar();
    return 0;
    }
  2. 正常情况下,运行结果如下,触发除零异常后,执行_except块中的代码

  3. 将其.exe文件用OD打开(创建进程或者附加进程都行),会发现程序会断在这里不动了,无论如何按F9,都无法继续执行。原因是KiDispatchException检测到了调试器的存在,因此会先交予调试器处理。

  4. 这时我们根据堆栈可以找到引发除零异常的参数,修改其值,就可以继续执行了

  5. 便会出现结果如下,可以看到调试器成功处理了异常,程序会正常执行,便不会继续寻找SEH

  6. 当然,也可以让调试器选择不处理,在OD的调试选项中,选择异常,可以忽略选择类型的异常,此时我们把除零异常选上,再次执行时,调试器就不会处理,于是便会分发给SEH去处理,结果如下

最后一道防线与二次分发

这一部分已经在未处理异常提过,这里用实验再简单的验证一下:

  1. 将刚刚的程序作如下修改:

    c
    1
    _except(1) ----> _except(0)
  2. 修改完后,可以看到,在忽略除零异常的情况下,还是会断在这里,这是因为在第一次分发时调试器没有处理,SEH也没有处理,最后一道防线检测到此时存在调试器,于是又发送了一次异常调试事件给调试器,即第二轮分发。如果这次还不处理,那么便会终止进程。

  3. 若没有被调试,则会查询是否通过SetUnhandledExceptionFilter注册顶层处理函数

    • 如果有就调用。
    • 如果没有,就弹出窗口,让用户选择终止进程还是启动即时调试器。

    实验结果如下,在不附加到调试器的情况下,在文件夹直接打开.exe文件,就可以看到未注册顶层处理函数的情况,只是此处没有选择启动即使调试器的选项。

反调试与反反调试

原理已经在未处理异常一篇中说明过了,利用的顶层处理函数的机制,只是当时没能成功运行程序,这次可以了,并且是一个新的例子。

  1. 编写运行如下代码:(环境:Windows XP,编译器:VC++6.0)

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #include "stdafx.h"
    #include "windows.h"

    long _stdcall callback(_EXCEPTION_POINTERS* excp)
    {
    excp->ContextRecord->Ecx = 1;
    return EXCEPTION_CONTINUE_EXECUTION;
    }

    int main(int argc, char* argv[])
    {
    //注册一个最顶层异常处理函数
    SetUnhandledExceptionFilter(callback);

    //除0异常
    _asm
    {
    xor edx,edx
    xor ecx,ecx
    mov eax,0x10
    idiv ecx
    }

    //程序正常执行
    printf("程序执行");

    getchar();
    return 0;
    }
  2. 由于异常会被注册的顶层函数(存储在kernel32!BaseCurrentTopLevelFilter的全局变量中)处理掉。因此程序是可以正常执行的(注意要从项目的文件夹中打开程序,不要直接在VC++6.0中运行)

  3. 如果把程序拖进调试器,情况就不一样了,由于检测到了调试器的存在,在第一次遇到异常时,会先交给调试器处理。

  4. 如果调试器选择忽略此类异常,由于NtQueryInformationProcess检测到了程序正在被调试,因此不会调用注册的顶层处理函数,所以程序无法得到修复。本程序比较简单,或许可以在调试器中手动修复程序,若是较为复杂的逻辑,修复起来就变得困难了。

除了上面这种利用顶层处理函数的机制进行反调试的手段,还有一种,也是比较常规的,就是进程不断的给调试器发送异常调试事件,调试器也无法区分哪个调试事件是有用的,会一并接收,从而达到反调试的目的。

总结

总结就一张包含了调试器的异常处理的图。

参考资料

参考笔记:

  • 张嘉杰笔记

参考教程:

  • 海哥中级逆向课程-异常的处理流程
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/16/%E8%B0%83%E8%AF%95%E4%B8%8E%E5%BC%82%E5%B8%B8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶