逆向工程核心原理——SEH原理分析

SEH基本说明

SEH是Windows操作系统提供的异常处理机制,在程序源代码中使用try、except、finally关键词来具体实现。SEH与C++中的try、catch异常处理不同,SEH是一种从属于VC++开发工具和Windwos操作系统的异常处理机制。

SEH练习1

先简单介绍练习示例程序seh.exe,该程序故意触发了内存非法访问异常,然后通过SEH机制来处理该异常。并且使用PEB信息向程序中添加简单的反调试代码,使程序在正常运行与调试运行时表现出不同的行为动作。

正常运行

seh.exe程序非常简单,双击运行弹出“Hello:)”字符串,虽然程序表面上是正常运行的,但是内部已经发生了异常,由于使用了SEH机制处理,所以可以正常运行。

调试运行

用OD打开seh.exe

然后F9运行,程序由于发生非法访问异常会暂停调试

401019地址的mov dword ptr ds:[eax],1指令触发异常,当前的eax值为0,所以该指令实际含义是向内存地址为0处写入值1。但试图向未分配的内存地址0写入某个值,就会发生内存非法访问异常。

注意

内存地址0虽然属于seh.exe进程的用户内存区域,但是由于是未分配的空间,所以无法随意访问。利用OD的内存映射(view-memory菜单),可以看到进程中内存地址0被标为未分配区域

我们查看OD的主窗口,当我F8单步下去时,这时候说执行不了的,下面还给了一句话

我们根据提示Shift+F9继续运行程序试试

这次程序检测到了调试器(内部有一段简单的调试器检测代码),程序在两种情况下的异常处理方式是不同的

OS异常处理方法

通过前面的实验,我们知道同一程序在正常运行和调试运行时表现出的行为动作是不同的,这是由Windows OS异常处理方法的不同造成的。

正常运行时的异常处理方法

进程运行过程中若发生异常,OS会委托进程处理。若进程代码中存在具体的异常处理(如SEH异常处理器)代码,则能顺利处理相关异常,程序继续执行。如果进程内部没有具体实现SEH,那么相关异常就无法处理,OS就启动默认的异常处理机制,终止进程运行。

调试运行时的异常处理方法

调试运行中发生异常时,处理方法与正常运行时不同。如果调试进程内部发生异常,OS会首先把异常抛给调试进程处理。调试器几乎拥有被调试者的所有权限,它不仅可以运行、终止被调试者,还拥有被调试进程的虚拟内存、寄存器的读写权限。所以调试过程中发生的所有异常错误都要先交给调试器管理。

遇到异常时经常采用的几种处理方法:

  1. 直接修改异常:代码、寄存器、内存

    被调试者发生异常时,调试器会在发生异常的代码处暂停,此时可以通过调试器直接修改问题的代码、内存、寄存器等,排除异常1后,调试器继续运行程序

  2. 将异常抛给被调试者

    如果被调试者内部存在异常处理函数(SEH)能够处理异常,那么异常通知会发送给被调试者,由它自己处理,此时与程序正常运行时的异常处理方式一样。

  3. OS默认的异常处理机制

    如果调试者与被调试者都无法处理当前的异常,则OS的默认异常处理会处理它,终止被调试进程,同时结束调试。

异常

