avatar

Catalog
SEH

前言

在学习了用户异常的分发后了解到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都有一个结构。其结构可如下:

c
1
2
3
4
5
6
//SEH结构体
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD* Next; //下一个节点,-1就是没有下一个节点了
PEXCEPTION_ROUTINE Handler; //指向异常处理函数
} EXCEPTION_REGISTRATION_RECORD;

堆栈中将这些SEH链接在一起就构建出一个如文章开头图片所示的异常链表。需要注意的是,SEH结构体是可扩展的,例如在前一篇学习VEH时,当调用AddVectoredExceptionHandler将VEH添加进全局链表时,可以看到VEH结构体有3个字段,比SEH多出一个prev字段,它用来指向前一个VEH。

c
1
2
3
4
5
6
//VEH结构体
typedef struct _VEH_REGISTRATION{
_VEH_REGISTRATION* next;
_VEH_REGISTRATION* prev;
PVECTORED_EXCEPTION_HANDLER pfnVeh;
}VEH_REGISTRATION, *PVEH_REGISTRATION;

SEH添加

在VEH一篇中学习过VEH的添加,仅仅需要定义VEH的异常处理函数,然后调用AddVectoredExceptionHandler就可以将VEH添加进全局链表,在调用AddVectoredExceptionHandler时会自动构建VEH结构。

SEH则有所不同,VEH是全局链表,微软提供了相应的API函数;而SEH是局部链表,需要自己手动插入,首先需要定义一个SEH结构体(至少要有两个成员,next和Handler),然后定义SEH异常处理函数,其原型如下:

c
1
2
3
4
5
6
7
EXCEPTION_DISPOSITION _cdecl MyEexception_handler
(
struct _EXCEPTION_RECORD *ExceptionRecord, //异常记录结构体
PVOID EstablisherFrame, //SEH结构体地址
struct _CONTEXT *ContextRecord, //存储异常发生时的上下文环境
PVOID DispatcherContext
)

函数内部实现处理相应异常的功能,之后让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插入链表中。代码如下:

c
1
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include "stdafx.h"
#include

//最原始的 SEH链表结构(这个结构怎么写都行)
struct _EXCEPTION
{
struct _EXCEPTION* Next;
DWORD Handler;
};

EXCEPTION_DISPOSITION _cdecl MyEexception_handler
(
struct _EXCEPTION_RECORD *ExceptionRecord, //异常结构体
PVOID EstablisherFrame, //SEH结构体地址
struct _CONTEXT *ContextRecord, //存储异常发生时的各种寄存器的值 栈位置等
PVOID DispatcherContext
)
{
MessageBox(NULL, "SEH异常处理函数执行了...", "SEH异常",NULL);
if (ExceptionRecord->ExceptionCode == 0xC0000094)
{
//ContextRecord->Eip = ContextRecord->Eip + 2;

ContextRecord->Ecx = 100;

return ExceptionContinueExecution;
}
return ExceptionContinueSearch;
}


int main()
{
DWORD temp;
_EXCEPTION Exception; //必须在当前线程堆栈的堆栈中

//fs[0]-> Exception
_asm
{
mov eax, fs:[0]
mov temp,eax
lea ecx,Exception
mov fs:[0],ecx
}
//为SEH成员赋值
Exception.Next = (_EXCEPTION*)temp;
Exception.Handler = (DWORD)&MyEexception_handler;

//创建异常
int val = 0;
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,1
idiv ecx //edx = eax /ecx

mov val,ecx
}

//摘除刚插入的SEH
_asm
{
mov eax, temp
mov fs:[0],eax
}

printf("val = %d",val);

getchar();
return 0;
}

同样,这里采用两种处理方案,只不过这次打印的值为ecx。

  • 修改Ecx的情况如下
  • 修改Eip的情况如下

SEH实战

2022年在打KCTF春季赛时,遇到一题会通过引发异常,来修改程序的执行流程,这样动态调试的时候就十分费劲,经常容易跑飞,在mb_mgodlfyn的分析文章中对比较关键的几处都进行了相应的 patch,并分享了两篇不错的 SEH 参考资料。这里记录下来,以后复习时也将此资料进行复习:

总结

  1. FS:[0]指向SEH链表的第一个成员
  2. SEH链表必须在当前线程的堆栈中
  3. 只有当VEH中的异常处理函数不存在或者不处理时才会轮到SEH链表中查找(处理用户异常时)
  4. 比起VEH,SEH有着更大的利用空间,通过获取到的上下文环境,让异常处理后的程序执行你想要执行的任意代码。

参考资料

参考书籍:

  • 《软件调试 卷2:Windows平台调试》p249~p259 —— 张银奎

参考教程:

  • 海哥逆向中级班

参考链接:

  1. CSDN:My classmates-SEH学习机笔记
  2. CSDN:鬼手56-VEH/SEH学习笔记
  3. 看雪:2022 KCTF 春季赛 第三题 石像病毒
  4. 简书:看雪CTF签到题SEH异常处理–MysteriousLetter2
  5. CSDN:SEH的非常好的总结
Author: cataLoc
Link: http://cata1oc.github.io/2020/08/23/SEH/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