SEH是Windows操作系统的异常处理机制,在程序源代码中使用__try、__except、__finally关键字来具体实现。
SEH练习示例 #1
示例程序seh.exe,该程序故意触发了内存非法访问异常,然后通过SEH机制来处理该异常,并且使用PEB信息向程序添加简单的反调试代码,使程序在正常运行与调试运行时表现出不同的行为动作。正常运行。 使用OllyDbg调试器打开seh.exe示例程序。
打开seh.exe程序后按F9键运行,发生非法访问异常后暂停调试。 401019地址处添加的MOV DWORD PTR DS:[EAX],1指令用来触发异常,当前EAX寄存器的值为0,所以该指令的实际含义是向内存地址0处写入值1,但是试图向未分配的内存地址0写入某个值时,就会触发非法访问异常。查看OllyDbg的状态窗口: 内存0处发生写入异常,若想将异常抛给程序,请使用Shift + F7/F8/F9组合键。根据调试器给出的提出按shift + F9键继续运行程序。 它与正常运行时弹出的对话框是不同的,消息内容为检测到调试器。其实,程序在这2种形式下使用的异常处理方式是不同的。以上就是逆向分析种常用的利用SEH机制的反调试技术。
OS的异常处理方法
同一程序正常运行与调试运行时表现出的行为动作是不同的,这是由Windos OS异常处理方法的不同造成的。正常运行时的异常处理方法。
进程运行过程中若发生异常,OS会委托进程处理,若进程代码存在具体的异常处理代码,则能顺利处理相关异常,程序继续运行,但如果进程内部没有具体实现SEH,那么相关异常就无法处理,OS就会启动默认的异常处理机制,终止进程运行。
调试运行时的异常处理方法
若被调试进程内部发生异常,OS会首先把异常抛出给调试进程处理,调试器几乎拥有被调试者的所有权限,它不仅可用运行、终止被调试者,还拥有被调试进程的虚拟内存、寄存器的读写权限。被调试者内部发生的所有异常都由调试器处理,所以调试过程中发生的所有异常都要先交由调试器管理(被调试者的SEH依据有限顺序推给调试器)。遇到异常时经常采用的几种处理方法如下所示: (1) 直接修改异常:代码、寄存器、内存。 => 被调试者发生异常时,调试器会在发生异常的代码处暂停,此时可用通过调试器直接修改有问题的代码、内存、寄存器等,排除异常后,调试器继续运行程序。 (2) 将异常抛给被调试者处理。=> 如果被调试者内部存在SEH(异常处理函数)能够处理异常,那么异常通知会发送给被调试者,由被调试者自行处理。=>前面的seh.exe练习使用的OllyDbg中的shift + F7/F8/F9命令可用直接将当前异常抛还给被调试者。 (3) OS默认的异常处理机制。=> 若调试器与被调试者都无法处理当前发生的异常,则OS的默认异常处理机制会处理它,终止被调试进程,同时结束调试。
异常
操作系统定义的异常。 5种最具有代表性的异常。
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:
[8000000
],1234
=> 内存地址80000000属于内核区域,用户模式下无法访问
EXCEPTION_BREAKPOINT(80000003) (1)在运行代码种设置断点后,CPU尝试执行该地址处的指令时,将发生EXCEPTION_BREAKPOINT异常,调试器就是利用该异常实现断点功能的。 (2)设置断点命令对应的汇编指令为INT3,对应的机制指令为0xCC,CPU运行代码的过程中若遇到汇编指令INT3,则会触发EXCEPTION_BREAKPOINT异常。 (3)在OllyDbg种再次打开seh.exe文件,转到401000地址处,按F2键设置好断点。 (4)在OllyDbg并未将用户设置的断点显示出来,因为这会降低代码的可读性,我们先使用PE Tools工具转储进程内存。 (5)查看文件偏移1000处,可用看到机器指令CC,也就是说,进程内存的实际值为0xCC,但是OllyDbg调试器在显示时先将其更改为原来的操作码“68”,然后再显示出来。EXCEPTION_ILLEGAL_INSTRUCTION(C000001D) (1)CPU遇到无法解析的指令时引发该异常,比如"0FFF"指令再x86 CPU种未定义,CPU遇到该指令将引发EXCEPTION_ILLEGAL_INSTRUCTION异常。 (2)使用OllyDbg调试器打开seh.exe,再EP代码地址处直接修改指令为0FFF,然后运行程序将引发EXCEPTION_ILLEGAL_INSTRUCTION异常,调试器暂停运行。 EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094) (1)INTEGER(整数)除法运算中,若分母为0(即被0除),则引发EXCEPTION_INT_DIVIDE_BY_ZERO异常。 (2)首先使用OllyDbg调试器打开seh.exe,使用汇编指令在EP代码处修改代码。 (3)401220地址处的DIV ECX指令执行EAX/ECX运算,然后将商保存到EAX寄存器,但由于此时ECX寄存器的值为0,即除法的分母为0,所以引发EXCEPTION_INT_DIVIDE_BY_ZERO异常,调试器暂停运行。EXCEPTION_SINGLE_STEP(80000004) (1)Single Step(单步)的含义时执行1条指令,然后暂停,CPU进入单步模式后,每执行一条指令就会引发EXCEPTION_SINGLE_STEP异常,暂停运行。 (2)将EFLAGS寄存器的TF位设置为1后,CPU就会进入单步工作模式。
SEH详细说明
SEH以链的形式存在,第一个异常处理器中若未处理相关异常,它就会被传递到下一个异常处理器。SEH是由_EXCEPTION_REGISTRATION_RECORD结构体组成的链表。
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
PEXCEPTION_REGISTRATION_RECORD Next
;
PEXCEPTION_DISPOSITION Handler
;
}EXCEPTION_REGISTRATION_RECORD
,*PEXCEPTION_REGISTRATION_RECORD
;
Next成员是指向下一个_EXCEPTION_REGISTRATION_RECORD结构体的指针,Handler成员是异常处理函数,若Next成员的值为FFFFFFFF,则表示它是链表的最后一个结点。 图中共存在3个SEH,发生异常时,该异常会按照(A)-> (B)-> ©的顺序依次传递,知道有异常处理器处理。
异常处理函数的定义
SEH异常处理函数定义如下:
EXCEPTION_DISPOSITION
_except_handler(
EXCEPTION_RECORD
*pRecord
,
EXCEPTION_REGISTRATION_RECORD
*pFrame
,
CONTEXT
*pContext
,
PVOID pValue
);
异常处理函数(异常处理器)接收4个参数输入,返回名为EXCEPTION_DISPOSITION的枚举类型,该异常处理函数由系统调用,是一个回调函数,系统调用它时会给该函数传递4个参数的值。第一个参数是执行EXCEPTION_RECORD结构体的指针,其中ExceptionCode与ExceptionAddress分别用来指出异常的类型以及发生异常的代码地址。
typedef struct _EXCEPTION_RECORD
{
DWORD ExceptionCode
;
DWORD ExceptionFlags
;
struct _EXCEPTION_RECORD
*ExceptionRecord
;
PVOID ExceptionAddress
;
DWORD NumberParameters
;
ULONG_PTR ExceptionInformation
[EXCEPTION_MAXIMUM_PARAMETERS
];
} EXCEPTION_RECORD
, *PEXCEPTION_RECORD
;
异常处理函数的第三个参数指向CONTEXT结构体的指针,CONTEXT结构体的定义如下:
typedef struct _CONTEXT
{
DWORD ContextFlags
DWORD Dr0
DWORD Dr1
DWORD Dr2
DWORD Dr3
DWORD Dr6
DWORD Dr7
FLOATING_SAVE_AREA FloatSave
;
DWORD SegGs
DWORD SegFs
DWORD SegEs
DWORD SegDs
DWORD Edi
DWORD Esi
DWORD Ebx
DWORD Edx
DWORD Ecx
DWORD Eax
DWORD Ebp
DWORD Eip
DWORD SegCs
DWORD EFlag
DWORD Esp
DWORD SegSs
BYTE ExtendedRegisters
[MAXIMUM_SUPPORTED_EXTENSION
];
} CONTEXT
;
CONTEXT结构体用来备份CPU寄存器的值,因为多线程环境下需要这样做,每个线程内部都拥有1个CONTEXT结构体,CPU暂停离开当前线程区运行其他线程时,CPU寄存器的值就会保存到当前线程的CONTEXT结构体中;CPU再次运行该线程时,会使用保存在CONTEXT结构体的值来覆盖当前CPU寄存器的值,然后从之前暂停的代码处继续执行,通过这种方法,OS可用在多线程环境下安全运行各线程。异常发生时,执行异常代码的线程就会中断运行,转而运行SEH(异常处理器),此时OS会把线程的CONTEXT结构体的指针传递给异常处理函数的相应参数。上述结构体成员中有一个Eip成员,在异常处理函数中将参数传递过来的CONTEXT.Eip设置为其他地址,然后返回异常处理函数,之前暂停的线程就会执行新设置的EIP地址处的代码。异常处理函数的返回值为EXCEPTION_DISPOSITION枚举类型:
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution
= 0,
ExceptionContinueSearch
= 1,
ExceptionNestedException
= 2,
ExceptionCollidedUnwind
= 3
} EXCEPTION_DISPOSITION
;
异常处理器处理异常后会返回ExceptionContinueExecution(0),从发生异常的代码处继续运行,若当前异常处理器无法处理异常,则返回ExceptionContinueSearch(1),将异常派送到SEH链的下一个异常处理器。
TEB.NtTib.ExceptionList
通过TEB结构体的NtTib成员可用很容易地访问进程的SEH链,方法非常简单。
TEB
.NtTib
.ExceptionList
= FS
:[0]
SEH安装方法
在C语言中使用__try、__except、__finally关键字就可用很容易地向代码添加SEH,在汇编语言中添加SEH的方法如下:
PUSH @MyHandler
; 异常处理器
PUSH DWORD PTR FS:
[0
]; Head of SEH Linked List
MOV DWORD PTR FS:
[0
], ESP
; 添加链表
将自身的EXCEPTION_REGISTRATION_RECORD结构体连接到EXCEPTION_REGISTRATION_RECORD结构体链表。
SEH练习示例
使用OllyDbg调试器打开seh.exe程序,运行到401000地址处(此处为seh.exe程序的main()函数)。
位于401000、401005、40100C地址处的3条指令与“SEH安装方法”中将的汇编指令是一样的。新添加的异常处理器就是位于40105A地址处的异常处理函数。
继续运行代码到401005地址处,查看FS:[0]的值,其值就是SEH链的起始地址。
从代码信息窗口中可用看到, FS:[0] = [002FC000] = 0019FF60,其中0019FF60就是SEH链的起始地址。在栈窗口查看地址0019FF60,可用发现第一个EXCEPTION_REGISTRATION_RECORD结构体,其中Next指针的值为0019FFCC,Handler=00402730。异常处理器地址402730存在于seh.exe进程的代码节区,该异常处理器时VC++生成PE文件时默认添加到其启动函数的。转到0019FFCC地址处,查看链表第二个EXCEPTION_REGISTRATION_RECORD结构体。 再次转到0019FFE4查看第三个EXCEPTION_REGISTRATION_RECORD结构体,第三个结构体Next成员的值为FFFFFFFF,所以第三个EXCEPTION_REGISTRATION_RECORD结构体也是SEH链表的最后一个结构体。
运行401005地址处的PUSH DWORD PTR DS:[0]指令。
栈中新创建了_EXCEPTION_REGISTRATION_RECORD结构体,继续执行40100C地址处的MOV DWORD PTR FS:[0],ESP指令。 栈窗口中出现了新生成的SEH的注释(Next = 0019FF60,Handler = 0040105A),新的异常处理器(40105A)就这样添加到SEH链。
OllyDbg调试器提供了查看SEH链的功能,在OllyDbg主菜单依次选择View-SEH Chain。 如果执行401019地址处的MOV DWORD PTR DS:[EAX],1指令,就会引发EXCEPTION_ACCESS_VIOLATION异常,此时程序处于调试之中,根据异常处理的顺序,OS会把控制权交给调试器,在40105A地址处设置断点,然后按Shift + F9组合键,再将异常派送给调试进程,调试暂停在设置的断点处(40105A)。
查看栈中存储的参数。 第一个参数(ESP+4)是指向EXCEPTION_RECORD结构体的指针pRecord(0019F9F4),查看结构体中的数据。 (1)参照关于EXCEPTION_RECORD结构体的定义可知,ExceptionCode为C0000005(EXCEPTION_ACESSS_VIOLATION),发生异常的代码地址为ExceptionAddress为401019. (2)第二个参数是指向EXCEPTION_REGISTRATION_RECORD结构体的指针(pFrame),其值为0019FF24,它是SEH链的起始地址。 (3)第三个参数是指向CONTEXT结构体的指针pContext(0019FA44),查看指针pContext所指的地址空间。CONTEXT是一个非常大的结构体,其中需要特别注意的Eip成员,它位于结构体偏移B8的位置,存储着发生异常的代码地址。
调试异常处理器
40105A地址处的异常处理器中存在调试器检测代码。
0040105A MOV ESI,DWORD PTR SS:
[ESP+C
] ; ESI
= pContext
[ESP+C]是异常处理器第三个参数pContext的值,以上命令用来将pContext地址传送到ESI寄存器。
0040105E MOV EAX,DWORD PTR FS:
[30
]; EAX
= address of PEB
上述指令用于将FS:[30]的值传送给EAX寄存器,FS:[30]就是PEB结构体的起始地址。
00401064 CMP BYTE PTR DS:
[EAX+2
],1
上述指令用于读取[EAX+2]地址中的1个字节值,然后与1比较,EAX当前保存着PEB的起始地址,所以[EAX+2]指的是PEB.BeingDebugged成员。 可用看到[EAX+2] = [002F9002] = PEB.BeingDebugged的值被设置为1,表示进程处于调试状态。
00401068 JNZ SHORT 00401076
CMP命令中的2个比较对象不同,则执行JNZ命令跳转,由于PEB.BeingDebugged的值为1,所以不跳转,即不执行该JNZ命令。 程序非调试运行时,执行此处会跳转到401076地址处,若程序处在调试状态,则跳过该JNZ指令,直接执行40106A地址处的指令。
0040106A MOV DWORD PTR DS:
[ESI+B8
],00401023
当前ESI寄存器保存着CONTEXT结构体的起始地址,[ESI+B8] = pContext -> Eip,当前该值为401019.上述指令用来将pContext->Eip值更改为401023,异常处理器终止时,发生异常的线程会运行401023地址处的代码。 在401023地址处设置断点。
00401074 JMP SHORT 00401080
由于pContext->Eip值已经发生改变,所以执行流程跳转到异常处理器的终止代码处(401080)。
00401076 MOV DWORD PTR DS:
[ESI+B8
],00401039
若程序运行在非调试状态下,则执行401068地址处的JNZ指令跳转到401076地址处,401076地址处的指令用来将pContext->Eip值更改为401039,401039地址处的代码用来弹出消息对话框,显示hello消息文本。
00401080 XOR EAX,EAX
00401082 RETN
最后两条指令中先将返回值(EAX)设置为0,然后异常处理器返回,返回值0代表EXCEPTION_CONTINUE_EXECUTION,表示异常得到处理,相关线程可用继续运行。运行到401082地址处的RETN指令时,控制权被返回至ntdll.dll模块中的代码区域,它属于系统区域,所以在OllyDbg中按F9运行键后,调试会在401023地址处暂停。使用F8指令使调试运行到401031地址处的CALL指令,弹出一个消息框,按确定按钮关闭消息框后,执行401037地址处的JMP SHORT 40104D指令,跳转到删除SEH的代码处。
删除SEH
调试运行到40104D地址处查看栈,EXCEPTION_REGISTRATION_RECORD结构体存储在其中(0019FF24),该结构体使SEH链中最初运行的异常处理器。401040处的POP DWORD PTR FS:[0]指令用来读取栈值(0019FF60),并将其放入FS:[0],FS:[0]是TEB.NtTib.ExceptionList,0019FF60就是下一个SEH起始地址。执行该命令后,前面注册的(0019FF24)SEH被从SEH链中删除,然后执行401054地址处的ADD ESP,4指令,将栈中的异常处理器地址也删除。
设置OllyDbg选项
OllyDbg调试器提供了调试选项,调试中程序发送异常时,调试器不会暂停,会自动将异常派送给调试者。在菜单中选择Options - Debugging options 。 Exception包含多个选项卡。
忽略Kernel32中发生的内存非法访问异常。 => 复选Ignore memory access violations in KERNEL32选项后,kernel32.dll模块中发生的内存非法访问异常都会被忽略。向被调试者派送异常,前面5个已经介绍过了,单击左侧复选框选中后,发生相应异常时OllyDbg调试器就会忽略该异常,并且将其派送给被调试者。ALL FPU exceptions,FPU时专门用于浮点数运算的处理器,它有一套专门指令,与普通x86指令的形态不同。复选后,处理FPU指令过程发生异常时,调试器会无条件将异常派送给被调试者处理。
Exceptions选项卡还有一个Ignore alse following custom exceptions for ranges选项,复选该选项后,用户可用直接添加(或删除)其他各种异常,发生这些异常时,调试器会将它们直接派送给被调试者处理。
简单练习
使用OllyDbg调试器打开seh.exe程序,然后在Exception选项卡中进行相应的设置。 如上设置后,程序在调试运行时发生以上6种异常时,调试器会忽略,将它们直接派送给被调试者。在seh.exe程序发生的EXECEPTION_ACCESS_VIOLATION异常会有自身的SEH处理(调试过程不会暂停),按F9键运行程序,直接弹出“Debugger detected”。