飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 5169|回复: 2

[x64] 【转载】SEH分析笔记(X64篇)

[复制链接]

该用户从未签到

发表于 2015-1-20 00:59:23 | 显示全部楼层 |阅读模式

标 题: 【原创】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
PYG19周年生日快乐!

该用户从未签到

 楼主| 发表于 2015-1-20 01:11:20 | 显示全部楼层
作者的官方站点,有X86和X64 SEH的分析文章: http://boxcounter.com/categories/all.html
PYG19周年生日快乐!
  • TA的每日心情

    2020-12-13 21:51
  • 签到天数: 14 天

    [LV.3]偶尔看看II

    发表于 2015-1-20 03:54:50 | 显示全部楼层
    感谢分享,学习了。。。
    PYG19周年生日快乐!
    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

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