wenmo8 发布的文章

简介

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

简介

STATUS_INVALID_CRUNTIME_PARAMETER,值为0xC0000417,又称CRT参数无效异常,其定义如下:

//
// MessageId: STATUS_INVALID_CRUNTIME_PARAMETER
//
// MessageText:
//
// An invalid parameter was passed to a C runtime function.
//
#define STATUS_INVALID_CRUNTIME_PARAMETER ((NTSTATUS)0xC0000417L)    // winnt

说明

此异常主要是在CRT的相关库函数对传进来的参数进行有效性检测而引发的。比如指针参数是否为空,缓冲区大小和传入的长度等。一般都是通过_invalid_parameter_noinfo->_invalid_parameter->_invoke_watson->_call_reportfault抛出。

异常结构填充

ExceptionAddress: 0f2846a9
ExceptionCode: 0xC0000417
ExceptionFlags: 00000001
NumberParameters:0

前面有两个随笔介绍了这两个异常(《异常STATUS_INVALID_PARAMETER(0xC000000D)》和《关于异常STATUS_INVALID_CRUNTIME_PARAMETER(0xC0000417)》),它们都是参数无效的异常,但针对的对象不一样。STATUS_INVALID_CRUNTIME_PARAMETER从这个命名来看,就是针对C/C++运行时库库函数的参数校验的,而STATUS_INVALID_PARAMETER是针对Windows系统服务和内核函数的参数校验。

经常在调试分析dmup时,会看到很多线程栈在函数的后面会带上FPO,如下所示:

00 00eff818 777beb0d ffffffff 00000000 0107a2ec ntdll!NtTerminateProcess+0xc (FPO: [2,0,0])
01 00eff8f0 762c4f32 00000000 77e8f3b0 ffffffff ntdll!RtlExitUserProcess+0xbd (FPO: [Non-Fpo])
02 00eff904 71e74ebc 00000000 00eff958 71e7518e KERNEL32!ExitProcessImplementation+0x12 (FPO: [1,0,0])
03 00eff910 71e7518d 00000000 dd53d81d 00000000 MSVCR120!__crtExitProcess+0x15 (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt\crtw32\startup\crt0dat.c @ 774]
04 00eff958 71e751b2 00000000 00000000 00000000 MSVCR120!doexit+0x115 (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt\crtw32\startup\crt0dat.c @ 678]
*** WARNING: Unable to verify checksum for ConsoleApplication3.exe
05 00eff96c 01391a02 00000000 a2730342 01391a53 MSVCR120!exit+0xf (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt\crtw32\startup\crt0dat.c @ 417]
06 00eff9a4 762c0419 00caa000 762c0400 00effa10 ConsoleApplication3!__tmainCRTStartup+0x114 (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c @ 649]
07 00eff9b4 777c66dd 00caa000 65564319 00000000 KERNEL32!BaseThreadInitThunk+0x19 (FPO: [Non-Fpo])
08 00effa10 777c66ad ffffffff 777e53d5 00000000 ntdll!__RtlUserThreadStart+0x2f (FPO: [SEH])
09 00effa20 00000000 01391a53 00caa000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])
由于不影响具体问题的分析,一直没关注,今天又看到,且有点闲,就查了相关资料,补齐这块知识。

什么是FPO

要知道答案,你必须回到史前时代。英特尔的8088处理器有一组非常有限的寄存器(我忽略了段寄存器),它们是:

AX BX CX DX IP
SI DI BP SP FLAGS

有了这样一组有限的寄存器,这些寄存器都被指定了特定的用途。AX、BX、CX和DX是“通用”寄存器,SI和DI是“索引”寄存器,SP是“堆栈指针”,BP是“帧指针”,IP是“指令指针”,and FLAGS是一个只读寄存器,它包含若干位,这些位表示处理器当前状态的信息(例如,前一个算术或逻辑指令的结果是0)。
BX、SI、DI和BP寄存器是特殊的,因为它们可以用作“索引”寄存器。索引寄存器对编译器至关重要,因为它们用于通过指针访问内存。换句话说,如果您的结构位于内存中的偏移量0x1234处,则可以将索引寄存器设置为值0x1234并访问相对于该位置的值。例如:

MOV    BX, [Structure]
MOV    AX, [BX]+4

将BX寄存器设置为[Structure]指向的内存值,并将AX的值设置为相对于该结构开头的第4个字节处的字。
需要注意的一点是,SP寄存器不是索引寄存器。这意味着要访问堆栈上的变量,需要使用不同的寄存器,这就是BP寄存器的来源-BP寄存器专门用于访问堆栈上的值。
当386出现时,他们将各种寄存器扩展到32位,并且他们修正了只有BX、SI、DI和BP可以用作索引寄存器的限制。