EXCEPTION_ACCESS_VIOLATION    	    0xC0000005     程序企图读写一个不可访问的地址时引发的异常。例如企图读取0地址处的内存。
EXCEPTION_ARRAY_BOUNDS_EXCEEDED     0xC000008C     数组访问越界时引发的异常。
EXCEPTION_BREAKPOINT                0x80000003     触发断点时引发的异常。
EXCEPTION_DATATYPE_MISALIGNMENT     0x80000002     程序读取一个未经对齐的数据时引发的异常。
EXCEPTION_FLT_DENORMAL_OPERAND      0xC000008D     如果浮点数的操作数是非正常的,则引发该异常。所谓非正常即它的值太小以至于不能用标准格式表示出来。
EXCEPTION_FLT_DIVIDE_BY_ZERO        0xC000008E     浮点数除法的除数是0时引发该异常。
EXCEPTION_FLT_INEXACT_RESULT        0xC000008F     浮点数操作的结果不能精确表示成小数时引发该异常。
EXCEPTION_FLT_INVALID_OPERATION     0xC0000090     该异常表示不包括在这个表内的其它浮点数异常。
EXCEPTION_FLT_OVERFLOW              0xC0000091     浮点数的指数超过所能表示的最大值时引发该异常。
EXCEPTION_FLT_STACK_CHECK           0xC0000092     进行浮点数运算时栈发生溢出或下溢时引发该异常。
EXCEPTION_FLT_UNDERFLOW             0xC0000093     浮点数的指数小于所能表示的最小值时引发该异常。
EXCEPTION_ILLEGAL_INSTRUCTION       0xC000001D     程序企图执行一个无效的指令时引发该异常。
EXCEPTION_IN_PAGE_ERROR             0xC0000006     程序要访问的内存页不在物理内存中时引发的异常。
EXCEPTION_INT_DIVIDE_BY_ZERO        0xC0000094     整数除法的除数是0时引发该异常。
EXCEPTION_INT_OVERFLOW              0xC0000095     整数操作的结果溢出时引发该异常。
EXCEPTION_INVALID_DISPOSITION       0xC0000026     异常处理器返回一个无效的处理的时引发该异常。
EXCEPTION_NONCONTINUABLE_EXCEPTION  0xC0000025     发生一个不可继续执行的异常时,如果程序继续执行,则会引发该异常。
EXCEPTION_PRIV_INSTRUCTION          0xC0000096     程序企图执行一条当前CPU模式不允许的指令时引发该异常。
EXCEPTION_SINGLE_STEP               0x80000004     标志寄存器的TF位为1时,每执行一条指令就会引发该异常。主要用于单步调试。
EXCEPTION_STACK_OVERFLOW            0xC00000FD     栈溢出时引发该异常

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)—>(C)的顺序依次传递,直到有异常处理器处理。

异常处理函数的定义

//此函数并未在MSDN上公开
EXCEPTION_DISPOSITION __cdecl _except_handler(
	EXCEPTION_RECODE *pRecord,
    EXCEPTION_REGISTRATION_RECORD *pFrame,		//指向SHE链中当前_EXCEPTION_REGISTRATION结构的地址
    CONTEXT  *pContext,
    PVOID pValue //用于内嵌异常的处理,读者可以暂时忽略它
);

这个异常处理函数由系统调用,是一个回调函数,系统调用它时会给出四个参数,且这4个参数中保存着与异常相关的信息。

第一个参数EXCEPTION_RECORD

typedef struct _EXCEPTION_RECORD {
    DWORD ExceptionCode;   //指出异常类型
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;   //发生异常的代码地址
    DWORD NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

第二个参数EXCEPTION_REGISTRATION_RECORD

指向异常帧结构的指针


ntdll!_EXCEPTION_REGISTRATION_RECORD
   +0x000 Next             : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 Handler          : Ptr32 _EXCEPTION_DISPOSITION 

第三个参数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;              // MUST BE SANITIZED
    DWORD   EFlags;             // MUST BE SANITIZED
    DWORD   Esp;
    DWORD   SegSs;
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

CONTEXT结构体用来备份CPU寄存器的值,因为多线程环境下需要这么做。每个线程内部都有一个CONTEXT结构体。CPU暂时离开当前线程去运行其他线程时,CPU寄存器的值就会保存到当前线程的CONTEXT结构体;CPU再次运行该线程时,会使用保存在CONTEXT结构体的值来覆盖CPU寄存器,然后从之前暂停的代码处继续执行。通过这种方式,OS可以在多线程下安全运行各线程。

异常发生的时候,执行异常代码的线程就会中断运行,转而运行SEH(异常处理函数),此时OS会把线程的CONTEXT结构体的指针传递给异常处理函数的参数。这个结构体中有一个EIP,在异常处理函数中将参数传递过来的EIP设置为其他的地址,然后再返回异常处理函数,这样,之前暂停的线程就会执行新设置的EIP地址处的代码,这在反调试中经常使用。

返回值EXCEPTION_DISPOSITION

typedef enum _EXCEPTION_DISPOSITION
{
         ExceptionContinueExecution       = 0, //回调函数处理了异常,可以从异常发生的指令处重新执行。
         ExceptionContinueSearch          = 1, //回调函数不能处理该异常,需要SEH链中的其他回调函数处理。
         ExceptionNestedException         = 2, //回调函数在执行中又发生了新的异常,即发生了嵌套异常
         ExceptionCollidedUnwind          = 3  //发生了嵌套的展开操作
} EXCEPTION_DISPOSITION;

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];
MOV DWORD PTR FS:[0],ESP;

