Windows软件调试学习笔记(四)—— 异常的处理流程
要点回顾
调试事件有多重类型、例如DLL加载、进程创建、线程创建等等。
其中最关键的调试事件是”异常“。
在调试过程中,不论是软件断点、硬件断点还是INT 3断点,都是通过异常来实现的。
异常的处理流程
流程图:
实验1:理解调试器与异常的关系
1)编译并运行以下代码:
#include <stdio.h>
#include <windows.h>
int main ()
{
int x = 100;
int y = 0;
int z;
_try
{
z = x / y; //除0异常
printf("无法执行的代码 \n");
}
_except(1)
{
printf("SEH异常处理代码 \n");
}
return 0;
}
执行结果:
2)设置调试器选项
3)使用调试器运行程序
程序停在40104E处,由于调试器没有除0异常处理代码,因此无法继续向下执行。
4)手动模拟异常处理,将[ebp-0x20]的值改为1,继续运行
程序成功向下运行。
思考:会不会存在某种异常没人处理的情况
答案:不存在,因为有UnhandledExceptionFilter(未处理异常)
未处理异常:最后一道防线
描述:在前面异常相关章节已经学习,任何一个线程,在启动时都会先布置最后一道防线。
__try
{
}
__except(UnhandledExceptionFilter(GetExceptionInformation())
{
//终止线程
//终止进程
}
UnhandledExceptionFilter执行流程:
1)通过NtQueryInformationProcess查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH,此时会进入第二轮分发。
2)如果没有被调试:
- 查询是否通过SetUnhandledExceptionFilter注册处理函数,如果有就调用。
- 如果没有通过SetUnhandledExceptionFilter注册处理函数,弹出窗口,让用户选择终止程序还是启动即时调试器。
- 如果用户没有启用即时调试器,那么该函数返回EXCEPTION_EXECUTE_HANDLER。
实验2:理解UnhandledExceptionFilter执行流程
1)编译以下代码
#include <stdio.h>
#include <windows.h>
int main ()
{
int x = 100;
int y = 0;
int z;
_try
{
z = x / y; //除0异常
printf("无法执行的代码 \n");
}
_except(0)
{
printf("SEH异常处理代码 \n");
}
getchar();
return 0;
}
2)双击运行程序
结论:
1)由于当前进程并未处于调试状态,因此不会调用UnhandledExceptionFilter。
2)由于当前程序并未使用SetUnhandledExceptionFilter注册处理函数,因此会让用户选择是否启动调试器。
通过UnhandledExceptionFilter的这个性质,可以借此实现反调试。
实验3:利用UnhandledExceptionFilter实现反调试
1)编译并运行以下代码:
#include <stdio.h>
#include <windows.h>
DWORD g_Test = 0;
LONG NTAPI TopLevelExcepFilter(PEXCEPTION_POINTERS pExcepInfo)
{
printf("顶级异常处理器修复异常 \n");
g_Test = 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
int main ()
{
int r = 0;
int x = 100;
SetUnhandledExceptionFilter(&TopLevelExcepFilter);
r = x / g_Test; //除0异常
printf("正常逻辑开始执行 \n");
for(int i=0; i<10; i++)
{
Sleep(1000);
printf("%d \n", i);
}
getchar();
return 0;
}
编译器运行结果:
双击运行结果:
由于调试器不会触发SetUnhandledExceptionFilter设置的顶层异常,因此程序处于被调试状态时无法回归到正常逻辑;且调试器在处理异常时,若处理结果与程序所需不符,程序也将无法得以正常执行。