安全矩阵

 找回密码
 立即注册
搜索
查看: 2639|回复: 0

windows SEH分析

[复制链接]

221

主题

233

帖子

792

积分

高级会员

Rank: 4

积分
792
发表于 2021-7-27 23:23:13 | 显示全部楼层 |阅读模式
windows SEH分析原创 s3cunDa [url=]合天网安实验室[/url] 合天网安实验室
微信号 hee_tian
功能介绍 为广大信息安全爱好者提供有价值的文章推送服务!
昨天
收录于话题

原创稿件征集

邮箱:edu@antvsion.com
QQ:3200599554
黑客与极客相关,互联网安全领域里
的热点话题
漏洞、技术相关的调查或分析
稿件通过并发布还能收获
200-800元不等的稿酬

与linux不同,windows的函数调用栈中存储了不止栈底指针(saved ebp)以及返回地址、局部变量、canary这几样,windows在栈中存放了许多的私货,这其中就包括了seh。

SEH什么是SEH?全称就是Structure Exception Handler,也就是结构化异常处理。
那么这个SEH,是干什么的呢?
SEH是Windows操作系统上 对 C/C++ 程序语言做的语法拓展,用于处理异常事件的程序控制结构。异常事件是指打断程序正常执行流程的不在期望之中的硬件、软件事件。硬件异常是CPU抛出的如 除0、数值溢出等;软件异常是操作系统与程序通过 RaiseException语句抛出的异常。Windows拓展了C语言的语法,用 try-except与 try-finally 语句来处理异常。异常处理程序可以释放已经获取的资源、显示出错信息与程序内部状态供调试、从错误中恢复、尝试重新执行出错的代码或者关闭程序等。一个 __try 语句不能既有 __except,又有 __finally。但 try-except与 try-finally语句可以嵌套使用。
简而言之言而总之,SEH就是为了处理一些异常而出现的,那么这个SEH存储在哪里呢?
在了解SEH存储在哪里之前,我们先了解两个概念:TEB和TIB
TEB是线程环境块。是操作系统为了保存每个现成的私有数据创建的。
TIB是线程信息块,是保存线程基本信息的数据结构。
TEB长这个样子:
  1. nt!_TEB
  2.    +0x000 NtTib            : _NT_TIB
  3.    +0x01c EnvironmentPointer : Ptr32 Void
  4.    +0x020 ClientId         : _CLIENT_ID
  5.    +0x028 ActiveRpcHandle  : Ptr32 Void
  6.    +0x02c ThreadLocalStoragePointer : Ptr32 Void
  7.    +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
  8.    +0x034 LastErrorValue   : Uint4B
  9.    +0x038 CountOfOwnedCriticalSections : Uint4B
  10.    +0x03c CsrClientThread  : Ptr32 Void
  11.    +0x040 Win32ThreadInfo  : Ptr32 Void
  12.    +0x044 User32Reserved   : [26] Uint4B
  13.    +0x0ac UserReserved     : [5] Uint4B
  14.    +0x0c0 WOW32Reserved    : Ptr32 Void
  15.    +0x0c4 CurrentLocale    : Uint4B
  16.    +0x0c8 FpSoftwareStatusRegister : Uint4B
  17.    +0x0cc SystemReserved1  : [54] Ptr32 Void
  18.    +0x1a4 ExceptionCode    : Int4B
  19.    +0x1a8 ActivationContextStack : _ACTIVATION_CONTEXT_STACK
  20.    +0x1bc SpareBytes1      : [24] UChar
  21.    +0x1d4 GdiTebBatch      : _GDI_TEB_BATCH
  22.    +0x6b4 RealClientId     : _CLIENT_ID
  23.    +0x6bc GdiCachedProcessHandle : Ptr32 Void
  24.    +0x6c0 GdiClientPID     : Uint4B
  25.    +0x6c4 GdiClientTID     : Uint4B
  26.    +0x6c8 GdiThreadLocalInfo : Ptr32 Void
  27.    +0x6cc Win32ClientInfo  : [62] Uint4B
  28.    +0x7c4 glDispatchTable  : [233] Ptr32 Void
  29.    +0xb68 glReserved1      : [29] Uint4B
  30.    +0xbdc glReserved2      : Ptr32 Void
  31.    +0xbe0 glSectionInfo    : Ptr32 Void
  32.    +0xbe4 glSection        : Ptr32 Void
  33.    +0xbe8 glTable          : Ptr32 Void
  34.    +0xbec glCurrentRC      : Ptr32 Void
  35.    +0xbf0 glContext        : Ptr32 Void
  36.    +0xbf4 LastStatusValue  : Uint4B
  37.    +0xbf8 StaticUnicodeString : _UNICODE_STRING
  38.    +0xc00 StaticUnicodeBuffer : [261] Uint2B
  39.    +0xe0c DeallocationStack : Ptr32 Void
  40.    +0xe10 TlsSlots         : [64] Ptr32 Void
  41.    +0xf10 TlsLinks         : _LIST_ENTRY
  42.    +0xf18 Vdm              : Ptr32 Void
  43.    +0xf1c ReservedForNtRpc : Ptr32 Void
  44.    +0xf20 DbgSsReserved    : [2] Ptr32 Void
  45.    +0xf28 HardErrorsAreDisabled : Uint4B
  46.    +0xf2c Instrumentation  : [16] Ptr32 Void
  47.    +0xf6c WinSockData      : Ptr32 Void
  48.    +0xf70 GdiBatchCount    : Uint4B
  49.    +0xf74 InDbgPrint       : UChar
  50.    +0xf75 FreeStackOnTermination : UChar
  51.    +0xf76 HasFiberData     : UChar
  52.    +0xf77 IdealProcessor   : UChar
  53.    +0xf78 Spare3           : Uint4B
  54.    +0xf7c ReservedForPerf  : Ptr32 Void
  55.    +0xf80 ReservedForOle   : Ptr32 Void
  56.    +0xf84 WaitingOnLoaderLock : Uint4B
  57.    +0xf88 Wx86Thread       : _Wx86ThreadState
  58.    +0xf94 TlsExpansionSlots : Ptr32 Ptr32 Void
  59.    +0xf98 ImpersonationLocale : Uint4B
  60.    +0xf9c IsImpersonating  : Uint4B
  61.    +0xfa0 NlsCache         : Ptr32 Void
  62.    +0xfa4 pShimData        : Ptr32 Void
  63.    +0xfa8 HeapVirtualAffinity : Uint4B
  64.    +0xfac CurrentTransactionHandle : Ptr32 Void
  65.    +0xfb0 ActiveFrame      : Ptr32 _TEB_ACTIVE_FRAME
  66.    +0xfb4 SafeThunkCall    : UChar
  67.    +0xfb5 BooleanSpare     : [3] UChar