SEH练习2

查看SEH链

用OD打开seh.exe程序,运行到0x401000地址处

圈出来的三条指令就是SEH安装方法的汇编指令,异常函数处理的地址为0x40105A

通过OD查找SEH链

在OD中通过View-SEH Chain可以查看SEH链

发生异常

当发生异常时,OS会把控制器交给调试器,此时并没有执行40105A处的代码,我们在40105A处下断点,然后shift+F12运行,此时会将异常派送给被调试进程自己处理

这个就是异常处理函数,我们可以分析一下

查看异常处理器参数

第一个参数,ESP+4,指向EXCEPTION_RECODE结构体的指针pRecord(12FAC0),前面已经说过该结构体第一个成员是异常类型,第四个成员是异常发生的地址。异常类型为C0000005,发生异常的代码地址为401019

第二个参数,ESP+8,是指向EXCEPTION_REGISTRATION_RECORD的指针pFrame,其值为12FF3C,这是SEH链的起始地址

第三个参数,ESP+C,是指向CONTEXT结构体的指针pContext(12FADC),查看指针所指的地址空间

这里特别注意偏移为B8的位置,这是成员EIP,存放着发生异常的代码地址

最后一个参数,ESP+10供系统内部使用,可以省略。

调试异常处理器

MOV ESI,DWORD PTR SS:[ESP+C];

ESP+C是异常处理器的第三个参数pContext,此时esi的地址就是pContext

MOV EAX,DWORD PTR FS:[30]
此时eax存放着PEB的结构体地址

CMP BYTE PTR DS:[EAX+2],1
读取EAX+2地址中的一个字节,然后与1比较,而EAX+2是PEB中偏移为2的成员,也就是BeingDebugged

JNZ SHORT 00401076
如果两者不一样就跳转,而此时BeingDebugged=1,所以不跳转,继续向下执行

MOV DWORD PTR SS:[ESI+B8],00401023
由于没有跳转,现在执行该语句,由于 ESI的值是篇ConText,所以B8处的EIP被改为00401023,此时发生异常的线程就会执行401023处的代码

XOR EAX,RAX
RETN
最后两句将返回值EAX置0,然后返回,返回0代表了EXCEPTION_CONTINUE_EXECUTION,异常得到处理,相关线程可以继续运行

可以看到401023是实现弹出Debugger Detected:(的代码

401039处的代码是实现弹出Hello:(的代码

删除SEH

在程序终止前删除已经注册的SEH

设置OD选项

OllyDbg调试器中提供了调试选项,调试中的程序发生异常时,调试器不会停止,自动将异常派送给被调试者,看上去与正常运行一样。

在OD的菜单栏中选择我Options—>Debugging options菜单,打开Degbugging options菜单,然后再选择异常选项

灵活运用这些选项,可以使我们在调试程序时,跳过异常,使调试过程不会暂停,被调试者会自己通过SEH处理


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 767778848@qq.com