SafeSEH对异常处理的保护原理:

编译选项/SafeSEH启动,VS2003以后默认启用。

生成SafeSEH表,放在PE文件中,调用异常处理函数的时候,将地址与SafeSEH表中的地址比较。

检查异常处理链是否位于当前程序的栈中,如果不在栈中,则程序终止异常处理函数的调用。

检查异常处理函数指针是否在程序的栈中,如果指向当前栈中,则终止异常处理函数的调用。

前面两项检查都通过后,程序调用一个全新的函数 RtlIsValidHandler ,对异常处理函数的有效性进行验证。

RtlIsValidHandler检测原理:

首先,该函数判断异常处理函数地址是不是在加载模块的内存空间,如果属于加载模块的内存空间,校验函数将依次进行如下校验:

(1)判断程序是否设置了IMAGE_DLLCHARACTERISTICS_NO_SEH 标识。如果设置了这个标识,这个程序内的异常会被忽略。所以当这个标志被设置时,函数直接返回校验失败。

(2)检测程序是否包含安全S.E.H 表。如果程序包含安全S.E.H 表,则将当前的异常处理函数地址与该表进行匹配,匹配成功则返回校验成功,匹配失败则返回校验失败。

(3)判断程序是否设置ILonly 标识。如果设置了这个标识,说明该程序只包含.NET 编译人中间语言,函数直接返回校验失败。

(4)判断异常处理函数地址是否位于不可执行页(non-executable page)上。当异常处理函数地址位于不可执行页上时,校验函数将检测DEP 是否开启,如果系统未开启DEP 则返回校验成功,否则程序抛出访问违例的异常。

如果异常处理函数的地址没有包含在加载模块的内存空间,校验函数将直接进行DEP 相关检测,函数依次进行如下校验:

(1)判断异常处理函数地址是否位于不可执行页(non-executable page)上。当异常处理函数地址位于不可执行页上时,校验函数将检测DEP 是否开启,如果系统未开启DEP 则返回校验成功,否则程序抛出访问违例的异常。

(2)判断系统是否允许跳转到加载模块的内存空间外执行,如果允许则返回校验成功,否则返回校验失败。

RtlIsValidHandler允许异常函数执行的情况

1)异常处理函数位于加载模块内存范围之外,DEP 关闭。

2)异常处理函数位于加载模块内存范围之内,相应模块未启用SafeSEH(安全S.E.H 表为空),同时相应模块不是纯IL。

3)异常处理函数位于加载模块内存范围之内,相应模块启用SafeSEH(安全S.E.H 表不为空),异常处理函数地址包含在安全SEH表中。

分析一下这三种情况的可行性。

(1)现在我们只考虑SafeSEH,不考虑DEP。排除DEP 干扰后,我们只需在加载模块内存范围之外找到一个跳板指令就可以转入shellcode 执行,这点还是比较容易实现的。

(2)在第二种情况中,我们可以利用未启用SafeSEH 模块中的指令作为跳板,转入shellcode执行,这也是为什么我们说SafeSEH 需要操作系统与编译器的双重支持。在加载模块中找到一个未启用的SafeSEH 模块也不是一件很困难的事情。

(3)这种情况下我们有两种思路可以考虑,一是清空安全S.E.H 表,造成该模块未启用SafeSEH 的假象;二是将我们的指令注册到安全S.E.H 表中。由于安全S.E.H 表的信息在内存中是加密存放的,所以突破它的可能性也不大,这条路我们就先放弃吧。

利用SafeSEH的缺陷

利用S.E.H 的终极特权!这种安全校验存在一个严重的缺陷——如果S.E.H 中的异常函数指针指向堆区,即使安全校验发现了S.E.H 已经不可信,仍然会调用其已被修改过的异常处理函数,因此只要将shellcode 布置到堆区就可以直接跳转执行!

绕过SafeSEH

1.攻击返回地址绕过

2.虚函数绕过

3.从堆中绕过 :shellcode布置在堆中 ,SEH处理函数指向这个地址即可

4.利用未启用SafeSEH模块绕过 :可以把这个模块的指令作为跳板,去执行shellcode

5.加载模块之外的地址绕过 :内存中有一些Map类型的映射文件,在这些文件中找到跳板指令覆盖SEH处理函数地址即可绕过

6.利用未启用SafeSEH的控件,且控件包含溢出漏洞可以被触发(IE浏览器控件)

实践利用加载模块之外的地址

1.我们使用上一篇中的代码,稍微修改来测试,关闭GS DEP ASLR, 开启 SafeSEH ,如果你有VC6 ,最好使用它来编译。

#include 
#include 
 
int zero = 0;
 
int MyException()
{
    printf("Error OverFlow %d", zero);
    return 1;
}
 
void __stdcall test(char* str, char* out)
{
    char buf[0x500] = { 0 };
 
    __try
    {
        strcpy(buf, str);
        zero  = 1 / zero;
 
    }
    __except (MyException())
    {
 
    }
}
 
 
int main(int arc, char** argv)
{
    char buf1[200];
    test(argv[1], buf1);
    return 0;
}

2.先用IDA查看一下代码,因为我用VS2019编译, 编译器会扩展SEH。

可以看到, 这里使用了第3代的异常处理模型 ,往栈中放入了不少东西,会影响我们的偏移。

用od插件搜索一下,都开启了SafeSEH保护。

3.调试一下看看,可以看到,输入0x500个字节的A后, 还差12个字节才可以覆盖到Handler。

修改参数 ,再次调试查看。

好的,现在可以看到,Handler已经被覆盖为 C , 那么现在需要找到跳板地址来跳到shellcode。

之前已经看过,所有模块都启用了SafeSEH,那么我们需要找到加载模块之外的跳板地址,内存映射查看 MAP类型的地址。

那么我们需要什么样的跳板指令呢?

观察寄存器,发现eax指向我们溢出的缓冲区,那么是否可以利用 jmp eax , call eax,来跳转到shellcode执行(答案是不行,eax是一个易失寄存器,在转到异常处理函数的过程中会被修改)。

好的, Next先不管,Handler需要什么样的跳板指令呢,按照之前利用SEH的总结, 我们需要 pop pop ret指令。

随便填写一个地址测试是否成功转到该地址 ,我们在MAP类型内存映射中,找到了 0x7FFA5BE8地址,7FFA2017 它的指令是 jmp eax。

好的,修改Handler为这个地址, 看一下是否可以转到这个地址执行 ,答案是可以,但是无法下断跟踪(且提示访问0地址)。

之后我又选择了一个 pop ret指令的地址, 没办法,只能找到这个指令了,推算一下, 也就是 jmp [esp+4]。

根据微软的解释 EstablisherFrame 是此函数的固定堆栈分配的基地址 ,也就是我们得到的地址是 系统设置的异常处理函数的ebp(好吧,日后详细研究一下)。

 

好的好的,可以看到程序已经转到栈上执行,如果有合适的跳板指令可以利用(没办法了,我使用win10进行测试)。

结语

1.可以看到限制我们进行漏洞利用的因素有很多,我们不得不研究新的手段来对抗微软的保护机制。

2.经过测试,如果你不是使用加载模块地址之外的地址,确实会与safeSEH表来进行对比,会提示异常 无效的异常处理程序。

3.经过这次实践,碰到了各种各样的问题,此时才能理解前人的智慧,不得不佩服。