2023年1月

在Visual Studio 2015中,引入了新的异常设置窗口,该窗口提供了一种快速的方法,可以将调试器配置为在抛出异常时中断。作为窗口更新的一部分,仅仅按异常类型过滤并不总是足够好,您需要对调试器何时中断抛出的异常进行更细粒度的控制。因此,在Visual Studio 2017中,引入了一个新功能,允许您控制抛出异常时的中断,而不仅仅是类型。您可以将模块名条件添加到异常中,以便只在您关心的模块引发异常时中断。

概述-中断和抛出

实际情况是,您需要处理代码中的异常,以便在出现意外情况时应用程序能够正常降级,但作为开发人员,您仍然需要找出应用程序遇到这些意外情况的原因。因此,您希望在抛出异常时调试应用程序,而不必在代码中导航,以确定处理的是什么异常以及处理的位置。当调试涉及已处理异常的问题时,可以使用“异常设置”窗口告诉调试器在引发异常时中断。这允许您在处理异常之前在调试器下停止并检查异常。您可以对特定异常类型或整个异常类别执行此操作。

如果始终调试一个程序,并且将异常类别设置为在抛出时中断,则可以在程序执行过程中尽早捕获与异常相关的问题,这有助于确保以正确的方式处理每个异常。

编写C/C++代码时,通常使用复杂的代码行来处理多个指针,例如,在单个行上访问多个指针。然而,当访问冲突发生时,很难破译这行代码的哪一部分是问题所在。为了调试这个问题,您可能已经将这一行代码分解成多行,但不会再这样了。当使用Visual Studio 2015更新1时发生这种情况时,您可以很容易地看到导致此异常的指针。现在,我们在异常对话框中直接显示一条消息,通知您哪个变量是nullptr。

让我们来看下面的小C++代码示例。在本例中,我们有一系列类,每个类都包含指向另一个类的指针,以及一个名为GetHelloWorld()的函数。在main()方法中,我们取消引用所有指针,以便打印“Hello World”。

    int main()
    {
       ClassA* A = new ClassA();
       printf(A-> B-> C-> D-> GetHelloWorld());
       return 0;
    }

如果您花了任何时间编写代码,很可能您不得不处理异常处理。在Visual Studio中,当异常被抛出或最终未被处理时,调试器可以帮助您通过中断来调试这些异常,就像在遇到断点时中断一样。下面我们将讨论异常的不同分类,以及如何配置调试器何时为这些异常中断。

异常分类

我们将从调试器中异常的分类类型开始。调试器按以下方式对异常进行分类:

First Chance Exceptions:

当应用程序中首次抛出异常时,这被归类为“First Chance”异常。此时,调试器不知道应用程序是否会捕获(处理)异常。所有异常都以第一次机会异常开始。

  • 每次引发异常时都会通知调试器。您可以在输出窗口和IntelliTrace中看到这些通知。
  • 您可以告诉调试器要中断的首次机会异常,就像启用断点一样。
  • 一旦由于首次出现异常而中断,可以通过单步执行或按“继续”继续调试。当您继续时,您的代码有机会处理此异常,如果没有,则该异常现在属于下面列出的分类之一。

User-unhandled Exceptions:

当用户代码中没有捕获(处理)第一次机会异常,并且在调用堆栈的“外部代码”中捕获时,这被归类为“用户未处理”异常。此分类仅适用于仅启用我的代码调试托管或JavaScript应用程序时。

  • 默认情况下,调试器将为所有用户未处理的异常而中断。
  • 一旦由于用户未处理的异常而中断,可以通过单步执行或按“继续”继续调试。异常可以在调用堆栈的“外部代码”中的某个位置处理,如果不是,则它将成为未处理的异常。
  • 您可以更改默认设置,但在大多数情况下,您可能不需要更改此设置。大多数框架,比如ASP.NET,都实现了全局异常处理程序,这样应用程序就不会崩溃,但是异常并没有得到正确的处理。调试器为用户未处理的异常提供中断功能,以便在这些情况下通知您。

Unhandled Exceptions:

当应用程序未捕获(处理)第一次机会异常并到达系统默认处理程序时,这被归类为“未处理”异常。

简介

STATUS_STACK_BUFFER_OVERRUN,值为0xC0000409,又称栈缓冲区溢出异常,其定义如下:

/
// MessageId: STATUS_STACK_BUFFER_OVERRUN
//
// MessageText:
//
// The system detected an overrun of a stack-based buffer in this application. This overrun could potentially allow a malicious user to gain control of this application.
//
#define STATUS_STACK_BUFFER_OVERRUN      ((NTSTATUS)0xC0000409L)    // winnt

说明

系统在此应用程序中检测到基于堆栈的缓冲区溢出。此溢出可能允许恶意用户获得此应用程序的控制权。同时现在的Windows系统上也不仅仅用来表达着个异常,也用来做一些会导致致命错误的安全检测,而引发进程快速失败。与所有其他异常代码不同,Fail Fast异常绕过所有异常处理程序(基于帧或向量)。如果启用了Windows错误报告,引发此异常将终止应用程序并调用Windows错误报告。本异常代码最初设计用于引发安全检查失败。具体来说,是违反警戒线(/GS)。随着时间的推移,出于非安全原因,应用程序利用了立即终止功能的愿望。这些应用程序利用第一个参数来指定场景(子代码)。原始的“安全检查失败”用例保留值为0。由于每个应用程序的性质,当前未定义异常参数值。

