windows異常處理學習總結

異常學習

一丶CPU異常

1.1 異常處理簡介

此爲Windows異常處理。學習異常可以幫助我們更好的調試程序。或者更好的尋找異常點。

先說一下異常產生之後要進行的操作

  • 異常記錄

  • 異常分發

  • 異常處理

異常產生後第一步就是要進行異常記錄 原因是首先要記錄哪裏產生了異常。 異常的類型是什麼。 第二步就是要分發異常 分發異常指的就是去尋找異常處理函數 找到函數之後就會進行調用,調用的函數我們就稱爲異常處理

1.2 異常的分類之CPU異常

異常分類有兩種,第一種就是CPU產生的異常。 第二種就是軟件模擬的產生的異常。

CPU異常如下:

C++代碼示例

  1. CPU異常示例
int main(int argc, char **argv)
{
    int x = 10;
    int y = 0;
    int c = x / y;

    return 0;
}

提示我們除零異常 這個是CPU可以檢測出來的。

  1. 軟件模擬的異常
void fun()
{
    throw 1;
}
int main(int argc, char **argv)
{
    fun();
    return 0;
}

當我們使用高級語言編寫代碼的時候。多多少少高級語言都會提供異常處理機制。

比如C++ 我們可以使用 throw 關鍵字來拋出一個異常。

這個就是軟件模擬的異常。

1.3 CPU異常的 記錄

CPU 檢測到異常之後會進行如下操作。

XP下

WIN10 64位下

這裏說下64位下的把。

其實最重要的就是 KiExceptionDisPatch 此函數會先 進行異常記錄

此函數結構大概爲:

__int64 __fastcall KiExceptionDispatch(
        int arg_ExceptionCode,   
        unsigned int arg_ExceptionNumberParameters,
        void *arg_ExceptionAddress,
        unsigned __int64 arg_ExceptionInformation, 
        char a5)  
參數 說明
arg_ExceptionCode 異常記錄的代碼
arg_ExceptionNumberParameters 異常記錄的附加參數大小
arg_ExceptionAddress 異常記錄出錯的地址
arg_ExceptionInformation 異常記錄附加參數數組信息
a5 暫時未知

在調用函數之前代碼彙編代碼如下:

.text:00000001401C72F3 B9 03 00 00 10                 mov     ecx, 10000003h
.text:00000001401C72F8 33 D2                          xor     edx, edx
.text:00000001401C72FA 4C 8B 85 E8 00+                mov     r8, [rbp+0E8h]
.text:00000001401C72FA 00 00
.text:00000001401C7301 E8 7A 71 00 00                 call    KiExceptionDispatch

傳遞了一個 10000003 值。此值就是我們的除零異常的的SWITCH值 注意 除零異常的值 應該是 0xC0000094 這個後面說。

下圖爲 KiExceptionDisPatch 異常記錄區域。可以看到會把我們的 switch 值 進行記錄 並且會記錄我們出現異常的地址

異常記錄結構體爲如下:

