0x01 向量化异常处理
1.1 向量化异常处理的使用
向量化异常处理的基本理念和SEH相似,也是注册一个回调函数。异常发生时会被系统的异常处理过程调用。可以通过API函数AddVectoredExceptionHandler注册VEH回调函数。原型如下
VEH回调函数也形成一个链表。若FirstHandler的值为0,则回调函数位于链表尾部,若参数为非0值,则至于VEH链表最前端。要将该函数的返回值保存下来用于卸载。
VEH回调函数所在的模块被卸载后,系统不能自动将回调函数从VEH链表上溢出,需要程序在退出的过程中玩完成写在工作。可以使用如下API实现。
该函数的参数就是上一个函数的返回值。
回调函数的参数PEXCEPTION_POINTERS与之前顶层回调函数中使用到的参数是一致的。
1.2 VEH与SEH的异同
当异常发生时,VEH会优先于SEH获取控制权(如果有调试器,调试器会优先于VEH回调函数),系统会自动调用注册的VEH回调函数,如果修复了异常,就返回EXCEPTION_CONTINUE_EXECUTION,在异常发生处以CONTEXT恢复环境,此时SEH过程被跳过,如果回调函数无法处理异常,回调函数返回EXCEPTION_CONTINUE_SEARCH,系统会采取与SEH大致的策略遍历VEH链表。如果遍历完也没处理异常,将控制权交给系统,系统遍历SEH链表上注册的回调函数。
VEH与SEH区别如下:
- 注册机制不同。SEH相关信息保存在栈中,后注册的回调函数位于SEH链前端。VEH是单独的链表,注册VEH的时候可以指定其位于链表的头部还是尾部。
- 优先级不同。VEH优先于SEH被调用。
- 作用范围不同。SEH基于线程(除了顶层异常处理)。VEH在整个进程范围都有效,可以捕获和处理所有线程产生的异常。
- VEH不需要栈展开。SEH涉及栈展开问题,会有两次被调用的机会。VEH实现不依赖栈,只有一次调用机会。
1.3 向量化异常处理的新内容
VCH。它使用下面两个API进行注册和注销。
分析ntdll!RtlDispatchException代码,VCH会在两种情况下被调用。
- SEH无法正常运行的情况下(如相关数据结构被破坏,未通过SafeSEH或SEHOP验证),SEH分发被跳过。
- 当SEH回调函数能够返回ntdll!RtlDispatchException函数时。因为SEH处理程序具有特殊性,在执行SEH回调函数时,不仅由可能跳转到其他位置执行(如跳跳转到try块结束),也有可能不再返回(终结处理),所以只有当SEH回调函数返回ExceptionContinueExecution,或者UEF函数修复了异常,亦或UEF函数因为调试器的存在,其终结处理被跳过,SEH会回调函数才返回ntdll!RtlDispatchException函数,此时才有机会执行VCH回调函数。
总的来说,VCH的调用时机在SEH处理之后,它的返回值是被忽略的。
0x02 x64平台上的异常处理
2.1 原生x64程序的异常分发
x64平台上的异常也有两次分发机会,并且支持VEH和SEH两种异常处理机制。不同的是SEH的相关数据结构变了,存储位置也变了。
在编译阶段,确定所有异常处理的Handler的地址并放在一个只读表中,当异常发生时,异常分发函数会根据异常分发的地址在这个表中查找相应的Handler进行处理。也就是说,这是一种新的基于Table的异常处理机制,所以不会被缓冲区溢出等破坏。这个和x86平台上的SafeSEH类似。
具体实施如下。
编译器提取了所有函数的起始地址,结束地址,函数的“序幕操作”(包括栈操作和寄存器操作),异常处理信息等,生成了两个表。其中一个表可称作函数信息表,包含当前程序中所有函数在内存中的位置信息(除了也函数,即哪些既不调用其他函数,也不包含异常处理的函数)。这些信息被放在单独的区段.pdata中,该区段的位置可以从PE头部的数据目录IMAGE_DIRECOTRY_EXCEPTION_DIRECOTY中找到,数据定义如下。
在该表中,每一条函数信息被称作一个FunctionEntry,所有FunctionEntry都是按照RVA的大小升序排列的,这样便于使用二分法快速查找。
除了函数的起始位置与结束位置,上述结构中还包括UnwindData数据,它也是一个RVA,所指向的数据结构包括函数的“序幕”操作(包括栈操作和寄存器操作)和异常处理信息等。这个表叫做UNWIND_DATA表。
UNWIND_INFO结构包含了函数开头的“序幕”操作,它的成员CountOfCodes指出了UNWIND_CODE结构的个数。紧随其后的是SCOPE_TABLE结构,与x86不同,它没有tryLevel,它记录了每个try块的起始位置和结束位置,还有FilterFunc和HandlerFunc的地址,以及异常函数的Handler函数。
在ScopeTable中,ScopeRecord的排列规则如下:对于并列的try块,按照RVA从小到大的顺序排列;对于嵌套的Try结构,按照由内到外的排列。
异常发生时,ntdll!RtlDispatchException函数根据rip调用ntdll!RtlLookupFunctionTable,查找rip位于哪一个模块的ExceptionTable中,然后调用RtlLookupFunctionEntry查找rip所在的FunctionEntry,并取它的UnWindInfo,从其中获取ExceptionHandler执行,在x64平台上,该函数通常是_C_specific_handler。
2.2 WOW64下的异常分发
WOW64的用户模式DLL列举如下:
- Wow64.dll管理进程和线程的创建,钩住异常分发和Ntoskrnl.exe导出的基本系统调用,实现文件重定向及注册表重定向
- Wow64Cpu.dll为每个WOW64内不允许的线程管理其32位CPU环境,针对32位与64位的CPU模式切换,提供与处理器体系结构相关的支持
- Wow64Win.dll截取Win32k.sys导出的GUI系统调用
- 64位ntdll.dll负责执行真正的系统调用
WOW64通过64位ntdll.dll的KiUserExceptionDipatcher钩住异常分发过程。在64位内核要给一个WOW64的进程分发异常的时候,亦称信息先到64位的KiUserExceptionDipatcher处。在该函数中,WOW64会捕获原生的异常及用户模式下的环境记录,然后将其转换为32位的,再交给32位ntdll的KiUserExceptionDipatcher。
0x03 异常处理程序设计的注意事项
3.1 回调函数的调用约定与寄存器保护
线程异常回调函数的调用约定是_cdecel。在异常处理嵌套的时候,注意对寄存器的保护。
3.2 应该用什么语言编写应用SEH
如果要实现精准控制,汇编最合适,利用高级语言会省力。当如果为了实现特殊的目的,如果反跟踪等,使用汇编语言编写更灵活。
3.3 SEH的实用性及新发展
3.4 VEH和SEH的使用时机
VEH全局,如果局部代码的需要用VEH,就要精准判断。
0x04 异常处理的实际应用
4.1 使用SEH对用户输入进行验证
4.1.1 应用层的输入合法验证
IsBadStringPtrA
4.1.2 驱动层的输入合法验证
在驱动层,可以使用与应用层类似的方式验证缓冲区的合法性,还需要验证来自应用层输入/输出的合法性。常用的APIProbeForRead和ProbeForWrite。当缓冲区不合法时,这两个API调用ExRaiseStatus函数主动抛出异常,所以必须在try块中组合使用。
4.2 SEH在加密与解密中的应用
由于SEH可以方便地操作县城环境的CONTEXT,并且不可以直接看到调用关系,所以可以实现一些隐秘的调用和特殊的功能。典型的功能就是主动制造一场,然后再异常程序中修改CONTEXT,以达到反调试的效果,或者是跳跃性的改变程序的流程,以达到反跟踪的目的。
文章评论