今天要介紹的是 Exception Handler,Windows Application 中常見的兩種 Exception Handler,分別是 SEH 和 VEH。
在 x86 處理器中, IDT
會存放 exception routine,而 IDTR
會指向 IDT
。
只能透過指令 sidt
和 lidt
存取 IDTR
。
每個 IDT
的 entry 中會存放 ISR
address 和 segment selector (GDT
的 index)
dt nt!_KIDTENTRY 80b95400
+0x000 Offset : 0xd200
+0x002 Selector : 8
+0x004 Access : 0x8e00
+0x006 ExtendedOffset : 0x8464
根據 offset 就可以找到 exception routine。
GDT
會存放這些 segment selector 在 physical memory 的起始值,像是常見的 cs
, ds
, fs
, gs
,也就是在拿 PEB 會用到的 TIB (Thread Information Block),也就是 CPU 正在在執行時可以參照的 thread 資訊。
struct _TEB64
{
struct _NT_TIB64 NtTib;
ULONGLONG EnvironmentPointer;
struct _CLIENT_ID64 ClientId;
ULONGLONG ActiveRpcHandle;
ULONGLONG ThreadLocalStoragePointer;
ULONGLONG ProcessEnvironmentBlock;
...
};
其中 TEB→TIB 會存放 ExceptionList
struct _NT_TIB64
{
ULONGLONG ExceptionList;
ULONGLONG StackBase;
ULONGLONG StackLimit;
ULONGLONG SubSystemTib;
union
{
ULONGLONG FiberData;
ULONG Version;
};
ULONGLONG ArbitraryUserPointer;
ULONGLONG Self;
};
Exception Handler Routine 是從 kernel 的 KiDispatchException
開始
雖然 ReactOS 並沒有完整實作 x64 的 RtlDispatchException
,但是可以知道執行的順序:
/*
* @unimplemented
*/
BOOLEAN
NTAPI
RtlDispatchException(
_In_ PEXCEPTION_RECORD ExceptionRecord,
_In_ PCONTEXT ContextRecord)
{
BOOLEAN Handled;
/* Perform vectored exception handling for user mode */
if (RtlCallVectoredExceptionHandlers(ExceptionRecord, ContextRecord))
{
/* Exception handled, now call vectored continue handlers */
RtlCallVectoredContinueHandlers(ExceptionRecord, ContextRecord);
/* Continue execution */
return TRUE;
}
/* Call the internal unwind routine */
Handled = RtlpUnwindInternal(NULL, // TargetFrame
NULL, // TargetIp
ExceptionRecord,
0, // ReturnValue
ContextRecord,
NULL, // HistoryTable
UNW_FLAG_EHANDLER);
/* In user mode, call any registered vectored continue handlers */
RtlCallVectoredContinueHandlers(ExceptionRecord, ContextRecord);
return Handled;
}
而 ReactOS 在 32-bits 的實作上,在執行完 VEH routine 後,會遞迴 EXCEPTION_REGISTRATION_RECORD
執行 SEH routine
/*
* @implemented
*/
BOOLEAN
NTAPI
RtlDispatchException(IN PEXCEPTION_RECORD ExceptionRecord,
IN PCONTEXT Context)
{
PEXCEPTION_REGISTRATION_RECORD RegistrationFrame, NestedFrame = NULL;
DISPATCHER_CONTEXT DispatcherContext;
EXCEPTION_RECORD ExceptionRecord2;
EXCEPTION_DISPOSITION Disposition;
ULONG_PTR StackLow, StackHigh;
ULONG_PTR RegistrationFrameEnd;
/* Perform vectored exception handling for user mode */
if (RtlCallVectoredExceptionHandlers(ExceptionRecord, Context))
{
/* Exception handled, now call vectored continue handlers */
RtlCallVectoredContinueHandlers(ExceptionRecord, Context);
/* Continue execution */
return TRUE;
}
/* Get the current stack limits and registration frame */
RtlpGetStackLimits(&StackLow, &StackHigh);
RegistrationFrame = RtlpGetExceptionList();
/* Now loop every frame */
while (RegistrationFrame != EXCEPTION_CHAIN_END)
{
/* Registration chain entries are never NULL */
ASSERT(RegistrationFrame != NULL);
/* Find out where it ends */
RegistrationFrameEnd = (ULONG_PTR)RegistrationFrame +
sizeof(EXCEPTION_REGISTRATION_RECORD);
/* Make sure the registration frame is located within the stack */
if ((RegistrationFrameEnd > StackHigh) ||
((ULONG_PTR)RegistrationFrame < StackLow) ||
((ULONG_PTR)RegistrationFrame & 0x3))
{
/* Check if this happened in the DPC Stack */
if (RtlpHandleDpcStackException(RegistrationFrame,
RegistrationFrameEnd,
&StackLow,
&StackHigh))
{
/* Use DPC Stack Limits and restart */
continue;
}
/* Set invalid stack and bail out */
ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID;
return FALSE;
}
//
// TODO: Implement and call here RtlIsValidHandler(RegistrationFrame->Handler)
// for supporting SafeSEH functionality, see the following articles:
// https://www.optiv.com/blog/old-meets-new-microsoft-windows-safeseh-incompatibility
// https://msrc-blog.microsoft.com/2012/01/10/more-information-on-the-impact-of-ms12-001/
//
/* Check if logging is enabled */
RtlpCheckLogException(ExceptionRecord,
Context,
RegistrationFrame,
sizeof(*RegistrationFrame));
/* Call the handler */
Disposition = RtlpExecuteHandlerForException(ExceptionRecord,
RegistrationFrame,
Context,
&DispatcherContext,
RegistrationFrame->Handler);
/* Check if this is a nested frame */
if (RegistrationFrame == NestedFrame)
{
/* Mask out the flag and the nested frame */
ExceptionRecord->ExceptionFlags &= ~EXCEPTION_NESTED_CALL;
NestedFrame = NULL;
}
/* Handle the dispositions */
switch (Disposition)
{
/* Continue execution */
case ExceptionContinueExecution:
{
/* Check if it was non-continuable */
if (ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE)
{
/* Set up the exception record */
ExceptionRecord2.ExceptionRecord = ExceptionRecord;
ExceptionRecord2.ExceptionCode =
STATUS_NONCONTINUABLE_EXCEPTION;
ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
ExceptionRecord2.NumberParameters = 0;
/* Raise the exception */
RtlRaiseException(&ExceptionRecord2);
}
else
{
/* In user mode, call any registered vectored continue handlers */
RtlCallVectoredContinueHandlers(ExceptionRecord, Context);
/* Execution continues */
return TRUE;
}
}
/* Continue searching */
case ExceptionContinueSearch:
if (ExceptionRecord->ExceptionFlags & EXCEPTION_STACK_INVALID)
{
/* We have an invalid stack, bail out */
return FALSE;
}
break;
/* Nested exception */
case ExceptionNestedException:
{
/* Turn the nested flag on */
ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL;
/* Update the current nested frame */
if (DispatcherContext.RegistrationPointer > NestedFrame)
{
/* Get the frame from the dispatcher context */
NestedFrame = DispatcherContext.RegistrationPointer;
}
break;
}
/* Anything else */
default:
{
/* Set up the exception record */
ExceptionRecord2.ExceptionRecord = ExceptionRecord;
ExceptionRecord2.ExceptionCode = STATUS_INVALID_DISPOSITION;
ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
ExceptionRecord2.NumberParameters = 0;
/* Raise the exception */
RtlRaiseException(&ExceptionRecord2);
break;
}
}
/* Go to the next frame */
RegistrationFrame = RegistrationFrame->Next;
}
/* Unhandled, bail out */
return FALSE;
}
有了背景知識就可以來簡單介紹 SEH 和 VEH。
SEH 其實就是 try+catch (try+except) 中的 exception
int main()
{
__try
{
__asm int 0x29
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("SEH catched the exception!\n");
}
return 0;
}
在 64-bits PE 中,多了 Exception Directory data directory 取代了在 32 bits 中會將 SEH 存在 Stack 的設計。在 32-bits PE,Stack-based Buffer Overflow 可以利用存放在 stack 的 SEH 執行指令。
在 64-bits PE 中,Exception Directory data directory,會有許多 _RUNTIME_FUNCTION
表示 exception handler 的結構
typedef struct _RUNTIME_FUNCTION {
DWORD BeginAddress; // Start RVA of SEH code chunk
DWORD EndAddress; // End RVA of SEH code chunk
DWORD UnwindData; // Rva of an UNWIND_INFO structure that describes this code frame
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
UNWIND_INFO
有 Exception Handler routine 的 address
// Unwind info flags
#define UNW_FLAG_EHANDLER 0x01
#define UNW_FLAG_UHANDLER 0x02
#define UNW_FLAG_CHAININFO 0x04
// UNWIND_CODE 3 bytes structure
typedef union _UNWIND_CODE {
struct {
UBYTE CodeOffset;
UBYTE UnwindOp : 4;
UBYTE OpInfo : 4;
};
USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;
typedef struct _UNWIND_INFO {
UBYTE Version : 3; // + 0x00 - Unwind info structure version
UBYTE Flags : 5; // + 0x00 - Flags (see above)
UBYTE SizeOfProlog; // + 0x01
UBYTE CountOfCodes; // + 0x02 - Count of unwind codes
UBYTE FrameRegister : 4; // + 0x03
UBYTE FrameOffset : 4; // + 0x03
UNWIND_CODE UnwindCode[1]; // + 0x04 - Unwind code array
UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
union {
OPTIONAL ULONG ExceptionHandler; // Exception handler routine
OPTIONAL ULONG FunctionEntry;
};
OPTIONAL ULONG ExceptionData[]; // C++ Scope table structure
} UNWIND_INFO, *PUNWIND_INFO;
在 32-bits PE 中,SEH chain 可以從 TEB 查找,最後會指向 catch 內的程式碼。
32-bits PE 也可以在編譯時加入 Safe SEH 的 flag,SEH 就不會存放在 stack。
VEH 的使用和 SEH 不太一樣,必須先使用 AddVectoredExceptionHandler
註冊。
以下是根據這篇文章中提供的一個簡單的 VEH Handler 範例:
LONG NTAPI MyVEHHandler(PEXCEPTION_POINTERS ExceptionInfo) {
printf("MyVEHHandler (0x%x)\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO) {
printf(" Divide by zero at 0x%p\n", ExceptionInfo->ExceptionRecord->ExceptionAddress);
ExceptionInfo->ContextRecord->Eip += 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
int main() {
AddVectoredExceptionHandler(1, MyVEHHandler);
int except = 5;
except /= 0;
return 0;
}
在 RtlpCallVectoredHandlers
會將 EXCEPTION_RECORD
傳給 VectoredHandlerList
的每個 VEH Handler,由 VectoredHandler 決定是否處理。
在範例程式碼中,只有在 ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO
的時候,VectoredHandler 才會作出後續的處理。
在取得 VEH Handler 的過程中會需要進行 decode,decode VEH Handler 的過程可以參考這篇 Dump VEH,下圖也呈現如何進行 decode:
(ref: https://dimitrifourny.github.io/2020/06/11/dumping-veh-win10.html)
另外想提一下這個有趣的專案,作者利用 VEH 執行存在 data section 中的程式碼。
利用 VEH Handler 可以控制 ExceptionInfo->ContextRecord
的特性,將本來只有 RW 的 assembly 複製到 ExceptionInfo->ContextRecord
中執行。
其實跟許多 Windows Process Injection 技巧的起源都差不多是這樣,找到執行過程中具有 RWX 的 page 便可以加以濫用。