2011年10月9日星期日

[轉載]SEH分析筆記(X86篇)四


三、展開 (unwind)
  為了說明這個概念,需要先回顧下異常發生後的處理流程。

  我們假設一系列使用SEH 的函數調用流程:
  func1 -> func2 -> func3。在 func3 執行的過程中觸發了異常。

  看看分發異常流程RtlRaiseException -> RtlDispatchException -> RtlpExecuteHandlerForException
  RtlDispatchException 會遍歷異常鍊錶,對每個EXCEPTION_REGISTRATION 都調用RtlpExecuteHandlerForException。
  RtlpExecuteHandlerForException 會調用EXCEPTION_REGISTRATION::handler,也就是PassThrough!_except_handler4。如咱們上面分析,該函數內部遍歷EXCEPTION_REGISTRATION::scopetable,如果遇到有scopetable_entry::lpfnFilter 返回EXCEPTION_EXECUTE_HANDLER,那麼scopetable_entry::lpfnHandler 就會被調用,來處理該異常。
  因為lpfnHandler 不會返回到PassThrough!_except_handler4,於是執行完lpfnHandler 後,就會從lpfnHandler 之後的代碼繼續執行下去。也就是說,假設func3 中觸發了一個異常,該異常被func1 中的__except 處理塊處理了,那__except 處理塊執行完畢後,就從其後的指令繼續執行下去,即異常處理完畢後,接著執行的就是func1 的代碼。不會再回到func2 或者func3,這樣就有個問題,func2 和func3 中佔用的資源怎麼辦?這些資源比如申請的內存是不會自動釋放的,豈不是會有資源洩漏問題?

  這就需要用到“展開”了。
  說白了,所謂“展開”就是進行清理。 (注:這裡的清理主要包含動態分配的資源的清理,棧空間是由func1 的“mov esp,ebp” 這類操作順手清理的。當時我被“誰來清理棧空間”這個問題困擾了很久… …)
 
  那這個展開工作由誰來完成呢?由func1 來完成肯定不合適,畢竟func2 和func3 有沒有申請資源、申請了哪些資源,func1 無從得知。於是這個展開工作還得要交給func2 和func3 自己來完成。

  展開分為兩種:“全局展開”和“局部展開”。
  全局展開是指針對異常鍊錶中的某一段,局部展開針對指定EXCEPTION_REGISTRATION。用上面的例子來講,局部展開就是針對func3 或func2 (某一個函數)內部進行清理,全局展開就是func2 和func3 的局部清理的總和。再歸納一下,局部展開是指具體某一函數內部的清理,而全局展開是指,從異常觸發點(func3)到異常處理點(func1)之間所有函數(包含異常觸發點func3)的局部清理的總和。

  來看反彙編代碼:

代碼:
    kd> uf PassThrough!_EH4_GlobalUnwind
        PassThrough!_EH4_GlobalUnwind [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 485]:
          f87205fa push ebp
          f87205fb mov ebp,esp
          f87205fd push ebx
          f87205fe push esi
          f87205ff push edi
          f8720600 push 0 ; pReturnValue
          f8720602 push 0 ; pExceptionRecord
          f8720604 push offset PassThrough!_EH4_GlobalUnwind+0x15 (f872060f) ; pReturnEip
          f8720609 push ecx ; pExceptionRegistration
          f872060a call PassThrough!RtlUnwind (f8720678)
          f872060f pop edi
          f8720610 pop esi
          f8720611 pop ebx
          f8720612 pop ebp
          f8720613 ret


  RtlUnwind 的原型:
        VOID RtlUnwind(
            PEXCEPTION_REGISTRATION pExceptionRegistration
            PVOID pReturnEip
            PEXCEPTION_RECORD pExceptionRecord,
            PVOID pReturnValue
        );


代碼:
    kd> uf PassThrough!RtlUnwind ; 提示:此函數wrk 提供了實現源碼
                  nt!RtlUnwind [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 423]:
                    80867358 push ebp
                    80867359 mov ebp,esp
                    8086735b sub esp,37Ch
                    80867361 mov eax,dword ptr [nt!__security_cookie (80895388)]
                    80867366 push esi
                    80867367 mov esi,dword ptr [ebp+10h] ; esi = pExceptionRecord
                    8086736a mov dword ptr [ebp-4],eax
                    8086736d push edi
                    8086736e lea eax,[ebp-2D8h] ; [ebp-2D8h] = l_pHighLimit
                    80867374 push eax
                    80867375 lea eax,[ebp-2D4h] ; [ebp-2D4h] = l_pLowLimit
                    8086737b push eax
                    8086737c call nt!RtlpGetStackLimits (80887cdc)
                    80867381 xor edi,edi
                    80867383 cmp esi,edi ; pExceptionRecord 是否為NULL