#defien EXCEPTION_MAXIMUM_PARAMETERS 15
typedef struct _EXCEPTION_RECORD32 {
    DWORD    ExceptionCode;
    DWORD ExceptionFlags;
    DWORD ExceptionRecord;
    DWORD ExceptionAddress;
    DWORD NumberParameters;
    DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD32, *PEXCEPTION_RECORD32;

typedef struct _EXCEPTION_RECORD64 {
    DWORD    ExceptionCode;
    DWORD ExceptionFlags;
    DWORD64 ExceptionRecord;
    DWORD64 ExceptionAddress;
    DWORD NumberParameters;
    DWORD __unusedAlignment;
    DWORD64 ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD64, *PEXCEPTION_RECORD64;

含義如下:

參數 說明
ExceptionCode 記錄異常的代碼
ExceptionFlags 記錄異常的狀態 確定我們是CPU異常還是 軟件模擬的異常 還是嵌套異常。根據不同場合值也不一樣。
ExceptionRecord 指向下一個異常記錄。當出現嵌套異常的時候會記錄下一個異常。
ExceptionAddress 記錄異常發生的地址
NumberParameters 附加參數
ExceptionInformation 附加參數指針

觀看結構體,其中最重要的其實就是前4個成員。 此結構體 我稱爲異常記錄結構體,此結構體也是我所說的異常產生的時候進行的第一步。記錄異常信息。此結構體中我們重點還需要知道 ExceptionCode ExceptionAddress 這兩個成員分別記錄的異常代碼和異常地址。這在我們調試的時候很有幫助。

假設我們是 DIV 0 的異常 那麼CPU 回去 IDT表中尋找 KiDivideErrorFaultShadow 此函數會最終調用到 KiDivideErrorFault

如果在WINDBG中調試的時候 輸入 !IDT 並且有符號的情況下則可以看到 IDT表中的符號名稱。

反彙編查看 KiDivideErrorFault 則可以看到異常流程。

1.4 CPU異常的分發

異常的分發就是去尋找異常處理函數。 我們繼續反彙編

KiExceptionDisPatch 可以看到異常分發函數

反彙編

再上圖上我們可以看到會繼續調用 KiDispatchException

此函數如下:

void KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN PKEXCEPTION_FRAME ExceptionFrame,
    IN PKTRAP_FRAME TrapFrame,
    IN KPROCESSOR_MODE PreviousMode,
    IN BOOLEAN FirstChance
    )
參數 說明
ExceptionRecord 異常記錄結構體
ExceptionFrame 異常信息框架
TrapFrame 陷阱框架
PreviousMode 先前模式
FirstChance 機會。第幾次異常

繼續反彙編

在反彙編過程中 可以看到。 他會對比異常記錄結構體中記錄的switch值 然後進行修復 如果我們是除零異常 則會修復異常錯誤代碼爲 0xC0000094;

在後面會着重說一下 異常分發和處理。 這裏先拋出問題。知道我們 CPU產生異常了 如何進行 異常記錄 異常分發掉喲個你的函數是哪個

1.5 CPU 異常記錄小總結

CPU產生除零異常後會調用 IDT表中 KiDivideErrorFaultShadow 然後此函數繼續調用 KiDivideErrorFault 這兩個函數都不會進行異常處理只是繼續往內核層面下發發。

KiDivideErrorFault 繼續調用 KiExceptionDispatch 然後繼續調用 KiDispatchException

其中異常記錄 是在 KiExceptionDispatch 中產生的。 KiExceptionDispatch 會生成一個 結構體 我們稱爲異常記錄結構體(EXCEPTION_RECORD) 此結構體來保存上層傳遞過來的 異常代碼 異常地址 等信息。

最中會交給 KiDispatchException 去分發。

還了解兩個函數。 這兩個函數可以在調試中幫助我們來快速定位問題。

void KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN PKEXCEPTION_FRAME ExceptionFrame,
    IN PKTRAP_FRAME TrapFrame,
    IN KPROCESSOR_MODE PreviousMode,
    IN BOOLEAN FirstChance
    )
__int64 __fastcall KiExceptionDispatch(
        int arg_ExceptionCode,   
        unsigned int arg_ExceptionNumberParameters,
        void *arg_ExceptionAddress,
        unsigned __int64 arg_ExceptionInformation, 
        char a5)  

二丶軟件模擬異常

2.1 反彙編下追蹤模擬異常

軟件模擬異常 我們可以使用上面的C++ 代碼。

void fun()
{
    throw 1;
}
int main(int argc, char **argv)
{
    fun();
    return 0;
}

斷點下在 throw 1位置 然後反彙編查看代碼

代碼中會調用 __CxxThrowException@8 函數。並且傳遞了兩個參數 一個參數爲 _TI1H 結構 另一個爲我們自定義的錯誤碼 在C++ 如何想要專門識別異常處理得專門開一篇文章介紹。 所以在這裏不在介紹。後面會單獨將。 我們繼續往下走。

繼續往下走則會調用到Kernel32中得 RaiseException 而Kernel32中的 RaiseException 只是一個stub 函數,它是對 Kernelbase.dll中得RaiseException 做了封裝。

所以我們直接IDA分析 KernelBase 主要 要拷貝32位的 Kernelbase.dll分析

此函數結構如下:

void __stdcall RaiseException(
        DWORD dwExceptionCode,
        DWORD dwExceptionFlags,
        DWORD nNumberOfArguments,
        const ULONG_PTR *lpArguments);

反彙編如下:

可以看到 還是生成一個 異常記錄結構體 並對其填充

如果是軟件異常 則異常記錄結構體填充爲1

記錄異常的地址填充爲了 RaiseException本身

記錄的異常代碼爲上層傳遞的。異常由當前編譯環境給填充的。

