- UID
 - 2198
 
 注册时间2005-6-29
阅读权限255
最后登录1970-1-1
副坛主 
    
 
 
 
该用户从未签到  
 | 
 
 
标 题: 【原创】SEH分析笔记(X64篇) 
作 者: boxcounter 
时 间: 2011-11-04,20:02:08 
链 接: http://bbs.pediy.com/showthread.php?t=142371 
 
以下内容为部分节选,全文请到看雪浏览或下载附件: 
 
SEH分析笔记(x64篇)_v1.0.0.rar
(459.61 KB, 下载次数: 6)
 
 
 
[不介意转载,但请注明出处 www.boxcounter.com 
        附件里有本文的原始稿,一样的内容,更好的高亮和排版。 
        本文的部分代码可能会因为论坛的自动换行变得很乱,需要的朋友手动复制到自己的代码编辑器就可以正常显示了] 
 
        在之前的《SEH分析笔记(X86篇)》中,我借助 wrk1.2 介绍了 x86 下 windows 系统内核中的 SEH 实现。这次我们来看看 x64 位 windows 系统内核中 SEH 的实现。 
        本文需要大家熟悉 x64 位系统的一些特性,比如调用约定、Prolog 和 Epilog。可以通过这几篇文章熟悉一下: 
        Overview of x64 Calling Conventions, MSDN 
        The history of calling conventions, part 5: amd64 , The Old New Thing 
        Everything You Need To Know To Start Programming 64-Bit Windows Systems, Matt Pietrek 
 
        首先回顾一下前一篇文章。 
        在 x86 windows 中,函数通过以下几个步骤来参与 SEH : 
        1. 在自身的栈空间中分配并初始化一个 EXCEPTION_REGISTRATION(_RECORD) 结构体。 
        2. 将该 EXCEPTION_REGISTRATION(_RECORD) 挂入当前线程的异常链表。 
 
        当某函数触发异常时,系统首先会通过调用 KiDispatchException 来给内核调试器一个机会,如果内核调试器没有处理该异常,则该机会被转给 RtlDispatchException,这个函数就开始分发该异常。分发过程为: 
        从当前线程的异常链表头开始遍历,对于每一个 SEH 注册信息(即 EXCEPTION_REGISTRATION(_RECORD)),调用其 Handler。根据 Handler 的返回值做相应的后续处理: 
                1. 返回 ExceptionContinueExecution,表示 Handler 已经修复了异常触发点,从异常触发点继续执行。 
                2. 返回 ExceptionContinueSearch,表示该 Handler 没有处理该异常,继续遍历异常链表。 
                3. Handler 没有修复异常触发点,但是却能处理该异常(某个 __except 过滤代码返回 EXCEPTION_EXECUTE_HANDLER)。这种情况下,处理完该异常后就从异常解决代码(__except 代码块)继续执行,Handler 不会返回。 
        以上是简略的 x86 SEH 流程,其中省略了很多细节,比如展开、错误处理、ExceptionNestedException 和 ExceptionCollidedUnwind 等等。 
 
        之所以在这里重温这个流程,是因为 x64 中 SEH 的流程总体思路也是如此,只是细节上做了一些修改。但这并不表示熟悉 x86 SEH 就能很轻松的掌握 x64 SEH。 
 
        本文分为四个部分:“异常注册”、“异常分发”、“展开、解决”和“ExceptionNestedException 和 ExceptionCollidedUnwind”。依然以 MSC 的增强版为分析对象。分析环境为:WDK 7600.16385.1,内置的 cl 的版本是15.00.30729.207,link 的版本是9.00.30729.207,测试虚拟机系统为 amd64 WinXP + wrk1.2。 
        
        在讲述之前,需要先定义几个名词,以简化后续的讲述。 
 
        RVA —— 熟悉 PE 格式的朋友都懂的,表示某个绝对地址相对于所在模块的基地址的偏移。 
        EXCEPT_POINT —— 异常触发点。 
        EXCEPT_FILTER —— __except 小括号内的异常过滤代码。 
        EXCEPT_HANDLER —— __except 大括号内的异常解决代码。 
        FINALLY_HANDLER —— __finally 大括号内的代码。 
 
        以下面的伪码为例, 
 
        1  __try 
        2  { 
        3      __try 
        4      { 
        5           *((ULONG*)NULL) = 0; 
        6      } 
        7      __except((STATUS_INVALID_PARAMETER == GetExceptionCode()) ? EXCEPTION_CONTINUE_SEARCH : EXCEPTION_EXECUTE_HANDLER) 
        8      { 
        9          ... 
        10     } 
        11 } 
        12 __finally 
        13 { 
        14     ... 
        15 { 
 
        EXCEPT_POINT 指的是行5中的代码。 
        EXCEPT_FILTER 指的是行7中的“(STATUS_INVALID_PARAMETER == GetExceptionCode()) ? EXCEPTION_CONTINUE_SEARCH : EXCEPTION_EXECUTE_HANDLER”。 
        EXCEPT_HANDLER 指的是行8到行10中所有的代码。 
        FINALLY_HANDLER 指的是行13到行15中所有的代码。 
 
 
        一、异常注册 
 
        在 x64 windows 中,异常注册信息发生了巨大的改变。x86 中异常注册信息是在函数执行过程中在栈中分配并初始化的。x64 中变成这样: 
        异常注册信息不再是动态创建,而是编译过程中生成,链接时写入 PE+ 头中的 ExceptionDirectory(参考 winnt.h 中 IMAGE_RUNTIME_FUNCTION_ENTRY 的定义)。ExceptionDirectory 里包含几乎所有函数的栈操作、异常处理等信息。 
 
        来看看新异常注册信息的数据结构: 
 
        typedef struct _RUNTIME_FUNCTION { 
            ULONG BeginAddress; 
            ULONG EndAddress; 
            ULONG UnwindData; 
        } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION; 
 
        typedef enum _UNWIND_OP_CODES { 
            UWOP_PUSH_NONVOL = 0, 
            UWOP_ALLOC_LARGE,       // 1 
            UWOP_ALLOC_SMALL,       // 2 
            UWOP_SET_FPREG,         // 3 
            UWOP_SAVE_NONVOL,       // 4 
            UWOP_SAVE_NONVOL_FAR,   // 5 
            UWOP_SPARE_CODE1,       // 6 
            UWOP_SPARE_CODE2,       // 7 
            UWOP_SAVE_XMM128,       // 8 
            UWOP_SAVE_XMM128_FAR,   // 9 
            UWOP_PUSH_MACHFRAME     // 10 
        } UNWIND_OP_CODES, *PUNWIND_OP_CODES; 
 
        typedef union _UNWIND_CODE { 
            struct { 
                UCHAR CodeOffset; 
                UCHAR UnwindOp : 4; 
                UCHAR OpInfo : 4; 
            }; 
        
            USHORT FrameOffset; 
        } UNWIND_CODE, *PUNWIND_CODE; 
        
        #define UNW_FLAG_NHANDLER 0x0 
        #define UNW_FLAG_EHANDLER 0x1 
        #define UNW_FLAG_UHANDLER 0x2 
        #define UNW_FLAG_CHAININFO 0x4 
 
        typedef struct _UNWIND_INFO { 
            UCHAR Version : 3; 
            UCHAR Flags : 5; 
            UCHAR SizeOfProlog; 
            UCHAR CountOfCodes; 
            UCHAR FrameRegister : 4; 
            UCHAR FrameOffset : 4; 
            UNWIND_CODE UnwindCode[1]; 
        
        // 
        // The unwind codes are followed by an optional DWORD aligned field that 
        // contains the exception handler address or a function table entry if 
        // chained unwind information is specified. If an exception handler address 
        // is specified, then it is followed by the language specified exception 
        // handler data. 
        // 
        //  union { 
        //      struct { 
        //          ULONG ExceptionHandler; 
        //          ULONG ExceptionData[]; 
        //      }; 
        // 
        //      RUNTIME_FUNCTION FunctionEntry; 
        //  }; 
        // 
        
        } UNWIND_INFO, *PUNWIND_INFO; 
 
        typedef struct _SCOPE_TABLE { 
            ULONG Count; 
            struct 
            { 
                ULONG BeginAddress; 
                ULONG EndAddress; 
                ULONG HandlerAddress; 
                ULONG JumpTarget; 
            } ScopeRecord[1]; 
        } SCOPE_TABLE, *PSCOPE_TABLE; 
 
        x64 中,MSC 为几乎所有的函数都登记了完备的信息,用来在展开过程中完整的回滚函数所做的栈、寄存器操作。登记的信息包括: 
        函数是否使用了 SEH、 
        函数使用的是什么组合的 SEH(__try/__except?__try/__finally?)、 
        函数申请了多少栈空间、 
        函数保存了哪些寄存器、 
        函数是否建立了栈帧, 
        等等, 
        同时也记录了这些操作的顺序(以保证回滚的时候不会乱套)。 
 
        这些信息就存储在 UNWIND_INFO 之中。 
        UNWIND_INFO 相当于 x86 下的 EXCEPTION_REGISTRATION。它的成员分别是: 
                Version —— 结构体的版本。 
                Flags —— 标志位,可以有这么几种取值: 
                        UNW_FLAG_NHANDLER (0x0): 表示既没有 EXCEPT_FILTER 也没有 EXCEPT_HANDLER。 
                        UNW_FLAG_EHANDLER (0x1): 表示该函数有 EXCEPT_FILTER & EXCEPT_HANDLER。 
                        UNW_FLAG_UHANDLER (0x2): 表示该函数有 FINALLY_HANDLER。 
                        UNW_FLAG_CHAININFO (0x4): 表示该函数有多个 UNWIND_INFO,它们串接在一起(所谓的 chain)。 
                SizeOfProlog —— 表示该函数的 Prolog 指令的大小,单位是 byte。 
                CountOfCodes —— 表示当前 UNWIND_INFO 包含多少个 UNWIND_CODE 结构。 
                FrameRegister —— 如果函数建立了栈帧,它表示栈帧的索引(相对于 CONTEXT::RAX 的偏移,详情参考 RtlVirtualUnwind 源码)。否则该成员的值为0。 
                FrameOffset —— 表示 FrameRegister 距离函数最初栈顶(刚进入函数,还没有执行任何指令时的栈顶)的偏移,单位也是 byte。 
                UnwindCode —— 是一个 UNWIND_CODE 类型的数组。元素数量由 CountOfCodes 决定。 
        需要说明几点: 
                1. 如果 Flags 设置了 UNW_FLAG_EHANDLER 或 UNW_FLAG_UHANDLER,那么在最后一个 UNWIND_CODE 之后存放着 ExceptionHandler(相当于 x86 EXCEPTION_REGISTRATION::handler)和 ExceptionData(相当于 x86 EXCEPTION_REGISTRATION::scopetable)。 
                2. UnwindCode 数组详细记录了函数修改栈、保存非易失性寄存器的指令。 
                3. MSDN 中有 UNWIND_INFO 和 UNWIND_CODE 的详细说明,推荐阅读。 
 
        那 UNWIND_INFO 是如何与其描述的函数关联起来的呢?答案是:通过一个 RUNTIME_FUNCTION 结构体。 
        RUNTIME_FUNCTION::BeginAddress 同 RUNTIME_FUNCTION::EndAddress 一起以 RVA 形式描述了函数的范围。 
        RUNTIME_FUNCTION::UnwindData 就是 UNWIND_INFO 了,它也是一个 RVA 值。 
 
        PE+ 中的 ExceptionDirectory 中存放着所有函数的 RUNTIME_FUNCTION,按 RUNTIME_FUNCTION::BeginAddress 升序排列。一旦触发异常,系统可以通过 EXCEPT_POINT 的 RVA 在 ExceptionDirectory 中二分查找到 RUNTIME_FUNCTION,进而找到 UNWIND_INFO。 
        
        前面有提到,MSC 为几乎所有的函数都登记了完毕的信息,那是不是有一些特殊函数没有登记信息呢? 
        是的。x64 新增了一个概念,叫做“叶函数”。熟悉数据结构的朋友可能第一时间就联想到“叶节点”。没错,“叶函数”的含义跟“叶节点”很类似,叶函数不会有子函数,也就是说它不会再调用任何函数。另外 x64 对这个概念额外加了一些要求:不修改栈指针(比如分配栈空间)、没有使用 SEH。总结下来就是:既不调用函数、又没有修改栈指针,也没有使用 SEH 的函数就叫做“叶函数”。 
        叶函数可以没有登记信息,原因很简单,它根本就没信息需要登记~ 
 
 
====================================================== 
 
 
关于unwind info 引用一下www.boxcounter.com 的解释: 
 
       x64 中,MSC 为几乎所有的函数都登记了完备的信息,用来在展开过程中完整的回滚函数所做的栈、寄存器操作。登记的信息包括: 
       函数是否使用了 SEH、 
       函数使用的是什么组合的 SEH(__try/__except?__try/__finally?)、 
       函数申请了多少栈空间、 
       函数保存了哪些寄存器、 
       函数是否建立了栈帧, 
       等等, 
       同时也记录了这些操作的顺序(以保证回滚的时候不会乱套)。 
 
 
       这些信息就存储在 UNWIND_INFO 之中。 
       UNWIND_INFO 相当于 x86 下的 EXCEPTION_REGISTRATION。它的成员分别是: 
               Version —— 结构体的版本。 
               Flags —— 标志位,可以有这么几种取值: 
                       UNW_FLAG_NHANDLER (0x0): 表示既没有 EXCEPT_FILTER 也没有 EXCEPT_HANDLER。 
                       UNW_FLAG_EHANDLER (0x1): 表示该函数有 EXCEPT_FILTER & EXCEPT_HANDLER。 
                       UNW_FLAG_UHANDLER (0x2): 表示该函数有 FINALLY_HANDLER。 
                       UNW_FLAG_CHAININFO (0x4): 表示该函数有多个 UNWIND_INFO,它们串接在一起(所谓的 chain)。 
               SizeOfProlog —— 表示该函数的 Prolog 指令的大小,单位是 byte。 
               CountOfCodes —— 表示当前 UNWIND_INFO 包含多少个 UNWIND_CODE 结构。 
               FrameRegister —— 如果函数建立了栈帧,它表示栈帧的索引(相对于 CONTEXT::RAX 的偏移,详情参考 RtlVirtualUnwind 源码)。否则该成员的值为0。 
               FrameOffset —— 表示 FrameRegister 距离函数最初栈顶(刚进入函数,还没有执行任何指令时的栈顶)的偏移,单位也是 byte。 
               UnwindCode —— 是一个 UNWIND_CODE 类型的数组。元素数量由 CountOfCodes 决定。 
       需要说明几点: 
               1. 如果 Flags 设置了 UNW_FLAG_EHANDLER 或 UNW_FLAG_UHANDLER,那么在最后一个 UNWIND_CODE 之后存放着 ExceptionHandler(相当于 x86 EXCEPTION_REGISTRATION::handler)和 ExceptionData(相当于 x86 EXCEPTION_REGISTRATION::scopetable)。 
               2. UnwindCode 数组详细记录了函数修改栈、保存非易失性寄存器的指令。 
               3. MSDN 中有 UNWIND_INFO 和 UNWIND_CODE 的详细说明,推荐阅读。 
 
 
       那 UNWIND_INFO 是如何与其描述的函数关联起来的呢?答案是:通过一个 RUNTIME_FUNCTION 结构体。 
       RUNTIME_FUNCTION::BeginAddress 同 RUNTIME_FUNCTION::EndAddress 一起以 RVA 形式描述了函数的范围。 
       RUNTIME_FUNCTION::UnwindData 就是 UNWIND_INFO 了,它也是一个 RVA 值。 
 
 
       PE+ 中的 ExceptionDirectory 中存放着所有函数的 RUNTIME_FUNCTION,按 RUNTIME_FUNCTION::BeginAddress 升序排列。一旦触发异常,系统可以通过 EXCEPT_POINT 的 RVA 在 ExceptionDirectory 中二分查找到 RUNTIME_FUNCTION,进而找到 UNWIND_INFO。 
        
       前面有提到,MSC 为几乎所有的函数都登记了完毕的信息,那是不是有一些特殊函数没有登记信息呢? 
       是的。x64 新增了一个概念,叫做“叶函数”。熟悉数据结构的朋友可能第一时间就联想到“叶节点”。没错,“叶函数”的含义跟“叶节点”很类似,叶函数不会有子函数,也就是说它不会再调用任何函数。另外 x64 对这个概念额外加了一些要求:不修改栈指针(比如分配栈空间)、没有使用 SEH。总结下来就是:既不调用函数、又没有修改栈指针,也没有使用 SEH 的函数就叫做“叶函数”。 
       叶函数可以没有登记信息,原因很简单,它根本就没信息需要登记~ 
 
 
       还有一个 SCOPE_TABLE 结构,熟悉 x86 SEH 的朋友应该很眼熟 :-),它等同于 x86 SEH 中的 REGISTRATIOIN_RECORD::scopetable 的类型。其成员有: 
               Count —— 表示 ScopeRecord 数组的大小。 
               ScopeRecord —— 等同于 x86 中的 scopetable_entry 成员。其中, 
                       BeginAddress 和 EndAddress 表示某个 __try 保护域的范围。 
                       HandlerAddress 和 JumpTarget 表示 EXCEPTION_FILTER、EXCEPT_HANDLER 和 FINALLY_HANDLER。具体对应情况为: 
                               对于 __try/__except 组合,HandlerAddress 代表 EXCEPT_FILTER,JumpTarget 代表 EXCEPT_HANDLER。 
                               对于 __try/__finally 组合,HandlerAddress 代表 FINALLY_HANDLER,JumpTarget 等于 0。 
                       这四个域通常都是 RVA,但当 EXCEPT_FILTER 简单地返回或等于 EXCEPTION_EXECUTE_HANDLER 时,HandlerAddress 可能直接等于 EXCEPTION_EXECUTE_HANDLER,而不再是一个 RVA。 
 
另外x64多了RtlVirtualUnwind可以虚拟执行完当前执行完当前抛出异常的函数,很高级。 
 
 
参考: 
Structured Exception Handling http://bbs.pediy.com/showthread.php?threadid=14042 
Moving to Windows x64 http://bbs.pediy.com/showthread.php?t=145198 
SEH分析笔记(X64篇)  http://www.boxcounter.com/showthread.php?tid=74 
msdn http://msdn.microsoft.com/zh-cn/library/ft9x1kdx 
《软件调试》,WRK 
 |   
 
 
 
 |