< 80867385 jne nt!RtlUnwind+0x5a (808673b2)
:
: nt!RtlUnwind+0x2f [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 452]:
: 80867387 mov eax,dword ptr [ebp+4]
: 8086738a lea esi,[ebp-37Ch] ; ebp-37Ch 是局部變量l_ExceptionRecord
: 80867390 mov dword ptr [ebp-37Ch],0C0000027h ; l_ExceptionRecord.ExceptionCode = STATUS_UNWIND
: 8086739a mov dword ptr [ebp-378h],edi ; l_ExceptionRecord.ExceptionFlags = 0
: 808673a0 mov dword ptr [ebp-374h],edi ; l_ExceptionRecord.ExceptionRecord = NULL
: 808673a6 mov dword ptr [ebp-370h],eax ; l_ExceptionRecord.ExceptionAddress = ret_addr (PassThrough!RtlUnwind 執行完畢後的返回地址)
: 808673ac mov dword ptr [ebp-36Ch],edi ; l_ExceptionRecord.ExceptionInformation[0] = 0;
:
: nt!RtlUnwind+0x5a [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 462]:
> 808673b2 cmp dword ptr [ebp+8],edi ; pExceptionRegistration 是否為NULL
 < 808673b5 je nt!RtlUnwind+0x65 (808673bd)
 :
 : nt!RtlUnwind+0x5f [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 463]:
 : 808673b7 or dword ptr [esi+4],2 ; l_ExceptionRecord.ExceptionFlags |= EXCEPTION_UNWINDING (0x2)
 :< 808673bb jmp nt!RtlUnwind+0x69 (808673c1)
 ::
 :: nt!RtlUnwind+0x65 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 466]:
 >: 808673bd or dword ptr [esi+4],6 ; l_ExceptionRecord.ExceptionFlags |= EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND (0x2 | 0x4)
  :
  : nt!RtlUnwind+0x69 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 466]:
  > 808673c1 push ebx
                    808673c2 lea eax,[ebp-2D0h] ; ebp-2D0 是局部變量l_Context
                    808673c8 push eax
                    808673c9 mov dword ptr [ebp-2D0h],10007h ; lContext.ContextFlags = CONTEXT_i386 | CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS (0x10000 | 0x1 | 0x2 | 0x3)

                    808673d3 call nt!RtlpCaptureContext (80887c50)
                    808673d8 mov eax,dword ptr [ebp+14h] ; eax = ReturnValue
                    808673db add dword ptr [ebp-20Ch],10h ; -20C = -2D0+C4, lContext.Esp += 0x10
                    808673e2 mov dword ptr [ebp-220h],eax ; -220 = -2D0+B0, lContext.Eax = ReturnValue
                    808673e8 call nt!RtlpGetRegistrationHead (80887d04)
                    808673ed mov ebx,eax ; ebx = 異常鍊錶頭,這裡暫命名為l_pExceptionRegistration