最後調用到 ntdll.dll中的RtlRaiseException

繼續反彙編 RtlRaiseException

函數如下:

void __stdcall __noreturn RtlRaiseException(
PEXCEPTION_RECORD ExceptionRecord)

反彙編如下:

可以看到首先 使用 RtlCaptureContext 獲取當前環境的異常上下文信息(上下文就是記錄寄存器的一些信息) 然後設置 異常記錄結構體中的異常地址爲返回地址。 最後調用 ZwRaiseException 調用到內核。

在內核中則是KiRaiseException處理異常。然後最終調用到 KiDispatchException

在調用KiDispatchException的時候 會把 EXCEPTION_RECORD.ExceptionCode最高位清零,用來區分CPU異常。 也就是上圖所示

2.2 軟件模擬異常 產生的總結圖

根據2.1小節,跟出了軟件模擬一個異常的時候的步驟。

那麼畫圖進行總結一下。

其中還了解到了兩個API

void __stdcall RaiseException(
        DWORD dwExceptionCode,
        DWORD dwExceptionFlags,
        DWORD nNumberOfArguments,
        const ULONG_PTR *lpArguments);
void __stdcall __noreturn RtlRaiseException(
PEXCEPTION_RECORD ExceptionRecord)

三丶CPU異常與軟件模擬異常圖總結

3.1 總結圖如下

其中 ring3的 記錄異常 會將記錄的異常地址設置爲 RaiseException的地址

異常代碼 由當前編譯環境決定

KiRaiseException會爲了區分是CPU一行還是軟件異常來設置 異常記錄.異常代碼 的最高位爲0

CPU的異常記錄函數爲 KiExceptionDisPatch 最後都會發送給 KiDispatchException進行異常分發

四丶內核層下的異常分發 KiDispatchException的詳解

4.1 內核異常分發 學習

  • 簡單瞭解

    我們知道 不管是 ring3 還是 ring0 出了異常都會走到 KiDispatchException 函數進行異常分發(也就是找異常處理函數) 但是此函數很複雜。但是學好此函數也就是學明白異常的關鍵。 這裏 在內核下發生的異常我們稱爲 內核異常 反之就是用戶模式異常

函數結構如下:

void KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN PKEXCEPTION_FRAME ExceptionFrame,
    IN PKTRAP_FRAME TrapFrame,
    IN KPROCESSOR_MODE PreviousMode,
    IN BOOLEAN FirstChance
    )

內核層面的流程

內核層面其實很簡單

  1. 保存TrapFrame(一些ring3的上下文信息) 返回ring3做準備

  2. 判斷先前模式 0 = 內核 1= ring3

  3. 判斷是否是第一次調試機會

  4. 判斷是否有內核調試器

  5. 如果沒有或者內核調試器不處理

  6. 調用RtlDispatchException

    7.如果返回值是FALSE 會繼續判斷是否有內核調試器

    8 根據7 決定發送到內核調試器 還是藍屏。

所以重點就是新函數 RtlDispatchException 此函數就是專門調用異常處理函數的

此函數結構如下:

BOOLEAN __stdcall RtlDispatchException(
    PEXCEPTION_RECORD ExceptionRecord,
    PCONTEXT Context)
{
    //xxxx
}

參數1 是記錄的異常信息

參數2 是記錄的上下文信息。 比如記錄了 RIP RAX 等寄存器

此函數本質上就是 遍歷異常處理鏈表 來進行異常處理

4.2 異常信息記錄鏈表結構體

異常信息結構體存儲在 FS:[0] 在ring3下也就是 TEB的環境塊

TEB結構塊如下:

nt!_TEB
   +0x000 NtTib            : _NT_TIB   
   +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB

其中第一個成員結構如下:

typedef struct _NT_TIB
{
     PEXCEPTION_REGISTRATION_RECORD ExceptionList;
     PVOID StackBase;
     PVOID StackLimit;
     PVOID SubSystemTib;
     union
     {
          PVOID FiberData;
          ULONG Version;
     };
     PVOID ArbitraryUserPointer;
     PNT_TIB Self;
} NT_TIB, *PNT_TIB;

NT_TIB結構的第一個成員記錄的就是異常記錄鏈表異常記錄鏈表中記錄的就是異常處理函數
結構如下:

typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;

RtlDispatchException本質上就是遍歷此結構體來進行異常函數尋找調用處理的。

