寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。
看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。
? 華麗的分割線 ?
概述
當使用者異常產生後,核心函式KiDispatchException
並不是像處理核心異常那樣在0環直接進行處理 ,而是修正3環EIP為KiUserExceptionDispatcher
函式後就結束了。這樣,當執行緒再次回到3環時,將會從KiUserExceptionDispatcher
函式開始執行,這個函式就是我們重點關注物件,我們先看一下它的流程:
- 呼叫
RtlDispatchException
,查詢並執行異常處理函式。 - 如果
RtlDispatchException
返回真,呼叫ZwContinue
再次進入0環,但執行緒再次返回3環時,會從修正後的位置開始執行。 - 如果
RtlDispatchException
返回假,呼叫ZwRaiseException
進行第二輪異常分發。
看完上面的流程之後,我們看看其反彙編:
; void __stdcall __noreturn KiUserExceptionDispatcher(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextFrame)
public _KiUserExceptionDispatcher@8
_KiUserExceptionDispatcher@8 proc near ; DATA XREF: .text:off_7C923428↑o
var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
ExceptionRecord = dword ptr 4
ContextFrame = dword ptr 8
mov ecx, [esp+ExceptionRecord]
mov ebx, [esp]
push ecx ; ContextRecord
push ebx ; ExceptionRecord
call _RtlDispatchException@8 ; RtlDispatchException(x,x)
or al, al
jz short loc_7C92E47A
pop ebx
pop ecx
push 0
push ecx
call _ZwContinue@8 ; ZwContinue(x,x)
jmp short loc_7C92E485
; ---------------------------------------------------------------------------
loc_7C92E47A: ; CODE XREF: KiUserExceptionDispatcher(x,x)+10↑j
pop ebx
pop ecx
push 0 ; FirstChance
push ecx ; ContextRecord
push ebx ; ExceptionRecord
call _ZwRaiseException@12 ; ZwRaiseException(x,x,x)
loc_7C92E485: ; CODE XREF: KiUserExceptionDispatcher(x,x)+1C↑j
add esp, -14h
mov [esp+EXCEPTION_RECORD.ExceptionCode], eax
mov [esp+EXCEPTION_RECORD.ExceptionFlags], 1
mov [esp+EXCEPTION_RECORD.ExceptionRecord], ebx
mov [esp+EXCEPTION_RECORD.NumberParameters], 0
push esp ; ExceptionRecord
call _RtlRaiseException@4 ; RtlRaiseException(x)
_KiUserExceptionDispatcher@8 endp ; sp-analysis failed
可以看出該函式會呼叫RtlDispatchException
,為了節省篇幅用虛擬碼如下:
BOOLEAN __stdcall RtlDispatchException(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextRecord)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
result = 0;
if ( RtlCallVectoredExceptionHandlers(ExceptionRecord, ContextRecord) )
return 1;
RtlpGetStackLimits(&LowLimit, &HighLimit);
ExceptionRecorda = 0;
exRecord = RtlpGetRegistrationHead(); // ExceptionList
if ( exRecord != -1 )
{
while ( 1 )
{
if ( exRecord < LowLimit
|| &exRecord[1] > HighLimit
|| (exRecord & 3) != 0
|| (handler = exRecord->Handler, handler >= LowLimit) && handler < HighLimit
|| !RtlIsValidHandler(exRecord->Handler) )
{
ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID;
return result;
}
if ( byte_7C99B3FA < 0 )
v11 = RtlpLogExceptionHandler(ExceptionRecord, ContextRecord, 0, exRecord, 0x10u);
RtlpExecuteHandlerForException(ExceptionRecord, exRecord, ContextRecord, &a4, exRecord->Handler);
v6 = v5;
if ( byte_7C99B3FA < 0 )
RtlpLogLastExceptionDisposition(v11, v5);
if ( ExceptionRecorda == exRecord )
{
ExceptionRecord->ExceptionFlags &= 0xFFFFFFEF;
ExceptionRecorda = 0;
}
if ( !v6 )
break;
if ( v6 == 1 )
{
if ( (ExceptionRecord->ExceptionFlags & 8) != 0 )
return result;
}
else
{
if ( v6 != 2 )
{
e.ExceptionCode = EXCEPTION_INVALID_DISPOSITION;
e.ExceptionFlags = 1;
e.ExceptionRecord = ExceptionRecord;
e.NumberParameters = 0;
RtlRaiseException(&e);
}
v8 = a4;
ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL;
if ( v8 > ExceptionRecorda )
ExceptionRecorda = v8;
}
exRecord = exRecord->Next;
if ( exRecord == -1 )
return result;
}
if ( (ExceptionRecord->ExceptionFlags & 1) != 0 )
{
e.ExceptionCode = EXCEPTION_NONCONTINUABLE_EXCEPTION;
e.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
e.ExceptionRecord = ExceptionRecord;
e.NumberParameters = 0;
RtlRaiseException(&e);
}
result = 1;
}
return result;
}
RtlCallVectoredExceptionHandlers
這個函式就是用來執行VEH
的。如果返回假,則說明沒有,後面的RtlpGetRegistrationHead
就會獲取SEH
,如果有就執行,它是在堆疊中的。
有了這些鋪墊後,我們來介紹VEH
和SEH
。
VEH
對於VEH
,這個是XP
及其之後才有的,中文為向量化異常結構處理。我們先看看它的處理流程:
CPU
捕獲異常資訊;- 通過
KiDispatchException
進行分發; KiUserExceptionDispatcher
呼叫RtlDispatchException
;RtlDispatchException
查詢VEH
處理函式連結串列 並呼叫相關處理函式;- 程式碼返回到
KiUserExceptionDispatcher
; - 呼叫
ZwContinue
再次進入0環(ZwContinue
呼叫NtContinue
,主要作用就是恢復_TRAP_FRAME
然後通過KiServiceExit
返回到3環); - 執行緒再次返回3環後,從修正後的位置開始執行;
如下是執行VEH
的虛擬碼:
BOOLEAN __stdcall RtlCallVectoredExceptionHandlers(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextRecord)
{
PRTL_VECTORED_HANDLER_ENTRY p; // esi
int (__stdcall *VectoredHandler)(EXCEPTION_POINTERS *); // eax
EXCEPTION_POINTERS ExceptionInfo; // [esp+4h] [ebp-8h] BYREF
BOOLEAN v6; // [esp+17h] [ebp+Bh]
if ( IsListEmpty(&RtlpCalloutEntryList) )
return 0;
ExceptionInfo.ExceptionRecord = ExceptionRecord;
ExceptionInfo.ContextRecord = ContextRecord;
RtlEnterCriticalSection(&RtlpCalloutEntryLock);
for ( p = RtlpCalloutEntryList.Flink; ; p = p->ListEntry.Flink )
{
if ( p == &RtlpCalloutEntryList )
{
v6 = 0;
goto EndProc;
}
VectoredHandler = RtlDecodePointer(p->VectoredHandler);
if ( VectoredHandler(&ExceptionInfo) == -1 )
break;
}
v6 = 1;
EndProc:
RtlLeaveCriticalSection(&RtlpCalloutEntryLock);
return v6;
}
剩餘的細節將會在總結與提升進行講解,下面我們來看看如何使用VEH
,如下是實驗程式碼:
#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>
typedef PVOID (NTAPI *VectoredExceptionHandler)(ULONG,_EXCEPTION_POINTERS*);
LONG NTAPI MyVectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
{
puts("進入異常處理函式……");
if (pExceptionInfo->ExceptionRecord->ExceptionCode==0xC0000094)
{
puts("異常函式處理了……");
pExceptionInfo->ContextRecord->Ecx = 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
int main(int argc, char* argv[])
{
HMODULE lib = LoadLibrary("kernel32.dll");
VectoredExceptionHandler AddVectoredExceptionHandler = (VectoredExceptionHandler)GetProcAddress(lib,"AddVectoredExceptionHandler");
AddVectoredExceptionHandler(1,(_EXCEPTION_POINTERS*)&MyVectoredExceptionHandler);
_asm
{
xor edx,edx;
xor ecx,ecx;
mov eax,0x10;
idiv ecx;
}
puts("繼續執行……");
system("pause");
return 0;
}
執行後會正常執行,並顯示異常處理資訊。
SEH
SEH
意為結構化異常處理,它的結構如下圖所示:
也就是說包裝的異常處理專案是以單向連結串列的形式管理的。必須具有兩個如上圖所示的成員,也就是說,這個結構是可以擴充套件的,有關擴充套件的將會在後續介紹,下面我們來看實驗程式碼:
#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>
struct MyException
{
MyException* prev;
DWORD handle;
};
EXCEPTION_DISPOSITION MyExceptionHandler(_EXCEPTION_RECORD* ExceptionRecord,void* Establisherframe,CONTEXT* context,void* DispatcherContext)
{
puts("進入異常處理……");
if (ExceptionRecord->ExceptionCode==0xC0000094)
{
puts("開始處理異常……");
context->Eip+=2;
return ExceptionContinueExecution;
}
return ExceptionContinueSearch;
}
int main(int argc, char* argv[])
{
DWORD tmp;
//初始化異常結構
MyException ex={(MyException*)tmp,(DWORD)MyExceptionHandler};
//加入 SEH
_asm
{
mov eax,fs:[0];
mov tmp,eax;
lea ecx,ex;
mov fs:[0],ecx;
}
//製造異常
_asm
{
xor edx,edx;
xor ecx,ecx;
mov eax,0x10;
idiv ecx;
}
//撤掉 SEH
_asm
{
mov eax,tmp;
mov fs:[0],eax;
}
puts("正常執行……");
system("pause");
return 0;
}
該程式正常執行,並列印異常處理結果。
編譯器擴充套件 SEH
初識
前面我們用自己的方式實現了SEH
的使用。異常處理很重要,但是,這個對於開發者很不友好。每次都要構造SEH
,退出函式要撤掉。編譯器提供了關鍵字,並對SEH
進行了擴充,使用如下圖所示:
_try // 掛入 SEH 連結串列
{
}
_except(/*過濾表示式*/) //異常過濾
{
//異常處理程式
}
對於過濾表示式的結果值,只能是-1
、0
、1
,它們表示的含義如下:
EXCEPTION_EXECUTE_HANDLER
(1) 執行except
裡面的程式碼EXCEPTION_CONTINUE_SEARCH
(0) 尋找下一個異常處理函式EXCEPTION_CONTINUE_EXECUTION
(-1) 返回出錯位置重新執行
我說只能是這三值,並沒有說只能寫這三個數字,你可以寫入表示式或者函式,使其得到的結果或者返回值是這仨值其中之一就可以,如下是我們的實驗程式:
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
_try
{
_asm
{
xor edx,edx;
xor ecx,ecx;
mov eax,0x10;
idiv ecx;
}
puts("繼續跑……");
}_except(1)
{
puts("異常處理……");
}
system("pause");
return 0;
}
執行該程式,只列印了except
裡面的,得到正確結果。
初步深入
我們接下來在彙編層面檢視它是如何實現的,首先我們檢視一下編譯器為我們擴充套件的結構,否則看程式碼是看不懂的。
struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
int _ebp;
};
然後我們所謂的結構就成立這樣子:
圖中的_except_handler3
是啥我們看它的反彙編是什麼就知道了:
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
00401010 push ebp
00401011 mov ebp,esp
00401013 push 0FFh
00401015 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed"+0Ch (00424030)
0040101A push offset __except_handler3 (00401400)
0040101F mov eax,fs:[00000000]
00401025 push eax
00401026 mov dword ptr fs:[0],esp
0040102D add esp,0B8h
00401030 push ebx
00401031 push esi
00401032 push edi
00401033 mov dword ptr [ebp-18h],esp
00401036 lea edi,[ebp-58h]
00401039 mov ecx,10h
0040103E mov eax,0CCCCCCCCh
00401043 rep stos dword ptr [edi]
_try
00401045 mov dword ptr [ebp-4],0
{
_asm
{
xor edx,edx;
0040104C xor edx,edx
xor ecx,ecx;
0040104E xor ecx,ecx
mov eax,0x10;
00401050 mov eax,10h
idiv ecx;
00401055 idiv eax,ecx
}
puts("繼續跑……");
00401057 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed" (00424024)
0040105C call puts (004011e0)
00401061 add esp,4
}_except(1)
00401064 mov dword ptr [ebp-4],0FFFFFFFFh
0040106B jmp $L865+17h (0040108a)
$L864:
0040106D mov eax,1
$L866:
00401072 ret
$L865:
00401073 mov esp,dword ptr [ebp-18h]
{
puts("異常處理……");
00401076 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xa1\xad\xa1\xad" (00425140)
0040107B call puts (004011e0)
00401080 add esp,4
}
00401083 mov dword ptr [ebp-4],0FFFFFFFFh
system("pause");
0040108A push offset string "pause" (0042401c)
0040108F call system (004010d0)
00401094 add esp,4
return 0;
00401097 xor eax,eax
}
00401099 mov ecx,dword ptr [ebp-10h]
0040109C mov dword ptr fs:[0],ecx
004010A3 pop edi
004010A4 pop esi
004010A5 pop ebx
004010A6 add esp,58h
004010A9 cmp ebp,esp
004010AB call __chkesp (004012d0)
004010B0 mov esp,ebp
004010B2 pop ebp
004010B3 ret
看不懂嗎?我們來畫個堆疊圖,如下所示:
標註*
的表示原來的值,是不是和結構體的成員對應起來了?注意不要以為只有黃色的區域,由於通常的函式採用ebp
定址,所以我沒有把ebp*
打上黃色底色。
下面我們來看看scopetable
成員,它的結構如下:
struct scopetable_entry
{
DWORD previousTryLevel; //上一個try{}結構編號
PDWRD lpfnFilter; //過濾函式的起始地址
PDWRD lpfnHandler; //異常處理程式的地址
}
我們來看看這個結構的內容是啥,最終它的成員如下:
scopetable.previousTryLevel = -1;
scopetable.lpfnFilter = 0x40106D;
scopetable.lpfnHandler = 0x401073;
正好把程式碼指令和地址逐個對應起來了。
繼續深入
如果異常處理有巢狀呼叫的情況會是怎麼樣呢?如下是測試程式碼:
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
_try
{
_try
{
_asm
{
xor edx,edx;
xor ecx,ecx;
mov eax,0x10;
idiv ecx;
}
}_except(1)
{
puts("測試");
}
puts("繼續跑……");
}_except(1)
{
puts("異常處理……");
}
system("pause");
return 0;
}
然後檢視反彙編結果:
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
00401010 push ebp
00401011 mov ebp,esp
00401013 push 0FFh
00401015 push offset string "\xb2\xe2\xca\xd4"+0Ch (00424050)
0040101A push offset __except_handler3 (00401450)
0040101F mov eax,fs:[00000000]
00401025 push eax
00401026 mov dword ptr fs:[0],esp
0040102D add esp,0B8h
00401030 push ebx
00401031 push esi
00401032 push edi
00401033 mov dword ptr [ebp-18h],esp
00401036 lea edi,[ebp-58h]
00401039 mov ecx,10h
0040103E mov eax,0CCCCCCCCh
00401043 rep stos dword ptr [edi]
_try
00401045 mov dword ptr [ebp-4],0
{
_try
0040104C mov dword ptr [ebp-4],1
{
_asm
{
xor edx,edx;
00401053 xor edx,edx
xor ecx,ecx;
00401055 xor ecx,ecx
mov eax,0x10;
00401057 mov eax,10h
idiv ecx;
0040105C idiv eax,ecx
}
}_except(1)
0040105E mov dword ptr [ebp-4],0
00401065 jmp $L872+17h (0040f5d4)
$L871:
00401067 mov eax,1
$L873:
0040106C ret
$L872:
0040106D mov esp,dword ptr [ebp-18h]
{
puts("測試");
00401070 push offset string "\xb2\xe2\xca\xd4" (00424044)
00401075 call puts (00401230)
0040107A add esp,4
}
0040107D mov dword ptr [ebp-4],0
puts("繼續跑……");
00401084 push offset string "\xbc\xcc\xd0\xf8\xc5\xdc\xa1\xad\xa1\xad" (00424034)
00401089 call puts (00401230)
0040108E add esp,4
}_except(1)
00401091 mov dword ptr [ebp-4],0FFFFFFFFh
00401098 jmp $L868+17h (004010b7)
$L867:
0040109A mov eax,1
$L869:
0040109F ret
$L868:
004010A0 mov esp,dword ptr [ebp-18h]
{
puts("異常處理……");
004010A3 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xa1\xad\xa1\xad" (00424024)
004010A8 call puts (00401230)
004010AD add esp,4
}
004010B0 mov dword ptr [ebp-4],0FFFFFFFFh
system("pause");
004010B7 push offset string "pause" (0042401c)
004010BC call system (00401120)
004010C1 add esp,4
return 0;
004010C4 xor eax,eax
}
004010C6 mov ecx,dword ptr [ebp-10h]
004010C9 mov dword ptr fs:[0],ecx
004010D0 pop edi
004010D1 pop esi
004010D2 pop ebx
004010D3 add esp,58h
004010D6 cmp ebp,esp
004010D8 call __chkesp (00401320)
004010DD mov esp,ebp
004010DF pop ebp
004010E0 ret
看程式碼發現還是隻是掛了一次,我們得看看scopetable
的內容是啥了:
00425168 FFFFFFFF 0040109A 004010A0
00425174 00000000 00401067 0040106D
00425180 00000000 00000000 00000000
0042518C 00000000 00000000 00000000
可以看到,這裡有兩個成員了。
finally 關鍵字
當然不僅僅有try_except
,還可以使用finally
,該關鍵字的作用就是隻要退出try
就執行裡面的函式,無論通過那種方式,如下是我們的實驗程式碼:
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
_try
{
return 0;
}__finally
{
puts("異常處理……");
system("pause");
}
return 0;
}
執行結果如下:
異常處理……
請按任意鍵繼續. . .
然後我們看看它在彙編層面是如何實現的,其反彙編如下:
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
00401010 push ebp
00401011 mov ebp,esp
00401013 push 0FFh
00401015 push offset string "stream != NULL"+10h (00425168)
0040101A push offset __except_handler3 (00401450)
0040101F mov eax,fs:[00000000]
00401025 push eax
00401026 mov dword ptr fs:[0],esp
0040102D add esp,0B4h
00401030 push ebx
00401031 push esi
00401032 push edi
00401033 lea edi,[ebp-5Ch]
00401036 mov ecx,11h
0040103B mov eax,0CCCCCCCCh
00401040 rep stos dword ptr [edi]
_try
00401042 mov dword ptr [ebp-4],0
00401049 push 0FFh
0040104B mov dword ptr [ebp-1Ch],0
{
00401052 lea eax,[ebp-10h]
00401055 push eax
00401056 call __local_unwind2 (0040139a)
0040105B add esp,8
return 0;
0040105E mov eax,dword ptr [ebp-1Ch]
00401061 jmp $L865+2 (00401080)
}__finally
{
puts("異常處理……");
00401063 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xa1\xad\xa1\xad" (00424024)
00401068 call puts (00401230)
0040106D add esp,4
system("pause");
00401070 push offset string "pause" (0042401c)
00401075 call system (00401120)
0040107A add esp,4
$L863:
0040107D ret
}
17: return 0;
0040107E xor eax,eax
}
00401080 mov ecx,dword ptr [ebp-10h]
00401083 mov dword ptr fs:[0],ecx
0040108A pop edi
0040108B pop esi
0040108C pop ebx
0040108D add esp,5Ch
00401090 cmp ebp,esp
00401092 call __chkesp (00401320)
00401097 mov esp,ebp
00401099 pop ebp
0040109A ret
可以看到在呼叫return 0;
之前,被插入了呼叫__local_unwind2
函式,正是這個函式能夠呼叫finally
裡面的程式碼的:
__local_unwind2:
0040139A push ebx
0040139B push esi
0040139C push edi
0040139D mov eax,dword ptr [esp+10h]
004013A1 push eax
004013A2 push 0FEh
004013A4 push offset __global_unwind2+20h (00401378)
004013A9 push dword ptr fs:[0]
004013B0 mov dword ptr fs:[0],esp
004013B7 mov eax,dword ptr [esp+20h]
004013BB mov ebx,dword ptr [eax+8]
004013BE mov esi,dword ptr [eax+0Ch]
004013C1 cmp esi,0FFh
004013C4 je __NLG_Return2+2 (004013f4)
004013C6 cmp esi,dword ptr [esp+24h]
004013CA je __NLG_Return2+2 (004013f4)
004013CC lea esi,[esi+esi*2]
004013CF mov ecx,dword ptr [ebx+esi*4]
004013D2 mov dword ptr [esp+8],ecx
004013D6 mov dword ptr [eax+0Ch],ecx
004013D9 cmp dword ptr [ebx+esi*4+4],0
004013DE jne __NLG_Return2 (004013f2)
004013E0 push 101h
004013E5 mov eax,dword ptr [ebx+esi*4+8]
004013E9 call __NLG_Notify (0040142e)
004013EE call dword ptr [ebx+esi*4+8]
__NLG_Return2:
004013F2 jmp __local_unwind2+1Dh (004013b7)
004013F4 pop dword ptr fs:[0]
004013FB add esp,0Ch
004013FE pop edi
004013FF pop esi
00401400 pop ebx
00401401 ret
關鍵呼叫在call dword ptr [ebx+esi*4+8]
,執行這個就會呼叫finally
裡的程式碼,這個呼叫流程又被成為異常展開。具體詳細的其他細節將會在總結與提升進行介紹。
下一篇
異常篇——總結與提升