分析崩溃以查找应用程序中的安全漏洞

如何确保程序中的崩溃不可利用?简而言之,答案很简单:假设每个崩溃都是可利用的,然后修复它!至少,这是一个质量问题,在产品交付给客户之前解决这个问题通常更便宜、更实用。执行确定可利用性所需的分析可能会相当昂贵。
分析与内存损坏相关的程序故障,以了解安全后果可能是一项复杂且容易出错的任务。必须考虑几个因素,包括缓冲区在内存中的位置、覆盖的可能目标、覆盖的大小、对覆盖期间可以使用的数据的限制、运行时执行环境的状态以及绕过任何现有缓解机制的能力。简而言之,您必须了解失败的根本原因,才能彻底回答这些问题。
记住,并非每一个失败都会以可观察的方式显现出来。其中一个例子是微软安全公告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命令),我们可以跟踪此寄存器中的数据流,直到确认寄存器中的值不会受到恶意输入的影响。事实上,在这个例子中,寄存器是通过自身的异或运算来调零的,直到到达崩溃指令时才使用它。有时,从崩溃的指令看不到缺陷的可利用性。例如,在分解以下指令之后,您可以看到,当失败的指令(在上一段中描述)后面紧跟着一个关键指令时,这是控制流的结果:
(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0x123:
3036a384 8b12            mov    eax, [ebx]           ds:0023:6c7d890d=??
0:000> u
mov    eax, [ebx]  -> crash, ebx points to invalid memory
...             (instructions not affecting regist an example er eax)
call        [eax]  ->  possibility of code execution

这个例子与第一个类似,但这次崩溃发生在将数据加载到寄存器eax的指令上。尽管此操作本身并不表示存在安全问题,但反汇编清楚地表明,对寄存器ebx的值的控制意味着对寄存器eax的值的控制,这将导致可能的代码执行。
剩下的情况可以通过在程序运行时模拟恶意数据注入进行分析,可以在调试器下手动进行,也可以通过调试工具自动进行。到达导致访问冲突的指令后,我们可以将源地址更改为指向有效的内存地址并再次执行该指令,也可以将随机数据放入目标寄存器并跳过错误指令,继续执行。我们将对遇到的每个访问冲突重复此过程,直到达到可利用的条件。
我们来分析两个例子。首先,让我们看看更改源地址时的跟踪数据流:

Application!Function+0xa70:
3036a37e 8b4708          mov     eax,dword ptr [edi+8] ds:0023:040fd004=????????

这里的崩溃是由寄存器edi中的错误值引起的。我们将更改它以指向有效的内存区域。有许多可能的选择,但在实践中,我们经常使用寄存器eip的当前值。这确保了edi新值周围相对较大的内存块是有效的,并增加了捕获包含代码的内存块的任何后续写入的可能性,因为它始终标记为只读:

0:000> r edi=eip
0:000> g

将寄存器edi设置为寄存器eip的当前值后,我们继续执行,在寄存器esi上遇到另一个异常,在该异常中我们重复该过程:

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xa76:
3036a384 f60601          test    byte ptr [esi],1           ds:0023:6c7d890d=??
0:000> r esi=eip
0:000> g

继续执行时,尝试将数据写入代码段的指令遇到写入访问冲突异常。这意味着攻击者还可以将数据写入任何内存地址:

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xbef:
3036a4fd 894710          mov     dword ptr [edi+10h],eax ds:0023:3036a38e=0c46f60d

如您所见,最初的读取访问冲突是一个真正的安全问题,可能会导致代码执行。现在让我们看看通过设置目标数据和跳过指令来跟踪数据流。分析与之前相同的崩溃时,我们将不断更改目标数据。在遇到初始异常后,我们将eax寄存器设置为任何易于跟踪的值,并按当前指令的大小增加寄存器eip以跳过它:

Application!Function+0xa70:
3036a37e 8b4708          mov     eax,dword ptr [edi+8] ds:0023:03f93004=????????
0:000> r eax=deadbeef
0:000> r eip=eip+3
0:000> g

继续执行时,我们遇到下一个异常,它没有目标,因此我们可以跳过该指令:

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xa76:
3036a384 f60601          test    byte ptr [esi],1           ds:0023:deadbefb=??
0:000> r eip=eip+3
0:000> g

进一步执行时,我们会遇到相同的写入访问冲突异常,这意味着可能存在可利用的问题:

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xbef:
3036a4fd 894710          mov     dword ptr [edi+10h],eax ds:0023:03f9300c=0c46f60d

虽然模拟恶意数据注入的两种方法通常产生相同的结果,但从实践来看,在许多情况下,它们覆盖不同的代码路径,只有一种方法显示问题的可利用性。到目前为止讨论的所有方法都允许快速查找潜在的可利用读取访问冲突崩溃,但我们必须记住,它们没有提供最终验证,即某个崩溃不是可利用的问题。

写入数据时访问冲突

数据写入期间的访问冲突表示可能的内存损坏,这几乎总是导致可利用的情况,并可能导致代码执行。这种写入通常是崩溃程序中存在的缓冲区溢出条件的指示器。在实践中,一些写访问冲突崩溃可能被证明是不可利用的。然而,这些情况要求您执行完整的数据流分析,以了解问题的根本原因。您需要确保损坏不会导致攻击者能够覆盖数据以任意影响执行流。
大多数与数据写入有关的访问冲突都可能导致可利用的情况,从而导致恶意代码执行。通常,代码执行是通过覆盖堆栈或堆上的(任意)内存来实现的。
在下面的示例中,当目标寄存器到达堆栈的上边界并命中未分配的内存区域时,内存复制指令发生崩溃。复制操作的大小来自地址[ebp-8]处的变量,崩溃表明它受攻击者控制:

 Application!Function+0x143: 3036a384 f3a4 rep movsb es:0023:00140000=?? ds:0023:0125432c=41414141 0:000> ub mov esi, [ebp-4] mov edi, [ebp+4] mov ecx, [ebp-8] rep movsb -> write access violation if value in ecx is big enough

我们可以自信地说,只有当攻击者控制的目标地址指向无效内存或不影响程序执行的数据时,写访问冲突才不会导致代码执行。实际上,只有一小部分写访问冲突满足此条件。在Windows用户模式环境中,对空指针或系统地址空间(通常是2GB以上的地址)的写入可能是不可利用问题的一个示例(假设无法分配地址为0x00000000的页)。此外,在服务器场景中,该指令将导致拒绝服务,如果由非管理员用户触发,则应将其视为安全漏洞。
此示例与前面讨论的有关读取访问冲突的情况类似。快速反汇编表明寄存器eax已初始化为值0,并且在到达崩溃指令之前未被更改:

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

不可利用的异常

在用户模式而非内核模式下,此类异常不太可能用于单阶段攻击。通常这些异常会导致拒绝服务。只有在某些情况下,当异常处理程序由于另一个漏洞而更改时,这些异常才会导致恶意代码执行。拒绝服务是服务器平台中的一个高优先级错误,工作站中的一个中等优先级错误。
这些异常的一个例子是静态/全局解引用(读和写)。此示例中的进程没有对页0x310000的读访问权限以发生读访问冲突,或者对同一页的写访问权限以触发写访问冲突。

mov ebp, 310046h
mov eax, [ebp+4h]

inc eax
mov [ebp+8h], eax

前两个例外需要仔细分析。如果静态/全局值指向内存中恶意代码可以无限长写入的地址,并且可以覆盖其他结构以阻止复杂的攻击,则必须将此情况标记为可攻击。如果它只允许您在不属于任何控制结构的地址上存储单个DWORD,则很可能无法利用它。但是,必须通过运行程序并观察值(也称为运行时分析)来验证此语句。如果存储的值在稍后的代码中没有使用,则它不会导致另一个可利用的条件,并且可以忽略。如果该值可以用作内存地址或在内存复制操作中使用,则利用该值的风险更高。
在不可控制的地址空间(第0页和类似情况)中的静态/全局地址上执行代码是另一个可利用性依赖于操作数地址处的内存页是否可控制写入的实例:

0040137F B8 DE C0 AD DE    mov     eax, 0DEADC0DEh
00401384 FF D0             call    eax

对静态/全局地址上的代码执行的分析具有进一步的含义。如果该地址属于永远不会与正常进程执行关联的页(第0页或地址高于0x8000000的页),则它不可利用。
除零操作也会触发异常,但此时无法直接利用它。这种情况需要进行额外的分析,以确定此异常的结果是否可以将CPU设置为执行可导致成功利用漏洞的代码:

004013D6 33 C9    xor    ecx, ecx
004013D8 8B C1    mov    eax, ecx
004013DA 40       inc    eax
004013DB F7 F1    div    ecx

未处理的C++异常可能会破坏进程的执行。在运行时,它们将导致应用程序终止。在调试器下,可以在未处理的异常之后继续。C++异常在运行库的异常抛出函数被调用时发生:

00401902    mov     [ebp+var_4], 1
00401909    push    offset __TI1H
0040190E    lea     eax, [ebp+var_4]
00401911    push    eax
00401912    call    __CxxThrowException@8 ; _CxxThrowException(x,x)

只有当异常处理程序机制已被覆盖时,才能利用此条件。否则未处理的C++异常是不可利用的。此异常的堆栈跟踪如下所示。

CommandLine: test.exe
Symbol search path is: srv*c:\Symbols*\\symbols\symbols
Executable search path is:
ModLoad: 00400000 00405000   test.exe
ModLoad: 7c900000 7c9b0000   ntdll.dll
ModLoad: 7c800000 7c8f5000   C:\WINDOWS\system32\kernel32.dll
ModLoad: 78130000 781cb000   C:\WINDOWS\WinSxS\x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0.50727.762_x-ww_6b128700\MSVCR
80.dll
ModLoad: 77c10000 77c68000   C:\WINDOWS\system32\msvcrt.dll
(1494.14c4): Break instruction exception - code 80000003 (first chance)
eax=00251eb4 ebx=7ffda000 ecx=00000004 edx=00000010 esi=00251f48 edi=00251eb4
eip=7c901230 esp=0012fb20 ebp=0012fc94 iopl=0    nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!DbgBreakPoint:
7c901230 cc              int     3
0:000> g
1
(1494.14c4): C++ EH exception - code e06d7363 (first chance)
(1494.14c4): C++ EH exception - code e06d7363 (!!! second chance !!!)
eax=0012fee0 ebx=00000000 ecx=00000000 edx=781c3c58 esi=0012ff68 edi=004033a4
eip=7c812a5b esp=0012fedc ebp=0012ff30 iopl=0    nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
kernel32!RaiseException+0x53:
7c812a5b 5e              pop     esi
0:000> kb
ChildEBP RetAddr  Args to Child
0012ff30 78158e69 e06d7363 00000001 00000003 kernel32!RaiseException+0x53
0012ff68 00401917 0012ff78 004022b8 00000001 MSVCR80!_CxxThrowException+0x46
0012ff7c 004011b2 00000001 00353098 003530d0 test!wmain+0x27
0012ffc0 7c816fd7 00011970 7c9118f1 7ffda000 test!__tmainCRTStartup+0x10f
0012fff0 00000000 004012fb 00000000 78746341 kernel32!BaseProcessStart+0x23

请注意,堆栈溢出异常可能隐藏运行时的其他问题,将代码流转移到其他路径,禁用编译器生成的保护机制(如/GS),或者释放仍在使用的内存,从而给应用程序带来不一致。当堆栈空间几乎被消耗时,应用程序将可能在具有堆栈溢出异常的C++方法中失败,并且重要的代码块将不会按顺序执行。在这种情况下,需要额外的真的。

/GS Exceptions

/GS (0xc0000409=STATUS_STACK_BUFFER_OVERRUN)异常是,当Windows检测到保护返回地址的安全cookie被篡改时,它将抛出这个异常。由于/GS的目标是将导致代码执行的缓冲区溢出转化为拒绝服务攻击,因此只要检测到此类崩溃,就可以确定存在安全漏洞。(不幸的是,由于内存错误、主板过度锁定、硬件故障和其他问题,有时验证cookie的代码会在没有实际缓冲区溢出的情况下被触发。)

在Windows Vista中,当检测到状态堆栈缓冲区溢出时(假设存在调试器),操作系统将引发int 3异常。在早期版本的Windows中,断点应该放在kernel32!UnhandledExceptionFilter检测安全cookie是否被篡改(否则进程将被终止,用户将不会得到通知)。
下面foo函数将通过将太多数据复制到堆栈缓冲区来溢出缓冲区,这将导致/GS cookie被覆盖。
<the /GS cookie is being setup in the function prolog>
0:000:x86> u gs!foo
gs!foo:
010011b9 8bff            mov     edi,edi
010011bb 55              push    ebp
010011bc 8bec            mov     ebp,esp
010011be 83ec18          sub     esp,18h
<global cookie will be moved to eax register>
010011c1 a100200001      mov     eax,dword ptr [gs!__security_cookie (01002000)] 
010011c6 53              push    ebx
010011c7 56              push    esi
010011c8 57              push    edi
010011c9 8b7d08          mov     edi,dword ptr [ebp+8]
<cookie will be placed on the stack (ebp-4)>
010011cc 8945fc          mov     dword ptr [ebp-4],eax 
<content of the source (src) and destination buffers (dst) – before the overrun>
0:000:x86> dv /V
000bfefc @ebp+0x08             src = 0x009d16cb "123456789012345678901234567890"
000bfedc @ebp-0x18             dst = char [20] ""
<value of the security cookie on the stack; note that it is located right after the buffer, before saved ebp (0x000bff24) and the return address (0x0100124a)> 
0:000:x86> dd 000bfedc+0n20 l3
000bfef0  0000b96f 000bff24 0100124a
<value of global cookie>
0:000:x86> dd gs!__security_cookie l1 
01002000  0000b96f
... code runs ...
<after the overrun has happened, the cookie got overwritten (with 0x34333231) as well as the last two bytes of the return address (with (0x3039)>
0:000:x86> dd 000bfedc+0n20 l3
000bfef0  34333231 38373635 01003039
<in the function epilog before it returns the /GS cookie gets checked (i.e. the return address has not been used yet)> 
0:000:x86> u gs!foo+0x54
gs!foo+0x54:
0100120d 59              pop     ecx
<cookie is placed in the ecx register>
0100120e 8b4dfc          mov     ecx,dword ptr [ebp-4]
01001211 5f              pop     edi
01001212 5e              pop     esi
01001213 5b              pop     ebx
01001214 e882020000      call    gs!__security_check_cookie (01002000)
01001219 c9              leave
0100121a c20400          ret     4
<the below functions compares the global cookie with the one that was stored in the stack (currently in ecx register)>
0:000:x86> r
eax=0000000c ebx=7efde000 ecx=34333231 edx=00000000 esi=00000000 edi=00000000
eip=0100149b esp=000bfed8 ebp=000bfef4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
gs!__security_check_cookie:
0100149b 3b0d00200001    cmp     ecx,dword ptr [gs!__security_cookie (01002000)] ds:002b:01002000=0000b96f
<if a debugger was attached to the process, then an (int 3) will be issued (in Windows Vista) before the process gets terminated>
0:000:x86>
STATUS_STACK_BUFFER_OVERRUN encountered
 (15d0.1708): Break instruction exception - code 80000003 (first chance)
ntdll!DbgBreakPoint:
773e0004 cc              int     3

NX Exceptions

NX(0xc000005=STATUS_ACCESS_VIOLATION)异常由Windows在检测到在未标记为可执行的页上执行的代码时抛出(换句话说,该页没有page_EXECUTE、page_EXECUTE_READ或其他相关标志)。NX在64位版本的操作系统中实施。一些应用程序(如解包器和数字权限管理(DRM))依赖于在堆上执行代码,因此并非每个NX异常都应视为安全漏洞。理解错误的根本原因以确保它不具有安全含义仍然是有意义的。
需要注意的是,这种类型的异常使用错误代码0xc000005,这不是NX特有的;任何行为错误的应用程序(例如,通过读取未分配的内存)都可能引发此错误。为了了解异常/错误代码是否确实与NX相关,我们需要查看页面上的保护集。如果页面未标记为可执行,则会出现NX异常;否则会出现其他类型的问题。例如,在下面例子中,遇到了第一次机会异常,但指令似乎是有效的,它引用了有效的数据。

(1424.a78): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
00000000`003b0020 2801            sub     byte ptr [rcx],al ds:00000000`7702e6aa=c3
0:000> r
rax=0000000000000001 rbx=000000000021fd10 rcx=000000007702e6aa
rdx=0000000000000000 rsi=000000000000000a rdi=0000000000000000
rip=00000000003b0020 rsp=000000000021fca0 rbp=00000000ff130000
 r8=000000000021fc98  r9=00000000ff130000 r10=0000000000000000
r11=0000000000000244 r12=00000000ff131728 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
00000000`003b0020 2801            sub     byte ptr [rcx],al ds:00000000`7702e6aa=c3
<when we execute the actual instruction again, we hit a second chance access violation>
0:000> t
(1424.a78): Access violation - code c0000005 (!!! second chance !!!)
00000000`003b0020 2801            sub     byte ptr [rcx],al ds:00000000`7702e6aa=c3
<a closer look at the protection of the page, shows that it does not have PAGE_EXECUTE set, this explains why the code cannot be executed>
0:000> !address 00000000`003b0020
 ProcessParametrs 00000000003b25c0 in range 00000000003b0000 00000000003e0000
 Environment 00000000003b1370 in range 00000000003b0000 00000000003e0000
    00000000003b0000 : 00000000003b0000 - 0000000000030000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000004 PAGE_READWRITE
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageHeap
                    Handle   00000000003b0000

正如您所看到的,分析程序故障的安全含义是一项复杂且潜在的易出错任务。我们讨论了在分析崩溃时可能遇到的最常见的异常,包括读/写访问违规、零除法、C++异常、GS异常和NX相关问题。要记住的关键点是,只有全面的根本原因分析才能确保正确诊断出给定的崩溃是否可利用。
为了提供一些有用的指南,帮助分析自己的应用程序,我们将这些信息汇总为快速参考。下面以表格形式捕获了本文中讨论的指导原则。例如,如果由于CPU试图将数据写入内存页而没有对其进行写访问,因此发生了写访问冲突,则需要修复该问题。

Exception Comments
Must Fix  
Write access violation The access violation happens when the CPU tries to write data to the memory page without write access to it.
Read access violation on the instruction pointer (access violation on EIP) The access violation happens when the CPU tries to execute an instruction on the memory page without read access to it.
Read access violation One of the following may occur: •The access violation happens on a rep assembly instruction (on an Intel processor) where the count register (ecx) is large. •The access violation happens on a mov instruction where the result is used as the destination of a call in the instructions immediately after the mov. •The access violation happens on a mov instruction where the result is later used in a rep instruction as the source (esi), destination (edi), or count (ecx).
Triage Required  
Read access violation If the access violation happens reading from NULL (address 0x00000000) or if the access violation happens reading from the memory address that is not controlled by the input and the value is not manageable by the attacker.
Usually Not Exploitable  
Division by zero If the access violation happens as a standalone issue and other structures (exception handlers, for example) are not corrupted before this access violation.
C++ exception Same as the above.

同样,图1(在本文前面介绍)旨在通过使用图结构来帮助您确定某个特定崩溃是否可利用,从而提供该进程的另一个视图。从最上面的节点开始,问自己下一步要访问哪个节点。继续此过程,直到到达不可利用或可利用节点。

posted on 2020-05-22 08:16  活着的虫子  阅读(670)  评论(0编辑  收藏  举报

导航