此結構如上 第一個成員是一個NEXT域 指向下一個 異常處理信息結構體的

第二個成員如下指向一個異常處理函數。異常處理函數結構如下

NTAPI
EXCEPTION_ROUTINE (
    _Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
    _In_ PVOID EstablisherFrame,
    _Inout_ struct _CONTEXT *ContextRecord,
    _In_ PVOID DispatcherContext
    );

異常處理函數 有四個參數 其中第一個參數我們都應該很熟悉了 ,就是異常信息記錄結構

參數3 是一個上下文 記錄着 寄存器的信息 也是很重要的參數。

異常處理函數 可以進行異常處理。 如果處理了那麼 返回值就是1 那麼就不會繼續尋找異常處理函數了。

4.3 內核異常處理總體流程總結圖

其中我們要知道 FS:[0]中指向的是一個異常記錄鏈表

typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;

異常記錄鏈表中的第二個成員是異常處理函數

NTAPI
EXCEPTION_ROUTINE (
    _Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
    _In_ PVOID EstablisherFrame,
    _Inout_ struct _CONTEXT *ContextRecord,
    _In_ PVOID DispatcherContext
    );

RtlDispatchException 會循環遍歷鏈表進行調用 異常處理函數

五丶用戶模式下的異常分發(內核) KiDispatchException的詳解

5.1 反彙編內核中的KiDispatchException 查看用戶異常異常流程

觀看反彙編總結如下:

  1. KeContextFromKframes 備份Trap_frame 到context中

  2. 判斷先前模式 內核調用還是用戶調用 0 = 內核 1 = 用戶

  3. 是否有第一次機會

  4. 是否有內核調試器

  5. 發送給ring3調試器 DbgkForwardException

  6. 如果3環調試器沒有處理異常那麼修正EIP爲 KiUserExceptionDispatcher

  7. 如果是CPU異常那麼通過 IRETD返回3環 如果是模擬異常 那麼直接通過系統調用返回3環

  8. 無論哪種方式當線程再次回到3環的時候都會調用 KiUserExceptionDispatcher

5.2 流程圖

其中需要注意的是最終由 ring3層的 KiUserExceptionDispatcher 開始分發異常

5.3 3環下的異常處理

ntdll.KiUserExceptionDispatcher 是操作系統返回到3環所進行調用的函數。

所以直接分析此函數即可。

此函數結構如下:

VOID KiUserExceptionDispatcher(
    __in PEXCEPTION_RECORD ExceptionRecord, 
    __in PCONTEXT ContextRecord)

反彙編代碼如下:

int __stdcall KiUserExceptionDispatcher(PEXCEPTION_RECORD a1, PCONTEXT a2)
{
  _EXCEPTION_RECORD *pRecord; // ebx
  int v4; // eax
  _DWORD v5[3]; // [esp-8h] [ebp-14h] BYREF
  _EXCEPTION_RECORD *pV6Record; // [esp+4h] [ebp-8h]
  _CONTEXT *v7; // [esp+8h] [ebp-4h]

  if ( !LdrDelegatedKiUserExceptionDispatcher )
  {
    pRecord = pV6Record;
    if ( RtlDispatchException(pV6Record, v7) )
      v4 = ZwContinue(v7, 0);
    else
      v4 = ZwRaiseException((int)pV6Record, (int)v7, 0);
    v5[0] = v4;
    v5[1] = 1;
    v5[2] = pRecord;
    v7 = 0;
    RtlRaiseException((PEXCEPTION_RECORD)v5);
  }
  return ((int (__stdcall *)(PEXCEPTION_RECORD, PCONTEXT))LdrDelegatedKiUserExceptionDispatcher)(a1, a2);
}

此結構進行的流程如下

  1. 調用 RtlDispatchException 查找並執行異常處理函數

  2. 如果Rtlxx返回真 那麼則調用 ZwContinue再次進入0環。但是再次返回3環的時候 會從修正的地址開始執行

  3. 如果Rtlxxx返回假 那麼會調用 ZwRaiseException進行第二輪的異常分發

六丶用戶模式下的VEH

6.1 3環下的RtlDispatchException 分析

在3環下我們分析下RtlDispatchException

在代碼中可以看到API調用

RtlpCallVectoredHandlers 這個函數的主要作用就是調用一個鏈表

函數原型如下:

