最近在看《0day安全:软件漏洞分析》那本书,初步接触一些windows下的溢出利用,和linux上还是有较大不同的。
本篇对应书上第6、11章节关于利用SEH异常处理来绕过GS的内容。
GS相当于Windows下的canary,这时通过溢出覆盖返回地址,来控制程序执行流程的方法就不再可行。在linux下绕过canary的方法也有一些,在windows下总的思路是一致的:
一、泄露canary的值(参考 %s打印栈上数据时的canary泄露)
二、canary检查的机制是生成随机canary后放到返回地址之前,并将其备份(例如linux下存到gs:0x14),在函数返回时,再把两处的canary通过异或来判断是否发生改动,如果能够同时修改这两个地方的canary值,就可以通过canary的检测。(难以利用)
三、利用其它溢出方式(虚函数,堆)。
以上三种比较通用,除此之外,linux下还有一种利用___stack_chk_fail()函数实现一次任意地址读的姿势(Stack Smashing Protector任意地址读)
而windows下还可以利用覆盖SEH异常处理指针,只要触发异常先于canary检查,就可以先去执行SEH异常处理函数从而实现利用。
SEH chain结构:
由pointer to next SEH构成单向链表,而handle指向异常处理函数。
具体的异常处理机制就不做过多说明。
Windows XP sp2之后引入了SafeSEH,接下来的内容主要针对SafeSEH,且暂时不考虑DEP。
SafeSEH针对指向异常处理函数的指针做了若干检查,使得我们直接覆盖其为shellcode地址变得不可行。先看看能够通过检查的情况:
1.异常处理函数位于加载模块内存范围之外,DEP关闭
2.异常处理函数位于加载模块内存范围之内,相应模块未启用SafeSEH,同时相关模块不是纯IL指令。
3.异常处理函数位于加载模块内存范围之内,相应模块启用SafeSEH,异常处理函数地址包含在安全S.E.H表中。
0x00 利用未启用SafeSEH模块绕过SafeSEH
实验环境:
Windows XP SP3 (DEP disabled)
Visual Studio 2008
VC++ 6.0
首先用vc6.0构建一个未启用SafeSEH的dll:
#include "stdafx.h"
#include "stdio.h"
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
return TRUE;
}
void jump()
{
puts("hereisit"); //主要用于定位ROP
__asm{
pop eax
pop eax
retn //一个pop pop ret 的rop,用法后面会提到
}
}
为了防止ROP的地址包含‘'\x00'被截断,需要手动设置dll的加载基址:
在 工程-->设置-->连接 的工程选项中添加 /base:"0x11120000"/
接着用vs2008构建一个启用SafeSEH的exe,其中调用之前的dll。(vs2008默认开启GS、SafeSEH)
char shellcode[]="”;
DWORD MyException(void)
{
printf("This 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"));
char str[200];
test(shellcode);
return 0;
}
先用Immunity Debugger的mona插件计算需要多少个字节才能覆盖到SEH的内容:
把生成的字符串放到shellcode数组中,od或者immunity debugger调试,此时SEH的pointer to next SEH被覆盖为41326841,由于是小端格式,倒过来用mona计算偏移:
我们只需在shellcode中填充’\x90'*216,接着就将覆盖SEH结构体。
栈中内容大致是这样的
调用异常处理函数的过程:
把0x12FE94处的四字节放入eax,然后call eax,这时栈的情况:
回看之前我们的rop:POP POP RETN,两次弹栈后将执行0x12FE90处的指令。
书中给出的shellcode布局是:
执行了我们伪造的异常处理函数,实际上是pop pop retn后,eip指向0x12fe90,\x90是nop指令,pop pop retn的地址相当于一个操作eax的指令无关紧要,那么可以一直滑向shellcode执行。
但是实际调试时,这样布局0x12ef98~0x12fe9c的位置会被破坏,不再是NOP指令无法滑向shellcode。
由于没有开启DEP,我在0x12FE90的位置放置了\xeb\x0b\x90\x90,对应汇编指令jmp 0x10,直接跳到0x12fea0即shellcode的第一条指令执行。
0x01 利用加载模块之外的地址绕过SafeSEH
当程序加载到内存中后,在它所占的整个内存空间中,除了我们平时常见的PE文件模块(EXE和DLL)之外,还有其他一些映射文件,例如,类型为MAP的映射文件,SafeSEH是无视它们的,所以我们可以在这些文件中寻找跳转指令。
这种情况下可用的跳板地址,除了pop pop retn序列外,还有:
call / jmp dword ptr [esp+0x8/0x14/0x1c/0x2c/0x44/0x50]
call / jmp dword ptr [ebp+0xc/0x24/0x30]
call / jmp dword ptr [ebp-0x4/0xc/0x18]
关于为什么这些跳板可以,有兴趣的童鞋可以自己调试跟一下。
书作者开发了一个插件叫做OllyFindAddr,与ollydbg自己的指令搜索不同,该插件不只在加载模块中搜索指令,而是在整个程序的内存空间搜索。
exploit!
后记
某次课堂练习复现一个简单的SafeSEH利用,系统是windows xp sp3 pro,od给的exp里不知道为什么要往seh handler填一个jmp ebx的地址,重新找了pop pop ret填入发现无法正常调用异常处理函数,最后发现根本找不到未开启safeSEH的模块。
然而发现exp中的jmp ebx是可以执行的,其位置位于地址空间的最后,一块未识别的模块。在其中搜索pop pop ret找不到,最后终于找到形如:
pop
mov ebx,
pop
mov ebx,
ret
的指令序列,完成利用。