< 808673ef jmp nt!RtlUnwind+0x1d1 (80867529)
:
: nt!RtlUnwind+0x9c [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 500]:
: > 808673f4 cmp ebx,dword ptr [ebp+8] ; cmp l_pExceptionRegistration,pExceptionRegistration
:< : 808673f7 jne nt!RtlUnwind+0xb0 (80867408)
:: :
:: : nt!RtlUnwind+0xa1 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 501]:
:: : ; pExceptionRegistration 等於l_pExceptionRegistration,說明展開完畢
:: : 808673f9 push edi ; TestAlert = FALSE
:: : 808673fa lea eax,[ebp-2D0h] ; eax = &l_Context
:: : 80867400 push eax
:: : 80867401 call nt!ZwContinue (8082c0b8) ; 這裡不會返回
::< : 80867406 jmp nt!RtlUnwind+0xe6 (8086743e)
::: :
::: : nt!RtlUnwind+0xb0 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 509]:
:>: : 80867408 cmp dword ptr [ebp+8],edi ; cmp pExceptionRegistration, NULL
: :< : 8086740b je nt!RtlUnwind+0xe6 (8086743e)
: :: :
: :: : nt!RtlUnwind+0xb5 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 509]:
: :: : 8086740d cmp dword ptr [ebp+8],ebx ; cmp pExceptionRegistration,l_pExceptionRegistration
: ::< : 80867410 jae nt!RtlUnwind+0xe6 (8086743e)
: ::: :
: ::: : nt!RtlUnwind+0xba [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 514]:
: ::: : ; pExceptionRegistration 在l_pExceptionRegistration 之下,即pExceptionRegistration 超出了異常鍊錶的棧範圍
: ::: : 80867412 lea eax,[ebp-32Ch] ; ebp-32Ch 是局部變量l_ExceptionRecord
: ::: : 80867418 push eax
: ::: : 80867419 mov dword ptr [ebp-32Ch],0C0000029h ; l_ExceptionRecord.ExceptionCode = STATUS_INVALID_UNWIND_TARGET
: ::: : 80867423 mov dword ptr [ebp-328h],1 ; l_ExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE (1)
: ::: : 8086742d mov dword ptr [ebp-324h],esi ; l_ExceptionRecord.ExceptionRecord = &l_ExceptionRecord
: ::: : 80867433 mov dword ptr [ebp-31Ch],edi ; l_ExceptionRecord.NumberParameters = 0
: ::: : 80867439 call nt!RtlRaiseException (80887a94)
: ::: :
: ::: : nt!RtlUnwind+0xe6 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 530]:
: >>> : 8086743e cmp ebx,dword ptr [ebp-2D4h] ; cmp l_pExceptionRegistration, l_pLowLimit
: : 80867444 lea edi,[ebx+8] ; edi = l_pExceptionRegistration->scopetable_entry
: < : 80867447 jb nt!RtlUnwind+0x162 (808674ba) ; l_pExceptionRegistration 低於線程棧底,跳到錯誤處理
: : :
: : : nt!RtlUnwind+0xf1 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 530]:
: : : 80867449 cmp edi,dword ptr [ebp-2D8h] ; cmp l_pExceptionRegistration->scopetable_entry,l_pHighLimit
: :< : 8086744f ja nt!RtlUnwind+0x162 (808674ba) ; l_pExceptionRegistration 高於線程棧頂,跳到錯誤處理
: :: :
: :: : nt!RtlUnwind+0xf9 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 530]:
: :: : ; l_pExceptionRegistration 處於合法棧中,檢查它是否正確對齊
: :: : 80867451 test bl,3 ; 檢查l_pExceptionRegistration 是否4字節對齊
: ::< : 80867454 jne nt!RtlUnwind+0x1a2 (808674fa) ; 沒對齊,跳到錯誤處理
: ::: :
: ::: : nt!RtlUnwind+0x102 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 580]:
: ::: : ; 進行局部展開
: ::: : 8086745a push dword ptr [ebx+4] ; l_pExceptionRegistration->Handler
: ::: : 8086745d lea eax,[ebp-2DCh] ; 這裡是局部變量l_Dis​​patchContext
: ::: : 80867463 push eax ;
: ::: : 80867464 lea eax,[ebp-2D0h] ; eax = &l_Context
: ::: : 8086746a push eax ; l_Context
: ::: : 8086746b push ebx ; l_pExceptionRegistration
: ::: : 8086746c push esi ; &l_ExceptionRecord
: ::: : 8086746d call nt!RtlpExecuteHandlerForUnwind (80887b54) ; 內部調用l_pExceptionRegistration->Handler
: ::: : 80867472 dec eax
: :::< : 80867473 je nt!RtlUnwind+0x156 (808674ae) ; 如果返回ExceptionContinueSearch 則跳轉
: :::: :
: :::: : nt!RtlUnwind+0x11d [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 586]:
: :::: : 80867475 dec eax
: :::: : 80867476 dec eax
: :::: < : 80867477 je nt!RtlUnwind+0x150 (808674a8) ; 如果返回ExceptionCollidedUnwind 則跳轉
: :::: : :
: :::: : : nt!RtlUnwind+0x121 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 621]:
: :::: : : ; 返回ExceptionContinueSearch 和ExceptionCollidedUnwind 之外的非法值
: :::: : : 80867479 and dword ptr [ebp-31Ch],0 ; l_ExceptionRecord.NumberParameters = 0
: :::: : : 80867480 lea eax,[ebp-32Ch] ; eax = &l_ExceptionRecord
: :::: : : 80867486 push eax
: :::: : : 80867487 mov dword ptr [ebp-32Ch],0C0000026h ; l_ExceptionRecord.ExceptionCode = STATUS_INVALID_DISPOSITION
: :::: : : 80867491 mov dword ptr [ebp-328h],1 ; l_ExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE (1)
: :::: : : 8086749b mov dword ptr [ebp-324h],esi ; l_ExceptionRecord.ExceptionRecord = &l_ExceptionRecord
: :::: : : 808674a1 call nt!RtlRaiseException (80887a94)
: ::::<: : 808674a6 jmp nt!RtlUnwind+0x156 (808674ae)
: :::::: :
: :::::: : nt!RtlUnwind+0x150 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 608]:
: :::::: : ; RtlpExecuteHandlerForUnwind 返回ExceptionCollidedUnwind,從l_Dis​​patchContext.RegistrationPointer 繼續處理
: :::::> : 808674a8 mov ebx,dword ptr [ebp-2DCh] ; l_pExceptionRegistration = l_Dis​​patchContext.RegistrationPointer
: ::::: :
: ::::: : nt!RtlUnwind+0x156 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 630]:
: ::::: : ; 局部展開完畢,從異常鍊錶中摘除l_pExceptionRegistration
: :::>> : 808674ae mov eax,ebx ; eax = l_pExceptionRegistration
: ::: : 808674b0 mov ebx,dword ptr [ebx] ; l_pExceptionRegistration = l_pExceptionRegistration.prev
: ::: : 808674b2 push eax
: ::: : 808674b3 call nt!RtlpUnlinkHandler (80887c10) ; 摘除
: :::< : 808674b8 jmp nt!RtlUnwind+0x1cf (80867527)
: :::: :
: :::: : nt!RtlUnwind+0x162 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 540]:
: :::: : ; l_pExceptionRegistration 並不處在當前線程棧中,這種情況不一定是出錯,有可能當前正在執行DPC。
: :::: : ; 當時還需要檢查是否對齊,不對齊就一定是出錯
: >>:: : 808674ba test bl,3 ; 檢查l_pExceptionRegistration 是否4字節對齊
: ::< : 808674bd jne nt!RtlUnwind+0x1a2 (808674fa)
: ::: :
: ::: : nt!RtlUnwind+0x167 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 540]:
: ::: : ; 如果當前正在執行DPC,那IRQL 一定不得低於DISPATCH_LEVEL(難道不是應該一定等於DISPATCH_LEVEL ?)
: ::: : 808674bf call dword ptr [nt!_imp__KeGetCurrentIrql (8080102c)]
: ::: : 808674c5 cmp al,2 ; DISPATCH_LEVEL (2)
: :::< : 808674c7 jb nt!RtlUnwind+0x1a2 (808674fa) ; 低於DISPATCH_LEVEL,跳到錯誤處理點
: :::: :
: :::: : nt!RtlUnwind+0x171 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 542]:
: :::: : 808674c9 mov eax,dword ptr fs:[00000020h] ; eax = 當前CPU 的KPCR::Prcb
: :::: : 808674cf cmp byte ptr [eax+95Ah],0 ; KPCR::Prcb->DpcRoutineActive (BOOLEAN 類型)
: :::: : 808674d6 mov ecx,dword ptr [eax+948h] ; ecx = KPCR::Prcb->DpcStack
: ::::< : 808674dc je nt!RtlUnwind+0x1a2 (808674fa) ; 當前不是在執行DPC,跳到錯誤處理點
: ::::: :
: ::::: : nt!RtlUnwind+0x186 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 547]:
: ::::: : ; KPCR::Prcb->DpcRoutineActive 為TRUE,當前異常是由DpcRoutine 觸發
: ::::: : 808674de cmp edi,ecx ; 比較l_pExceptionRegistration->scopetable_entry 和DPC 棧頂
: :::::< : 808674e0 ja nt!RtlUnwind+0x1a2 (808674fa) ; 高出DPC 棧頂,跳到錯誤處理點
: :::::: :
: :::::: : nt!RtlUnwind+0x18a [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 547]:
: :::::: : 808674e2 lea eax,[ecx-3000h] ; eax = KPCR::Prcb->DpcStack - KERNEL_STACK_SIZE, 即DPC 棧底
: :::::: : 808674e8 cmp ebx,eax ; 比較l_pExceptionRegistration 和DPC 棧底
: ::::::< : 808674ea jb nt!RtlUnwind+0x1a2 (808674fa) ; 低於DPC 棧底,跳到錯誤處理點
: ::::::: :
: ::::::: : nt!RtlUnwind+0x194 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 555]:
: ::::::: : 808674ec mov dword ptr [ebp-2D8h],ecx ; l_pHighLimit = DPC 棧頂
: ::::::: : 808674f2 mov dword ptr [ebp-2D4h],eax ; l_pLowLimit = DPC 棧底
: :::::::<: 808674f8 jmp nt!RtlUnwind+0x1cf (80867527)
: :::::::::
: ::::::::: nt!RtlUnwind+0x1a2 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 564]:
: ::::::::: ; 棧錯誤
: >:>>>>>:: 808674fa and dword ptr [ebp-31Ch],0
: : :: 80867501 lea eax,[ebp-32Ch] ; eax = &l_ExceptionRecord
: : :: 80867507 push eax
: : :: 80867508 mov dword ptr [ebp-32Ch],0C0000028h ; l_ExceptionRecord.ExceptionCode = STATUS_BAD_STACK
: : :: 80867512 mov dword ptr [ebp-328h],1 ; l_ExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE (1)
: : :: 8086751c mov dword ptr [ebp-324h],esi ; l_ExceptionRecord.ExceptionRecord = &l_ExceptionRecord
: : :: 80867522 call nt!RtlRaiseException (80887a94)
: : ::
: : :: nt!RtlUnwind+0x1cf [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 493]:
: > >: 80867527 xor edi,edi
: :
: : nt!RtlUnwind+0x1d1 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 493]:
> : 80867529 cmp ebx,0FFFFFFFFh ; cmp l_pExceptionRegistration, EXCEPTION_CHAIN​​_END
               < 8086752c jne nt!RtlUnwind+0x9c (808673f4)
                
                  nt!RtlUnwind+0x1da [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 647]:
                    80867532 cmp dword ptr [ebp+8],0FFFFFFFFh ; cmp pExceptionRegistration, EXCEPTION_CHAIN​​_END
                    80867536 pop ebx
                    80867537 lea eax,[ebp-2D0h] ; eax = &l_Context
                    8086753d push edi ; bTestAlert = FALSE 或bFirstChance = FALSE
                    8086753e push eax
