Windows异常学习笔记(五)—— 未处理异常
前言
一、学习自
滴水编程达人
中级班课程,官网:https://bcdaren.com
二、海东老师牛逼!
要点回顾
windows异常处理流程:
CPU检测到异常
↓
查IDT表执行中断处理函数
↓
CommonDispatchException //存储异常相关信息
↓
KiDispatchException //异常分发处理函数,查找由哪一个处理程序处理这个异常
//判断0环异常还是3环异常
//若为3环,修正EIP,指向KiUserExceptionDispatcher(ntdll.dll)
↓
KiUserExceptionDispatcher //通过RtlDispatchException查找异常处理函数位置,VEH/SEH
↓
RtlDispatchException //先查找VEH,若找不到,查找SEH(从FS:[0]开始遍历)
↓
VEH
↓
SEH
思考:会不会存在一种情况,SEH中也没有程序能处理当前异常
答案:一般来说,不存在
最后一道防线
描述:当我们程序刚开始执行时,编译器已经替我们注册了一个异常处理程序,因此叫做最后一道防线
实验一:理解最后一道防线
1)编译并运行以下代码(在return处设置断点)
#include <stdio.h>
#include <windows.h>
int main()
{
int x = 1;
return 0;
}
2)查看堆栈调用情况
可以发现,程序并不是从main函数开始运行的,而是从kernel32.dll的某个位置开始运行的
3)查看kernel32中的代码
4)查看7181704b位置调用的函数的具体代码
7C81704B call 7C8024D6 //在堆栈中注册了一个异常处理程序
↓
7C8024D6 push 7C839AC0h
7C8024DB mov eax,fs:[00000000] //读取了FS:[0]
7C8024E1 push eax
7C8024E2 mov eax,dword ptr [esp+10h]
7C8024E6 mov dword ptr [esp+10h],ebp
7C8024EA lea ebp,[esp+10h]
7C8024EE sub esp,eax
7C8024F0 push ebx
7C8024F1 push esi
7C8024F2 push edi
7C8024F3 mov eax,dword ptr [ebp-8]
7C8024F6 mov dword ptr [ebp-18h],esp
7C8024F9 push eax
7C8024FA mov eax,dword ptr [ebp-4]
7C8024FD mov dword ptr [ebp-4],0FFFFFFFFh
7C802504 mov dword ptr [ebp-8],eax
7C802507 lea eax,[ebp-10h]
7C80250A mov fs:[00000000],eax //修改了FS:[0]
7C802510 ret
5)使用IDA进行查看(BaseProcessStart)
6)总结:当我们程序刚开始执行时,编译器已经替我们注册了一个异常处理程序,因此在主线程中,找不到SEH的情况基本是不会发生的
思考:如果新起一个线程,会出现找不到SEH的情况吗?
答案:不会,参考实验二
实验二:新线程的最后一道防线
1)编译并运行以下代码(在新线程中设置断点)
#include <stdio.h>
#include <windows.h>
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
int x = 1;
return 0;
}
int main()
{
CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
getchar();
return 0;
}
2)当中断在新线程中时,查看新线程堆栈调用情况
可以发现,线程也不是从我们提供的线程函数开始运行的,仍然是从kernel32.dll的某个位置开始运行的
3)查看kernel32中的代码
在地址7C80B6E3处也调用了地址7C8024D6的代码(__SEH_prolog),注册了异常处理函数
总结
- 无论是主线程,还是新创建的线程,都存在最后一道防线,即编译器为我们注册的异常处理程序的结构
- 这个异常处理程序相当于以下代码
__try { } __except(UnhandledExceptionFilter(GetExceptionInformation()) { //终止线程 //终止进程 }
UnhandledExceptionFilter
描述:
- 最后一道防线调用的异常处理程序的过滤函数
- 只有当它的返回值为EXCEPTION_CONTINUE_SEARCH(0)时,当前线程不存在对应的SEH
- 只有在当前程序处于调试状态时,才会发生上述情况
执行流程:
- 通过NtQueryInformationProcess查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH,此时会进入第二轮分发
- 如果没有被调试:
1)查询是否通过SetUnhandledExceptionFilter注册处理函数 如果有就调用
2)如果没有通过SetUnhandledExceptionFilter注册处理函数,弹出窗口,让用户选择终止程序还是启动即时调试器
3)如果用户没有启用即时调试器,那么该函数返回EXCEPTION_EXECUTE_HANDLER,此时会执行except中的代码
实验三:理解UnhandledExceptionFilter
1)编译并运行以下代码
#include <stdio.h>
#include <windows.h>
long __stdcall callback(_EXCEPTION_POINTERS *excp)
{
excp->ContextRecord->Ecx = 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
int main()
{
SetUnhandledExceptionFilter(callback);
__asm
{
xor edx, edx
xor ecx, ecx
mov eax, 0x10
idiv ecx
}
printf("程序正常执行\n");
getchar();
return 0;
}
2)执行结果
3)双击exe执行,执行结果
4)总结
- 当程序处于调试状态时,UnhandledExceptionFilter返回EXCEPTION_CONTINUE_SEARCH,此时该异常没有SEH能够进行处理,程序无法向下运行
- 当程序不处于调试状态时,UnhandledExceptionFilter返回EXCEPTION_EXECUTE_HANDLER,异常由注册的callback函数进行处理
思考:遇到这种情况如何进行调试
答案:HOOK NtQueryInformationProcess
未处理异常
执行流程:
KiUserExceptionDispatcher
↓
RtlDispatchException //查找并执行异常处理函数
//如果返回真,调用ZwContinue再次进入0环
//但线程再次返回3环时,会从修正后的位置开始执行
//如果返回假,调用ZwRaiseException进行第二轮异常分发
//(参见KiUserExceptionDispatcher代码)