真是個不可思議的巧合。僅隔幾天,我就要解決兩個與嵌套異常處理程序有關的問題。具體來說,導致堆棧溢出的嵌套異常的無限循環。這是一個非常致命的組合。堆棧溢出對於調試來說是一個極其嚴重的錯誤;嵌套異常意味着異常處理程序遇到了一個異常,這是不可能的;更糟糕的是,堆棧損壞也在幕后發生。請繼續閱讀以了解診斷嵌套異常的技巧,以及首先可能導致這些異常的原因。
案例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>+0x1115e
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 006dedf0 006deda0 ntdll!ExecuteHandler+0x24
006ded88 58b3115e 006deda0 006dedf0 006deda0 ntdll!KiUserExceptionDispatcher+0xf
006df0d4 00a85706 00000000 00000000 00000000 <Unloaded_Helper.dll>+0x1115e
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>+0x1115e: 58b3115e ?? ???
0:000> .exr 006deda0
ExceptionAddress: 58b3115e (<Unloaded_Helper.dll>+0x0001115e)
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+0x4e
04 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。