Visual Studio 2013 C ++项目有一个/GS
开关,用于在运行时启用缓冲区安全检查验证。自升级到VS 2013以来,我们遇到了更多STATUS_STACK_BUFFER_OVERRUN错误,并怀疑它与改进的新编译器中缓冲区溢出检查有关。我一直试图验证这一点,并更好地了解如何检测缓冲区溢出。即使由语句更新的内存只更改同一范围内堆栈上另一个局部变量的内容,也会报告缓冲区溢出这一事实让我感到困惑!因此,它必须不仅要检查变更是否会损坏内存而不是“拥有”#34;通过局部变量,但更改不会影响除分配给单个更新语句所引用的变量之外的任何局部变量。这是如何运作的?自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
之后),但程序终止时缓冲区溢出。从技术上讲,它应该被视为缓冲区溢出,但我不知道它是如何被检测到的,因为它仍然在局部变量中#?存储并没有真正破坏局部变量之外的任何东西。
这张带内存布局的屏幕截图证明了这一点。步进一行后,程序因缓冲区溢出错误而中止。
我刚刚在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);
}
答案 0 :(得分:2)
您正在看到/ 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。
答案 1 :(得分:1)
来自Visual Studio 2013中/GS
上的MSDN page:
安全检查
在编译器识别为缓冲区溢出问题的函数上,编译器在返回地址之前在堆栈上分配空间。在函数入口上,分配的空间加载了一个安全cookie,该cookie在模块加载时计算一次。在函数退出时,以及在64位操作系统上的帧展开期间,将调用辅助函数以确保cookie的值仍然相同。不同的值表示可能发生了堆栈的覆盖。如果检测到不同的值,则终止该过程。
有关详细信息,同一页面引用Compiler Security Checks In Depth:
什么/ GS做
/ GS开关在缓冲区和返回地址之间提供“减速带”或cookie。如果溢出写入返回地址,则必须覆盖放在它和缓冲区之间的cookie,从而产生新的堆栈布局:
- 功能参数
- 功能返回地址
- 帧指针
- Cookie
- 异常处理程序框架
- 本地声明的变量和缓冲区
- Callee保存寄存器
稍后将更详细地检查cookie。函数的执行确实随着这些安全检查而改变。首先,当调用函数时,要执行的第一个指令位于函数的序言中。 prolog至少为堆栈上的局部变量分配空间,例如以下指令:
sub esp, 20h
该指令留出32个字节供函数中的局部变量使用。当使用/ GS编译函数时,函数prolog将预留另外四个字节并添加另外三个指令,如下所示:
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和返回地址的逻辑xor的指令,最后是一条指令,用于将cookie存储在返回地址正下方的堆栈中。从现在开始,该函数将按正常方式执行。当一个函数返回时,最后要执行的是函数的epilog,它与prolog相反。如果没有安全检查,它将回收堆栈空间并返回,如下面的说明:
add esp,20h
ret
使用/ GS编译时,安全检查也会放在epilog中:
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匹配的值。然后回收堆栈空间,然后执行__security_check_cookie例程的JMP指令,而不是执行RET指令。
__security_check_cookie例程很简单:如果cookie没有改变,它会执行RET指令并结束函数调用。如果cookie无法匹配,则例程调用report_failure。然后,report_failure函数调用__security_error_handler(_SECERR_BUFFER_OVERRUN,NULL)。这两个函数都在C运行时(CRT)源文件的seccook.c文件中定义。