EAX EBX ECX EDX EIP
ESI EDI EBP ESP FLAGS

这是件好事,突然之间,编译器可以使用其中的6个,而不是被限制在3个索引寄存器中。
因为索引寄存器是用来访问结构的,所以对于编译器来说,它们就像黄金一样——更多的索引寄存器是一件好事,而获得更多索引寄存器几乎是值得的。
一些非常聪明的人意识到,由于ESP现在是一个索引寄存器,EBP寄存器不再需要专门用于访问堆栈上的变量。下面是使用EBP:

MyFunction:
    PUSH    EBP
    MOV     EBP, ESP
    SUB      ESP, <LocalVariableStorage>
    MOV     EAX, [EBP+8]
      :
      :
    MOV     ESP, EBP
    POP      EBP
    RETD

要访问堆栈上的第一个参数(EBP+0是EBP的旧值,EBP+4是返回地址),可以执行以下操作:

MyFunction:
    SUB      SP, <LocalVariableStorage>
    MOV     EAX, [ESP+4+<LocalVariableStorage>]
      :
      :
    ADD     SP, <LocalVariableStorage>
    RETD

突然之间,EBP可以重新利用,并用作另一个通用寄存器!编译人员称这种优化为“帧指针省略(Frame Pointer Omission)”,它的首字母缩写是FPO。FPO是一种优化,它压缩或者省略了在栈上为该函数创建框架指针的过程。这个选项加速了函数调用,因为不需要建立和移除框架指针(ESP,EBP)了。同时,它还解放出了一个寄存器,用来存储使用频率较高的变量。只在IntelCPU的架构上才有这种优化。目前已经讨论过的任何一种调用约定都保存了前一函数中栈的信息(压栈ebp,然后让ebp = esp,再移动esp来保存局部变量)。一个FPO的函数可能会保存前一函数的栈指针(ESP,EBP),但是并不为当前的函数调用设立EBP。相反,他使用EBP来存储一些其他的变量。debugger 会计算栈指针,但是debugger必须得到一个使用FPO的提醒,该提醒是基于FPO类型的信息的来完成的。这项特性可以在MS Visual C++专业版和企业版中开启。使用的是编译器的/Oy选项。

FPO的数据结构可以在Microsoft的SDK中的winnt.h中找到,其中包含了描述栈框架内容的信息。这些信息被使用在debugger上,或者其他的需要在栈中寻找FPO函数的程序中。KV命令可以显示出包括FPO信息在内的额外的运行时信息。
但FPO有一个小问题。如果您查看MyFunction的pre-FPO示例,您会注意到例程中的第一条指令是PUSH EBP,然后是MOV EBP,特别是它有一个有趣且非常有用的副作用。它实际上创建了一个单独的链接列表,将每个调用者的帧指针链接到一个函数。从一个例程的EBP中,可以恢复一个函数的整个调用堆栈。这对于调试器来说是难以置信的有用——这意味着调用栈是相当可靠的,即使没有被调试器的所有模块的符号。不幸的是,当FPO被启用时,堆栈帧的列表丢失了——信息根本没有被跟踪。
为了解决这个问题,编译器人员将启用FPO时丢失的信息放入二进制文件的PDB文件中。因此,当有模块的符号时,可以恢复所有堆栈信息。
在NT 3.51中,所有的Windows二进制文件都启用了FPO,但是在Vista中的Windows二进制文件却被关闭了,因为它不再是必需的—机器从1995年开始变得足够快,以至于FPO所实现的性能改进不足以抵消FPO在调试和分析中所带来的痛苦。

FPO: [2,0,0]代表什么?

我们已经知道FPO是什么了,可是[2,0,0]是什么呢,看如下的表

FPO数据表示形式 (FPO: [ebp addr][x,y,z])
x代表 作为参数压栈了的DWORDS个数
y代表 作为局部变量压栈了的DWORDS个数
z代表 在开场代码中(prologue)压栈了的寄存器个数
ebp addr代表 仅在EBP在开场代码中保存了的时候显示

本文最开始的例子中,由于调试器有正确的symbol,所以调试器会计算出栈底(Frame Pointer)的位置,而不是在EBP之中保存它。比如说,第一个参数的位置是栈底+0x8,返回值的位置是栈底+0x4. 开启了FPO之后,这些值就不能通过ebp + 0x8这样拿到了,跟ebp等值的栈底(Frame Pointer)需要计算才能拿到。