異常學習
一丶CPU異常
1.1 異常處理簡介
此爲Windows異常處理。學習異常可以幫助我們更好的調試程序。或者更好的尋找異常點。
先說一下異常產生之後要進行的操作
-
異常記錄
-
異常分發
-
異常處理
異常產生後第一步就是要進行異常記錄
原因是首先要記錄哪裏產生了異常。 異常的類型是什麼。 第二步就是要分發異常
分發異常指的就是去尋找異常處理函數
找到函數之後就會進行調用,調用的函數我們就稱爲異常處理
。
1.2 異常的分類之CPU異常
異常分類有兩種,第一種就是CPU產生的異常。 第二種就是軟件模擬的產生的異常。
CPU異常如下:
C++代碼示例
- CPU異常示例
int main(int argc, char **argv)
{
int x = 10;
int y = 0;
int c = x / y;
return 0;
}
提示我們除零異常
這個是CPU可以檢測出來的。
- 軟件模擬的異常
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
)
內核層面的流程
內核層面其實很簡單
-
保存TrapFrame(一些ring3的上下文信息) 返回ring3做準備
-
判斷先前模式 0 = 內核 1= ring3
-
判斷是否是第一次調試機會
-
判斷是否有內核調試器
-
如果沒有或者內核調試器不處理
-
調用
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 查看用戶異常異常流程
觀看反彙編總結如下:
-
KeContextFromKframes
備份Trap_frame 到context中 -
判斷先前模式 內核調用還是用戶調用 0 = 內核 1 = 用戶
-
是否有第一次機會
-
是否有內核調試器
-
發送給ring3調試器
DbgkForwardException
-
如果3環調試器沒有處理異常那麼修正EIP爲
KiUserExceptionDispatcher
-
如果是CPU異常那麼通過 IRETD返回3環 如果是模擬異常 那麼直接通過系統調用返回3環
-
無論哪種方式當線程再次回到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);
}
此結構進行的流程如下
-
調用
RtlDispatchException
查找並執行異常處理函數 -
如果Rtlxx返回真 那麼則調用
ZwContinue
再次進入0環。但是再次返回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
異常記錄結構體
-
參數2
上下文信息保存了寄存器的信息
-
參數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;
}
實現效果如下: