深入C/C++之基于Cookie的安全检查(VS2005)

http://blog.csdn.net/masefee/article/details/4633305

昨天在试着逆向一个有时间期限的LIB时,发现一些特别的检查函数,在之前的VC2003中是没有的,这些函数可谓是重量级函数。由于个人比较看不惯自己不懂的东西,出于不愤之情绪研究了下这些函数。首先在这里介绍个人认为较之其他几个更为重要的一种安全检查方式——基于Cookie的缓冲区溢出安全检查!

为了在发布版本中也能检测到缓冲区溢出,防止程序因缓冲区而受到攻击,VS2005(VC8)便增加了基于Cookie的安全检查。

在计算机领域,Cookie一词最早出现在网站开发中,是指网站的服务程序通过浏览器保存到客户端的少量数据,这些数据都是二进制的,或者是经过加密的,通常用来记录用户身份和登录情况等信息。后来这个词被泛指一方签发给另一方的认证或者标志信息。

在之前的博文中,都提到过堆栈调用及EBP,RET返回地址,还有局部变量在栈帧中存放的各种微妙关系。相信大家也有清晰的了解,如果不清晰呢,可以参见Shell Code,HOOK API这两篇。嘿嘿!

VC8编译器在编译可能发生缓冲区溢出的函数时,会定义一个很特别局部变量,如果不加分析的话,这个局部变量还真不知道代表什么意思。而且它的值又是怎么算出来的?,它有什么作用? 可能有的朋友不是没有注意到这个细节就是觉得没有必要追究它。不过个人认为熟悉了堆栈调用里面的原理会对我们的调试能力和差错能力有很明显的提高。好了,转入正题。编译器增加的这个局部变量时紧挨着EBP的存放地址的。顺序就是:VAR COOKIE EBP RET ARGS。这个Cookie的变量位于函数体内的局部变量和EBP的存放地址之间,具体表示就是:[EBP-4]。这个专门用于保存Cookie的变量被称为Cookie变量。是一个32位的无符号整数。它的值是从全局变量__security_cookie得到的。我们可以看看这个全局变量的定义:

#ifdef _WIN64
         #define DEFAULT_SECURITY_COOKIE 0x00002B992DDFA232
#else 

         #define DEFAULT_SECURITY_COOKIE 0xBB40E64E
#endif

DECLSPEC_SELECTANY UINT_PTR __security_cookie = DEFAULT_SECURITY_COOKIE;

DECLSPEC_SELECTANY UINT_PTR __security_cookie_complement = ~(DEFAULT_SECURITY_COOKIE);

由于我测试的机子是32位的,这里只看32位的。有朋友是64位的可以测试下。这里个定义位于gs_cookie.c文件中。文件位于编译器目录的VC/crt/src下。这里可以看到,最开始的时候这个全局变量被初始化成了0xBB40E64E。为什么是这个值,这里不是讨论的重点,还有待研究!嘿嘿。

上面说到最开始的时候被初始化为了0xBB40E64E。言外之意后面还会对它进行处理?答案是肯定的!下一次初始化是在我们程序进入主函数之前的:__tmainCRTStartup函数里。

抛开预处理和机器的条件判断,这个函数的原型如下:

int mainCRTStartup( void )
{
        /*
         * The /GS security cookie must be initialized before any exception
         * handling targetting the current image is registered.  No function
         * using exception handling can be called in the current image until
         * after __security_init_cookie has been called.
         */
        __security_init_cookie();

        return __tmainCRTStartup();
}

我的机子抛开后是调用的这个函数。现在的硬件环境大多数也是这个。这里可以看到在这里会调用__security_init_cookie()这个函数对__security_cookie变量再次初始化。这个函数也是能看到原型的,它位于gs_support.c文件中。进去看看,抛开一编译时的判断条件其原型主体部分为:

void __cdecl __security_init_cookie()

{

    UINT_PTR cookie;
    FT systime={0};
    LARGE_INTEGER perfctr;

    GetSystemTimeAsFileTime(&systime.ft_struct);

    cookie = systime.ft_struct.dwLowDateTime;
    cookie ^= systime.ft_struct.dwHighDateTime;

    cookie ^= GetCurrentProcessId();
    cookie ^= GetCurrentThreadId();
    cookie ^= GetTickCount();

    QueryPerformanceCounter(&perfctr);

    cookie ^= perfctr.LowPart;
    cookie ^= perfctr.HighPart;

    __security_cookie = cookie;
    __security_cookie_complement = ~cookie;

}

这里可以看出来,为了取得好的随机性,先是取出时间,异或之,然后是分别跟其他一些列具有随机性的数据(进程ID,线程ID,TickCount和性能计数器)进行异或运算。这个变量因为是全局的,在这里( mainCRTStartup启动函数)初始化后在进程过程中将不会再改变。如果想要查看这个cookie变量的值。可以再调试的时候拉出“即时窗口(Immediate)”,在里面输入__security_cookie回车就能看到了。

好了,在上面介绍完了Cookie变量的产生、初始化和作用后,下面来看看使用。

写个最简单的测试:

int main( void )

{

    char a[ 20 ];

    strcpy( a, "masefee" );

    return 0;

}