复制代码
TIB长这个样子:
  1. // Code in https://source.winehq.org/source/include/winnt.h#2635

  2. typedef struct _NT_TIB{
  3.     struct _EXCEPTION_REGISTRATION_RECORD *Exceptionlist; // 指向当前线程的 SEH
  4.     PVOID StackBase;    // 当前线程所使用的栈的栈底
  5.     PVOID StackLimit;   // 当前线程所使用的栈的栈顶
  6.     PVOID SubSystemTib; // 子系统
  7.     union {
  8.         PVOID FiberData;
  9.         ULONG Version;
  10.     };
  11.     PVOID ArbitraryUserPointer;
  12.     struct _NT_TIB *Self; //指向TIB结构自身
  13. } NT_TIB;
复制代码
可以看到,其中的_EXCEPTION_REGISTRATION_RECORD *Exceptionlist就是指向当前线程的SEH的指针。
那么这个_EXCEPTION_REGISTRATION_RECORD就是SEH的结构体,具体来说长这个样子
  1. //  Code in https://source.winehq.org/source/include/winnt.h#2623

  2. typedef struct _EXCEPTION_REGISTRATION_RECORD{
  3.     struct _EXCEPTION_REGISTRATION_RECORD *Next; // 指向下一个结构的指针
  4.     PEXCEPTION_ROUTINE Handler; // 当前异常处理回调函数的地址
  5. }EXCEPTION_REGISTRATION_RECORD;
