Visual Studio 2013如何检测缓冲区溢出

How does Visual Studio 2013 detect buffer overrun

Visual Studio 2013 C++ 项目有一个 /GS 开关以在运行时启用缓冲区安全检查验证。自升级到 VS 2013 以来,我们遇到了更多 STATUS_STACK_BUFFER_OVERRUN 错误,并且怀疑它与新编译器中改进的缓冲区溢出检查有关。我一直在尝试验证这一点并更好地理解如何检测到缓冲区溢出。即使语句更新的内存仅更改同一范围内堆栈上另一个局部变量的内容,也会报告缓冲区溢出,这让我感到困惑!因此,它必须不仅检查更改不会破坏内存而不是 "owned" 由局部变量,而且更改不会影响除分配给单个更新语句引用的变量之外的任何局部变量.这是如何运作的?自 VS 2010 以来它有变化吗?

编辑: 这是一个示例,说明了 Mysticial 的解释未涵盖的情况:

void TestFunc1();

int _tmain(int argc, _TCHAR* argv[])
{
   TestFunc1();
   return 0;
}

void TestFunc1()
{
   char buffer1[4] = ("123");
   char buffer2[4] = ("456");
   int diff = buffer1 - buffer2;
   printf("%d\n", diff);
   getchar();
   buffer2[4] = '[=11=]';
}

输出是4,表示即将被覆盖的内存在buffer1的范围内(紧接在buffer2之后),但随后程序因缓冲区溢出而终止.从技术上讲,它应该被认为是缓冲区溢出,但我不知道它是如何被检测到的,因为它仍然在局部变量的存储中,并没有真正破坏局部变量之外的任何东西。

这张带有内存布局的截图证明了这一点。单步执行一行后,程序因缓冲区溢出错误而中止。

我刚刚在 VS 2010 中尝试了相同的代码,虽然调试模式捕获了缓冲区溢出(缓冲区偏移量为 12),但在发布模式下它没有捕获它(缓冲区偏移量为 8)。所以我认为 VS 2013 收紧了 /GS 开关的行为。

编辑 2: 我设法用这段代码偷偷通过了 VS 2013 范围检查。它仍然没有检测到更新一个局部变量的尝试实际上更新了另一个:

void TestFunc()
{
   char buffer1[4] = "123";
   char buffer2[4] = "456";
   int diff;
   if (buffer1 < buffer2)
   {
      puts("Sequence 1,2");
      diff = buffer2 - buffer1;
   }
   else
   {
      puts("Sequence 2,1");
      diff = buffer1 - buffer2;
   }

   printf("Offset: %d\n", diff);
   switch (getchar())
   {
   case '1':
      puts("Updating buffer 1");
      buffer1[diff] = '!';
      break;
   case '2':
      puts("Updating buffer 2");
      buffer2[diff] = '!';
      break;
   }
   getchar(); // Eat enter keypress
   printf("%s,%s\n", buffer1, buffer2);
}

来自 Visual Studio 2013 年 /GS 上的 MSDN page

Security Checks

On functions that the compiler recognizes as subject to buffer overrun problems, the compiler allocates space on the stack before the return address. On function entry, the allocated space is loaded with a security cookie that is computed once at module load. On function exit, and during frame unwinding on 64-bit operating systems, a helper function is called to make sure that the value of the cookie is still the same. A different value indicates that an overwrite of the stack may have occurred. If a different value is detected, the process is terminated.

更多详情,同页参考Compiler Security Checks In Depth:

What /GS Does

The /GS switch provides a "speed bump," or cookie, between the buffer and the return address. If an overflow writes over the return address, it will have to overwrite the cookie put in between it and the buffer, resulting in a new stack layout:

  • Function parameters
  • Function return address
  • Frame pointer
  • Cookie
  • Exception Handler frame
  • Locally declared variables and buffers
  • Callee save registers

The cookie will be examined in more detail later. The function's execution does change with these security checks. First, when a function is called, the first instructions to execute are in the function’s prolog. At a minimum, a prolog allocates space for the local variables on the stack, such as the following instruction:

sub esp, 20h

This instruction sets aside 32 bytes for use by local variables in the function. When the function is compiled with /GS, the functions prolog will set aside an additional four bytes and add three more instructions as follows:

sub   esp,24h
mov   eax,dword ptr [___security_cookie (408040h)]
xor   eax,dword ptr [esp+24h]
mov   dword ptr [esp+20h],eax

The prolog contains an instruction that fetches a copy of the cookie, followed by an instruction that does a logical xor of the cookie and the return address, and then finally an instruction that stores the cookie on the stack directly below the return address. From this point forward, the function will execute as it does normally. When a function returns, the last thing to execute is the function’s epilog, which is the opposite of the prolog. Without security checks, it will reclaim the stack space and return, such as the following instructions:

add   esp,20h
ret

When compiled with /GS, the security checks are also placed in the epilog:

mov   ecx,dword ptr [esp+20h]
xor   ecx,dword ptr [esp+24h]
add   esp,24h
jmp   __security_check_cookie (4010B2h)

The stack's copy of the cookie is retrieved and then follows with the XOR instruction with the return address. The ECX register should contain a value that matches the original cookie stored in the __security_cookie variable. The stack space is then reclaimed, and then, instead of executing the RET instruction, the JMP instruction to the __security_check_cookie routine is executed.

The __security_check_cookie routine is straightforward: if the cookie was unchanged, it executes the RET instruction and ends the function call. If the cookie fails to match, the routine calls report_failure. The report_failure function then calls __security_error_handler(_SECERR_BUFFER_OVERRUN, NULL). Both functions are defined in the seccook.c file of the C run-time (CRT) source files.

您看到了对 /GS 机制的改进,它首先添加到 VS2012。最初 /GS 可以检测缓冲区溢出,但仍然存在一个漏洞,攻击代码可以踩踏堆栈但绕过 cookie。大致是这样的:

void foo(int index, char value) {
   char buf[256];
   buf[index] = value;
}

如果攻击者可以操纵 index 的值,那么 cookie 就无济于事了。此代码现在重写为:

void foo(int index, char value) {
   char buf[256];
   buf[index] = value;
   if (index >= 256) __report_rangefailure();
}

只是简单的索引检查。如果没有附加调试器,当触发时,使用 __fastfail() 立即终止应用程序。背景 is here.