什么是 SafeSEH ?

测试代码:

(借用网上的代码,通过这段代码编译分析)

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/*---------------------------------------------------------------------
SEH_Excp.cpp - Sample to explore exception handling of SEH.
Software Debugging by Raymond Zhang, All rights reserved.
---------------------------------------------------------------------*/
#include <excpt.h>
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>

char g_szDefPara[] = "0123456789";
int ExcptionFilter(LPEXCEPTION_POINTERS pException, char** ppPara)
{
PEXCEPTION_RECORD pER = pException->ExceptionRecord;
PCONTEXT pContext = pException->ContextRecord;

printf("Exception Info: code=%08X, addr=%08X, flags=%X.\n",
pER->ExceptionCode, pER->ExceptionAddress,
pER->ExceptionFlags);

printf("Context Info: EIP=%08X, EAX=%08X.\n",
pContext->Eip, pContext->Eax);

if (*ppPara == NULL && pER->ExceptionCode == STATUS_ACCESS_VIOLATION)
{
*ppPara = g_szDefPara;
pContext->Eip -= 3;
printf("New EIP=%08X and *ppPara=%s.\n",
pContext->Eip, *ppPara);
return EXCEPTION_CONTINUE_EXECUTION;
}

return EXCEPTION_EXECUTE_HANDLER;
}
void FuncA(char* lpsz)
{
printf("Entering FuncA with lpsz=%s.\n", lpsz);
__try
{
*lpsz = '2';
}
__except (ExcptionFilter(GetExceptionInformation(), &lpsz))
{
printf("Exexcuting handling block in FuncB.\n");
}
printf("Exiting from FuncA with lpsz=%s.\n", lpsz);
}
void SehPrint(char* szName, LPEXCEPTION_POINTERS pException)
{
PEXCEPTION_RECORD pER = pException->ExceptionRecord;
printf("%s:%x\n", szName, pER->ExceptionFlags);
}
int FuncB(int nPara)
{
printf("Entering FuncB with Para=%d.\n", nPara);
__try
{
nPara = 1 / nPara;

*(int*)0 = 1;
}
__except (SehPrint("FuncB", GetExceptionInformation()),
GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("Executing handling block in FuncB [%X].\n",
GetExceptionCode());
}
printf("Exiting from FuncB with Para=%d.\n", nPara);
return nPara;
}
int main(int argc, char* argv[])
{
int nRet = 0;

FuncA(argv[1]);

__try
{
nRet = FuncB(argc - 1);
}
__except (SehPrint("main", GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER)
{
printf("Executing exception handling block in main [%X].\n",
GetExceptionCode());
}

printf("Exit from main with nRet=%d.\n", nRet);
return nRet;
}

在了解 SafeSEH 之前,我们要首先了解什么是 SEH, 然后才知道为什么会出现 SafeSEH 这个东西。

1. 结构化异常处理(SEH)

结构化异常处理 (SEH) 是 C 和 C++ 的 Microsoft 扩展,用于处理某些特殊代码情况,例如硬件故障,正常。 尽管 Windows 和 Microsoft C++ 支持 SEH,但我们建议在 C++ 代码中使用 ISO 标准 C++ 异常处理。 它提高了代码的可移植性和灵活性。 但是,为了维护现有代码,或者对于特定类型的程序,你仍可能必须使用 SEH。

更加详细的内容,请参考微软的官方文档:https://learn.microsoft.com/zh-cn/cpp/cpp/structured-exception-handling-c-cpp?view=msvc-170

我们下面说的是具体结构问题。

保存结构化异常,在系统内部需要一套结构来保存处理的函数数据, 所有的结构化异常数据都保存在 TEB 中, 也就是线程环境块,是系统为每一个线程分配的一个线程管理的数据块,其中又包括了线程信息块 TIB, TIB 块是 TEB 结构的第一部分,在windows系统中, TIB 的结构为 _NT_TIB, 所以,我们可以通过 TEB的地址,来获取 _NT_TIB 的数据内容。如下:

通过上面的地址,我们分析 _NT_TIB 结构,如下:

我们通过上面的结构,就能看到异常处理的链,保存的是一个 _EXCEPTION_REGISTRATION_RECORD 的结构, 那么我们查看这个结构,数据如下:

我们得到相关的 _EXCEPTION_REGISTRATION_RECORD 的结构如下图:

  • _EXCEPTION_REGISTRATION_RECORD 指向下一个异常结构

  • _EXCEPTION_DISPOSITION 异常处理的代码地址

我们继续分析程序,因为上述的结构化异常处理是一个链表,我们打印几个点查看相关的数据,如下图:

我们上图能看处理,异常处理的 Handler 正好是我们程序的异常处理的地方:

1
+0x004 Handler          : 0x00ec1ad0     _EXCEPTION_DISPOSITION  test_safeseh!_except_handler4+0

我们通过上图知道,我们自己的异常处理结构的位置为:0x0136fc0c, 然后我们查看一下 esp的值,也就是当前函数栈帧的所在地址范围, 如下图:

通过上图,我们可以看出来, 异常处理的结构基本就是在栈帧的所在范围。记住这一点,后续的对于通过利用 SEH 实现漏洞利用很重要。

2. 什么是 SafeSEH ?

SafeSEH(安全结构化异常处理程序)是一种针对 32 位可执行文件的 Windows 二进制保护机制,已经存在了一段时间。启用该选项后,链接器会在构建二进制文件时在 SEHandlerTable 中创建一个有效异常处理程序地址列表。这种保护可防止执行损坏的异常处理程序,这是一种常见的利用技术。当抛出异常并且处理程序的地址由攻击者控制时,他们可以使用的地址选择有限。由于所有现代操作系统上都有 DEP(数据执行保护),攻击者选择的地址必须是可执行的,通常这些地址仅限于可执行模块的 .text 部分内的地址。在启用 SafeSEH 的模块中选择地址时,会将其与 SEHandlerTable 中的有效地址列表进行比较,除非找到,否则不会执行。

注意: SafeSEH 只针对 32位的程序,对于64位程序,有其他的处理办法来实现类似思路。

1. VisualStudio开发中设置 /SafeSEH 选项

我们本身的测试程序是加入了 /SafeSEH 选项的,那么我们查看PE结构如下:

SE Handler Table 地方存在,这个地方就是保留的 SafeSEH 的一个对照表,用于防止 SEH 被修改

我们在看看,把 /SafeSEH 选项去掉的情况,如下图:

发现 SE Handler Table 已经是空的了,说明不存在这个表了。

3. 关于x64下的SEH

SEH 在 x86下是通过Stack保存的,但在x64位下,是通过PE的节保存的: .pdata, 参考的文章可以看:

参考

  1. 结构化异常SEH处理机制详细介绍(一) : https://www.cnblogs.com/yilang/p/11233935.html

  2. Analyzing Safe Exception Handlers: https://warroom.rsmus.com/analyzing-safe-exception-handlers/

  3. /SafeSEH选项https://learn.microsoft.com/en-us/cpp/build/reference/safeseh-image-has-safe-exception-handlers?view=msvc-170


什么是 SafeSEH ?
https://xxxxnnxxxx.github.io/2023/07/19/What's the SafeSEH/
作者
xxxxnnxxxx
发布于
2023年7月19日
许可协议