复制代码

可以看到,是一个很经典的单链表结构。其中,TEB存放于fs段开头位置,fs[0]即为TIB,TIB第一个字段就保存了SEH链表的头部指针。而SEH链表中其他的节点存储在栈中。

当异常发生后,程序都干了什么?当程序发生异常后,工作流程如下:

           
  •         产生硬件异常通过 IDT调用异常处理例程, 产生软件异常通过 API的层层调用产地异常信息。而异常又由于发生位置不同,分为内核异常和用户态异常,二者最后都会靠 kiDispathException函数来进行异常分发;

           
  •         当内核产生异常时,程序处理流程进入到 KiDispatchException 函数,在该函数内备份当前线程 R3 的 TrapFrame(即栈帧的基址)。异常处理首先判断这是否是第一次异常,判断是否存在内核调试器,如果有内核调试器,则把当前的异常信息发送给内核调试器;如果没有内核调试器或者内核调试器没有处该异常 , 则进入步骤3,调用 RtlDispatchException。

           
  •         内核异常进入 RtlDispatchException 函 数, 如果RtlDispatchException 函数没有处理该异常,那么将再次尝试将异常发送到内核调试器,如果此时内核调试器仍然不存在或者没有处理该异常,那么此时系统会直接蓝屏;

           
  •         如果是用户态异常则经过 KiDispatchException进行用户态异常分发和处理。如果是第一次分发异常,则调用 DbgKForwardException将异常分发到内核调试器;如果内核调试器不存在或没有处理异常,则尝试将异常分发给用户态调试器;如果异常被处理,则进入步骤10;如果用户态调试器不存在或未处理异常,则检测是否是第一次处理异常,如果是第一次处理异常则进入第5步中的异常数据准备;

           
  •         准备一个返回ntdll!KiUserExceptionDispatcher 函数的应用层调用栈,结束本次KiDispatchException 函数的运行,调用KiServiceExit 返回用户层。此时函数栈帧是ntdll!KiUserExceptionDispatcher的执行环境,用户态线程从执行 ntdll!KiUserExceptionDispatcher 开始执行。该函数调用 ntdll!RtlDispatchException进行异常的分发,进入第 6 步

           
  •         通过 RtlCallVectoredExceptionHandlers遍历 VEH链表尝试查找异常处理函数;如果 VEH未处理异常。则从 fs[0]读取 ExceptionList并开始执行 SEH 函数处理,进入步骤7

           
  •         如果SEH没有处理函数处理该异常,则检查用户是否通过SetUnhandledExceptionFilter函数注册过进程的异常处理函数,如果用户注册过异常处理函数,调用该异常处理函数,如果异常没有被成功处理或没有自定义的异常处理函数,则进入步骤3

           
  •         如果最后仍没有处理该异常,便会主动调用 NtRaiseException将该异常重新跑出来,但是此时不是第一次分发,此时 NtRaiseException流程重新调用了 ntdll!KiDispatchException,并再次进入用户态异常的处理分支,进入步骤9

           
  •         第二次进入用户态异常处理时,不会再尝试发送到内核调试器,也不会再进行异常分发,而是直接尝试发送到用户态体异常调试器,如果最后异常仍未被处理则进入步骤11

           
  •         异常被处理,调用 NtContine,将之前保存的 TrapFrame还原,程序继续从异常处正常运行;

           
  •         异常不能被处理,系统调用 ntdll!KiDispatchException 调用 ZeTerminateProcess结束进程。

也就是说,异常发生后,大概顺序是:内核->调试器->VEH->SEH
这里我们不关注内核和调试器,这个VEH又是个啥呢?

           

