天在试着逆向一个有时间期限的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或者更多的需要唯一性的数据上?