SEH的介绍及实战

根据我们上节的异常讲解中,我们说过了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
在这里插入图片描述至此,我们的异常处理就结束了。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值