Vectored Exception Handling was introduced in Windows XP.[7] Vectored Exception Handling is made available to Windows programmers using languages such as C++ and Visual Basic. VEH does not replace Structured Exception Handling (SEH), rather VEH and SEH coexist, with VEH handlers having priority over SEH handlers.[1][7] Compared with SEH, VEH works more like kernel-delivered Unix signals.[8](wikipedia)
主要大体内容就是说VEH与SEH共存,且VEH优先级比SEH高。
我们可以注册多个VEH,VEH之间通过双向链表链接,所以相对于SEH,VEH可以指定位置。同时VEH保存在堆中。
当异常发生的时候,系统将遍历VEH链表,尝试处理异常。
SEH工作原理讲了那么多别的,最终我们还是要回到最主要的问题上,SEH是个啥,他怎么工作的呢?
在线程初始化的时候,会自动在栈中安装一个SEH结构体,作为默认异常处理,他的next就是0xFFFFFFFFF,而这个异常程序大家应该都很熟悉,就是windows程序崩溃时那个弹窗,打印出来出错函数地址。
如果程序中使用了try、excpt、assert来处理异常信息,那么编译器就会在栈中压入一个SEH结构体,同时插入链表中。
当出现异常的时候,操作系统会先中断程序,然后从TIB中取出第一个SEH结构体(也就是最近的SEH结构),使用其中的handler处理这个异常。
如果这个异常处理函数处理不了这个异常,那么就顺着next往上找别的异常处理函数,直到找到一个可以处理这个异常的函数或者到底部,也就是弹出错误窗口然后杀死线程。
通常处理完异常后,需要执行展开(Unwind)操作,该操作先通知目标结点前的各异常处理函数释放资源,然后将之前的SEH链全部删除。该操作通常由各高级语言Rtl模块来完成,Win32汇编操作时既可以不展开,也可以手工展开,还可以使用RtlUnwind函数展开。
unwind当一个函数注册一个SEH的时候,通常都会干这些事:

  1. push    一堆附加数据
  2. push   offset _Handler
  3. push   fs:[0]        //next
  4. mov    fs:[0],esp       //make head -> new seh
复制代码
当触发异常调用SEH机制时,每个异常函数都需要四个重要的参数:
  • pExcept:指向EXCEPTION_RECORD的结构体的指针,其中包含了异常相关信息,如地址、异常类型等。
  • pFrame:指向栈中的SEH结构体
  • pContext:指向context结构体,包含了所有寄存器状态信息。
  • pDispatch:不知道干嘛的

在执行处理函数前,系统会将上述参数压栈,然后调用异常处理函数。
异常处理函数结束时有两个返回值:0代表处理成功,返回原来程序发生异常的地方,继续执行。
1代表失败,那么就继续顺着SEH链表往后找可以处理这个异常的函数。
当系统找到了可以处理异常的函数后,系统会将已经遍历过的异常处理函数在调用一边,这个过程就是unwind操作。
其目的就是通知前面失败的SEH,系统已经处理完了异常了,要把你们都搞掉,清理现场打包走人,然后将前面失败的SEH从链表里面删除。
那么为什么需要unwind操作呢?
如果说程序通过层层的调用在SEH链表中找到了一个可以成功处理的handler,那么这时异常被处理成功返回,此时如果直接根据context恢复现场,会涉及到许多压栈操作,那么这些压栈操作就会破坏原来的SEH链表信息,fs[0]指向一个错误地址,程序将发生异常。
具体unwind做了什么呢?有兴趣的可以参考下这篇文章:https://blog.csdn.net/LPWSTR/art ... 1001.2014.3001.5501
safe SEH既然SEH存储在栈上,那么我们可以通过栈溢出修改SEH handler函数指针为shellcode地址,然后触发异常,函数进入SEH handler,就可以执行shellcode了。
为了针对这一种攻击手法,就有了safe SEH保护措施,那么safe SEH都做了哪些检查呢?
  • 检查异常处理链是否存在于当前程序栈中,如果不是,就终止异常处理函数调用。
  • 检查异常处理函数指针是否指向栈中,如果指向,终止异常处理函数调用。
  • 前面两个都通过后,调用新的函数RtlIsValidHandler,对异常处理函数做一个有效性验证。

