嵌套异常的循环
真是个不可思议的巧合。仅隔几天,我就要解决两个与嵌套异常处理程序有关的问题。具体来说,导致堆栈溢出的嵌套异常的无限循环。这是一个非常致命的组合。堆栈溢出对于调试来说是一个极其严重的错误;嵌套异常意味着异常处理程序遇到了一个异常,这是不可能的;更糟糕的是,堆栈损坏也在幕后发生。请继续阅读以了解诊断嵌套异常的技巧,以及首先可能导致这些异常的原因。
案例1:VC异常过滤器中的读取错误
客户机有多个转储文件供我查看,它们都显示了一个非常疯狂的模式。应用程序中会发生异常,这是完全可以预料和处理的异常。但是,它不会被正常处理,而是会导致无限的嵌套异常级联,最终导致进程崩溃,出现堆栈溢出。为了简洁起见,这里有一个简短的图片:
0:000> kcn 10000 ... <repeated hundreds more times> 19e9 <Unloaded_Helper.dll> 19ea NestedExceptions1!exception_filter 19eb NestedExceptions1!trigger_exception 19ec MSVCR120D!_EH4_CallFilterFunc 19ed MSVCR120D!_except_handler4_common 19ee NestedExceptions1!_except_handler4 19ef ntdll!ExecuteHandler2 19f0 ntdll!ExecuteHandler 19f1 ntdll!KiUserExceptionDispatcher 19f2 <Unloaded_Helper.dll> 19f3 NestedExceptions1!exception_filter 19f4 NestedExceptions1!trigger_exception 19f5 MSVCR120D!_EH4_CallFilterFunc 19f6 MSVCR120D!_except_handler4_common 19f7 NestedExceptions1!_except_handler4 19f8 ntdll!ExecuteHandler2 19f9 ntdll!ExecuteHandler 19fa ntdll!KiUserExceptionDispatcher 19fb <Unloaded_Helper.dll> 19fc NestedExceptions1!exception_filter 19fd NestedExceptions1!trigger_exception 19fe MSVCR120D!_EH4_CallFilterFunc 19ff MSVCR120D!_except_handler4_common 1a00 NestedExceptions1!_except_handler4 1a01 ntdll!ExecuteHandler2 1a02 ntdll!ExecuteHandler 1a03 ntdll!KiUserExceptionDispatcher 1a04 NestedExceptions1!trigger_exception 1a05 NestedExceptions1!main 1a06 NestedExceptions1!__tmainCRTStartup 1a07 NestedExceptions1!mainCRTStartup 1a08 kernel32!BaseThreadInitThunk 1a09 ntdll!__RtlUserThreadStart 1a0a ntdll!_RtlUserThreadStart
在前面的调用堆栈中,很明显exception_filter试图调用卸载的DLL(Helper.DLL)中的函数。这又会导致一个异常(很可能是访问冲突),该异常会将控制权转移到异常过滤器,而我们在嵌套的异常循环中处于非常深的位置。顺便说一下,如果您知道要查找什么,那么很容易跟踪异常链。以下是几帧的kb输出:
006deb54 00a85706 00000000 00000000 00000000 <Unloaded_Helper.dll>+0x6666665e
006dec28 00a85eab 006dec4c 0f4e3924 00000000 NestedExceptions1!exception_filter+0x26
006dec30 0f4e3924 00000000 00000000 00000000 NestedExceptions1!trigger_exception+0x6b
006dec44 0f4e9268 006deda0 006dedf0 00000001 MSVCR120D!_EH4_CallFilterFunc+0x12
006dec7c 00a866d2 00a90000 00a81041 006deda0 MSVCR120D!_except_handler4_common+0xb8
006dec9c 7794c881 006deda0 006df734 006dedf0 NestedExceptions1!_except_handler4+0x22
006decc0 7794c853 006deda0 006df734 006dedf0 ntdll!ExecuteHandler2+0x26
006ded88 7794c6bb 006deda0 006dedf0006deda0 ntdll!ExecuteHandler+0x24
006ded88 58b3115e 006deda0 006dedf0 006deda0 ntdll!KiUserExceptionDispatcher+0xf
006df0d4 00a85706 00000000 00000000 00000000 <Unloaded_Helper.dll>+0x6666665e
006df1a8 00a85eab 006df1cc 0f4e3924 00000000 NestedExceptions1!exception_filter+0x26
006df1b0 0f4e3924 00000000 00000000 00000000 NestedExceptions1!trigger_exception+0x6b
006df1c4 0f4e9268 006df324 006df374 00000001 MSVCR120D!_EH4_CallFilterFunc+0x12
006df1fc 00a866d2 00a90000 00a81041 006df324 MSVCR120D!_except_handler4_common+0xb8
006df21c 7794c881 006df324 006df734 006df374 NestedExceptions1!_except_handler4+0x22
006df240 7794c853 006df324 006df734 006df374 ntdll!ExecuteHandler2+0x26
006df30c 7794c6bb 006df324 006df374 006df324 ntdll!ExecuteHandler+0x24
006df30c 00a85e8f 006df324 006df374 006df324 ntdll!KiUserExceptionDispatcher+0xf
006df744 00a86068 00000000 00000000 7ebab000 NestedExceptions1!trigger_exception+0x4f
006df818 00a86a79 00000001 00c37b88 00c35178 NestedExceptions1!main+0x28
006df868 00a86c6d 006df87c 76dc919f 7ebab000 NestedExceptions1!__tmainCRTStartup+0x199
006df870 76dc919f 7ebab000 006df8c0 77960bbb NestedExceptions1!mainCRTStartup+0xd
006df87c 77960bbb 7ebab000 35ed4a97 00000000 kernel32!BaseThreadInitThunk+0xe
006df8c0 77960b91 ffffffff 7794c9d2 00000000 ntdll!__RtlUserThreadStart+0x20
006df8d0 00000000 00a812d5 7ebab000 00000000 ntdll!_RtlUserThreadStart+0x1b
突出显示的值(ntdll!ExecuteHandler的第二个参数)是上下文记录,前面的值是异常记录。您可以使用.cxr和.exr命令在WinDbg中检查它们:
0:000> .exr 006df324 E
xceptionAddress: 00a85e8f (NestedExceptions1!trigger_exception+0x0000004f)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 00000000
Attempt to write to address 00000000
0:000> .cxr 006dedf0
eax=cccccccc ebx=00000000 ecx=00000000 edx=00000000 esi=006df0dc edi=006df1a8 eip=58b3115e esp=006df0d8 ebp=006df1a8 iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
<Unloaded_Helper.dll>+0x6666665e: 58b3115e ?? ???
0:000> .exr 006deda0
ExceptionAddress: 58b3115e (<Unloaded_Helper.dll>+0x0006666665e)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000010
NumberParameters: 2
Parameter[0]: 00000008
Parameter[1]: 58b3115e
Attempt to execute non-executable address 58b3115e
这表明原始异常是trigger_exception函数中的访问冲突,但它被另一个访问冲突所掩盖,该访问冲突是由从卸载的DLL执行代码引起的。
但最初的问题比卸载的DLL更微妙。在实际应用中,exception_filter实际上是由VisualC++处理的异常过滤器,用于处理C++异常:msvcr*!__InternalCxxFrameHandler。不知何故,它会触发嵌套异常,异常代码是0xc0000060xc0000006 (IN_PAGE_ERROR: in-page I/O error) 和 I/O error code 0xc000020c (STATUS_CONNECTION_DISCONNECTED)。该嵌套异常会将控制权再次传输到__InternalCxxFrameHandler,并会一次又一次地命中相同的嵌套错误。
有了这些信息,我们开始调查。导致嵌套异常的实际内存访问是一个数据结构传递给了 __InternalCxxFrameHandler处理程序,它存储在二进制文件中,并包含由C++编译器发出的异常处理信息。此信息不经常访问-仅在发生异常时才需要。
最后一个难题是应用程序是从一个网络驱动器运行的,这个驱动器被大量的机器访问,给服务器的网络连接带来了很大的负载。因此,工作流程如下:
- 应用程序的初始化路径早期发生异常
- 还发生连接错误,导致应用程序二进制文件所在的网络驱动器暂时断开连接
- VisualC++异常过滤器需要访问存储在应用程序二进制文件中的数据结构,但数据结构尚未从网络驱动器缓存在RAM中,因为只有在异常发生时才访问该数据结构。
- 异常筛选器随后失败,并出现I/O错误,该错误再次将控制权传递给异常筛选器-导致异常的嵌套循环
通过编写访问大型只读全局数据结构(只读全局数据也存储在应用程序二进制文件中)的异常过滤器,并从网络驱动器运行示例应用程序,我能够重建此问题。当我在导致异常之前禁用了网络适配器时,我们得到了上面描述的症状。
我们是怎么解决这个问题的?事实证明,VisualC++有一个名为/SWAPRUN的链接器设置(具体地说,/SWAPRUN:NET),它可以用来指示系统在执行之前将应用程序二进制加载到内存中。这意味着应用程序不可能开始运行,但由于连接问题,二进制文件的某些部分会突然变得不可用。显然,首先最好避免连接问题,但是考虑到网络本身是不可靠的。
案例2:导致堆栈溢出的堆栈损坏
第二个案例展示了导致堆栈溢出的嵌套异常链。唯一的区别是堆栈也已损坏。下面是堆栈底部的一些帧:
0:000> kn ... <repeated hundreds more times> f04 002be8c4 7794c881 0xcccccccc f05 002be8e8 7794c853 ntdll!ExecuteHandler2+0x26 f06 002be9b0 7794c6bb ntdll!ExecuteHandler+0x24 f07 002be9b0 cccccccc ntdll!KiUserExceptionDispatcher+0xf f08 002becfc 7794c881 0xcccccccc f09 002bed20 7794c853 ntdll!ExecuteHandler2+0x26 f0a 002bede8 7794c6bb ntdll!ExecuteHandler+0x24 f0b 002bede8 cccccccc ntdll!KiUserExceptionDispatcher+0xf f0c 002bf134 7794c881 0xcccccccc f0d 002bf158 7794c853 ntdll!ExecuteHandler2+0x26 f0e 002bf224 7794c6bb ntdll!ExecuteHandler+0x24 f0f 002bf224 010b51d5 ntdll!KiUserExceptionDispatcher+0xf f10 002bf674 cccccccc NestedExceptions2!trigger_exception+0x65 f11 002bf748 010b5cd9 0xcccccccc f12 002bf798 010b5ecd NestedExceptions2!__tmainCRTStartup+0x199 f13 002bf7a0 76dc919f NestedExceptions2!mainCRTStartup+0xd f14 002bf7ac 77960bbb kernel32!BaseThreadInitThunk+0xe f15 002bf7f0 77960b91 ntdll!__RtlUserThreadStart+0x20 f16 002bf800 00000000 ntdll!_RtlUserThreadStart+0x1b
堆栈上的0xCCCCCC地址看起来像是堆栈损坏的必然征兆。实际上,您可能已经有了一个有效的假设:堆栈已被0xcccccccc损坏,ntdll中的异常处理代码以某种方式尝试从地址0xcccccccc执行代码。这会导致嵌套异常,我们的情况与case#1相同。如果我们查看异常记录,我们可以确认。(第一个异常记录是根本原因,第二个异常记录是由于无效的异常处理程序造成的):
0:000> .exr 002bf23c ExceptionAddress: 010b51d5 (NestedExceptions2!trigger_exception+0x00000065) ExceptionCode: c0000005 (Access violation) ExceptionFlags: 00000000 NumberParameters: 2 Parameter[0]: 00000001 Parameter[1]: 00000000 Attempt to write to address 00000000 0:000> .exr 002bee00 ExceptionAddress: cccccccc ExceptionCode: c0000005 (Access violation) ExceptionFlags: 00000010 NumberParameters: 2 Parameter[0]: 00000000 Parameter[1]: cccccccc Attempt to read from address cccccccc
但也有一些微妙之处。为什么ntdll试图从无效地址0xcccccccc执行代码?答案是,在32位Windows应用程序中,异常过滤器的地址存储在堆栈中,作为名为异常注册记录的数据结构的一部分。如果该结构已损坏,则ntdll中的异常处理代码可能会尝试执行无效地址,认为它是指向异常筛选器的指针。
但这实际上是一个相当严重的安全漏洞!如果攻击者可以覆盖异常注册记录,然后触发异常,则可以执行任意代码。事实上,开发异常注册记录是克服Visual C++ 2003(/GS标志)中引入的堆栈防御的方法之一。
幸运的是,Windows有一些技巧。首先,Visual C++ 2003引入了一个链接器标志,称为。当此标志可用时,链接器在二进制文件中嵌入所有有效异常处理程序的目录。在运行时,Windows可以检查即将运行的异常处理程序是否在有效异常处理程序列表中。如果不是,它将直接拒绝执行无效的异常处理程序,并停止进程-这比尝试执行未知的异常处理程序要安全得多。
其次,Windows Vista SP1和Windows Server 2008引入了另一种系统范围的保护机制SEHOP(结构化异常处理覆盖保护)。默认情况下,在客户端操作系统(Windows Vista、Windows 7、Windows 8)上禁用SEHOP,在服务器操作系统(Windows server 2008和Windows server 2012)上启用SEHOP。SEHOP是一个相当简单的防御:当它打开时,每个线程在链的开头都有一个虚拟的异常注册记录。当发生异常时,ntdll中的异常处理代码将验证当前异常处理程序链是否以该虚拟异常注册记录终止。如果没有,则系统假设异常处理程序链已被篡改,并终止进程。
当这些防御打开时,上述情况根本不可能发生。将阻止将控件传输到0xCCCCCC,就像它是异常处理程序一样,并且该进程将突然终止。因此,我们可以得出结论,发生此崩溃的系统不符合现代安全指南:应用程序应该使用/SAFESEH编译,并且系统应该启用SEHOP。顺便说一下,从Windows7开始,您可以使用ImageFileExecutionOptions注册表项为单个进程配置SEHOP,而不会影响系统的其余部分。
例如,下面是使用/SAFESEH编译二进制文件时发生的情况(注意,启用软件DEP的/NXCOMPAT标志也是必需的):
:000> kn
# ChildEBP RetAddr
00 008aee54 779c625f ntdll!NtWaitForMultipleObjects+0xc
01 008af2b8 779c5e38 ntdll!RtlReportExceptionEx+0x3eb
02 008af314 779e81bf ntdll!RtlReportException+0x9b
03 008af394 7798b2e3 ntdll!RtlInvalidHandlerDetected+0x4e04 008af3ec 7797734a ntdll!RtlIsValidHandler+0x13f1a
05 008af484 7794c6bb ntdll!RtlDispatchException+0xfc
06 008af484 01284ef5 ntdll!KiUserExceptionDispatcher+0xf
07 008af8d4 cccccccc NestedExceptions2!trigger_exception+0x65
WARNING: Frame IP not in any known module. Following frames may be wrong.
08 008af9a8 012858c9 0xcccccccc
09 008af9f8 01285a0d NestedExceptions2!__tmainCRTStartup+0x199
0a 008afa00 76dc919f NestedExceptions2!mainCRTStartup+0xd
0b 008afa0c 77960bbb kernel32!BaseThreadInitThunk+0xe
0c 008afa50 77960b91 ntdll!__RtlUserThreadStart+0x20
0d 008afa60 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> !analyze -v
... <removed for brevity>
DEFAULT_BUCKET_ID: APPLICATION_FAULT
PROCESS_NAME: NestedExceptions2.exe
ERROR_CODE: (NTSTATUS) 0xc00001a5 - An invalid exception handler routine has been detected.
EXCEPTION_CODE: (NTSTATUS) 0xc00001a5 - An invalid exception handler routine has been detected.
... <removed for brevity>
重要的是,不存在嵌套异常的无限循环。进程立即终止,并调用Windows错误报告。另一方面,当为特定进程启用SEHOP时,它会阻止无效的异常处理程序执行。在一个典型的WER转储文件中,结果显示为原始异常(本应正常处理)最终未处理:
0:000> !analyze -v
... FAULTING_IP: NestedExceptions2!trigger_exception+65 002c51d5 c705000000002a000000 mov dword ptr ds:[0],2Ah
EXCEPTION_RECORD: ffffffff -- (.exr 0xffffffffffffffff)
ExceptionAddress: 002c51d5 (NestedExceptions2!trigger_exception+0x00000065)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000008
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 00000000 Attempt to write to address 00000000 ...
BUGCHECK_STR: APPLICATION_FAULT_NULL_POINTER_WRITE_SEHOP
PRIMARY_PROBLEM_CLASS: NULL_POINTER_WRITE_SEHOP
DEFAULT_BUCKET_ID: NULL_POINTER_WRITE_SEHOP ...
FAILURE_BUCKET_ID: NULL_POINTER_WRITE_SEHOP_c0000005_NestedExceptions2.exe!trigger_exception
BUCKET_ID: APPLICATION_FAULT_NULL_POINTER_WRITE_SEHOP_nestedexceptions2!trigger_exception+65
ANALYSIS_SOURCE: UM FAILURE_ID_HASH_STRING: um:null_pointer_write_sehop_c0000005_nestedexceptions2.exe!trigger_exception ...
但有一个很好的暗示是SEHOP参与了其中——分析中“SEHOP”这个词出现了多次。我不确定是否有更好的方法来确定SEHOP的参与,但这对我来说已经足够了
结论
首先,在处理嵌套异常的无限循环时,不要惊慌。您需要标识启动链的原始异常,然后查找重复模式。链中某个点上的异常筛选器/处理程序必须已失败,并且一系列控制传输将返回到同一个异常筛选器。
其次,结构化异常处理是一种易受攻击的机制,特别是在32位应用程序中。确保使用编译器和操作系统提供的所有保护:SafeSEH、DEP和SEHOP。