SEH原理和例题

SEH原理和例题

WINDOWS下的异常处理

异常列表

参考文章

https://www.cnblogs.com/Sna1lGo/p/14732048.html#:~:text=Windows中,则直接程序崩溃。

https://bbs.pediy.com/thread-249592.htm

SEH

SEH介绍

SEH(Structured Exception Handling)结构化异常处理,是windows操作系统默认的错误处理机制,它允许我们在程序产所错误时使用特定的异常处理函数处理这个异常,尽管提供的功能预取为处理异常,但由于其功能的特点,也往往大量用于反调试。

重要成员介绍

1、异常处理函数

1
2
3
4
5
6
7
EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
);

第一个参数指向结构体EXCEPTION_RECORD

操作系统常见异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
EXCEPTION_ACCESS_VIOLATION     0xC0000005     程序企图读写一个不可访问的地址时引发的异常。例如企图读取0地址处的内存。
EXCEPTION_ARRAY_BOUNDS_EXCEEDED 0xC000008C 数组访问越界时引发的异常。
EXCEPTION_BREAKPOINT 0x80000003 触发断点时引发的异常。
EXCEPTION_DATATYPE_MISALIGNMENT 0x80000002 程序读取一个未经对齐的数据时引发的异常。
EXCEPTION_FLT_DENORMAL_OPERAND 0xC000008D 如果浮点数操作的操作数是非正常的,则引发该异常。所谓非正常,即它的值太小以至于不能用标准格式表示出来。
EXCEPTION_FLT_DIVIDE_BY_ZERO 0xC000008E 浮点数除法的除数是0时引发该异常。
EXCEPTION_FLT_INEXACT_RESULT 0xC000008F 浮点数操作的结果不能精确表示成小数时引发该异常。
EXCEPTION_FLT_INVALID_OPERATION 0xC0000090 该异常表示不包括在这个表内的其它浮点数异常。
EXCEPTION_FLT_OVERFLOW 0xC0000091 浮点数的指数超过所能表示的最大值时引发该异常。
EXCEPTION_FLT_STACK_CHECK 0xC0000092 进行浮点数运算时栈发生溢出或下溢时引发该异常。
EXCEPTION_FLT_UNDERFLOW 0xC0000093 浮点数的指数小于所能表示的最小值时引发该异常。
EXCEPTION_ILLEGAL_INSTRUCTION 0xC000001D 程序企图执行一个无效的指令时引发该异常。
EXCEPTION_IN_PAGE_ERROR 0xC0000006 程序要访问的内存页不在物理内存中时引发的异常。
EXCEPTION_INT_DIVIDE_BY_ZERO 0xC0000094 整数除法的除数是0时引发该异常。
EXCEPTION_INT_OVERFLOW 0xC0000095 整数操作的结果溢出时引发该异常。
EXCEPTION_INVALID_DISPOSITION 0xC0000026 异常处理器返回一个无效的处理的时引发该异常。
EXCEPTION_NONCONTINUABLE_EXCEPTION 0xC0000025 发生一个不可继续执行的异常时,如果程序继续执行,则会引发该异常。
EXCEPTION_PRIV_INSTRUCTION 0xC0000096 程序企图执行一条当前CPU模式不允许的指令时引发该异常。
EXCEPTION_SINGLE_STEP 0x80000004 标志寄存器的TF位为1时,每执行一条指令就会引发该异常。主要用于单步调试。
EXCEPTION_STACK_OVERFLOW 0xC00000FD 栈溢出时引发该异常。

异常回调函数_except_handler的第二个参数是一个指向establisher frame结构体的指针,这是SEH中一个很重要的参数,但是现在暂时忽略。

第三个参数是一个指向结构体CONTEXT的指针,CONTEXT结构体定义在WINNT.H里,它代表了特定线程的注册值。当用在SEH时,CONTEXT就表示异常发生时的注册值。顺带说一句,这个CONTEXT结构体与GetThreadContext和SetThreadContext所使用的结构体是同一个。

第四个参数DispatcherContext也可以暂时忽略。

返回值

根据不同的返回值,确定是否处理