< 8086753f jne nt!RtlUnwind+0x1f0 (80867548)
:
: nt!RtlUnwind+0x1e9 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 656]:
: ; 展開整個異常鍊錶
: 80867541 call nt!ZwContinue (8082c0b8)
:< 80867546 jmp nt!RtlUnwind+0x1f6 (8086754e)
::
:: nt!RtlUnwind+0x1f0 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 667]:
:: ; pExceptionRegistration 等於EXCEPTION_CHAIN​​_END。
:: ; 沒有理解這裡觸發異常是為什麼,但是不妨礙分析異常處理流程。
>: 80867548 push esi ; &l_ExceptionRecord
 : 80867549 call nt!ZwRaiseException (8082ccd4)
 :
 : nt!RtlUnwind+0x1f6 [c:\wrk\wrk-v1.2\base\ntos\rtl\i386\exdsptch.c @ 671]:
 > 8086754e mov ecx,dword ptr [ebp-4]
                    80867551 pop edi
                    80867552 pop esi
                    80867553 call nt!__security_check_cookie (80874e87)
                    80867558 leave
                    80867559 ret 10h

        kd> uf 80887be9​​ ; RtlpExecuteHandlerForUnwind 的異常處理函數
        nt!ExecuteHandler2+0x61 [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 329]:
          80887be9​​ mov ecx,dword ptr [esp+4] ; ecx = pExceptionRecord
          80887bed test dword ptr [ecx+4],6 ; test pExceptionRecord->ExceptionFlags, EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND
          80887bf4 mov eax,1 ; eax = ExceptionContinueSearch
          80887bf9 je nt!ExecuteHandler2+0x85 (80887c0d)

        nt!ExecuteHandler2+0x73 [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 341]:
          ; 展開過程中 Handler 觸發異常
          80887bfb mov ecx,dword ptr [esp+8] ; ecx = pExceptionRegistration
          80887bff mov edx,dword ptr [esp+10h] ; edx = pDispatcherContext
          80887c03 mov eax,dword ptr [ecx+8] ; eax = pExceptionRegistration->prev
          80887c06 mov dword ptr [edx],eax ; pDispatcherContext->RegistrationPointer = pExceptionRegistration->prev,
                                                    ; 然後pDispatcherContext->RegistrationPointer 就是展開過程中正在被展開的
                                                    ; 那個 EXCEPTION_REGISTRATION_RECORD
          80887c08 mov eax,3 ; eax = ExceptionCollidedUnwind

        nt!ExecuteHandler2+0x85 [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 349]:
          80887c0d ret 10h


    kd> uf RtlpExecuteHandlerForUnwind
        nt!RtlpExecuteHandlerForUnwind [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 142]:
          80887b54 mov edx,offset nt!ExecuteHandler2+0x61 (80887be9​​)
          80887b59 lea ecx,[ecx]
          80887b5c push ebx
          80887b5d push esi
          80887b5e push edi
          80887b5f xor eax,eax
          80887b61 xor ebx,ebx
          80887b63 xor esi,esi
          80887b65 xor edi,edi
          80887b67 push dword ptr [esp+20h] ; pExceptionRoutine
          80887b6b push dword ptr [esp+20h] ; pDispatcherContext
          80887b6f push dword ptr [esp+20h] ; pContext
          80887b73 push dword ptr [esp+20h] ; pExceptionRegistration
          80887b77 push dword ptr [esp+20h] ; pExceptionRecord
          80887b7b call nt!ExecuteHandler2 (80887b88)
          80887b80 pop edi
          80887b81 pop esi
          80887b82 pop ebx
          80887b83 ret 14h

    kd> uf nt!ExecuteHandler2
        nt!ExecuteHandler2 [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 188]:
          80887b88 push ebp
          80887b89 mov ebp,esp
          80887b8b push dword ptr [ebp+0Ch] ; pExceptionRegistration
          80887b8e push edx ; _EXCEPTION_REGISTRATION_RECORD::Handler (nt!ExecuteHandler2+0x61 (80887be9​​))
          80887b8f push dword ptr fs:[0] ; _EXCEPTION_REGISTRATION_RECORD::Next
          80887b96 mov dword ptr fs:[0],esp; 注意:這裡使用的是原始版本的異常處理
          80887b9d push dword ptr [ebp+14h] ; pDispatcherContext
          80887ba0 push dword ptr [ebp+10h] ; pContext
          80887ba3 push dword ptr [ebp+0Ch] ; pExceptionRegistration
          80887ba6 push dword ptr [ebp+8] ; pExceptionRecord
          80887ba9 mov ecx,dword ptr [ebp+18h] ; ecx = pExceptionRoutine
          80887bac call ecx ; pExceptionRoutine(...),即pExceptionRegistration->handler(...)
          80887bae mov esp,dword ptr fs:[0]
          80887bb5 pop dword ptr fs:[0] ; 摘掉當前_EXCEPTION_REGISTRATION_RECORD
          80887bbc mov esp,ebp
          80887bbe pop ebp
          80887bbf ret 14h

  代碼不長,主要功能也不復雜:從異常鍊錶頭開始遍歷,一直遍歷到指定EXCEPTION_REGISTRATION_RECORD,對每個遍歷到的EXCEPTION_REGISTRATION_RECORD,執行RtlpExecuteHandlerForUnwind 進行局部展開。
  這段代碼裡有一個細節我沒想明白,或者我想複雜了。在nt!RtlUnwind 地址80867401 處,當展開到指定EXCEPTION_REGISTRATION 後,RtlUnwind 通過ZwContinue 返回,而不是使用ret 指令。通過靜態分析和動態分析我都沒有找到用ZwContinue 的好處,或者不可替代的原因。如果有朋友有不同的結論,請分享一下。
  在分析全局展開時發生一件很囧的事,我反彙編完成,梳理流程的時候,總感覺“這個邏輯怎麼這麼熟悉,貌似在哪見過~ 難道wrk 裡有源碼?”,翻開wrk,果然有…… 不過話說回來,在反彙編分析過程讓我對一些細節理解的更深刻了(也只能這麼安慰自己了……)。

  下面我們來看看局部展開。
  在前面講PassThrough!_except_handler4 時,有提到該函數既負責處理異常也負責局部展開。其區分功能的標誌就是判斷EXCEPTION_RECORD::ExceptionFlags 是否包含EXCEPTION_UNWIND 標誌位。可以參考PassThrough!_except_handler4 中地址為f87203dc 的指令:
  f87203dc test byte ptr [eax+4],66h ; pExceptionRecord->ExceptionFlags & EXCEPTION_UNWIND,判斷是異常處理過程還是展開過程
 
  該標誌是在RtlUnwind 中被設置的,可以參考RtlUnwind 中地址為808673b7 和808673bd 出的指令:
  808673b7 or dword ptr [esi+4],2 ; l_ExceptionRecord.ExceptionFlags |= EXCEPTION_UNWINDING (0x2)
  808673bd or dword ptr [esi+4],6 ; l_ExceptionRecord.ExceptionFlags |= EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND (0x2 | 0x4)

  反彙編代碼:

  首先是我整理的_EH4_LocalUnwind 的原型:
       VOID fastcall _EH4_LocalUnwind(
            PEXCEPTION_REGISTRATION pExceptionRegistartion,
            ULONG ulUntilTryLevel
            )