那么这个函数又做了哪些检查呢?
  • 判断程序设置IMAGE_DLLCHARACTERISTICS_NO_SEH标识。设置了,异常就忽略,函数返回校验失败。
  • 检测程序是否包含SEH表。如果包含,则将当前异常处理函数地址与该表进行匹配,匹配成功返回校验成功,否则失败。
  • 判断 程序是否设置ILonly标识。设置了,标识程序只包含.NET编译人中间语言,函数直接返回校验失败
  • 判断异常处理函数是否位于不可执行页(non-executable page)上。若位于,校验函数将检测DEP是否开启,如若系统未开启DEP则返回校验成功;否则程序抛出访问违例的异常

如果异常处理函数的地址没有包含在加载模块的内存空间。校验函数将直接执行DEP相关检测,函数将依次进行如下检验:
  • 判断异常处理函数是否位于不可执行页(non-executable page)上。若位于,校验函数将检测DEP是否开启,如若系统未开启DEP则返回校验成功;否则程序抛出访问违例的异常
  • 判断系统是否允许跳转到加载模块的内存空间外执行,如允许则返回校验成功;否则返回校验失败

其伪代码如下:
  1. BOOL RtlIsValidHandler(handler)
  2. {
  3.     if (handler is in image){    //在加载模块内存空间内
  4.         if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag ser)
  5.             return FALSE;
  6.         if (image has a SafeSEH table)     //含有安全SEH表,说明程序启用SafeSEH
  7.             if (handler found in the table)    // 异常处理函数地址出现在安全SEH表中
  8.                 return TRUE;
  9.             else    // 异常处理函数未出现在安全SEH表中
  10.                 return FALSE;
  11.         if (image is a .NET assembly with the ILonly flag set)     //只包含IL
  12.             return FALSE;
  13.     }
  14.     if (handler is on a non-executable page){    // 跑到不可执行页上
  15.         if (ExecuteDispatchEnable bit set in the process flags)    //DEP关闭
  16.             return TRUE;
  17.         else
  18.             raise ACESS_VIOLATION; //抛出访问违例异常
  19.     }
  20.     if (handler is not in an image){    // 在加载模块内存之外,并且在可执行页上
  21.         if (ImageDispatchEnable bit set in the process flags)    // 允许在加载模块内存空间外执行
  22.             return TRUE;
  23.         else
  24.             return FALSE;
  25.     }
  26.     return TRUE;    //前面所有条件都满足就允许这个异常处理函数执行
  27. }
复制代码
那么,如果我们想绕过safe SEH来攻击SEH的话,如何绕过呢?首先前两点很好解决,修复SEH的next的指针,然后不把shellcode指向栈上即可。
那么后面的RtlIsValidHandler函数怎么办?
我们从这个函数逻辑分析,看一下什么情况才能允许我们执行SEH处理函数:
  • 异常处理函数位于加载模块内存范围之外,DEP关闭
  • 异常处理函数位于加载模块内存范围之内,相应模块未启用SafeSEH(安全SEH表为空),同时相应模块不是纯IL
  • 异常处理函数位于加载模块范围之内,相应模块启用SafeSEH(安全SEH表不为空),异常处理函数地址包含在安全SEH表中