异常填充结构

ExceptionAddress: 0f2846a9 (msvcr120!_invoke_watson+0x0000000e)
ExceptionCode: c0000409 (Security check failure or stack buffer overrun)
ExceptionFlags: 00000001
NumberParameters: 1
   Parameter[0]: 00000005//异常子代码
Subcode: 0x5 FAST_FAIL_INVALID_ARG

如何确保程序中的崩溃不可利用?简而言之,答案很简单:假设每个崩溃都是可利用的,然后修复它!至少,这是一个质量问题,在产品交付给客户之前解决这个问题通常更便宜、更实用。执行确定可利用性所需的分析可能会相当昂贵。
分析与内存损坏相关的程序故障,以了解安全后果可能是一项复杂且容易出错的任务。必须考虑几个因素,包括缓冲区在内存中的位置、覆盖的可能目标、覆盖的大小、对覆盖期间可以使用的数据的限制、运行时执行环境的状态以及绕过任何现有缓解机制的能力。简而言之,您必须了解失败的根本原因,才能彻底回答这些问题。
记住,并非每一个失败都会以可观察的方式显现出来。其中一个例子是微软安全公告MS07-017中讨论的GDI远程代码执行问题。负责调用易受攻击的解析代码的软件使用异常处理程序来从几乎所有可能生成的异常中恢复,并像没有发生任何异常一样继续运行。另一个不太明显的例子可以在某些类型的堆栈和堆内存损坏中找到,可能发生了故障,但程序的当前状态及其执行环境没有显示任何明显的迹象。

本文提供了有关如何分析程序崩溃的指导,以考虑可能的安全隐患,例如启用任意代码执行的内存损坏或至少拒绝服务的情况。我们将列举您在查看这些类型的问题时可能遇到的常见硬件和软件异常。我们也会提供一些一般性的指导,你可以在这样的调查中使用。例如,下图给出了调查过程的图形路径,以帮助您确定特定崩溃是否可利用。重要的是要记住,这些只是指导原则,只有全面的根本原因分析才能确保您已正确诊断为不可利用的崩溃。新技术或现有攻击技术的变种一直在被发现。

 

最常见的崩溃原因是硬件或软件异常。典型的现代处理器可以生成许多不同类型的硬件异常,但在Windows环境中,只有其中一些会产生与软件安全相关的问题。最常见的硬件异常是访问冲突。我们将首先介绍如何分析硬件异常,然后是软件异常。

访问冲突

当指令或程序执行导致的内存访问不满足处理器体系结构或内存管理单元结构定义的某些条件时,现代处理器会生成访问冲突异常(0xc000005=状态访问冲突)。
虽然纯崩溃只能导致拒绝服务条件,但不能安全地假设崩溃不能用于实现更危险的效果,包括代码执行。在分析崩溃时,您应该假设整个内存体(除了一些小的异常)都处于潜在攻击者的控制之下;因此,在大多数情况下,访问冲突可能导致由攻击者控制的数据。此语句适用于指令读取或写入数据时发生的异常。
如果访问冲突会导致您的数据受到攻击者的控制,则从内存读取导致的每个访问冲突都会转化为加载攻击者控制的数据。这种行动的安全效果并不总是容易确定的。您可以对二进制或源代码执行完整的数据流分析,以找到源地址控制的范围以及在某些执行点向程序提供随机数据的结果。这是一项耗时且富有挑战性的任务。作为回应,我们开发了简单的启发式方法来快速分析代码执行潜力的读取访问冲突崩溃。
如下例所示,寄存器eax中的无效内存指针导致崩溃。在这种情况下,对内存内容的控制使攻击者能够完全控制程序流:

Application!Function+0x133:
3036a384 eb32            call     [eax]           ds:0023:6c7d890d=??
0:000> ub
mov    eax, [ebx]  ->  eax = invalid memory pointer
...                         (instructions not affecting register eax)
call        [eax]  ->  crash

如果攻击者无法充分控制正在读取的地址,则可以将其视为拒绝服务条件。例如,在典型的Windows用户模式环境中,在初始化的空指针处发生的、不受攻击者影响的崩溃本身不会导致代码执行。
在下面的示例中,您可以看到崩溃是由引用寄存器eax中的地址0值引起的:

Application!Function+0x133:
3036a384 8b14            mov     ecx, [eax]           ds:0023:00000000=??
0:000> ub
xor    eax, eax
...                         (instructions not affecting register eax)
cmp    ebx, 2
jne        label123
mov    ecx, [eax]  ->  crash, eax = 0 (NULL)

通过反汇编(使用Visual Studio命令行调试器中的ub命令),我们可以跟踪此寄存器中的数据流,直到确认寄存器中的值不会受到恶意输入的影响。事实上,在这个例子中,寄存器是通过自身的异或运算来调零的,直到到达崩溃指令时才使用它。有时,从崩溃的指令看不到缺陷的可利用性。例如,在分解以下指令之后,您可以看到,当失败的指令(在上一段中描述)后面紧跟着一个关键指令时,这是控制流的结果: