第48章 SEH
本文最后更新于:2022年5月27日 下午
第48章 SEH
SEH是Windows操作系统默认的异常处理机制。逆向分析中,SEH除了基本的异常处理功能外,还大量应用于反调试程序。本章将学习SEH相关知识。
48.1 SEH
基本说明
SEH是Windows操作系统提供的异常处理机制,在程序源代码中使用__try、__except、__finally
关键字来具体实现。本章我们将从代码逆向分析角度来介绍SEH,并通过练习示例详细了解SEH的基本工作原理及其在反调试中的具体使用方法。
SEH与C中的try、catch异常处理具有不同结构,请各位不要混淆。从时间上看, 与C的try、catch异常处理相比,微软先创建出了 SEH机制,然后才将它搭载到VC中。所以SEH是一种从属于VC开发工具和Windows操作系统的异常处理机制。
48.2 SEH练习示例#1
先简单介绍练习示例程序seh.exe,该程序故意触发了内存非法访问(Memory Access Violation)异常,然后通过SEH机制来处理该异常。并且使用PEB信息向程序添加简单的反调试代码,使程序在正常运行与调试运行时表现出不同的行为动作。
示例程序seh.exe并没有相应的源代码,其编写过程如下:先使用VC++编写一个空的main()函数,然后选择合适的选项编译,生成一个不执行任何动作的PE文件。使用OllyDbg打开该文件,借助调试器的汇编功能添加汇编代码,最终保存为seh.exe文件。
本示例程序在Windows XP&7(32位)中正常运行。
48.2.1正常运行
seh.exe程序非常简单,双击运行,弹出一个消息框,显示“Hello:)” 字符串,如图48-1所示。
表面上程序正常运行,其实进程内部已经发生了异常,但由于使用SEH机制进行了处理,所以程序运行正常
48.2.2调试运行
使用OllyDbg调试器打开seh.exe示例程序,如图48-2所示。
发生异常导致调试暂停
在OllyDbg中打开seh.exe程序后按F9键运行,发生非法访问异常后暂停调试,如图48-3所示。
401019地址处添加的MOV DWORD PTR DS:[EAX],1
指令用来触发异常,当前EAX寄存器的值为0,所以该指令的实际含义是向内存地址0处写入值1。但试图向未分配的内存地址0处写入某个值时,就会触发内存非法访问异常。
内存地址0虽然属于seh.exe进程的用户内存区域,但由于是未分配的空间,所以无法随意访问。查看OllyDbg的内存映射(View-Memory菜单),可以看到进程中内存地址0被标识为未分配区域(参考图48-4)。
如上所述,访问未分配的内存区域时,就会触发非法访问异常。
那么,为什么被调试进程发生异常时会暂停呢?这只是意味着运行的时候很正常,下面仔细分析原因。
发生异常时调试器运行
在图48-3中查看OllyDbg的状态窗口,可以看到如下警告语句:
“Access violation when writing to [00000000] - use Shift+F7/F8/F9 to pass exception to program”
即在内存0处发生写入异常,若想将异常抛给程序,请使用Shift+F7/F8/F9组合键。
其中,“将异常抛给程序”是什么意思呢?暂且放下诸多疑问,根据调试器给出的提示按Shift+F9键继续运行程序。调试运行开始后弹岀消息对话框,如图48-5所示。
从图48-5中弹出的消息框可以看到,它与程序正常运行时弹岀的对话框是不同的,消息内容为“检测到调试器”(我向程序中插入了一段简单的调试器检测代码)。以上练习示例的目的就在于,观察程序在正常运行与调试运行时表现出的不同行为。其实,程序在这2种形式运行下使用的异常处理方式是不同的(下面会讲解)。以上就是逆向分析中常用的“利用SEH机制的反调试技术”。
接下来详细讲解OS的异常与异常处理机制,还要仔细了解SEH具体的实现方法,以及在调试器中处理异常的方法。
调试运行练习示例时,有时调试器不会像图48-3那样暂停,而会一直正常运行。这是因为设置了OllyDbg的选项,或者安装了某个特定插件。遇到这种情况请参考后面“设置OllyDbg选项”的内容。
48.3 OS的异常处理方法
通过前面的学习我们了解到,同一程序(seh.exe)在正常运行与调试运行时表现岀的行为动
作是不同的。这是由Windows OS异常处理方法的不同造成的。
48.3.1 正常运行时的异常处理方法
进程运行过程中若发生异常,OS会委托进程处理。若进程代码中存在具体的异常处理(如SEH异常处理器)代码,则能顺利处理相关异常,程序继续运行。但如果进程内部没有具体实现SEH,那么相关异常就无法处理,OS就会启动默认的异常处理机制,终止进程运行(参考图48-6)。
48.3.2 调试运行时的异常处理方法
调试运行中发生异常时,处理方法与上面有些不同。若被调试进程内部发生异常,OS会首先把异常拋给调试进程处理。调试器几乎拥有被调试者的所有权限,它不仅可以运行、终止被调试者,还拥有被调试进程的虚拟内存、寄存器的读写权限。需要特别指岀的是,被调试者内部发生的所有异常(错误)都由调试器处理。所以调试过程中发生的所有异常(错误)都要先交由调试器管理(被调试者的SEH依据优先顺序推给调试器)。像这样,被调试者发生异常时,调试器就会暂停运行,必须采取某种措施来处理异常,完成后继续调试。遇到异常时经常采用的几种处理方法如下所示。
(1)直接修改异常:代码、寄存器、内存
被调试者发生异常时,调试器会在发生异常的代码处暂停,此时可以通过调试器直接修改有问题的代码、内存、寄存器等,排除异常后,调试器继续运行程序。
遇到图48-3中的异常时,采用直接修改异常的方法进行如下处理。
- 由于EAX寄存器所指的地址值错误,所以只要把EAX寄存器的值修改为有效的内存地址即可。
- 由于401019地址处的代码触发了异常,使用OllyDbg的汇编(Space)或编辑(Ctrl+E)功能将相关代码修改为NOP指令,运行后也可排除异常。
- 也可以使用OllyDbg的New Origin here(Ctrl+Gray *)功能改变程序的运行路径(因为无法直接修改EIP寄存器,所以需要借助该功能修改)。
请不要随意使用这些修改方法,必须在明确知道程序错误的情形下才能使用。
(2) 将异常抛给被调试者处理
如果被调试者内部存在SEH(异常处理函数)能够处理异常,那么异常通知会发送给被调试者,由被调试者自行处理。这与程序正常运行时的异常处理方式是一样的。前面的seh.exe练习示例中,使用OllyDbg中的Shift+F7/F8/F9命令(StepInto/StepOver/Run)可以直接将当前异常抛还给被调试者。
(3) OS默认的异常处理机制
若调试器与被调试者都无法处理(或故意不处理)当前发生的异常,则OS的默认异常处理机制会处理它,终止被调试进程,同时结束调试。
48.4异常
学习异常处理前,有必要了解操作系统中定义的异常。
1 |
|
以上异常列表中,我们调试时会经常接触5种最具代表性的异常,接下来分别介绍(其他异常请参考MSDN)。
48.4.1 EXCEPTION_ACCESS_VIOLATION(C0000005)
试图访问不存在或不具访问权限的内存区域时,就会发生EXCEPTION_ACCESS_VIOLATION(非法访问异常,该异常最常见)。
MOV DWORD PTR DS:[0], 1
→ 内存地址0处是尚未分配的区域。
ADD DWORD PTR DS:[401000],1
→ .text节区的起始地址401000仅具有“读”权限(无“写”权限)。
XOR DWORD PTR DS:[80000000],1234
→ 内存地址80000000属于内核区域,用户模式下无法访问。
48.4.2 EXCEPTION_BREAKPOINT(80000003)
在运行代码中设置断点后,CPU尝试执行该地址处的指令时,将发生EXCEPTION_BREAKPOINT异常。调试器就是利用该异常实现断点功能的。下面仔细了解实现方法。
INT3
设置断点命令对应的汇编指令为INT3,对应的机器指令(IA-32指令)为0xCC。CPU运行代码的过程中若遇到汇编指令INT3,则会触发EXCEPTION_BREAKPOINT异常。在OllyDbg调试器某个地址处设置好断点后,确认该地址处的指令是否真会变为INT3(0xCC)。在OllyDbg中再次打开seh.exe文件,转到401000地址处(011+0),按F2键设置好断点,如图48-7所示。
从图48-7中可以看到,虽然401000地址处设置了断点,但是该地址处的指令并未变为INT3(汇编指令),也未由 “68” 变为“CC”(机器指令)。为什么跟前面讲的不一样呢?其实,这是OllyDbg耍的一个小花招。由于在OllyDbg中按F2键设置的断点是用户用来调试的临时断点(User Temporary Break Point),所以不需要在调试画面中显示。在代码与内存中将用户设置的临时断点全部显示出来,反而会大大降低代码的可读性,给代码调试带来不便。换言之,实际进程内存中401000地址处的指令 “68” 已经被更改为“CC”,但是为了调试方便,OllyDbg并未将其显示出来。将进程内存转储之后可以看到更改后的CC指令,先使用PE Tools工具转储进程内存,如图48-8所示。以seh_dump.exe文件名保存转储文件后,使用Hex Editor工具打开,查看图48-7中位于401000地址处(文件偏移地址为1000)的指令,如图48-9所示。
seh_dump.exe 的 ImageBase 为400000,所以 VA 401000对应 RVA 1000(RVA=VAImageBase)。由于seh_dump.exe是直接由seh.exe进程内存转储而来的,所以RVA就 是RAW(文件偏移量)。
查看文件偏移1000处,可以看到机器指令CC(INT3指令-Breakpoint)。也就是说,图48-7中进程内存的实际值为0xCC,但是OllyDbg调试器在显示时先将其更改为原来的操作码 “68”,然后再显示出来。
以上就是断点内部工作原理,灵活运用这一原理能为程序调试带来很大便利。比如,使用Hex Editor工具打开PE文件,修改EP地址对应的文件偏移处的第一个字节为CC,然后运行该PE文件就会发生EXCEPTION_BREAKPOINT异常,经过OS的默认异常处理后终止运行。若在系统注册表中将默认调试器设置为OllyDbg,那么发生以上异常时OS会自动运行OllyDbg调试器,附加发生异常的进程(第八部分中将详细讲解利用这一原理调试的方法)。
48.4.3 EXCEPTION_ILLEGAL_INSTRUCTION(C000001D)
CPU遇到无法解析的指令时引发该异常。比如 “0FFF”指令在x86 CPU中未定义,CPU遇到该指令将引发EXCEPTION_ILLEGAL_INSTRUCTION异常。
下面使用OllyDbg调试器进行简单测试。首先使用OllyDbg调试器打开seh.exe,在EP代码地址处直接修改指令为0FFF,然后运行程序将引发EXCEPTION_ILLEGAL_INSTRUCTION异常,调试器暂停运行,如图48-10所示。
48.4.4 EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094)
INTEGER(整数)除法运算中,若分母为0(即被0除),则引发EXCEPTION_INT_DIVIDE_BY_ZERO异常。编写应用程序时偶尔会发生该异常,分母为变量时,该变量在某个瞬间变为0,执行除法运算就会引发EXCEPTION_INT_DIVIDE_BY_ZERO异常。下面进行简单测试,首先在OllyDbg调试器中打开seh.exe,使用汇编指令(Space)在EP代码处修改代码,即图48-11粗线框中的代码,然后运行程序。
401220地址处的DIV ECX指令执行EAX/ECX运算,然后将商保存到EAX寄存器。但由于此时ECX寄存器的值为0,即除法的分母为0,所以引发EXCEPTION_INT_DIVIDE_BY_ZERO异常,调试器暂停运行。
48.4.5 EXCEPTION_SINGLE_STEP(80000004)
Single Step(单步)的含义是执行1条指令,然后暂停。CPU进入单步模式后,每执行一条指令就会引发EXCEPTION_SINGLE_STEP异常,暂停运行。将EFLAGS寄存器的TF(Trap Flag, 陷阱标志)位设置为1后,CPU就会进入单步工作模式。
关于陷阱标志与单步的详细说明请参考第52章。
48.5 SEH详细说明
48.5.1 SEH 链
SEH以链的形式存在。第一个异常处理器中若未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。从技术层面看,SEH是由_EXCEPTION_REGISTRATION_RECORD
结构体组成的链表。
1 |
|
Next成员是指向下一个_EXCEPTION_REGISTRATION_RECORD结构体的指针,Handler成员是异常处理函数(异常处理器)。若Next成员的值为FFFFFFFF,则表示它是链表的最后一个结点。图48-12直观形象地描述了进程SEH链的结构。
图48-12中共存在3个SEH(异常处理器),发生异常时,该异常会按照(A)→(B)→(C)的顺序依次传递,直到有异常处理器处理。
48.5.2异常处理函数的定义
SEH异常处理函数(SEH函数)定义如下:
1 |
|
异常处理函数(异常处理器)接收4个参数输入,返回名为EXCEPTION_DISPOSITION的枚举类型(enum)。该异常处理函数由系统调用,是一个回调函数,系统调用它时会给出代码48-4中的4个参数,这4个参数中保存着与异常相关的信息。首先,第一个参数是指向EXCEPTION_RECORD结构体的指针,EXCEPTION_RECORD结构体的定义如下:
1 |
|
请注意该结构体中ExceptionCode与ExceptionAddress这2个成员,ExceptionCode成员用来指出
异常类型,ExceptionAddress成员表示发生异常的代码地址。代码48-4中异常处理函数的第三个参
数是指向CONTEXT结构体的指针,CONTEXT结构体的定义如下(供IA-32使用):
1 |
|
CONTEXT结构体用来备份CPU寄存器的值,因为多线程环境下需要这样做。每个线程内部都拥有1个CONTEXT结构体。CPU暂时离开当前线程去运行其他线程时,CPU寄存器的值就会保存到当前线程的CONTEXT结构体;CPU再次运行该线程时,会使用保存在CONTEXT结构体的值来覆盖CPU寄存器的值,然后从之前暂停的代码处继续执行。通过这种方式,OS可以在多线程环境下安全运行各线程。
众所周知,多线程的实现基于CPU的时间片切分机制(Time-Slicing)。这种机制下,CPU会用一定时间(时间片)依次运行各线程,时间片极短,使多个线程看上去就像在同时运行一样(根据线程的优先级,各线程在获取CPU控制权的次数上有差异)。
异常发生时,执行异常代码的线程就会中断运行,转而运行SEH(异常处理器/异常处理函数),此时OS会把线程的CONTEXT结构体的指针传递给异常处理函数(异常处理器)的相应参数。代码48-6的结构体成员中有1个Eip成员(偏移量:B8)。在异常处理函数中将参数传递过来的CONTEXT.Eip设置为其他地址,然后返回异常处理函数。这样,之前暂停的线程会执行新设置的EIP地址处的代码(反调试中经常采用这一技术,练习示例seh.exe中也采用了该技术,后面会详细分析)。在代码48-4中可以看到异常处理函数的返回值为EXCEPTION_DISPOSITION枚举类型,下面了解一下该类型。
1 |
|
异常处理器处理异常后会返回ExceptionContinueExecution(0),从发生异常的代码处继续运行。若当前异常处理器无法处理异常,则返回ExceptionContinueSearch(1),将异常派送到SEH链的下一个异常处理器。
我们在后面会调试seh.exe的异常处理函数(异常处理器)来进一步了解其工作原理。
48.5.3 TEB.NtTib.ExceptionList
通过TTB结构体的NtTib成员可以很容易地访问进程的SEH链,方法非常简单。
如图48-13所示,TEB.NtTib.ExceptionList成员是TEB结构体的第一个成员。FS段寄存器指向段内存的起始地址,TEB结构体即位于此,所以通过下列公式可以轻松获取TEB.NtTib.ExceptionListd的地址。
关于TEB结构体的详细说明请参考第46章。
48.5.4 SEH安装方法
在C语言中使用__try、__except、 __finally
关键字就可以很容易地向代码添加SEH。在汇编
语言中添加SEH的方法更加简单,如代码48-8所示。
1 |
|
看代码48-8就容易理解了。“在程序代码中安装SEH”就是指,将自身的异常处理器添加到已有的SEH链。从技术层面讲,就是将自身的EXCEPTION_REGISTRATION_RECORD结构体链接到EXCEPTION_REGISTRATION_RECORD结构体链表。前面出现的seh.exe程序就是采用上述汇编代码添加的SEH,下面再次调试seh.exe程序以进一步了解添加SEH的方法及其工作原理。
48.6 SEH练习示例 #2(seh.exe)
首先使用OllyDbg调试器打开seh.exe程序,运行到401000地址处(此处为seh.exe程序的main()函数)。我编写的全部代码如图48-14所示。
位于401000、401005、40100C地址处的3条指令与“SEH安装方法”中讲的汇编指令是一样的。从图48-14中可以看到,新添加的异常处理器就是位于40105A地址处的异常处理函数。
48.6.1 查看SEH链
继续运行代码到401005地址处,查看FS:[0]的值,其值就是SEH链的起始地址,如图48-15所示。
从代码信息窗口中可以看到,FS:[0]=[7FFDF000]=12FF78,其中12FF78就是SEH链的起始地址(即EXCEPTION_REGISTRATION_RECORD结构体链表的起始地址)。在上图的栈窗口中查看地址12FF78,可以发现第一个EXCEPTION_REGISTRATION_RECORD结构体(Next=12FFC4,Handler=402730)。异常处理器地址402730存在于seh.exe进程的代码节区(该异常处理器是VC++生成PE文件时默认添加到其启动函数的,请各位自行查看位于402730地址处的异常处理器代码)。然后转到12FFC4地址处,查看链表中的第二个EXCEPTION_REGISTRATION_RECORD结构体(参考图48-16)。
从图48-16可以看到,第二个结构体的Next成员值为FFFFFFFF,所以第二个EXCEPTION_REGISTRATION_RECORD结构体也是SEH链表的最后一个结构体。异常处理器地址为7717D74D,它位于ntdll.dll模块的代码区域,是OS的默认异常处理器(创建进程时,OS会自动产生默认的SEH)。
48.6.2 添加SEH
运行401005地址处的PUSH DWORD PTRFS:[0]指令(参考图48-15),查看栈窗口,如图48-17所示。
栈中新创建了_exception_registration_record
结构体。继续执行40100C地址处的MOV DWORD PTR FS:[0],ESP
指令(参考图48-15),查看栈窗口,如图48-18所示。
栈窗口中岀现了新生成的SEH的注释(Next=12FF78, Handler=40105A)。新的异常处理器(40105A)就这样添加到SEH链。
只看代码48-6会感觉很难,实际调试并查看栈就比较容易理解。
OllyDbg调试器中提供了查看SEH链的功能。在OllyDbg主菜单中依次选择View-SEH Chain项目,即可打开SEH链查看窗口。
如图48-19所示,在SEH链窗口中可以看到添加在顶端的异常处理器(40105A)。
48.6.3发生异常
如果执行401019地址处的MOV DWORD PTR DS:[EAX],1
指令(参考图48-14),就会引发EXCEPTION_ACCESS_VIOLATION异常(该异常已做说明,此处不再赘述)。此时程序处在调试之中,根据异常处理的顺序,OS会把控制权交给调试器(异常处理器(40105A)未运行)。在40105A地址处设置断点,然后按Shift+F9组合键,再将异常派送给被调试进程(seh.exe),调试器暂停在设置的断点处(40105A)。
如图48-20所示,被调试者会调用注册在自身SEH链中的异常处理器来处理异常。设置好断点后,接下来即可调试异常处理器。
48.6.4 查看异常处理器参数
调试SEH时,栈中存储的参数(关于参数的说明请参考代码48-4)如图48-21所示。
第一个参数(ESP+4)是指向EXCEPTION_RECORD结构体的指针pReord(12FAC0),查看结构体中的数据,如图48-22所示。
参考图48-22以及代码48-5中关于EXCEPTION_RECORD结构体的定义可知,ExceptionCode(pRecord+0)为 C0000005(EXCEPTION_ACCESS_VIOLATION),发生异常的代码地址ExceptionAddress为401019(对照图48-14可知发生异常的代码地址是准确的)。
第二个参数(ESP+8)是指向EXCEPTION_REGISTRATION_RECORD结构体的指针(pFrame),其值为12FF3C,它是SEH链的起始地址。
第三个参数(ESP+C)是指向CONTEXT结构体的指针pContext(12FADC),查看指针pContext所指的地址空间。
如图48-23所示,CONTEXT是一个非常大的结构体(大部分成员的值为NULL)。其中需要特别注意的是Eip成员,它位于从结构体偏移B8的位置,存储着发生异常的代码地址。
最后一个参数pValue(ESP+10)供系统内部使用,可以忽略。
48.6.5调试异常处理器
40105A地址处的异常处理器(参考图48-20)中存在着调试器检测代码。虽然简单,却是非常具有代表性的反调试代码。下面仔细分析一下。
1 |
|
[ESP+C]是异常处理器第三个参数pContext的值。以上命令用来将pContext地址(12FC58)传送到ESI寄存器。
1 |
|
上述指令用于将FS:[30]的值传送给EAX寄存器,FS:[30]就是PEB结构体的起始地址(7FFDD000,参考图48-24)。
1 |
|
上述指令用于读取[EAX+2]地址中的1个字节值,然后与1比较。由于EAX当前保存着PEB的起始地址,所以[EAX+2]指的是PEB.BeingDebugged成员。
1 |
|
从图48-25可以看到,[EAX+2]=[7FFD7002]=PEB.BeingDebugged的值被设置为1,表示进程处于调试状态。
1 |
|
若上一条CMP命令中的2个比较对象不同,则执行JNZ(Jump if Not Zero)命令跳转。由于PEB.BeingDebugged的值为1,所以不跳转,即不执行该JNZ指令,如图48-26所示
程序非调试运行时,执行此处会跳转到401076地址处。若程序处在调试运行状态,则跳过该JNZ指令,直接执行40106A地址处的指令。
1 |
|
由于当前进程处于调试之中,所以会执行上述指令。当前ESI寄存器中保存着CONTEXT结构体的起始地址(pContext=12FC58)。从图48-26可知,[ESI+B8]=[12FD10]=pContext→Eip(当前值为401019)。
也就是说,上述指令用来将pContext→Eip值更改为401023。异常处理器终止时,发生异常的线程会运行401023地址处的代码。如图48-27所示,401023地址处的代码用来弹岀一个消息框,显示 “Debugger Detected:(” 消息文本。
为了方便调试,在401023地址处设置断点。
1 |
|
由于pContext→Eip值已经发生改变,所以执行流跳转到异常处理器的终止代码处(401080)。
1 |
|
若程序运行在非调试状态下,则执行401068地址处的JNZ指令(参考图48-26),跳转到401076地址处。如上所示,401076地址处的指令用来将pContext→Eip值更改为401039, 401039地址处的代码用来弹出消息对话框,显示“Hello:)” 消息文本(参 考图48-28)。
1 |
|
最后两条指令中先将返回值(EAX)设置为0,然后异常处理器返回。返回值0代表EXCEPTION_CONTINUE_EXECUTION,表示异常得到处理,相关线程可以继续运行(参考代码48-7)。
本练习示例(seh.exe)的目的在于向各位展示使用SEH进行反调试的技术。所以在代码中故意引发了异常,然后在SEH中根据调试与否修改了运行分支。若熟悉了该技术,调试压缩器/保护器类的文件时会非常有帮助。
运行到401082地址处的RETN指令时,控制权被返回至ntdll.dll模块中的代码区域,它属于系统区域,所以在OllyDbg中按F9运行键后,调试会在401023地址处(设置有断点)暂停(参考图48-27)。
使用StepOver(F8)指令使调试运行到401031地址处的CALL指令,弹出一个消息框。按“确定”按钮关闭消息框后,执行401037地址处的JMP SHORT 40104D指令,跳转到删除SEH的代码处(40104D)。
48.6.6 删除 SEH
在程序终止前删除已注册的SEH,如图48-29所示。
调试运行到40104D地址处查看栈,EXCEPTION_REGISTRATION_RECORD结构体存储在其中(12FF3C),该结构体是SEH链中最初运行的异常处理器。40104D处的POP DWORD PTR FS:[0]
指令用来读取栈值(12FF78),并将其放入FS:[0]。FS:[0]是TEB.NtTib.ExceptionList,12FF78就是下一个SEH的起始地址。执行该命令后,前面注册的SEH(12FF3C)被从SEH链中删除。然后
执行401054地址处的ADD ESP,4
指令,将栈中的异常处理器地址(40105A)也删除。请各位反复调试,查清栈中数据变化的情况。
48.7 设置OllyDbg选项
我们已经学习了SEH的工作原理,并通过练习示例了解了利用SEH进行反调试的技术。本章最重要、最关键的内容概括如下:
通过处理使被调试者将自身异常首先发送给调试器。
上述原理作用下,程序在正常运行与调试运行时有不同的分支代码,借助SEH实现的反调试技术非常多,这为代码调试带来诸多不便,使调试更加困难。那么,有没有更方便的调试方法呢?OllyDbg调试器提供了调试选项,调试中的程序发生异常时,调试器不会暂停,会自动将异常派送给被调试者(看上去与正常运行一样)。在OllyDbg的菜单栏中选择Options - Debugging options菜单(快捷键Alt+O),打开Debugging options对话框,如图48-30所示。
然后在Debugging options对话框中选择Exceptions选项卡。
如图48-31所示,Exceptions选项卡包含多个选项,下面逐一介绍。
48.7.1 忽略KERNEL32中发生的内存非法访问异常
复选Ignore memory access violations in KERNEL32选项后,kernel32.dll模块中发生的内存非法访问异常都会被忽略(该选项默认处于选中状态,保持不变即可)。
48.7.2 向被调试者派送异常
Ignore(pass to program)following exceptions选项下存在多个异常复选框,如图48-32所示。
Ignore(pass to program)following exceptions选项共有6个异常选项,前5个已经介绍过了。单击左侧复选框选中后,发生相应异常时OllyDbg调试器就会忽略该异常,并且将其派送给被调试者。
接下来简单介绍All FPU exceptions选项。FPU(Floating Point Unit,浮点运算单元)是专门 用于浮点数运算的处理器,它有一套专用指令,与普通x86指令的形态结构不同。复选All FPU exceptions选项后,处理FPU指令过程发生异常时,调试器会无条件将异常派送给被调试者处理。
48.7.3其他异常处理
Exceptions选项卡中还有一个Ignore also following custom exceptions for ranges选项,如图48-31所示。复选该选项后,用户可以直接添加(或删除)其他各种异常,发生这些异常时,调试器会将它们直接派送给被调试者处理。调试时灵活运用OllyDbg的Exceptions选项,可以在不暂停调试器的前提下自动规避使用SEH实现的反调试“花招”,从而继续调试。
48.7.4简单练习
首先在OllyDbg调试器中打开seh.exe程序,然后在Exceptions选项卡中进行相应设置,如图48-33所示。
如上设置后,程序在调试运行时发生以上6种异常时,调试器会忽略,将它们直接派送给被调试者。所以seh.exe程序中发生的EXECEPTION_ACCESS_VIOLATION异常会由自身的SEH处 理(调试过程不会暂停)。关闭Debugging options对话框后,按F9运行程序,直接弹岀“Debugger detected:消息框。
程序运行过程发生异常时,调试器会将它们派送给被调试者的SEH处理,调试器不会暂停而直接弹出图48-34所示的消息框。通过SEH内部的调试器检测代码(PEB.BeingDebugged)弹出与正常运行时完全不同的消息框(相关解决方法请参考第50章)。
我并未在一开始的时候就介绍OllyDbg调试器的Exceptions选项,而是将它放在本章最后,目的在于先向各位讲解SEH的内部工作原理。不理解内部工作原理,只学习相关工具的使用技巧,就如同在沙滩上建房子一样。SEH用途广泛,若想学好逆向分析技术,必须先掌握SEH的内部工作原理。
48.8小结
SEH大量应用于压缩器、保护器、恶意程序(Malware),用来反调试。大家研究与调试SEH的过程中,会进一步加深对Wiondows OS内部结构的认识,提高自身逆向分析技术水平。关于SEH的讲解先到这里,后面的调试练习中还会遇到。