其中的DEP就是类似于linux中的NX,即堆栈数据段不可执行。
第一种情况还是比较简单的,在模块外的地址空间写shellcode或者找一个跳板跳到shellcode即可。
第二种情况,可以利用未开启safe SEH的模块中找到一条跳转指令跳到shellcode。
第三种情况有两种方式,一是清空SEH表,欺骗系统未开启safeSEH,二是将我们的指令注册到SEH表中(难度比较大)。
除了以上三种方式,有更为简单的攻击手法:
1.不攻击SEH
2.如果SEH异常处理函数指向堆区域,及时安全校验发现SEH已经不可信,仍然会调用其已经被修改的异常处理函数,所以只需要将shellcode搞到堆即可绕过。
SEHOP世界上没有什么事情是套娃解决不了的,如果有,那就再加一层套娃。
针对于SEH攻击,SEHOP(SEH Overwrite Protection)横空出世。
SEHOP主要任务就是来检测SEH链表的完整性,在调用handler之前系统会先遍历链表,看一下最后一个节点是否为系统固定的最终处理函数,如果是,那么皆大欢喜;不是的话,那么不进行异常处理,程序退出。
  1. if (process_flags & 0x40 == 0)  // 如果没有SEH记录则不进行检测
  2. {
  3.     if (record != 0xFFFFFFFF)  // 开始检测
  4.     {
  5.         do
  6.         {
  7.             if (record < stack_bottom || record > stack_top) // SEH 记录必须位于栈中
  8.                 goto corruption;
  9.             if ((char *)record + sizeof(EXCEPTION_REGISTRATION) > stack_top) // SEH 记录结构需完全在栈中
  10.                 goto corruption;
  11.             if ((record & 3) != 0) // SEH记录必须4字节对齐
  12.                 goto corruption;
  13.             handler = record->handler;
  14.             if (handler >= stack_bottom && handler < stack_top) // 异常处理函数地址不能位于栈中
  15.                 goto corruption;
  16.             record = record->next;
  17.         } while (record != 0xFFFFFFFF); // 遍历S.E.H链
  18.     }
  19.     if ((TEB->word_at_offset_0xFCA & 0x200) != 0)
  20.     {
  21.         if (handler != &FinalExceptionHandler) // 核心检测,地球人都知道,不解释了
  22.         goto corruption;
  23.     }
  24. }
复制代码
所以相应的绕过方法就是伪造一个SEH链,修复SEH链完整性。SEH scopetablescopetable指向了一个用于描述函数中所有__try代码块的数组。在SEH4中,scopetable是一个被加密过后的scopetable的地址(xor cookie)
filterfunc指向异常过滤函数(__except中的表达式),handlerfunc指向except代码块。
如果filterdunc是NULL,那么Handlerfunc就指向__finally代码块。
具体有多少个try,体现在trylevel中。
  1. struct _EH4_SCOPETABLE {
  2.         DWORD GSCookieOffset;
  3.         DWORD GSCookieXOROffset;
  4.         DWORD EHCookieOffset;
  5.         DWORD EHCookieXOROffset;
  6.         _EH4_SCOPETABLE_RECORD ScopeRecord[1];
  7. };

  8. struct _EH4_SCOPETABLE_RECORD {
  9.         DWORD EnclosingLevel;
  10.         long (*FilterFunc)();
  11.             union {
  12.             void (*HandlerAddress)();
  13.             void (*FinallyFunc)();
  14.     };
  15. };
复制代码


在函数开始时,回先保存上个函数的ebp,然后将try level、加密后的scope table、sehhandler、seh next、异常指针、esp指针以及gs压栈,gs就是类似于canary(security cookie xor ebp)的东西,。
scopetable加密的方式就是异或一下securitycookie。
针对 __except_handler函数,如果我们伪造一个 scope table,把里面的 FilterFunc或者 FinallyFunc改为 system('cmd')的地址,然后把这个伪造的 scope table通过溢出覆盖掉原 scope table,就能够getshell。
当然由于 栈中存储的 scope table地址是 _EH4_SCOPETABLE_addr ^ _security_cookie得来,所以我们也得知道 __security_cookie的实际值。同时覆盖时,也不可避免覆盖掉 GS Cookie,next SEH 和 except_handler,但也必须保证这三个值的正确性。
参考链接实操推荐:利用SEH机制Exploit_it
https://www.hetianlab.com/expc.d ... =weixin-wemedia#stu


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 11:41 , Processed in 0.014103 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表