前言
通过前几篇对调试的学习,现在可以对调试的过程有个整体的认识,本篇介绍调试与异常之间的关系,尽管在之前一个篇章中,已经比较详细的概括了异常相关的各个知识点,但是调试本身就相当于给调试器发送一个异常类型的调试事件。这里就再回顾一下。
调试器下的异常分发
在之前学习用户异常的分发与内核异常的分发时,由于我们直接分析了KiDispatchException的执行流程,所以我们知道在异常分发时,会先判断调试器是否存在,尽管在当时的实验中并没有加入调试器的实验。本篇就要验证一下有无调试器时,异常分发的流程:
编写运行如下代码:(环境:Windows XP,编译器:VC++6.0)
c1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}正常情况下,运行结果如下,触发除零异常后,执行_except块中的代码
将其.exe文件用OD打开(创建进程或者附加进程都行),会发现程序会断在这里不动了,无论如何按F9,都无法继续执行。原因是KiDispatchException检测到了调试器的存在,因此会先交予调试器处理。
这时我们根据堆栈可以找到引发除零异常的参数,修改其值,就可以继续执行了
便会出现结果如下,可以看到调试器成功处理了异常,程序会正常执行,便不会继续寻找SEH
当然,也可以让调试器选择不处理,在OD的调试选项中,选择异常,可以忽略选择类型的异常,此时我们把除零异常选上,再次执行时,调试器就不会处理,于是便会分发给SEH去处理,结果如下
最后一道防线与二次分发
这一部分已经在未处理异常提过,这里用实验再简单的验证一下:
将刚刚的程序作如下修改:
c1
_except(1) ----> _except(0)
修改完后,可以看到,在忽略除零异常的情况下,还是会断在这里,这是因为在第一次分发时调试器没有处理,SEH也没有处理,最后一道防线检测到此时存在调试器,于是又发送了一次异常调试事件给调试器,即第二轮分发。如果这次还不处理,那么便会终止进程。
若没有被调试,则会查询是否通过SetUnhandledExceptionFilter注册顶层处理函数:
- 如果有就调用。
- 如果没有,就弹出窗口,让用户选择终止进程还是启动即时调试器。
实验结果如下,在不附加到调试器的情况下,在文件夹直接打开.exe文件,就可以看到未注册顶层处理函数的情况,只是此处没有选择启动即使调试器的选项。
反调试与反反调试
原理已经在未处理异常一篇中说明过了,利用的顶层处理函数的机制,只是当时没能成功运行程序,这次可以了,并且是一个新的例子。
编写运行如下代码:(环境:Windows XP,编译器:VC++6.0)
c1
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
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;
}由于异常会被注册的顶层函数(存储在kernel32!BaseCurrentTopLevelFilter的全局变量中)处理掉。因此程序是可以正常执行的(注意要从项目的文件夹中打开程序,不要直接在VC++6.0中运行)
如果把程序拖进调试器,情况就不一样了,由于检测到了调试器的存在,在第一次遇到异常时,会先交给调试器处理。
如果调试器选择忽略此类异常,由于NtQueryInformationProcess检测到了程序正在被调试,因此不会调用注册的顶层处理函数,所以程序无法得到修复。本程序比较简单,或许可以在调试器中手动修复程序,若是较为复杂的逻辑,修复起来就变得困难了。
除了上面这种利用顶层处理函数的机制进行反调试的手段,还有一种,也是比较常规的,就是进程不断的给调试器发送异常调试事件,调试器也无法区分哪个调试事件是有用的,会一并接收,从而达到反调试的目的。
总结
总结就一张包含了调试器的异常处理的图。
参考资料
参考笔记:
- 张嘉杰笔记
参考教程:
- 海哥中级逆向课程-异常的处理流程