BOOLEAN
 NTAPI
 RtlpCallVectoredHandlers(IN PEXCEPTION_RECORD ExceptionRecord,
                          IN PCONTEXT Context,
                          IN PLIST_ENTRY VectoredHandlerList)
 {
     ...
 }

它總共三個三次

  1. 參數1 異常記錄結構體

  2. 參數2 上下文信息保存了寄存器的信息

  3. 參數3 鏈表

此鏈表則是全局鏈表 我們也稱爲VEH。 如果它找到了則返回 則 RtlDispatchException 就不再往下執行了。

ReactOS: sdk/lib/rtl/vectoreh.c Source File 此鏈接有人將這個函數逆向出來了。有興趣可以看一下。

6.1.2 VEH的全局鏈表

VEH是全局的,是因爲當出現異常的時候首先就是先調用它來進行處理。 假設我們有自己異常要進行處理。我們則可以加入到全局鏈表中。讓其幫我們處理異常。

下面演示如何添加VEH 讓其幫我們處理

首先是VEH 處理函數

//#define EXCEPTION_EXECUTE_HANDLER      1       VEH不使用
//#define EXCEPTION_CONTINUE_SEARCH      0       未處理
//#define EXCEPTION_CONTINUE_EXECUTION (-1)      已處理
LONG NTAPI VectExceptionRoutine(PEXCEPTION_POINTERS pExceptionInfo)
{
    ::MessageBoxA(NULL, "Veh Execute", "VehTitle", 0);
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xC0000094)
    {
        pExceptionInfo->ContextRecord->Eip = pExceptionInfo->ContextRecord->Eip + 2;
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

VEH處理函數的參數是一個 PEXCEPTION_POINTERS 結構,此結構如下:

typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord;
    PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

此結構的第一個成員是 異常記錄結構體。 此結構體上面說過很多次了。記錄的就是當前異常的一些信息。(異常代碼 異常地址 異常標誌等等)

參數2 是一個Context上下文信息 Context上下文信息裏面就保存了我們發生異常時候的寄存器。這裏面最重要的就是 EIP/RIP

VEH的異常處理函數只能返回兩個值。 要麼是 已經處理 要麼是 未處理

下面就是插入 異常處理函數到VEH鏈表中了。

這裏我們需要使用Ntdll中的函數 RtlAddVectoredExceptionHandler 在之前是使用

Kernel32中的 AddVectoredExceptionHandler 但是在我逆向的時候只發現了Ntdll用有此函數了。

所以我們動態獲取來進行使用。

完整代碼:

typedef PVOID (NTAPI * PfnRtlAddVectoredExceptionHandler)(int isInsertListHeader, PEXCEPTION_POINTERS arg_poniter);

PfnRtlAddVectoredExceptionHandler RtlAddVectoredExceptionHandler = NULL;
//#define EXCEPTION_EXECUTE_HANDLER      1       VEH不使用
//#define EXCEPTION_CONTINUE_SEARCH      0       未處理
//#define EXCEPTION_CONTINUE_EXECUTION (-1)      已處理
LONG NTAPI VectExceptionRoutine(PEXCEPTION_POINTERS pExceptionInfo)
{
    ::MessageBoxA(NULL, "Veh Execute", "VehTitle", 0);
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xC0000094)
    {
        //註釋掉的爲第一種修復方式,修復EIP + 2 跳過除0的硬編碼。
        //pExceptionInfo->ContextRecord->Eip = pExceptionInfo->ContextRecord->Eip + 2;
        pExceptionInfo->ContextRecord->Ecx = 1;  //第二種方式 修復除數爲1 那麼則不會觸發異常
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}
int main(int argc, char **argv)
{
    HMODULE hModule = NULL;
    hModule = GetModuleHandle("ntdll.dll");
    if (hModule == NULL)
    {
        return 0;
    }
    RtlAddVectoredExceptionHandler =
        reinterpret_cast<PfnRtlAddVectoredExceptionHandler>
        (::GetProcAddress(hModule, "RtlAddVectoredExceptionHandler"));
    if (RtlAddVectoredExceptionHandler == NULL)
    {
        return 0;
    }
    RtlAddVectoredExceptionHandler(0, (PEXCEPTION_POINTERS)&VectExceptionRoutine);

    //構造異常
    __asm
    {
        xor edx,edx
        xor ecx,ecx
        mov eax,0x10
        idiv ecx
    }

    printf("異常處理成功\n");
    system("pause");
    return 0;
 }

實現效果如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章