代碼:
    kd> uf PassThrough!_EH4_LocalUnwind
        PassThrough!_EH4_LocalUnwind [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 529]:
          f8720614 push ebp
          f8720615 mov ebp,dword ptr [esp+8] ; ebp = pExceptionRegistartion->_ebp
          f8720619 push edx ; ulUntilTryLevel
          f872061a push ecx ; pExceptionRegistartion
          f872061b push dword ptr [esp+14h] ; push __security_cookie
          f872061f call PassThrough!_local_unwind4 (f87204ec)
          f8720624 add esp,0Ch
          f8720627 pop ebp
          f8720628 ret 8

    PassThrough!_EH4_LocalUnwind 的原型:
       VOID local_unwind4(
           PEXCEPTION_REGSTRATION pExceptionRegstration,
           ULONG ulUntilTryLevel
           )

    kd> uf PassThrough!_local_unwind4
      PassThrough!_local_unwind4 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 177]:
        f87204ec push ebx
        f87204ed push esi
        f87204ee push edi
        f87204ef mov edx,dword ptr [esp+10h] ; edx = __security_cookie
        f87204f3 mov eax,dword ptr [esp+14h] ; eax = pExceptionRegistartion
        f87204f7 mov ecx,dword ptr [esp+18h] ; ecx = ulUntilTryLevel
        f87204fb push ebp
        f87204fc push edx ; __security_cookie
        f87204fd push eax ; pExceptionRegistartion
        f87204fe push ecx ; ulUntilTryLevel
        f87204ff push ecx ; 這裡壓入的值後續被f8720513 處的指令修改
        f8720500 push offset PassThrough!_unwind_handler4 (f872056f) ; _EXCEPTION_REGISTRATION_RECORD::handler
        f8720505 push dword ptr fs:[0] ; _EXCEPTION_REGISTRATION_RECORD::prev, 注意這裡使用的是原始版本
        f872050c mov eax,dword ptr [PassThrough!__security_cookie (f87220b0)]
        f8720511 xor eax,esp ; 這裡是用esp 來異或,而esp 此時指向新_EXCEPTION_REGISTRATION_RECORD
        f8720513 mov dword ptr [esp+8],eax ; [esp+8] 被賦予加密後的__security_cookie
        f8720517 mov dword ptr fs:[0],esp
    
      PassThrough!_local_unwind4+0x32 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 209]:
   >> f872051e mov eax,dword ptr [esp+30h] ; eax = pExceptionRegistartion
   :: f8720522 mov ebx,dword ptr [eax+8] ; ebx = pExceptionRegistration->scopetable, 將ebx 假稱為l_pCurScopeEntry
   :: f8720525 mov ecx,dword ptr [esp+2Ch] ; ecx = &__security_cookie
   :: f8720529 xor ebx,dword ptr [ecx] ; 解密scopetable
   :: f872052b mov esi,dword ptr [eax+0Ch] ; esi = pExceptionRegistration->trylevel
   :: f872052e cmp esi,0FFFFFFFEh ; cmp pExceptionRegistration->trylevel,TRYLEVEL_NONE
