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

10

Visual Studio 2013 C++项目具有/GS开关,可在运行时启用缓冲区安全检查验证。自从升级到VS 2013以来,我们遇到了更多的STATUS_STACK_BUFFER_OVERRUN错误,并且怀疑这与新编译器中改进的缓冲区溢出检查有关。我一直在尝试验证这一点,并更好地理解如何检测缓冲区溢出。令我困惑的是,即使一个语句更新的内存只会更改同一作用域中堆栈上另一个局部变量的内容,缓冲区溢出也会报告!因此,它必须检查不仅更改不会破坏非本地变量所“拥有”的内存,而且更改也不会影响除分配给单个更新语句的那个变量之外的任何本地变量。这是如何工作的?自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] = '\0';
}
输出为4,表示即将被覆盖的内存位于buffer1的范围内(紧接在buffer2之后),但随后程序以缓冲区溢出而终止。严格来说,它应该被视为缓冲区溢出,但我不知道它是如何被检测出来的,因为它仍然在局部变量的存储范围内,实际上并没有破坏任何局部变量之外的东西。
这个内存布局的屏幕截图证明了它。经过一行代码的单步操作后,程序因缓冲区溢出错误而中止。 Debugger screenshot with memory layout 我刚才在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);
}

3
相信它的其中一个作用是在栈对象旁插入虚拟数据,并在方便的时候(如函数退出时)检查它们。如果数据发生了变化,那么它就知道有东西破坏了它。但这只是我的猜测。 - Mysticial
@Mysticial 这并不能解释我的测试用例,我计算了两个本地变量缓冲区之间的偏移量是相邻的,但更新第一个时仍然检测到了溢出。 - BlueMonkMN
1
展示一个例子说明你的意思,包括变量地址的输出。我相当确定Mystical是正确的。 - Mats Petersson
@MatsPetersson 我已经添加了证据,说明一定还有其他事情发生了。 - BlueMonkMN
@Henrik,你必须在发布模式下运行才能看到缓冲区偏移量为4。问题是在这种情况下如何捕获溢出。 - BlueMonkMN
显示剩余4条评论
2个回答

2

您正在看到对/GS机制的改进,这是在VS2012中首次添加的。最初/GS可以检测缓冲区溢出,但仍存在一种漏洞,攻击代码可以践踏堆栈但绕过cookie保护。 大致如下:

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

如果攻击者可以操纵索引的值,那么cookie就没有用了。现在这段代码已经被重写为:
void foo(int index, char value) {
   char buf[256];
   buf[index] = value;
   if (index >= 256) __report_rangefailure();
}

只是简单的索引检查。当触发时,如果没有调试器附加,就会立即使用__fastfail()终止应用程序。有关详细信息,请参见此处


如果您查看我的编辑2下的示例代码,有趣的是,这并没有触发缓冲区溢出。仔细阅读您提供的链接后,我意识到这是因为溢出检查是特别针对NULL('\0')字符触发的!很有趣,谢谢! - BlueMonkMN
看起来许多这样的情况可以在编译时报告。有没有办法在编译时启用对其中一些情况进行报告? - BlueMonkMN

1

来自Visual Studio 2013中关于/GSMSDN页面

安全检查

对于编译器识别出可能存在缓冲区溢出问题的函数,编译器在返回地址之前在堆栈上分配空间。在函数进入时,分配的空间将加载一个安全cookie,该cookie在模块加载时计算一次。在函数退出时,在64位操作系统上进行帧展开期间,将调用帮助程序函数以确保cookie的值仍然相同。不同的值表示可能已经发生了堆栈覆盖。如果检测到不同的值,则终止该进程。

有关更多详细信息,请参考同一页面中的编译器安全检查深入部分:

/GS的作用

/GS开关在缓冲区和返回地址之间提供了一个“速度障碍”或cookie。如果溢出写入返回地址,它将需要覆盖放置在它和缓冲区之间的cookie,导致新的堆栈布局:

  • 函数参数
  • 函数返回地址
  • 帧指针
  • Cookie
  • 异常处理程序框架
  • 本地声明的变量和缓冲区
  • 被调用者保存的寄存器

稍后将更详细地讨论cookie。这些安全检查确实会改变函数的执行方式。首先,当调用函数时,要执行的第一条指令位于函数的前奏中。至少,前奏在堆栈上为本地变量分配空间,例如以下指令:

sub esp, 20h

这个指令为函数中的本地变量分配了32字节的空间。当函数使用 /GS 编译时,函数的 prolog 将再额外分配4个字节,并添加以下三个指令:

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

Prolog部分包含一个指令,该指令获取cookie的副本,紧随其后的是将cookie和返回地址进行逻辑异或运算的指令,最后是将cookie直接存储在返回地址下方的指令。从此时开始,函数将按照通常的方式执行。当函数返回时,最后要执行的是函数的epilog,它是prolog的相反操作。如果没有安全检查,它将回收堆栈空间并返回,例如以下指令:
add   esp,20h
ret

当使用/GS进行编译时,安全检查也会放置在结尾处:
mov   ecx,dword ptr [esp+20h]
xor   ecx,dword ptr [esp+24h]
add   esp,24h
jmp   __security_check_cookie (4010B2h)

堆栈中的cookie副本被检索,然后与返回地址执行XOR指令。ECX寄存器应包含与__security_cookie变量中存储的原始cookie匹配的值。然后回收堆栈空间,而不是执行RET指令,执行JMP指令到__security_check_cookie例程。
__security_check_cookie例程很简单:如果cookie未更改,则执行RET指令并结束函数调用。如果cookie不匹配,则该例程调用report_failure。report_failure函数然后调用__security_error_handler(_SECERR_BUFFER_OVERRUN, NULL)。这两个函数在C运行时(CRT)源文件的seccook.c文件中定义。

正如您在我新添加的示例代码和截图中所看到的,我认为您在这里描述的任何内容都无法捕获我所看到的情况。 - BlueMonkMN
@BlueMonkMN 这不是VS13的改进,缓冲区溢出也会在VS2012中报告。在两种情况下,VS都使用范围检查。 - Nikos Athanasiou
没错,我关心的变化是由Hans Passant指出的,它是在VS 2012中引入的。我们从VS 2010升级到了VS 2013,所以我不确定哪个版本引入了这种行为。 - BlueMonkMN

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接