深入C/C++之基于CheckStackVars的安全检查(VS2008)

本文详细介绍了Visual Studio 2008中CheckStackVars的安全检查机制,包括其工作原理、如何检测栈中数组的越界访问以及如何在调试版本下利用该机制进行边界检查。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近一直忙毕业的相关事情,加上工作,转眼间,又到月底了,之前承诺的每月一篇博文,前几天就一直在寻找到底要写什么,近两天又突然发现有很多东西可以写。本篇就先延续之前的一篇基于Cookie的安全检查机制(深入C/C++之基于Cookie的安全检查(VS2005))来介绍下另外一种在DEBUG版本下的安全检查,也就是CheckStackVars检查,话不多说,直接进入正题。

 

在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函数,再来看看此函数内部是怎么检测的,看看此函数的反汇编: 

_RTC_CheckStackVars:
00411500  mov         edi,edi
00411502  push        ebp 
00411503  mov         ebp,esp
00411505  push        ecx 
00411506  push        ebx 
00411507  push        esi 
00411508  push        edi 
00411509  xor         edi,edi                       // 清零
0041150B  mov         esi,edx                     // 将_RTC_framedesc结构指针赋值给esi
0041150D  cmp         dword ptr [esi],edi   // 比较varCount是否为0,if( _Fd->varCount != 0 )
0041150F  mov         ebx,ecx                     // 将TestVars的栈帧赋值给ebx
00411511  mov         dword ptr [i],edi       // 这里的i应该是循环变量,将数组的个数赋值给i,i = _Fd->varCount ;
00411514  jle         _RTC_CheckStackVars+58h (411558h)
00411516  mov         eax,dword ptr [esi+4]       //  +4之后就是_RTC_framedesc.variables指针
00411519  mov         ecx,dword ptr [eax+edi]   //  _RTC_vardesc->addr了,就是数组的首地址相对于TestVars的EBP的偏移量
0041151C  add         eax,edi                              // 将eax定位到_RTC_vardesc结构首地址
0041151E  cmp         dword ptr [ecx+ebx-4],0CCCCCCCCh  //  [ecx+ebx-4]等价于ebp-addr-4,也就是array的前面4个保护字节
00411526  jne         _RTC_CheckStackVars+36h (411536h) //  如果不等于0xcccccccc就报错_RTC_StackFailure
00411528  mov         edx,dword ptr [eax+4]     // eax+4就是_RTC_vardesc->size,表示数组的大小
0041152B  add         edx,ecx                             // ecx当前是偏移量,加上size后就是array数组尾部相对于ebp的偏移量
0041152D  cmp         dword ptr [edx+ebx],0CCCCCCCCh  // edx+ebx即是数组array尾部的后4个保护字节,然后比较
00411534  je          _RTC_CheckStackVars+4Ah (41154Ah)
00411536  mov         eax,dword ptr [esi+4]      // esi+4为_RTC_framedesc.variables指针
00411539  mov         ecx,dword ptr [eax+edi+8] // eax+edi+8即是_RTC_vardesc->name,用于报错提示
0041153D  mov         edx,dword ptr [ebp+4]
00411540  push        ecx    // 传入越界的数组名
00411541  push        edx   // 传入EBP+4的地址,此地址正是_RTC_CheckStackVars的返回地址,用于定位
00411542  call        _RTC_StackFailure (4110CDh) // 调用此函数后,弹出异常MessageBox,提示哪个数组越界
00411547  add         esp,8
0041154A  mov         eax,dword ptr [i]   // 存在多个数组需要检查时有用
0041154D  inc         eax 
0041154E  add         edi,0Ch                 // 定位到下一个_RTC_vardesc结构
00411551  cmp         eax,dword ptr [esi]
00411553  mov         dword ptr [i],eax
00411556  jl          _RTC_CheckStackVars+16h (411516h)  // 循环
00411558  pop         edi 
00411559  pop         esi 
0041155A  pop         ebx 
0041155B  mov         esp,ebp
0041155D  pop         ebp 
0041155E  ret

 

以上过程稍微解析得有点复杂,其主要原理就是读取_RTC_vardesc结构,挨个对每个数组进行前后边界检查,如果发生更改,则调用_RTC_StackFailure函数,最后弹出错误信息框,信息如:

Run-Time Check Failure #2 - Stack around the variable 'array' was corrupted.

 

这里需要说明一点,如果存在多个数组需要检查时,每个数组的name是紧挨着的,同时紧接着跟在多个_RTC_vardesc结构之后,内存分布如下:

 

[数组个数, _RTC_vardesc地址] [ 多个_RTC_vardesc结构(数组)][ 每个数组的name]

 

这些位置分布都是编译器直接写在代码里的。

 

这样就能实现简单的边界检查了,前面提到了,这种检查只是会检查前后边界,如果在程序中越界访问,但是没有修改或者写的值就是边界检查的值0xcccccccc,那也不会检测出代码已经有越界隐患。因此最主要的还是要小心谨慎。编译器总不能为我们做所有的事情。以上过程会在栈内存里加上边界检查值,所以在Debug版本下是比较实用的。在Release下不会这么浪费空间,因此越界就显得更加危险了。

 