< :: f8720531 je PassThrough!_local_unwind4+0x75 (f8720561) ; pExceptionRegistration->trylevel 等於TRYLEVEL_NONE,無需遍歷,返回
: ::
: :: PassThrough!_local_unwind4+0x47 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 216]:
: :: f8720533 mov edx,dword ptr [esp+34h] ; edx = ulUntilTryLevel
: :: f8720537 cmp edx,0FFFFFFFEh ; cmp ulUntilTryLevel, TRYLEVEL_NONE
:< :: f872053a je PassThrough!_local_unwind4+0x54 (f8720540)
:: ::
:: :: PassThrough!_local_unwind4+0x50 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 219]:
:: :: f872053c cmp esi,edx ; cmp pExceptionRegistration->trylevel, ulUntilTryLevel
::<:: f872053e jbe PassThrough!_local_unwind4+0x75 (f8720561) ; 已經遍歷到指定scopetable_entry 了,返回
:::::
::::: PassThrough!_local_unwind4+0x54 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 221]:
:>::: f8720540 lea esi,[esi+esi*2]
: ::: f8720543 lea ebx,[ebx+esi*4+10h] ; ebx - pExceptionRegistration->scopetable[pExceptionRegistration->trylevel]
: ::: f8720547 mov ecx,dword ptr [ebx] ; ecx = l_pCurScopeEntry->previousTryLevel
: ::: f8720549 mov dword ptr [eax+0Ch],ecx ; pExceptionRegistration->trylevel = ebx->l_pCurScopeEntry->previousTryLevel
: ::: f872054c cmp dword ptr [ebx+4],0 ; cmp pExceptionRegistration->lpfnFilter,NULL
: :<: f8720550 jne PassThrough!_local_unwind4+0x32 (f872051e)
: : :
: : : PassThrough!_local_unwind4+0x66 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 240]:
: : : f8720552 mov ecx,1
: : : f8720557 mov eax,dword ptr [ebx+8] ; eax = pExceptionRegistration->lpfnHandler
: : : f872055a call PassThrough!_NLG_Call (f8720630) ; 調用pExceptionRegistration->lpfnHandler
: : < f872055f jmp PassThrough!_local_unwind4+0x32 (f872051e)
: :
: : PassThrough!_local_unwind4+0x75 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 248]:
> > f8720561 pop dword ptr fs:[0]
        f8720568 add esp,18h
        f872056b pop edi
        f872056c pop esi
        f872056d pop ebx
        f872056e ret
    
        kd> uf PassThrough!_NLG_Call
        PassThrough!_NLG_Call [d:\5359\minkernel\crts\crtw32\misc\i386\nlgsupp.asm @ 120]:
          f8720630 call eax
          f8720632 ret


  PassThrough!_EH4_LocalUnwind 中f8720500 處的指令用到的異常處理函數PassThrough!_unwind_handler4 的反彙編代碼:



代碼:
    kd> uf PassThrough!_unwind_handler4
        PassThrough!_unwind_handler4 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 294]:
          f872056f mov ecx,dword ptr [esp+4] ; ecx = pExceptionRecord
          f8720573 test dword ptr [ecx+4],6 ; test pExceptionRecord->ExceptionFlags, EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND
          f872057a mov eax,1 ; eax = ExceptionContinueSearch (1)
          f872057f je PassThrough!_unwind_handler4+0x45 (f87205b4)

        PassThrough!_unwind_handler4+0x12 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 307]:
          ; 在展開過程中又觸發了異常,那麼處理該新異常(實際操作只是做展開,即清理掉該異常佔用的資源,並沒有真正處理該異常)
          f8720581 mov eax,dword ptr [esp+8] ; eax = pExceptionRegistration
          f8720585 mov ecx,dword ptr [eax+8] ; ecx = __security_cookie, 注意這裡不是__pExceptionRegistration->scopetable
          f8720588 xor ecx,eax ; 這裡用pExceptionRegistration 解密,PassThrough!_local_unwind4 中也正是用它進行加密的
                                    ; 參考 f8720511 處的指令
          f872058a call PassThrough!__security_check_cookie (f8720638)
          f872058f push ebp
          f8720590 mov ebp,dword ptr [eax+18h] ; eax+18h 是被PassThrough!_local_unwind4 壓入棧的ebp,
                                                    ; 參考 f87204fb 處的指令
          f8720593 push dword ptr [eax+0Ch] ; push ulUntilTryLevel
          f8720596 push dword ptr [eax+10h] ; pExceptionRegistartion
          f8720599 push dword ptr [eax+14h] ; __security_cookie
          f872059c call PassThrough!_local_unwind4 (f87204ec)
          f87205a1 add esp,0Ch
          f87205a4 pop ebp
          f87205a5 mov eax,dword ptr [esp+8] ; eax = pExceptionRegistration
          f87205a9 mov edx,dword ptr [esp+10h] ; edx = pDispatcherContext (調整pDispatcherContext,改變遍歷順序)
          f87205ad mov dword ptr [edx],eax ; pDispatcherContext->RegistrationPointer = pExceptionRegistration
          f87205af mov eax,3 ; eax = ExceptionCollidedUnwind (3)

        PassThrough!_unwind_handler4+0x45 [d:\5359\minkernel\crts\crtw32\misc\i386\exsup4.asm @ 336]:
          f87205b4 ret


  到這裡概要流程就講完了。在處理異常和展開過程中多處涉及到遍歷操作,咱們來總結一下這些遍歷操作。
  1. 在異常處理過程中,每個被"捲入是非"的異常都至少會遍歷異常鍊錶兩次(如果發生嵌套異常,比如在展開過程中
EXCEPTION_REGISTRATION_RECORD::Handler 又觸發異常,則會遍歷更多次。不過這也可以算作是一個新異常了。看如何理解。 )。
  一次是在RtlDispatchException 中,遍歷的目的是找到願意處理該異常的_EXCEPTION_REGISTRATION_RECORD。
  另一次是在展開過程中、RtlUnwind 函數內,遍歷的目錄是為了對每個遍歷到的EXCEPTION_REGISTRATION_RECORD 進行局部展開。
  2. 同樣的,每個被"捲入是非"的異常的scopetable 也會被遍歷至少兩次,
  一次是在modulename!_except_handler? 中,遍歷目的也是找到願意處理該異常的scopetable_entry。
  另一次是在展開過程中、_local_unwind4 函數內,遍歷的目的是找到所有指定範圍內的scopetable_entry::lpfnFilter 為NULL 的scopetable_entry,調用它們的lpfnHandler (即__finally 處理塊)。

  在展開過程中,__finally 代碼塊會被執行,在執行過程中有可能觸發新的異常,增強版通過返回ExceptionCollidedUnwind (3) 來標識這種情況(參考PassThrough!_unwind_handler4 中f87205af 處的指令)。咱來回顧下這類返回值:


代碼:
    typedef enum _EXCEPTION_DISPOSITION {
        ExceptionContinueExecution,
        ExceptionContinueSearch,
        ExceptionNestedException,
        ExceptionCollidedUnwind
    } EXCEPTION_DISPOSITION;


  前面的代碼中已經展示了上述4中的三種:
  ExceptionContinueExecution 表示繼續執行(異常已經被修復);
  ExceptionContinueSearch 表示繼續搜索(當前搜索到的異常註冊信息不處理);
  ExceptionCollidedUnwind 表示在展開過程中再次觸發異常。
  ExceptionNestedException 呢?到目前咱們還沒有遇到過,什麼情況會用到它?
 
  從字面上看ExceptionNestedException 大概意思是“嵌套的異常”,是不是可以理解為“處理異常過程中再次觸發的異常”?比如類似於ExceptionCollidedUnwind,只不過ExceptionCollidedUnwind 是在展開過程。而ExceptionNestedException 是在處理異常過程中?
  咱們順著這個思路去尋找,首先來看PassThrough!_except_handler4,它是異常處理的入口了。處理和展開都是它負責的。可是翻遍了它和它的工具函數的反彙編碼也沒有找到它直接返回或者通過註冊異常處理信息來間接返回。於是我決定繼續向上層即調用者方向搜尋,於是找到瞭如下彙編碼:


代碼:
        kd> uf RtlpExecuteHandlerForException
        nt!RtlpExecuteHandlerForException [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 87]:
          80887b4c mov edx,offset nt!ExecuteHandler2+0x3a (80887bc2)
          80887b51 jmp nt!ExecuteHandler (80887b5c)

        nt!ExecuteHandler [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 160]:
          80887b5c push ebx
          80887b5d push esi
          80887b5e push edi
          80887b5f xor eax,eax
          80887b61 xor ebx,ebx
          80887b63 xor esi,esi
          80887b65 xor edi,edi
          80887b67 push dword ptr [esp+20h]
          80887b6b push dword ptr [esp+20h]
          80887b6f push dword ptr [esp+20h]
          80887b73 push dword ptr [esp+20h]
          80887b77 push dword ptr [esp+20h]
          80887b7b call nt!ExecuteHandler2 (80887b88)
          80887b80 pop edi
          80887b81 pop esi
          80887b82 pop ebx
          80887b83 ret 14h

        kd> uf ExecuteHandler2
        nt!ExecuteHandler2 [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 188]:
          80887b88 push ebp
          80887b89 mov ebp,esp
          80887b8b push dword ptr [ebp+0Ch]
          80887b8e push edx
          80887b8f push dword ptr fs:[0]
          80887b96 mov dword ptr fs:[0],esp ; 註冊新的異常處理結構
          80887b9d push dword ptr [ebp+14h]
          80887ba0 push dword ptr [ebp+10h]
          80887ba3 push dword ptr [ebp+0Ch]
          80887ba6 push dword ptr [ebp+8]
          80887ba9 mov ecx,dword ptr [ebp+18h]
          80887bac call ecx
          80887bae mov esp,dword ptr fs:[0]
          80887bb5 pop dword ptr fs:[0]
          80887bbc mov esp,ebp
          80887bbe pop ebp
          80887bbf ret 14h

        kd> uf 80887bc2 ; ExecuteHandler2 使用的異常處理函數
        nt!ExecuteHandler2+0x3a [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 268]:
          80887bc2 mov ecx,dword ptr [esp+4]
          80887bc6 test dword ptr [ecx+4],6
          80887bcd mov eax,1 ; eax = ExceptionContinueSearch (1)
          80887bd2 jne nt!ExecuteHandler2+0x5e (80887be6)

        nt!ExecuteHandler2+0x4c [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 279]:
          80887bd4 mov ecx,dword ptr [esp+8]
          80887bd8 mov edx,dword ptr [esp+10h]
          80887bdc mov eax,dword ptr [ecx+8]
          80887bdf mov dword ptr [edx],eax
          80887be1 mov eax,2 ; eax = ExceptionNestedException (2)

        nt!ExecuteHandler2+0x5e [C:\wrk\wrk-v1.2\base\ntos\rtl\i386\xcptmisc.asm @ 287]:
          80887be6 ret 10h

  RtlpExecuteHandlerForException 函數是在RtlDispatchException 中被調用。 RtlDispatchException 在遍歷異常鏈過程中並不直接調用EXCEPTION_REGISTRATION_RECORD::Handler,而是通過RtlpExecuteHandlerForException 來間接調用。 RtlpExecuteHandlerForException 通過ExecuteHandler2 建立一個異常處理塊。
  PassThrough!_except_handler4 處理異常的過程中,如果再次觸發異常,則會由地址為80887bc2 的異常處理函數返回ExceptionNestedException。因為這幾個函數都很簡單,而且跟之前分析的函數很類似,不再贅述。

  分析完這些,我有個疑問還是沒有解開,我個人一直很​​奇怪增強版的使用方式為何不能這樣:
        __try
        {
        }
        __except()
        {
        }
        __finally
        {
        }
  其中__except 過濾塊和處理塊可以省略。
  但是很遺憾,MSC 中__except 和__finally 只能選其一。 (當然,咱們可以用雙層__try 來實現同樣的效果,但是總感覺不太爽,特別是會導致代碼塊被迫縮進兩次)
  在分析的過程中我也發現我希望的這種方式更合理一些,__except 負責處理異常,在處理異常代碼中被執行。如果沒有處理異常,那麼在展開過程中__finally 代碼塊被執行,做一些清理操作。這樣兩者都存在,各負責各的,不是更好嗎?不知道是不是什麼歷史原因。

  還有一個地方我也覺得不太完美。我們分析原始版本的時候發現,原始版本自身並沒有直接使用展開,RtlDispatchException 等異常處理函數並沒有直接調用RtlUnwind。後者實際上是在增強版中才用到。我個人感覺這種模型並不完美,原始版本並沒有自成體系,而是與增強版糾結在一起,沒有很好的分層。

  另外,在實際應用中SEH 機制可能會導致很難分析的內存洩漏。我們來看一個例子,
  調用流程:func1 -> func2
  其中,func1 的代碼如下:
        VOID Func1()
        {
            __try
            {
                Func2();
            }
            __except(EXCEPTION_EXECUTE_HANDLER)
            {
                // 一些善後處理
            }
        }
 
  Func2 沒有應用SEH,它申請了一塊內存,對這塊內存進行操作,然後釋放該內存。但是在操作內存時候觸發異常,異常被Func1 處理了,但是因為Func2 沒有__finally 處理塊,於是展開過程中Func2 並沒有機會去釋放這塊內存。結果就是:程序依然“正常”的在運行,但是實際上已經造成內存洩漏。隨著程序執行,洩漏的資源可能越來越多。最後導致嚴重的系統故障。
       遇到這種問題,程序猿通過靜態分析是很難找到洩漏的原因的。

  到這裡差不多就囉嗦完了。最後,來一段總結。

  本文只是我分析x86 下windows 異常處理機製過程的一些筆記的集合。因為興趣的原因,我只分析了內核部分,應用層SEH 我沒有琢磨。感興趣的朋友可以參考《軟件調試》中的相關內容,貌似挺詳細的。
  後續我會抽時間再琢磨琢磨x64 下windows 的異常處理機制,前段時間查閱x64 資料的時候看到其異常處理機制調整了很多,比如EXCEPTION_REGISTRATION 不在是放在棧上,而是做成表。如果內部實現改變的較多,我會再寫一份筆記來分享。 sinister 師傅有過這麼一段話,我很認同:
  “交流都是建立在平等的基礎上,在抱怨氛圍和環境不好的同時應該先想一想自己究竟付出了多少?只知索取不願付出的人也就不用抱怨了,要怪也只能怪自己。發自己心得的人無非是兩種目的,一是引發一些討論,好糾正自己錯誤的認識,以便從中獲取更多的知識使自己進步的更快。二是做一份備忘,當自己遺忘的時候能夠馬上找到相關資料。”
  其中提到的兩種目的我都有 :-)
  還是那句老話,FIXME。

没有评论:

发表评论