如何在Windows系统上报告堆栈缓冲区溢出?

9
在下面的代码中,我已经使用了所有记录的方法来检测异常并生成诊断。它使用C++的try/catch关键字,用__try/__catch扩展关键字捕获SEH异常,使用Windows' AddVectoredExceptionHandler()和SetUnhandledExceptionFilter() winapi函数来安装VEH/SEH过滤器。
在Visual C++ 2003上运行: /GS: 输出"hello,world!"并以退出代码0终止。 /GS-: 输出"hello,world!"并以退出代码0终止。
在Visual C++ 2013上运行: /GS: 没有输出,以退出代码-1073740791终止。 /GS-: 输出"hello,world!"并以退出代码0终止。
如何在启用/GS的VS2013编译程序中生成诊断?
#include "stdafx.h"
#include <Windows.h>

#define CALL_FIRST 1  
#define CALL_LAST 0

LONG WINAPI MyVectoredHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
    UNREFERENCED_PARAMETER(ExceptionInfo);

    printf("MyVectoredHandler\n");
    return EXCEPTION_CONTINUE_SEARCH;
}

LONG WINAPI MyUnhandledExceptionFilter(_In_ struct _EXCEPTION_POINTERS *ExceptionInfo)
{
    printf("SetUnhandledExceptionFilter\n");

    return EXCEPTION_CONTINUE_SEARCH;
}

void f()
{
    __try
    {
        char p[20] = "hello,world!";
        p[24] = '!';
        printf("%s\n", p);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        printf("f() exception\n");
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    AddVectoredExceptionHandler(CALL_FIRST, MyVectoredHandler);
    SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);

    try{
        f();
    }
    catch (...){
        printf("catched f exception\n");
    }
    return 0;
}

1
你的代码导致了未定义行为。唯一可靠的“检测”方法是在执行越界访问之前检查边界。 - M.M
1
例如,如果您使用了一个能够进行检测的容器类型,那么会发生什么呢?例如,使用 std::string 来表示字符串,并使用 at 成员函数进行索引。 - user1084944
堆栈溢出很难检测。只有在覆盖堆栈帧时才会触发。尝试开启运行时异常数组越界。这可能有效。 - cup
3个回答

6
处理堆栈缓冲区溢出检测的CRT函数 __report_gsfailure(),假定堆栈帧损坏是由恶意软件攻击引起的。这种恶意软件传统上会操纵fs:[0] SEH异常过滤器(存储在堆栈帧上),以触发恶意软件负载的异常处理程序之一,从而将数据转换为可执行代码的方式之一。
因此,该CRT函数无法假定抛出异常是安全的。在VS2013中包含的CRT已不再做出这种假设,回到了~VS2005。如果操作系统支持它,则会失败快速,并确保已注册的VEH/SEH异常处理程序也无法看到该异常。砰,崩溃到桌面上,除非您连接了调试器,否则不会有诊断信息。
/SAFESEH选项可以防止此类恶意软件攻击,因此它不像以前那样严重。如果您仍处于代码遭受堆栈损坏错误的阶段,并且您的应用程序还不够受欢迎,无法成为恶意软件的目标,则考虑替换CRT函数是一件可以考虑的事情。
务必与您的主管商讨这个问题,您永远不想对此负个人责任,因为这对您的客户来说是巨大的责任。历史上很少有程序员编写的代码会让整个企业在一个月内停业。但肯定不会是什么美好的事情。
将此代码粘贴到主()函数附近的某个地方:
__declspec(noreturn) extern "C"
void __cdecl __report_gsfailure() {
    RaiseException(STATUS_STACK_BUFFER_OVERRUN, EXCEPTION_NONCONTINUABLE, 0, nullptr);
}

并计划很快再次删除它。


添加自定义 __report_gsfailure 后链接失败:错误 LNK2005:___report_gsfailure 已经在 LIBCMT.lib(gs_report.obj) 中定义。 - liuaifu
1
嗯,这是经过测试的代码,在VS2015上验证过了。不知道中文文本说什么,感觉像是gs_report.obj已经被另一个库引入了,所以现在有两个。使用链接器的/VERBOSE选项查找为什么使用了gs_report.obj。并且强烈建议使用/MD而不是/MT进行构建。 - Hans Passant
感谢您提供的解决方案。根据我的研究,msvcrtd还会引入另一个__report_gsfailure,因此这仅适用于发布版本。 - Sven Nilsson

4

按照提问的方式,没有解决方案。

在标准C++中,数组越界将导致未定义行为,因此没有特定的结果得到保证。无法给出可靠的结果并不是编译器的问题-它是允许的行为。

我不知道有任何一种实现能够保证对溢出做出任何特定的响应 - VS肯定没有。这并不令人惊讶,因为编译器不需要那样做(也就是说,这基本上是未定义行为的意义)。这种情况之所以会出现是因为经常难以可靠或一致地检测到这种情况。

这意味着检测数组越界的唯一一致方法是在使用它们访问数组元素之前检查数组索引是否有效,并采取适当的操作(例如,抛出可以捕获的异常而不是执行错误操作)。缺点是它不提供一个简单或可靠的方法来捕捉任意代码中的错误,除非修改所有代码来执行所需的检查。


1
我希望能够评论已接受的答案,但是我刚加入并没有足够的声望来做到这一点。
我尝试了使用Visual Studio 2017的解决方案,并且需要进行一些更改才能使解决方案编译成功。
首先,我必须更改__report_gsfailure的签名以匹配Microsoft的一个头文件,以修复编译错误。
__declspec(noreturn) extern "C" void __cdecl __report_gsfailure(_In_ uintptr_t _StackCookie)
{
    RaiseException(STATUS_STACK_BUFFER_OVERRUN, EXCEPTION_NONCONTINUABLE, 0, nullptr);
}

接下来我遇到了一个LNK2005错误,通过在项目属性的链接器->命令行中添加/FORCE:MULTIPLE,我成功地进行了修正。


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