1
2
3
4
5
4种返回值及含义
1.ExceptionContinueExecution(0):回调函数处理了异常,可以从异常发生的指令处重新执行。
2.ExceptionContinueSearch(1):回调函数不能处理该异常,需要要SEH链中的其他回调函数处理。
3.ExceptionNestedException(2):回调函数在执行中又发生了新的异常,即发生了嵌套异常
4.ExceptionCollidedUnwind(3):发生了嵌套的展开操作

CONTEXT结构体

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
typedef struct _CONTEXT
{
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
} CONTEXT;

2 、结构体EXCEPTION_RECORD

1
2
3
4
5
6
7
8
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

第一个参数ExceptionCode是一个由操作系统分配给异常的数值,在WINNT.H里用#define定义了一系列的由STATUS_为前缀的异常代码,比如STATUS_ACCESS_VIOLATION 的异常代码是 0xC0000005,我们可以从Windows NT DDK的头文件NTSTATUS.H中找到更加完备的异常代码。可以理解为异常的类型
第四个参数ExceptionAddress异常发生的地址。
其他的参数可以暂时忽略。

3 、EXCEPTION_REGISTRATION

1
2
3
4
EXCEPTION_REGISTRATION struc
prev dd ?
handler dd ?
_EXCEPTION_REGISTRATION ends

SEH是一个链表,而这个结构存储每个结点的信息,第一个成员指向下一个结点的handler,第二个成员handler指向处理异常的函数,每次添加一个异常就会添加在SEH链表的头结点位置,也就是头插法

那么程序是怎么找到这个链表的呢

线程信息块的第一个DWORD(在基于Intel CPU的机器上是FS:[0])指向这个链表的头部。

5、线程信息块TIB/TEB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; //异常的链表

PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;

union {
PVOID FiberData;
DWORD Version;
};

PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;

结构图

重要成员结构图

异常处理流程

当异常出现时,程序先判断是否处于调试状态,如果处于调试状态,那么就会向调试器发送EXCEPTION_DEBUG_EVENT事件。当异常抛给调试器后,调试器有两种选择

1 修改触发异常的代码继续执行(程序会停在触发异常的代码处,导致异常的代码无法执行)

2 忽略异常交给SEH执行

这里假如选择第二种,那么 系统检查异常所处的线程并在这个线程环境中查看fs:[0]来确定是否安装SEH异常处理回调函数,如果有则调用它

异常处理函数执行

(1)回调函数尝试处理这个异常,如果可以正确处理的话,则修正错误并将返回值设置为ExceptionContinueExecution,这时系统将结束整个查找过程。

(2)如果回调函数返回ExceptionContinueSearch,相当于告诉系统它无法处理这个异常,系统将根据SEH链中的prev字段得到前一个回调函数地址并重复步骤1,直至链中的某个回调函数返回ExceptionContinueExection为止,查找结束。

如果调试器还是不去处理这个异常或进程没有被调试,那么系统检查有没有Final型的异常处理回调函数(也就是C语言中的__finally),如果有,就去调用它,当这个回调函数返回时,系统会根据这个函数的返回值做相应的动作。

从这里我们也可以看到__finally的代码是一定会执行的

如果没有安装Final型回调函数,系统直接调用默认的异常处理程序终止进程。

堆栈展开

(1)什么是展开操作

①当发生异常时,系统遍历EXCEPTION_REGISTRATION结构链表,从链表头,直到它找到一个处理这个异常的处理程序。一旦找到,系统就再次重新遍历这个链表,直到处理这个异常的节点为止(即返回ExceptionContinueExecution的节点)。在这第二次遍历中,系统将再次调用每个异常处理函数。关键的区别是,在第二次调用中,异常标志被设置为2。这个值被定义为EH_UNWINDING

②注意展开操作是发生在出现异常时,这个异常回调函数拒绝处理的时候(即返回ExceptionContinueSearch)。这里系统会从链表头开始遍历(因异常的嵌套,可理解为由内层向外层,也就是多层的__try和except函数时,先从内部开始,然后往外),所以各异常回调函数会第1次被依次调用,直到找到同意处理的节点。然后,再重新从链表头开始(即由内向外)第2次调用以前那些曾经不处理异常的节点,直到同意处理的那个异常的节点为止。