从上面的分析过程来看,可以写出_RTC_CheckStackVars函数的伪代码,如下:

 

 

这段代码可以直接通过编译,并起到相应的检查功能,上面检查失败我这里暂时使用的__asm int 3进行中断,后面的注释是真正的_RTC_CheckStackVars函数调用的错误函数,_RTC_StackFailure用于弹出错误信息和定位调试器的光标到这个返回地址。

 

以下代码是用于测试这段伪代码的功能:

 

上面的代码是合法的,调用了检查函数之后没有任何的越界访问,如果要测试失败的情况,则将:

//array1[ 10 ] = 0;
//array2[ 10 ] = 0;

这两句的注释取消,就会在第二个__asm int 3出中断。

 

以上就是CheckStackVars的所有原理,基于这种检查机制还能发散出很多的东西,并且也可以自己实现一套规则,在一些关键的代码处设置这道检测关卡,也是非常有用的。本文到此结束,希望大家多提意见,欢迎拍砖!

<think>我们被要求解释C/C++汇编函数sub_4FE的实现细节及其作用。由于我们没有具体的反汇编代码,我们需要根据常见的反汇编模式来推测。通常,函数名如sub_4FE表示一个在地址0x4FE处的子函数。根据提供的引用,我们可以推测该函数可能涉及以下内容:1.引用[1]涉及析构函数的汇编形态,引用[2]和[3]是关于C语言函数的反汇编分析,引用[4]是关于C++类的成员函数和this指针的反汇编,引用[5]提到了一个_RTC_CheckStackVars函数,该函数用于栈变量检查(可能是安全检查,如缓冲区溢出)。因此,我们假设函数sub_4FE可能是一个普通的C函数,或者是一个类的成员函数,或者涉及一些特定的操作(如栈检查)。由于没有具体代码,我们只能根据常见的反汇编知识进行一般性解释。步骤:1.分析函数开头:通常会有保存栈基址、分配栈空间、保存寄存器等操作。2.分析函数体:执行具体的操作,可能包括算术运算、条件分支、循环、函数调用等。3.分析函数结尾:恢复寄存器、释放栈空间、返回等。另外,注意函数是否使用了参数,以及如何处理参数(通过寄存器或栈传递)。根据引用[5],如果函数中调用了_RTC_CheckStackVars,那么该函数可能是在Debug模式下编译的,并且进行了栈变量检查(用于检测栈变量是否被破坏)。因此,我们可以假设函数sub_4FE可能包含以下步骤:-函数开头(序言):```assemblypushebpmovebp,espsubesp,N;为局部变量分配空间```-函数体中可能包含一些操作,比如:-加载参数(如果参数通过栈传递,则从[ebp+8]开始获取)-进行一些计算-可能调用其他函数(如_RTC_CheckStackVars)-函数结尾(尾声):```assemblymovesp,ebppopebpretn```但是,由于没有具体代码,我们无法给出确切的分析。然而,用户可能希望我们根据提供的引用内容来推测,特别是引用[5]中的_RTC_CheckStackVars函数。这个函数是运行时检查(Run-TimeCheck)的一部分,用于检查栈上的局部变量是否被破坏(例如,缓冲区溢出)。因此,如果sub_4FE函数中调用了这个函数,那么它可能包含一些对局部数组或缓冲区的操作,并在返回前检查这些变量。根据引用[5]的描述,在调用strcpy之后会调用_RTC_CheckStackVars。因此,我们可以假设在sub_4FE中可能有类似的操作:1.定义局部数组或缓冲区。2.向这些缓冲区中写入数据(可能使用strcpy、memcpy等函数)。3.调用_RTC_CheckStackVars来检查栈变量是否被破坏。因此,函数sub_4FE的作用可能是进行一些字符串操作或缓冲区操作,并进行安全检查。由于没有实际的反汇编代码,我们只能给出这样的假设性分析。为了更具体,我们可以构造一个假设的反汇编代码并解释:假设反汇编代码如下(我们虚构一个典型的场景):```sub_4FEprocnearpushebpmovebp,espsubesp,58h;分配88字节栈空间pushebxpushesipushedileaedi,[ebp-58h]movecx,16hmoveax,0CCCCCCCChrepstosd;用0xCC初始化栈空间(调试模式);假设函数有两个参数:arg1和arg2moveax,[ebp+8];第一个参数movecx,[ebp+0Ch];第二个参数;将第一个参数复制到局部变量中mov[ebp-4],eax;定义一个局部缓冲区(假设在ebp-50h)leaesi,[ebp-50h];将第二个参数指向的字符串复制到局部缓冲区pushecx;第二个参数(字符串指针)pushesi;目标缓冲区callstrcpyaddesp,8;调用栈检查函数call_RTC_CheckStackVars;其他操作...popedipopesipopebxmovesp,ebppopebpretnsub_4FEendp```解释:1.函数开头分配栈空间,并初始化(调试模式下用0xCC填充,未初始化数据通常用这个值)。2.获取参数(假设两个参数),并将第一个参数保存到局部变量。3.将第二个参数指向的字符串复制到局部缓冲区(大小为80字节?因为分配了88字节,其中一部分用于其他局部变量)。4.调用_RTC_CheckStackVars检查栈变量是否被破坏(特别是局部缓冲区是否溢出)。5.恢复寄存器,返回。作用:该函数可能是用于安全地复制字符串,并在复制后进行栈检查,以防止缓冲区溢出导致的程序异常。但是,请注意,这只是一个假设的例子。实际的反汇编代码需要具体分析。由于用户没有提供具体的反汇编代码,我们只能根据常见的反汇编模式以及引用中提到的内容进行推测。因此,我们的回答是:函数sub_4FE的具体实现细节需要根据实际的反汇编代码来确定,但根据常见的模式,它可能包含以下部分:-函数序言:保存栈基址,分配栈空间,保存寄存器。-函数体:可能包含参数处理、局部变量操作、函数调用(特别是如果涉及缓冲区操作,可能会调用字符串操作函数,并在之后调用_RTC_CheckStackVars进行栈变量检查)。-函数尾声:恢复寄存器,释放栈空间,返回。其作用可能是执行特定的操作(如数据处理、字符串操作等),并在调试模式下进行栈变量安全检查。为了更准确地分析,需要提供具体的反汇编代码。根据要求,我们生成相关问题:§§相关问题§§1.如何获取函数sub_4FE的具体反汇编代码?2.在反汇编中,常见的函数调用约定有哪些?sub_4FE使用的是哪种约定?3._RTC_CheckStackVars函数的作用是什么?它如何检测栈变量是否被破坏?4.在反汇编中如何识别局部变量和函数参数?5.调试模式下栈空间初始化为何常用0xCC?注意:由于用户没有提供具体的反汇编代码,我们的分析是推测性的。如果用户能提供具体的反汇编代码,我们可以进行更详细的分析。</think>### 函数 `sub_4FE` 的反汇编分析 根据提供的引用内容,函数 `sub_4FE` 的反汇编逻辑可能涉及以下核心实现细节(需结合具体反汇编代码验证): --- #### **1. 函数入口与栈帧初始化** ```assembly push ebp mov ebp, esp sub esp, XXh ; 为局部变量分配栈空间 ``` - 保存调用者的栈基址(`ebp`),建立新栈帧(`ebp = esp`) - 通过 `sub esp, XXh` 分配局部变量空间(`XXh` 为字节数) - 调试模式下可能用 `0xCC` 填充栈空间(未初始化数据标记)[^5] --- #### **2. 参数访问与局部变量操作** ```assembly mov eax, [ebp+8] ; 访问第一个参数 mov [ebp-4], eax ; 存储到局部变量 ``` - 参数通过栈传递: - `[ebp+8]`:第一个参数(32位系统) - `[ebp+0Ch]`:第二个参数 - 局部变量通过负偏移访问: - `[ebp-4]`:第一个局部变量 - `[ebp-8]`:第二个局部变量 --- #### **3. 核心功能逻辑(推测)** 根据引用[2][3][5]的常见模式: - **可能包含算术运算**: ```assembly add eax, [ebp-8] ; 变量加法 imul ecx, edx ; 乘法 ``` - **可能涉及内存操作**: ```assembly lea edi, [ebp-10h] ; 加载局部数组地址 mov byte ptr [edi], 'A' ; 字符赋值 ``` - **可能调用安全函数**: ```assembly call _RTC_CheckStackVars ; 栈变量安全检查[^5] ``` --- #### **4. 函数返回与栈清理** ```assembly mov eax, [ebp-4] ; 返回值存入eax mov esp, ebp ; 恢复栈指针 pop ebp ; 恢复调用者栈帧 retn ; 返回 ``` - 返回值通过 `eax` 传递 - 通过 `mov esp, ebp` 释放局部变量空间 - `retn` 指令返回到调用点 --- ### **函数作用推测** 基于典型反汇编模式: 1. **数据处理函数**: - 可能执行数值计算(如引用[2]的 `b = i + (*j) + a`) - 可能涉及字符串/数组操作(如引用[5]的 `strcpy` 后接安全检查) 2. **调试辅助功能**: - 若包含 `_RTC_CheckStackVars` 调用,则用于检测栈变量溢出(调试模式特有)[^5] 3. **类成员函数可能性**: - 若使用 `this` 指针(如引用[4]),首个参数可能是对象地址 `[ebp+8] = this` - 可能访问成员变量:`mov ecx, [eax+4]`(`eax=this`) > **注**:具体功能需结合完整反汇编代码验证,关键线索包括: > - 参数数量和类型 > - 调用的子函数(如 `strcpy`, `printf`) > - 分支跳转逻辑(`cmp`/`jz` 指令) --- ### **逆向分析建议步骤** 1. 定位参数访问指令(`[ebp+8]`, `[ebp+0Ch]`) 2. 追踪 `eax`/`ecx` 寄存器的数据流 3. 识别关键跳转(条件判断循环) 4. 检查函数调用列表(`call` 指令目标地址) 5. 验证栈平衡(入口/出口的 `esp` 一致性) ---
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值