问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
SEH异常之编译器原理探究
我们在之前已经了解了VEH和SEH异常,在这里我们来深入探究一下编译器为我们提供的`_try_except`和`_try_finally`的原理实现。
0x00 前言 ======= 我们在之前已经了解了VEH和SEH异常,在这里我们来深入探究一下编译器为我们提供的`_try_except`和`_try_finally`的原理实现。 0x01 \_try\_except原理 ====================  调用`_except_handle3`这个异常处理函数,这里并不是每个编译器的异常处理函数都是相同的,然后存入结构体,将esp的值赋给`fs:[0]`,再就是提升堆栈的操作  每个使用 `_try _except`的函数,不管其内部嵌套或反复使用多少`_try _except`,都只注册一遍,即只将一个 `_EXCEPTION_REGISTRATION_RECORD`挂入当前线程的异常链表中(对于递归函数,每一次调用都会创建一个 `_EXCEPTION_REGISTRATION_RECORD`,并挂入线程的异常链表中)。 ```c++ typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPTION_ROUTINE Handler; } EXCEPTION_REGISTRATION_RECORD; ``` 可以看到只有一个异常处理函数  那么这里编译器是如何做到只用一个异常处理函数的呢?编译器把原来`_EXCEPTION_REGISTRATION_RECORD`结构进行了拓展,添加了三个成员 ```c++ struct _EXCEPTION_REGISTRATION{ struct _EXCEPTION_REGISTRATION *prev; void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD); struct scopetable_entry *scopetable; int trylevel; int _ebp; }; ``` 新堆栈结构如下  scopetable ---------- ```c++ struct scopetable_entry { DWORD previousTryLevel //上一个try{}结构编号 PDWRD lpfnFilter //过滤函数的起始地址 PDWRD lpfnHandler //异常处理程序的地址 } ``` 查看地址可以发现有三个结构体  存储着的正式异常函数的开始地址和结束地址  第一个值`previousTryLevel`是上一个`try`结构的编号,这里如果在最外层就是-1,如果在第二层就是0,如果在第三层就是1,以此类推   trylevel -------- 该成员表示代码运行到了哪个`try`结构里面,进入一个`try`则加1,`try`结构执行完成之后则减1   \_except\_handler3 ------------------ 1.CPU检测到异常 -> 查中断表执行处理函数 -> `CommonDispatchException` -> `KiDispatchException` -> `KiUserExceptionDispatcher` -> `RtlDispatchException` ->`VEH` -> `SEH` 2.执行`_except_handler3`函数 <1> 根据`trylevel`选择`scopetable`数组 <2> 调用`scopetable`数组中对应的`lpfnFilter`函数 > 1.EXCEPTION\_EXECUTE\_HANDLER(1) 执行except代码 > > 2.EXCEPTION\_CONTINUE\_SEARCH(0) 寻找下一个 > > 3.EXCEPTION\_CONTINUE\_EXECUTION(-1) 重新执行 <3> 如果`lpfnFilter`函数返回0 向上遍历 直到`previousTryLevel=-1` 假设有两个异常点  首先找到`trylevel`为0  然后找到异常过滤表达式为1  然后遍历数组的`lpfnFilter`   如果返回值为1则调用异常处理函数,如果为0则该异常函数不处理,如果为-1则继续从原异常点向下执行   假设在B这个地方出异常,得到`trylevel`为2  那么这里就回去遍历`lpfnFilter`为2的地方  假设这里返回值为0,则继续查找,注意这个地方是向上查找,首先判断当前`previousTryLevel`的值是否为-1,如果为-1就停止查找(-1代表已经是最外层)`try`结构,然后再向上找,假设这里返回值仍然为0,判断`previousTryLevel`的值为-1,就停止查找,没有找到响应的异常处理函数  0x02 \_try\_finally原理 ===================== 无论`try`结构体中是什么代码,都会执行`finally`里面的代码 ```c++ // SEH6.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> VOID ExceptionTest() { __try { return; printf("Other code"); } __finally { printf("Must run this code"); } } int main(int argc, char* argv[]) { ExceptionTest(); getchar(); return 0; } ```  局部展开 ---- 当`try`里面没有异常,而是`return`、`continue`、`break`等语句时,就不会走`_except_handle3`这个函数,而是调用`_local_unwind2`进行展开  然后调用`[ebx + esi*4 + 8]`  跟进去就到了`finally`语句块的地方  我们探究一下实现的原理,这里本来应该是`lpfnFilter`参数,指向异常处理过滤的代码的地址,但是这里是0。只要这个地方的地址为0就是`finally`语句块  全局展开 ---- ```c++ // SEH6.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> VOID ExceptionTest() { __try { __try { __try { *(int*)0 = 1; } __finally { printf("Must run this code : A"); } } __finally { printf("Must run this code : B"); } } __except(1) { printf("Here is Exception_functions"); } } int main(int argc, char* argv[]) { ExceptionTest(); getchar(); return 0; } ``` 全局展开就是一层一层的向上找异常处理函数,`finally`模块还是照常执行   0x03 未处理异常 ========== 入口程序的最后一道防线 -----------  这里调用`mainCRTStartup()`,然后调用入口程序  相当于这里才是一个进程开始执行的地方  这里有一个call调用,跟进去看看  发现有修改`fs:[0]`的操作,这里就相当于编译器为我们注册了一个异常处理函数  这里到`kernel32.dll`里面的`BaseProcessStart`里面看一下,这里有一个注册SEH异常处理函数的操作  线程启动的最后一道防线 ----------- ```c++ // SEH7.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> DWORD WINAPI ThreadProc(LPVOID lpParam) { int i = 1; return 0; } int main(int argc, char* argv[]) { CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); getchar(); return 0; ``` 可以发现线程也是从`kernel32.dll`开始的  然后跟进调用  可以发现还是注册了一个异常处理函数  还是去IDA里面看`BaseThreadStart`函数,发现也注册了一个`SEH`异常的函数  ### UnhandledExceptionFilter 相当于编译器为我们生成了一段伪代码 ```c++ __try { } __except(UnhandledExceptionFilter(GetExceptionInformation()) { //终止线程 //终止进程 } ``` 只有程序被调试时,才会存在未处理异常 UnhandledExceptionFilter的执行流程: ```c++ 1) 通过NtQueryInformationProcess查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH,此时会进入第二轮分发 2) 如果没有被调试: 查询是否通过SetUnhandledExceptionFilter注册处理函数 如果有就调用 如果没有通过SetUnhandledExceptionFilter注册处理函数 弹出窗口 让用户选择终止程序还是启动即时调试器 如果用户没有启用即时调试器,那么该函数返回EXCEPTION_EXECUTE_HANDLER ``` ### SetUnhandledExceptionFilter 如果没有通过`SetUnhandledExceptionFilter`注册异常处理函数,则程序崩溃   测试代码如下,我自己构造一个异常处理函数`callback`并用`SetUnhandledExceptionFilter`注册,构造一个除0异常,当没有被调试的时候就会调用`callback`处理异常,然后继续正常运行,如果被调试则不会修复异常,因为这是最后一道防线,就会直接退出,起到反调试的效果 ```c++ // SEH7.cpp : Defines the entry point for the console application. // #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); _asm { xor edx,edx xor ecx,ecx mov eax,0x10 idiv ecx } printf("Run again!"); getchar(); return 0; } ``` 直接启动可以正常运行  使用od打开则直接退出   ### KiUserExceptionDispatcher 只有当前程序处于调试的时候才可能产生未处理异常 ```c++ 1) 调用RtlDispatchException 查找并执行异常处理函数 2) 如果RtlDispatchException返回真,调用ZwContinue再次进入0环,但线程再次返回3环时,会从修正后的位置开始执行。 3) 如果RtlDispatchException返回假,调用ZwRaiseException进行第二轮异常分发 (参见KiUserExceptionDispatcher代码) ``` 
发表于 2022-05-05 09:54:42
阅读 ( 6624 )
分类:
漏洞分析
1 推荐
收藏
0 条评论
请先
登录
后评论
szbuffer
30 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!