③ 当异常被处理后,即异常代码执行完毕后,程序恢复的位置由处理该异常的函数决定。即,当异常已经被处理完毕,并且所有前面的异常帧都已经被展开之后,流程从处理异常的那个回调函数决定的地方开始继续执行。

④展开操作完成后,同意处理异常的回调函数也必须负责把Fs:[0]恢复到处理这个异常的EXCEPTION_REGISTRATION上,即展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除,这个异常处理也就成了SEH链表的第1个节点。这样下次遇到异常时还能保证从内层到外层寻找的流程。

异常处理的实现

1、__try、except和finally

Visual C++中的__try{}/__finally{}和__try{}/__except{}结构本质上是对Windows提供的SEH的封装。

try函数必须且只能跟着一个except或者一个finally,

当try块中的代码发生异常时,__except()中的过滤程序就被调用。

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
#include<stdio.h>
#include<string.h>
#include<Windows.h>
int handle()
{
printf("handling\n");
return 1;
}
int main()
{
__try
{
__try
{
int x = 0;
int y = x / x;//构造一个除0异常,交由handle进行处理
}
__finally
{
printf("111\n");//无论是否处理成功,都会执行
}
}
__except (handle())//当返回值为-1,表示返回到原来的地方重新执行,0的话表示异常处理失败,1则为成功
{
printf("222\n");
}
return 0;
}

执行结果

这里我们构造了一个除零异常,遇到异常后抛给except的处理函数,返回1(EXCEPTION_EXECUTE_HANDLER)表示处理完成,然后再回来执行finally的代码,但是返回值改为-1的话,他会继续回到出现异常的地方,然后执行,那么就会一直在异常处理进行

返回值为-1

返回值为EXCEPTION_CONTINUE_SEARCH(0)时,会往外层找,因为我们这里就只有一个except,所以无法往外找,所以我们修改一下代码

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
#include<stdio.h>
#include<string.h>
#include<Windows.h>
int handle1()
{
printf("成功解决\n");
return EXCEPTION_EXECUTE_HANDLER;
}
int handle()
{
printf("未被解决\n");
return EXCEPTION_CONTINUE_SEARCH;
}
int main()
{
__try
{
__try
{
int x = 0;
int y = x / x;//构造一个除0异常,交由handle进行处理
}
__except(handle())
{
}
}
__except (handle1())//当返回值为-1,表示返回到原来的地方继续执行,0的话表示异常处理失败,1则为成功
{
printf("222\n");
}
return 0;
}

这里我们嵌套了两层except,从内往外执行,因为内层的无法处理,即返回0,那么就往外找处理异常的函数

执行结果

ida中的效果

可以看到ida会自动加注释

2、插入汇编代码构造

我们下面插入汇编来创建一个异常的结构体

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
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>

DWORD scratch;

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
struct _EXCEPTION_RECORD* ExceptionRecord,
void* EstablisherFrame,
struct _CONTEXT* ContextRecord,
void* DispatcherContext)
{
unsigned i;

printf("Hello from an exception handler\n");

ContextRecord->Eax = (DWORD)&scratch;//处理异常,将eax的值改为全局变量的地址

return ExceptionContinueExecution;
}

int main()
{
DWORD handler = (DWORD)_except_handler;

__asm
{
push handler //将我们的异常处理函数压入栈中
push FS : [0] //将原本的SEH链压入栈,这样就构造了新的EXCEPTION_REGISTRATION
mov FS : [0] , ESP //将当前线程信息块TIB的第一个DWORD放入新的EXCEPTION_REGISTRATION
}

__asm
{
mov eax, 0 //构造访问异常
mov[eax], 1
}

printf("After writing!\n");

__asm
{
mov eax, [ESP]
mov FS : [0] , EAX //还原为原来的SEH链
add esp, 8 //从栈中弹出新的EXCEPTION_REGISTRATION
}

return 0;
}

堆栈图

也就是先将我们处理异常的函数压入栈中,再把SEH链的头节点压入栈,这时候把ESP的值赋给FS:[0]也就是让SEH链指向这里,SEH链第一个节点(两个参数)第一个存储的就是原来链的地址,第二个是我们的处理函数,那么处理异常的时候,就会先让我们的异常处理函数执行,最后将ESP地址存储的值还给FS:[0],也就是还原SEH链,add esp,8还原堆栈