上面说过,编译器会在可能发生缓冲区溢出的函数插入Cookie变量和安全检查。这样一个小例子足以让它检查了。他已经发现可能存在危险了。是不是很智能? - -

要看这个函数一开始怎么写入Cookie变量的,可以打断点在红色的括号处或者F11单步。这里又得在反汇编里面进行了,这里不厌其烦的从汇编里面去看问题,包括以前的文章基本跟汇编有联系。这里不是别的,只是个人认为还是很有必要从汇编的角度去了解高级语言的原理。很多是很必要的。当然这里也只能从汇编去分析这个Cookie变量的写入过程及检查过程!忍耐一下! - -

就这里这个简单的例子,DEBUG模式下反汇编如下:


0043BEF0  push        ebp 
0043BEF1  mov         ebp,esp
0043BEF3  sub         esp,0E0h
0043BEF9  push        ebx 
0043BEFA  push        esi 
0043BEFB  push        edi 
0043BEFC  lea         edi,[ebp-0E0h]
0043BF02  mov         ecx,38h
0043BF07  mov         eax,0CCCCCCCCh
0043BF0C  rep stos    dword ptr es:[edi]
0043BF0E  mov         eax,dword ptr [___security_cookie (4B7A74h)]
0043BF13  xor         eax,ebp
0043BF15  mov         dword ptr [ebp-4],eax
0043BF18  push        offset string "masefee" (4AA938h)
0043BF1D  lea         eax,[ebp-1Ch]
0043BF20  push        eax 
0043BF21  call        @ILT+3880(_strcpy) (437F2Dh)
0043BF26  add         esp,8
0043BF29  xor         eax,eax
0043BF2B  push        edx 
0043BF2C  mov         ecx,ebp
0043BF2E  push        eax 
0043BF2F  lea         edx,[ (43BF5Ch)]
0043BF35  call        @ILT+3115(@_RTC_CheckStackVars@8) (437C30h)
0043BF3A  pop         eax 
0043BF3B  pop         edx 
0043BF3C  pop         edi 
0043BF3D  pop         esi 
0043BF3E  pop         ebx 
0043BF3F  mov         ecx,dword ptr [ebp-4]
0043BF42  xor         ecx,ebp
0043BF44  call        @ILT+920(@__security_check_cookie@4) (43739Dh)

0043BF49  add         esp,0E0h
0043BF4F  cmp         ebp,esp
0043BF51  call        @ILT+7205(__RTC_CheckEsp) (438C2Ah)
0043BF56  mov         esp,ebp
0043BF58  pop         ebp 
0043BF59  ret             

这就是main函数的所有反汇编代码。首先看红色的一句指令。是将___security_cookie变量的值给取出来。然后蓝色的指令就是将取出来的Cookie全局变量与当前EBP的值进行异或运算。与EBP异或当然有好处。

1. 可以增加随机性,尽可能使不同函数的安全Cookie都不同。

2. 可以检查EBP是否被破坏,因为在函数结束检查Cookie时,还会将Cookie变量值再次与EBP异或,如果EBP的值没有变化,那么就能恢  复成原来的___security_cookie值。

这些细节地方不得不佩服微软的设计师们的缜密和扩展的思维!

上面绿色的指令便是关键的一步,它是将异或后的值存入[ebp-4]中。当前的EBP就是最开始压入EBP后ESP的值,也就是压入的EBP的地址。减4就刚好挨着一进函数时压入的EBP的地址减4。好了!Cookie变量已经在栈帧中了。

下面一步就看最后的检查部分,粉色的部分前两句指令是将Cookie变量的值重新取出来并异或还原并保存到ECX中。保存到ECX是有目的的。之后再讨论。第三句粉色的CALL就是调用__security_check_cookie 函数了。其原型非常简单:

void __declspec(naked) __fastcall __security_check_cookie(UINT_PTR cookie)
{
    /* x86 version written in asm to preserve all regs */
    __asm

    {
        cmp ecx, __security_cookie
        jne failure
        rep ret /* REP to avoid AMD branch prediction penalty */
    failure:
        jmp __report_gsfailure
    }
}

这个函数位于编译器目录下的VC/crt/src/intel/secchk.c中。为了降低对可执行文件大小和运行性能的影响,这个函数直接用汇编写的。而且只有4条指令,不存在任何变量和寄存器(标志寄存器除外)的改变。因为是使用的快速调用协定。因为唯一的一个参数都是存放在ECX中,直接进行CMP比较的。红色的部分就是与全局的Cookie变量进行比较。如果相同就正常,如果不同就jmp failure报错。跳转到__report_gsfailure函数。这样既检查了EBP是否合法,又检查了Cookie变量的合法性。

好了,基本上写完了。上面留了两个点,一是mian函数里面我有标志了一句橙色的语句,这个也是一个检查。将在后面的博文中提到。二是__report_gsfailure函数的整个过程。也将在后面的博文中深入阐述。上面有什么不对的地方还望大家批评。我很希望得到大家的指点!

思考:

      1. 这种模式的安全检查能够移植到我们平常的项目中的哪些地方,在我们用户代码上显示进行检查?

      2. 这种产生尽可能随机的方式用于类似于生成对象的全局唯一ID或者更多的需要唯一性的数据上?

 

你可能感兴趣的:(C++,C语言)