windows x86 SEH学习
前言
参考师傅博客:
https://www.yunzh1jun.com/2022/05/27/WindowsSEH/
http://s0rry.cn/archives/yi-chang-chu-li-yuan-li-ji-zhan-zhan-kai-xiang-guan-cao-zuo
参考其他文章:
参考视频链接:
这是youtube上的,b站有人搬运了
https://www.bilibili.com/video/BV1UU4y1K7et?spm_id_from=333.337.search-card.all.click
这两个视频是一样的,但是如果有条件最好还是看youtube上的,也有中文翻译。
这个视频非常推荐,建议仔细观看!
一些需要扫盲的知识
读者们可能在系统学习SEH之前了解过相关名词,什么异常处理,什么SEH链之类的,这里讲一下我在学习过程中感到疑惑的知识。
SEH是Windows操作系统提供的一种异常处理方式,也就是说,它跟语言的选择没有关系,你当然可以使用C语言,也可以使用其他语言,甚至还可以使用汇编语言! 但是,要使用SEH必要的条件是你要在Windows操作系统上使用!这意味着你不能在linux下使用。
我们这里讲解的是windows x86平台级别的SEH异常处理,关键词是
__try / __except,要注意与try/catch的区别。
下面是二者的区别(来自chatgpt)
|
在这里我们主要关注TIB的部分。
TIB(线程信息块)的部分定义:
typedef struct _NT_TIB{ |
上面的部分是每个程序都有的,具体情况在这里不多作阐释,只解释和本文相关的知识。
其中我们要注意TIB中的第一个成员,这个成员是一个链表结构,这就是我们所说的SEH链了。
当程序发生异常时,会通过一定的方法在这个SEH链中寻找异常的解决办法,并沿着这个链一直找,没错,SEH链中存储着异常的解决办法。
接下来,我们就讲一下这个结构体的相关知识:
typedef struct _EXCEPTION_REGISTRATION_RECORD { |
这个结构体就是一个简单的链表,Handler成员就是具体的异常处理函数,Next成员指向下一个_EXCEPTION_REGISTRATION_RECORD.
然而这个版本的SEH链太简单了,这么简单肯定不符合我们的认知。(太简单的版本肯定就不安全)
于是编译器就提供了更复杂的版本,当然,在系统级别还是上面这个简单的版本,编译器提供的只是在这个简单版本的扩充。
struct C_EXCEPTION_REGISTARATION_RECORD |
可以看到第三个成员就是原先的版本。
这里千万要注意,这个复杂版本的结构体是编译器级别的!
可以理解为,编译器作为一个程序员,它自己定义了一个结构体,操作系统是不会知道这个结构体的存在的!它只认得它原来的简单链表!
0x02: 异常处理函数注册
OK,现在我们已经知道,操作系统提供了一个简单链表,我们可以通过在这个简单链表上添加自己的异常处理函数,从而实现我们想要的异常处理。
这里以汇编语言来解释如何注册异常处理函数。
PUSH @MyHandler ; 要添加的异常处理器 |
其中FS[0]就是SEH链的头节点。
从这里也可以看到,x86平台上是以栈来存储这个链表的,注册异常处理函数的时候,只需要压入我们异常处理函数,再压入原先的SEH头结点,再把当前的节点设置为新的头结点。
很好,这很符合我们对链表的认知。
当然,在高级语言中也可以使用这样的注册方法。
但是,在VS的C语言中,我们还可以使用另一种更方便的方法来进行异常处理函数的注册。
__try { |
在这里,要注意,我们要更换我们的思维了,现在我们不必使用原先的简陋的SEH链表,我们可以使用编译器提供的更高级的链表。
这里再提一嘴 这里是编译器级别的!,但是底层系统级别的仍然是简陋的SEH链表。
以下内容引用自云之君师傅的博客。
__try |
OK ,现在我们也已经简单了解了这几个关键词的使用。
接下来又是一个非常关键的内容:
不要认为__try/__except关键词使用的异常处理机制与我们之前讲的SEH机制不同!,在编译器层次中,会将关键词中的异常处理转化成SEH节点并插入SEH链表!
这里就要涉及到新的结构体:
struct C_EXCEPTION_REGISTARATION_RECORD |
其中每个成员都很重要。
其中ScopeTable也是个结构体,这个结构体中包含了我们之前讲解关键词中给出的过滤器函数和异常处理函数。
struct SCOPETABLE_ENTRY |
OK,这里可能有的同学就会有问题了,既然在scopetable中有了异常处理函数,那么原来的EXCEPTION_REGISTRATION_RECORD中的异常处理函数是干嘛的?
这个问题非常好,因为这也正是我学习的过程中产生疑问的地方。
具体来说,EXCEPTION_REGISTRATION_RECORD中的异常处理函数会调用scopetable中的异常处理函数。
#0x03:异常处理机制的实现过程
发生异常时,我们的异常处理机制是如何工作的?
这里涉及到栈展开等一系列用文字很难表达清楚的事情。
即使看了这么多大佬的博客,我也没有看到一篇能更通俗易懂的讲解这个过程的文章。
所以,我也没有自信能够在很短的文字内就可以将这个过程讲解给你。
因此,这里我推荐观看视频讲解:
简单来说,我们的异常处理函数是在函数头的位置注册的,当发生异常时,EXCEPTION_REGISTRATION_RECORD中的异常处理函数会一层一层调用scopetable中的函数,直到找到一个能处理这个异常的函数。找到这个能够处理这个异常的异常处理函数肯定需要一个标志,标志是什么呢?
在于Handler的返回值。
Handler的返回值是一个枚举类型_EXCEPTION_DISPOSITION。
typedef enum _EXCEPTION_DISPOSITION |
这里再讲解一个我在学习的时候遇到的困惑点:
_EXCEPTION_DISPOSITION和 PEXCEPTION_ROUTINE。
前者是一个枚举类型,是异常处理函数的返回值,后者是函数指针类型,是一个指向异常处理函数的指针。
OK,这里,还有一个点:这个返回值和前面提到的过滤器的返回值不一样!!!!
找到这个异常处理函数还涉及一个栈展开,详细讲解请移步视频。
相信看完视频,读者就已经能够理解大部分内容了。
0x04: 通过异常处理机制实现反调试
通过上面的学习,我们已经大致了解了异常处理机制的工作原理。
那么很容易就可以知道,在异常处理函数或者过滤器函数中我们可以进行一些猥琐的操作。
这里就要详细讲解一下过滤器函数和异常处理函数。
过滤器函数中的参数不仅仅有GetExceptionCode(),还有其他的可能。
GetExceptionCode |
上面的内容来自云之君师傅的博客。
其中 EXCEPTION_POINTERS 定义如下:
typedef struct _EXCEPTION_POINTERS { |
ExceptionRecord定义如下:
typedef struct _EXCEPTION_RECORD { |
Context:
typedef struct _CONTEXT { |
也就是说,在过滤器函数中是可以获得包含异常代码、异常地址、异常发生时的上下文信息等等。
因此在过滤器函数中可以对context进行更改,并返回EXCEPTION_CONTINUE_EXECUTION
这样就可以实现反调试功能了。
而在except块中的内容,也就是异常处理函数,实际上并不是函数,只是一段代码而已,也就是说它是无法F5的。
还记得那个简陋SEH链表中的异常处理函数吗?
这个函数才是一个真正的函数。
定义如下:
EXCEPTION_DISPOSITION __cdecl _except_handler |
其中的前三个成员我们都已经在前面介绍过了。
很显然,在这个函数中也可以进行猥琐操作,但是如果要在这个函数中进行猥琐操作的话,就不能使用 __try/____except关键词了,而应该使用最朴素的链表来进行操作。