别人的另一种实现

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
//1.挂入链表相当于这部分
//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;

//下面是2,3
EXCEPTION_DISPOSITION _cdecl MyEexception_handler
(
struct _EXCEPTION_RECORD *ExceptionRecord, //异常结构体
PVOID EstablisherFrame, //SEH结构体地址
struct _CONTEXT *ContextRecord, //存储异常发生时的各种寄存器的值 栈位置等
PVOID DispatcherContext
)
{
if (ExceptionRecord->ExceptionCode == 0xC0000094) //2.异常过滤
{
ContextRecord->Eip = ContextRecord->Eip + 2; //3.异常处理
ContextRecord->Ecx = 100;

return ExceptionContinueExecution;
}
return ExceptionContinueSearch;
}

参考文章

https://www.cnblogs.com/yilang/p/11238201.html

https://www.cnblogs.com/DeeLMind/p/6866239.html

https://www.cnblogs.com/salomon/archive/2012/06/20/2556134.html

https://bbs.pediy.com/thread-249592.htm

SEH利用

https://idiotc4t.com/code-and-dll-process-injection/seh-code-execute

https://introspelliam.github.io/2017/06/29/0day/windows-异常处理中的漏洞利用/

https://bbs.pediy.com/thread-268036.htm

https://bbs.pediy.com/thread-140970.htm

简单利用

我们可以自己构造异常,然后先处理掉异常,再执行我们的shellcode

题目——hagme2022——week2——creakme2

在静态分析中,只找到了tea加密算法,直接写脚本解不出来,去查看汇编代码(因为有时候ida没办法识别出一些汇编)

第一个框对应上上面那句代码,这里按/就会显示出来对应反编译过来的伪代码

下面两个框的都是没被识别的语句

在图中位置下断点进行动态调试,会报错

因为除数不能为0,去看看汇编

按;写下注释

开始动态调试

选择yes,发现进入到这段未被反编译的语句

第二次执行到这一段代码,可以看到ecx寄存器的值不是0,不会触发异常

发现没有执行异或,而是继续往下执行,这就是下一步进行的反汇编语句

因此可以知道,当变量num的最高位为0的时候,会触发异常,这时候系统会交给SEH进行处理,即__try代码

__except会执行异常后代码

按空格查看执行顺序

会发现这样一段独立出来的汇编代码

所以就可以写脚本,因为是unsigned int,右移31位就能知道最高位了

脚本

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
#include <stdio.h>
#include <string.h>



int main()
{
unsigned int tmp1, tmp2;
unsigned int num;
unsigned int init_num=0;
int Buf2[8] = { 0 };
unsigned int key[10] = { 1,2,3,4,5,6,7,8.9,0 };
Buf2[0] = 0x457E62CF;
Buf2[1] = 0x9537896C;
Buf2[2] = 0x1F7E7F72;
Buf2[3] = 0xF7A073D8;
Buf2[4] = 0x8E996868;
Buf2[5] = 0x40AFAF99;
Buf2[6] = 0xF990E34;
Buf2[7] = 0x196F4086;

for (int i = 0; i < 32; i++)
{
init_num += 2654435761;
if ((init_num >>31) == 0)
{
init_num ^= 0x1234567;
}
}//0C78E4D05

for (int j = 0; j < 8; j += 2)
{
tmp1 = Buf2[j], tmp2 = Buf2[j + 1];
num = init_num;
for (int i = 0; i < 32; i++)
{
tmp2 -= (num + key[(num >> 11) & 3]) ^ (tmp1 + ((16 * tmp1) ^ (tmp1 >> 5)));
if ((num>>31) == 0)
{
num ^= 0x01234567;
}
num -= 2654435761;
tmp1 -= (num + key[num & 3]) ^ (tmp2 + ((16 * tmp2) ^ (tmp2 >> 5)));

}
Buf2[j] = tmp1;
Buf2[j + 1] = tmp2;
}
for (int i = 0; i < 8; i++)
{
printf("%x\n", Buf2[i]);
}
printf("%s", Buf2);
}