根据我们上节的异常讲解中,我们说过了SEH(Structured Exception Handler)是在无调试器接手的情况下,系统会遍历SE链,然后寻找相应的SEH函数来处理异常。
首先稍微复习下我们的TEB
kd> dt _teb
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
..........
teb的第一项是一项_NT_TIB是纪录着线程信息块的结构,里面就有着我们今天的主角。
查看下tib的结构
ntdll!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB
-------------seperrator-------------
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD *Next; //指向下一个ERR
PEXCEPTION_ROUTINE Handler; //异常处理函数
}EXCEPTION_REGISTRATION_RECORD
-------------seperrator-------------
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution = 0,
ExceptionContinueSearch = 1,
ExceptionNestedException = 2,
ExceptionCollidedUnwind = 3
} EXCEPTION_DISPOSITION;
看这个第一项,就是一个SE的链,查看一下它的结构,正是一条异常处理链。x86平台下,fs:[0]就是指向TEB的指针,同时里面的值也就是指向第一个SEH的指针。x64下是gs:[0]。每一个结点的结构都是一个ERR(异常处理记录)。并且这条链是一条单向链,所以决定了它插入和删除只能在头部,也就说新注册的异常处理函数只会在头部新增。并且TEB是每个线程所私有的,所以每个线程的SEH是不同的,也就是SEH只对本线程有效,并且系统会维护链表最后的next指向0xFFFFFFFF。
异常处理回调函数原型如下:
__cdecl
_except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext);
同时,我们在上次讲了,KiDispatchException这个函数是在内核态的,而内核的栈与ring3的栈用的不是同一个,所以会进行相应的变换,相应的代码如下:
//
// If the SS segment is not 32 bit flat, there is no point
// to dispatch exception to frame based exception handler.
//
if (TrapFrame->HardwareSegSs != (KGDT_R3_DATA | RPL_MASK) ||
TrapFrame->EFlags & EFLAGS_V86_MASK ) {
ExceptionRecord2.ExceptionCode = STATUS_ACCESS_VIOLATION;
ExceptionRecord2.ExceptionFlags = 0;
ExceptionRecord2.NumberParameters = 0;
ExRaiseException(&ExceptionRecord2);
}
//
// Compute length of context record and new aligned user stack
// pointer.
//
UserStack1 = (ContextFrame.Esp & ~CONTEXT_ROUND) - CONTEXT_ALIGNED_SIZE;
//
// Probe user stack area for writability and then transfer the
// context record to the user stack.
//
ProbeForWrite((PCHAR)UserStack1, CONTEXT_ALIGNED_SIZE, CONTEXT_ALIGN);
RtlCopyMemory((PULONG)UserStack1, &ContextFrame, sizeof(CONTEXT));
//
// Compute length of exception record and new aligned stack
// address.
//
Length = (sizeof(EXCEPTION_RECORD) - (EXCEPTION_MAXIMUM_PARAMETERS -
ExceptionRecord->NumberParameters) * sizeof(ULONG) +3) &
(~3);
UserStack2 = UserStack1 - Length;
//
// Probe user stack area for writeability and then transfer the
// context record to the user stack area.
// N.B. The probing length is Length+8 because there are two
// arguments need to be pushed to user stack later.
//
ProbeForWrite((PCHAR)(UserStack2 - 8), Length + 8, sizeof(ULONG));
RtlCopyMemory((PULONG)UserStack2, ExceptionRecord, Length);
//
// Push address of exception record, context record to the
// user stack. They are the two parameters required by
// _KiUserExceptionDispatch.
//
*(PULONG)(UserStack2 - sizeof(ULONG)) = UserStack1;
*(PULONG)(UserStack2 - 2*sizeof(ULONG)) = UserStack2;
//
// Set new stack pointer to the trap frame.
//
KiSegSsToTrapFrame(TrapFrame, KGDT_R3_DATA);
KiEspToTrapFrame(TrapFrame, (UserStack2 - sizeof(ULONG)*2));
//
// Force correct R3 selectors into TrapFrame.
//
TrapFrame->SegCs = SANITIZE_SEG(KGDT_R3_CODE, PreviousMode);
TrapFrame->SegDs = SANITIZE_SEG(KGDT_R3_DATA, PreviousMode);
TrapFrame->SegEs = SANITIZE_SEG(KGDT_R3_DATA, PreviousMode);
TrapFrame->SegFs = SANITIZE_SEG(KGDT_R3_TEB, PreviousMode);
TrapFrame->SegGs = 0;
//
// Set the address of the exception routine that will call the
// exception dispatcher and then return to the trap handler.
// The trap handler will restore the exception and trap frame
// context and continue execution in the routine that will
// call the exception dispatcher.
//
TrapFrame->Eip = (ULONG)KeUserExceptionDispatcher;
return;
KeUserExceptionDispatcher会来遍历SEH会返回用户层,然后遍历SEH,所以用户的栈是有以下东西的,从低地址到高地址依次是PointerToExceptionRecord,PointerToContext,ExceptionRecord,Context这四个。
而前两个指针后续又被封装成了_EXCEPTION_POINTERS,也就是这个。
nt!_EXCEPTION_POINTERS
+0x000 ExceptionRecord : Ptr32 _EXCEPTION_RECORD
+0x004 ContextRecord : Ptr32 _CONTEXT
SEH的作用范围与安装它的函数的范围一样,举个例子。
main函数里定义了一个处理异常,它的安装函数在main函数,所以它能用main函数中的资源。函数返回前会卸载。
程序会用如下代码来安装一个SEH。
依次入栈SEH和原本的nextRecord指针。然后将现在的fs:[0]修改为我们的处理函数,也就是说,这个确实非常巧妙。
这里可能会有点难以理解。首先假设原本的SEH链是这样的。
然后先将我们的handler推进去,然后推进去了一个fs:[0],这个fs:[0]是这个0x3000的ERR的地址,也就是变成这样的
然后将fs:[0]填充我们的这个ERR的地址,此时因为这个结构布置到栈上,所以会mov fs:[0],esp。
最后返回前的时候,就很简单的将next地址赋值到fs:[0]就可以卸载我们的ERR了。
分析一个实例:
很容易看出这是一个Access violation,因为esi指向的是一个空地址。
在KiUserExceptionDispatcher下断,然后键入命令gn,gn表示内核调试器不处理异常,所以根据流程它会下发给KiUserExceptionDispatcher,此时的堆栈应该根据我们分析的代码来说,应该是前两个是PointerToExceptionRecord = 0x19fa48和PointerToContext = 0x0019fa98。所以ExceptionRecord的地址就是第三个四字节c0000005的那个位置的地址。
看下ExceptionRecord的值
ExceptionCode转换成16进制是0xc0000005,即EXECUTION_ACCESS_VIOLATION,ExceptionFlags是0,表示是可继续执行的异常。ExceptionAddress正是我们那条触发访问违规的那条地址。ExceptionInformation是一个动态的结构,所以该ExceptionRecord的大小是4 * 5+ 2 * sizeof(ULONG *)
接下来看一下Context结构
明显的看到发生异常的线程的环境被完整的保存了下来,esi = 0, eip = 0x401038.
我们再来看下源码的情况
;异常处理回调函数
myHandler proc C uses ebx esi edi pExcept,pFrame,pContext,pDispatch
invoke MessageBox,0,addr messuc,addr szTit,MB_APPLMODAL or MB_OK
invoke ExitProcess,0
myHandler endp
;程序入口点
_Start:
assume fs:nothing
push ebp
mov ebp,esp
push offset myHandler //压入我们的handler
push fs:[0] //压入之前的ERR
mov fs:[0],esp //fs:[0]更新成我们的处理地址
xor esi,esi
mov eax, dword ptr [esi]
invoke MessageBox,0,addr mesfail,addr szTit,MB_APPLMODAL or MB_OK
mov esp,dword ptr fs:[0] //使esp指向我们的ERR,此时[esp+0]就是下一个ERR的地址
pop dword ptr fs:[0] //让fs:[0]恢复到未安装我们的handler的情况
mov esp,ebp
pop ebp
retn
END _Start
handler:0x00401060是MessageBox的调用,上面四个push是MessageBox的参数
此处下断,观察堆栈和TIB的情况如何
堆栈,此时压入的依次是后一个ERR的地址,我们的handler:
TIB,可以看到此时的ExceptionList是我们的ERR的地址,正是0x0019ff78
至此,我们的异常处理就结束了。