前言
在学习了用户异常的分发后了解到KiUserExceptionDispatcher会调用RtlDispatchException函数来查找并调用异常处理函数,类似的内核异常处理时也会调用0环的RtlDispatchException函数来查找处理函数。
上一篇在学习VEH时比对过两者的差异,即处理用户异常时会先查找VEH,再查找SEH;而处理内核异常仅查找SEH。VEH在前一篇已经分析过,本篇就来学习一下SEH。
ExceptionList
fs:[0]
在学习内核异常的分发时,已经了解过异常链表(也就是SEH)的存在,在内核通过FS:[0]可以找到链表头,并通过Next指针依次找到下一个异常处理函数。那么处理用户异常时,是如何找到ExceptionList(异常链表)的呢?
由图,0环的FS:[0]指向KPCR,3环的FS:[0]指向TEB,观察图中TEB结构,可以发现,TEB的第一个成员也是NtTib,又NtTib的第一个成员就是ExceptionList,所以在3环时,也是通过FS:[0]找到异常链表的。
SEH结构
SEH,即结构化异常处理(Structured Exception Handling),异常链表中的每个SEH都有一个结构。其结构可如下:
1 | //SEH结构体 |
在堆栈中将这些SEH链接在一起就构建出一个如文章开头图片所示的异常链表。需要注意的是,SEH结构体是可扩展的,例如在前一篇学习VEH时,当调用AddVectoredExceptionHandler将VEH添加进全局链表时,可以看到VEH结构体有3个字段,比SEH多出一个prev字段,它用来指向前一个VEH。
1 | //VEH结构体 |
SEH添加
在VEH一篇中学习过VEH的添加,仅仅需要定义VEH的异常处理函数,然后调用AddVectoredExceptionHandler就可以将VEH添加进全局链表,在调用AddVectoredExceptionHandler时会自动构建VEH结构。
SEH则有所不同,VEH是全局链表,微软提供了相应的API函数;而SEH是局部链表,需要自己手动插入,首先需要定义一个SEH结构体(至少要有两个成员,next和Handler),然后定义SEH异常处理函数,其原型如下:
1 | EXCEPTION_DISPOSITION _cdecl MyEexception_handler |
函数内部实现处理相应异常的功能,之后让SEH结构体的handler指向该异常处理函数。这样,SEH结构体就构造完成了。
接下来,就需要将SEH插入到ExceptionList中,ExceptionList的链表头位于FS:[0]的位置,相当于第一个SEH结构体的首地址,获取这个地址,令新的SEH结构体的next指向这个地址,并修改FS:[0]指向的地址为当前SEH块的地址,这样,就相当于把新构建的SEH插入到了异常链表的首位,完成了SEH添加,具体的操作方法会在后面的代码示出。
RtlDispatchException
在处理用户异常时,3环的RtlDispatchException会先查找VEH,再去查找SEH。前一篇分析过会调用函数RtlCallVectoredExceptionHandlers来遍历VEH链表,寻找对应的异常处理函数。本篇来看看它是如何实现查找SEH链表的。
RtlpGetStackLimits
首先来看这个函数,调用它获取了FS:[8]和FS:[4]的值。先来看看这两个值分别是什么。
可以看出,调用该函数目的是为了获取StackBase以及StackLimit的值,这两个字段在线程切换时我们经常见到,用来描述一个线程栈空间的大小。
这么做的目的是,SEH链表必须位于线程的栈空间,因此不能超出这两个值的范围。从而调用该函数用来检测SEH链表是否位于线程的堆栈中。
RtlpGetRegistrationHead
这里和内核异常处理时一样,用来获取SEH链表头,只不过内核处理时查找链表头的流程为:
- FS:[0] -> KPCR -> NtTib -> ExceptionList
用户异常时流程为:
- FS:[0] -> TEB -> NtTib -> ExceptionList
查找的都是FS:[0],但是0环与3环对应的结构不同,最终都能查到当前线程堆栈的SEH链表头。
验证与执行
再往下,先调用了RtlIsValidHandler函数,用来验证异常处理函数是否有效。再调用RtlpExecuteHandlerForException函数,执行异常处理函数。
SEH实验
至此,通过观察3环RtlDispatchException函数的执行流程,了解了用户异常查找SEH链表并调用异常处理函数的过程。这里进行一个小实验,与VEH的实验一样,构造一个SEH,将其插入SEH链表,并触发异常调用自己构造的SEH的异常处理函数。不太一样的时候,SEH需要手动构造一个结构,并需要手动将SEH插入链表中。代码如下:
1 |
|
同样,这里采用两种处理方案,只不过这次打印的值为ecx。
SEH实战
2022年在打KCTF春季赛时,遇到一题会通过引发异常,来修改程序的执行流程,这样动态调试的时候就十分费劲,经常容易跑飞,在mb_mgodlfyn的分析文章中对比较关键的几处都进行了相应的 patch,并分享了两篇不错的 SEH 参考资料。这里记录下来,以后复习时也将此资料进行复习:
总结
- FS:[0]指向SEH链表的第一个成员
- SEH链表必须在当前线程的堆栈中
- 只有当VEH中的异常处理函数不存在或者不处理时才会轮到SEH链表中查找(处理用户异常时)
- 比起VEH,SEH有着更大的利用空间,通过获取到的上下文环境,让异常处理后的程序执行你想要执行的任意代码。
参考资料
参考书籍:
- 《软件调试 卷2:Windows平台调试》p249~p259 —— 张银奎
参考教程:
- 海哥逆向中级班
参考链接: