当时代码的重点在管道通信上,就重点看了管道的问题,没有仔细阅读每个地方,找了大概有15分钟竟然没找到,这大大激发了我的好奇心,于是我决定仔细看看所有的代码,结果一下子就发现了问题,原来在程序的开始的地方有如下的代码:
不知道你看到了没,在初始化w_buf的时候,内存越界了,但这种内存越界并不是都会直接core在越界的代码上,像这里,越界core一般发生在程序return之后,就会导致抛signal 11, Segmentation fault错误。return之后的core,往往会让开发者不知道具体core在哪里了,有时不是很容易发现,gdb也看不到堆栈。
在VS2008下,函数的栈空间里如果存在数组,就会自动加上CheckStackVars检查,顾名思义,就是用来检查局部数据是否访问越界。相对来说,这种检查只能起到一定的作用,并不会所有越界访问都能检查到,根据后面的原理介绍会了解到这点。既然是检查局部的,那么在函数内定义的static类型数组或者函数外部的全局数组并不会采用此检查,既然是检查数组,那么如果函数内没有局部数组时,此检查也不会存在。
首先来看一个简单的例子,验证这个检查的存在:
void TestVars( void )
{
int bf = 0xeeeeeeee;
char array[10] = { 0 };
int bk = 0xffffffff;
strcpy( array, "masefee" );
}
int main( void )
{
TestVars();
return 0;
}
在这个例子中,存在一个数组array,这里刻意定义了另外两个变量,用于看这两个变量与数组array的内存分布情况。这样就能清晰的了解到CheckStackVars这个检查的原理。然后来看看Debug下,TestVars函数内部的3个局部变量的内存分布情况。断点打在strcpy这句上,分布如下:
ff ff ff ff cc cc cc cc cc cc cc cc 00 00 00 00 00 00 00 00 00 00cc cc cc cc cc cc cc cc cc cc ee ee ee ee
bk array bf
上面的关系已经很明确了,我们发现,在C++的代码中看,bf、array、bk三者在内存分布上应该是连续的,紧挨着的。但是这里并不是这样的,看看bf和array之间居然像个10个字节之远。原因在于,在VS2008的debug版本下,局部变量之间并不是连续存放在栈内存里的,而是以4字节对齐的方式,前后都会有保护字节的。这里的保护字节占4个字节,值为0xcc,很明显这是汇编指令int 3中断的代码字节。因此这里bk和bf变量前后都会有4个字节的0xcc。上面绿色的部分就是,在数组array两端也有4字节的0xcc。上面黑色加粗的部分即是,array数组一共占10字节,要以4字节对齐,所以要补两字节,因此多了两个0xcc,因此导致bf和array之间相隔10字节。上面array后面紧挨着的本应该是两个0xcc,用于补充对齐。这里故意标识到后面去了。这里这样标识的意图是为了说明CheckStackVars这个检查的原理。
好了,清楚了内存分布情况,那么CheckStackVars在什么时间执行检查的呢,在C++代码上并不能显示的看到,于是来翻翻TestVars函数的反汇编代码:
TestVars:
004113B0 push ebp
004113B1 mov ebp,esp
004113B3 sub esp,0F0h
004113B9 push ebx
004113BA push esi
004113BB push edi
004113BC lea edi,[ebp-0F0h]
004113C2 mov ecx,3Ch
004113C7 mov eax,0CCCCCCCCh
004113CC rep stos dword ptr es:[edi]
004113CE mov eax,dword ptr [___security_cookie (417004h)]
004113D3 xor eax,ebp
004113D5 mov dword ptr [ebp-4],eax
004113D8 mov dword ptr [ebp-0Ch],0EEEEEEEEh
004113DF mov byte ptr [ebp-20h],0
004113E3 xor eax,eax
004113E5 mov dword ptr [ebp-1Fh],eax
004113E8 mov dword ptr [ebp-1Bh],eax
004113EB mov byte ptr [ebp-17h],al
004113EE mov dword ptr [ebp-2Ch],0FFFFFFFFh
004113F5 push offset string "masefee" (415804h)
004113FA lea eax,[ebp-20h]
004113FD push eax
004113FE call @ILT+160(_strcpy) (4110A5h)
00411403 add esp,8
00411406 push edx
00411407 mov ecx,ebp
00411409 push eax
0041140A lea edx,[ (411438h)]
00411410 call @ILT+130(@_RTC_CheckStackVars@8) (411087h)
00411415 pop eax
00411416 pop edx
00411417 pop edi
00411418 pop esi
00411419 pop ebx
0041141A mov ecx,dword ptr [ebp-4]
0041141D xor ecx,ebp
0041141F call @ILT+25(@__security_check_cookie@4) (41101Eh)
00411424 add esp,0F0h
0041142A cmp ebp,esp
0041142C call @ILT+320(__RTC_CheckEsp) (411145h)
00411431 mov esp,ebp
00411433 pop ebp
00411434 ret
00411435 lea ecx,[ecx]
00411438 db 01h
00411439 db 00h
0041143A db 00h
0041143B db 00h
0041143C db 40h
0041143D db 14h
0041143E db 41h
0041143F db 00h
00411440 db e0h
00411441 db ffh
00411442 db ffh
00411443 db ffh
00411444 db 0ah
00411445 db 00h
00411446 db 00h
00411447 db 00h
00411448 db 4ch
00411449 db 14h
0041144A db 41h
0041144B db 00h
0041144C db 61h
0041144D db 72h
0041144E db 72h
0041144F db 61h
00411450 db 79h
00411451 db 00h
从TestVars的反汇编代码可以清楚的看到,黑色加粗的部分就是前一篇博文介绍的,在本篇注意看在strcpy调用之后,又调用了_RTC_CheckStackVars函数,这是一个什么样的函数?先来看看他的原型:
void __fastcall _RTC_CheckStackVars( void *_Esp, _RTC_framedesc *_Fd );
这是一个fastcall函数,因此两个参数都是通过寄存器进行传递的。第二个参数是一个结构体类型,再来看看这个结构体的定义:
typedef struct _RTC_framedesc
{
int varCount; // 要检查的数组的个数
_RTC_vardesc *variables; // 要检查的数组的相关信息
} _RTC_framedesc;
这个结构体定义在rtcapi.h头文件中的,_RTC_vardesc 也是一个结构体类型,看看定义:
typedef struct _RTC_vardesc
{
int addr; // 数组的首地址相对于EBP的偏移量
int size; // 数组的大小字节数
char *name; // 数组的名字
} _RTC_vardesc;
以上面的例子来填充这个结构体之后,结构体的数据就是:
_RTC_framedesc.varCount = 1;
_RTC_vardesc->addr = array - EBP; // 这里array在低地址,所以addr最终为负
_RTC_vardesc->size = 10;
_RTC_vardesc->name = "array";
好了,这下清楚了信息的存储,再回到上面的反汇编代码,在调用_RTC_CheckStackVars函数之前,注意红色粗体的一句指令,将ebp赋值给了ecx寄存器,再将411438h这个地址值赋值给了edx,由于_RTC_CheckStackVars函数是fastcall,因此通过这两个寄存器进行传递参数,而不是push操作。ecx就是保存的TestVars函数的栈帧,edx这个地址有点奇怪,本来是应该传递_RTC_framedesc结构指针的,难道这个411438h地址值就是_RTC_framedesc结构体变量所在的内存地址?从上面的反汇编代码可以看到,下面从411438h地址开始,多了一段奇怪的数据,本应该函数下面不会有这么一段数据的,在Debug下大多数情况都是0xcc填充的。咱们仔细观察下这段数据,或者直接将411438h这个地址值copy到内存窗口里看:
0x00411438 01 00 00 00 40 14 41 00 e0 ff ff ff 0a 00 00 00 4c 14 41 00 61 72 72 61 79 00
看看上面的数据,是不是就是_RTC_framedesc结构应该有的数据?答案是肯定的,红色的部分就是_RTC_framedesc.variables指针的值,指向的位置就是紧跟其后,这是编译器故意这么处理的。当然可以是其它地方。这是编译器直接把这些信息记录在代码段的,并且紧跟在所记录的函数代码之后。因此不要误认为这些信息是在程序执行期间才写进去或填充的_RTC_framedesc结构。
了解到这里,发现整个规则都是有理有据的,并且设计都是很良好的。也能又一次感受MS的伟大。呵呵,废话了!
上面既然将两个参数都给了_RTC_CheckStackVars函数,再来看看此函数内部是怎么检测的,看看此函数的