SEH

  1. SEH是Windows操作系统的异常处理机制,在程序源代码中使用__try、__except、__finally关键字来具体实现。

SEH练习示例 #1

  1. 示例程序seh.exe,该程序故意触发了内存非法访问异常,然后通过SEH机制来处理该异常,并且使用PEB信息向程序添加简单的反调试代码,使程序在正常运行与调试运行时表现出不同的行为动作。
  2. 正常运行。
    在这里插入图片描述
  3. 使用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的异常处理方法

  1. 同一程序正常运行与调试运行时表现出的行为动作是不同的,这是由Windos OS异常处理方法的不同造成的。
  2. 正常运行时的异常处理方法。
  • 进程运行过程中若发生异常,OS会委托进程处理,若进程代码存在具体的异常处理代码,则能顺利处理相关异常,程序继续运行,但如果进程内部没有具体实现SEH,那么相关异常就无法处理,OS就会启动默认的异常处理机制,终止进程运行。
  1. 调试运行时的异常处理方法
  • 若被调试进程内部发生异常,OS会首先把异常抛出给调试进程处理,调试器几乎拥有被调试者的所有权限,它不仅可用运行、终止被调试者,还拥有被调试进程的虚拟内存、寄存器的读写权限。
  • 被调试者内部发生的所有异常都由调试器处理,所以调试过程中发生的所有异常都要先交由调试器管理(被调试者的SEH依据有限顺序推给调试器)。
  • 遇到异常时经常采用的几种处理方法如下所示:
    (1) 直接修改异常:代码、寄存器、内存。 => 被调试者发生异常时,调试器会在发生异常的代码处暂停,此时可用通过调试器直接修改有问题的代码、内存、寄存器等,排除异常后,调试器继续运行程序。
    (2) 将异常抛给被调试者处理。=> 如果被调试者内部存在SEH(异常处理函数)能够处理异常,那么异常通知会发送给被调试者,由被调试者自行处理。=>前面的seh.exe练习使用的OllyDbg中的shift + F7/F8/F9命令可用直接将当前异常抛还给被调试者。
    (3) OS默认的异常处理机制。=> 若调试器与被调试者都无法处理当前发生的异常,则OS的默认异常处理机制会处理它,终止被调试进程,同时结束调试。

异常

  1. 操作系统定义的异常。
    在这里插入图片描述
  2. 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详细说明

  1. SEH以链的形式存在,第一个异常处理器中若未处理相关异常,它就会被传递到下一个异常处理器。
  2. 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)-> ©的顺序依次传递,知道有异常处理器处理。
  1. 异常处理函数的定义
  • 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    // -|               +00h
    DWORD           Dr0             //  |               +04h
    DWORD           Dr1             //  |               +08h
    DWORD           Dr2             //  >调试寄存器     +0Ch
    DWORD           Dr3             //  |               +10h
    DWORD           Dr6             //  |               +14h
    DWORD           Dr7             // -|               +18h

    FLOATING_SAVE_AREA FloatSave;   //浮点寄存器区      +1Ch~~~88h

    DWORD           SegGs           //-|                +8Ch
    DWORD           SegFs           // |\段寄存器       +90h
    DWORD           SegEs           // |/               +94h
    DWORD           SegDs           //-|                +98h

    DWORD           Edi             //________          +9Ch
    DWORD           Esi             // |  通用          +A0h
    DWORD           Ebx             // |   寄           +A4h
    DWORD           Edx             // |   存           +A8h
    DWORD           Ecx             // |   器           +ACh
    DWORD           Eax             //_|___组_          +B0h

    DWORD           Ebp             //++++++            +B4h
    DWORD           Eip             // |控制            +B8h
    DWORD           SegCs           // |寄存            +BCh
    DWORD           EFlag           // |器组            +C0h
    DWORD           Esp             // |                +C4h
    DWORD           SegSs           //++++++            +C8h

    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, //在OS内部使用
	ExceptionCollidedUnwind = 3 //在OS内部使用
} EXCEPTION_DISPOSITION;
  • 异常处理器处理异常后会返回ExceptionContinueExecution(0),从发生异常的代码处继续运行,若当前异常处理器无法处理异常,则返回ExceptionContinueSearch(1),将异常派送到SEH链的下一个异常处理器。
  1. TEB.NtTib.ExceptionList
  • 通过TEB结构体的NtTib成员可用很容易地访问进程的SEH链,方法非常简单。
TEB.NtTib.ExceptionList =  FS:[0]

在这里插入图片描述

  1. 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练习示例

  1. 使用OllyDbg调试器打开seh.exe程序,运行到401000地址处(此处为seh.exe程序的main()函数)。
    在这里插入图片描述
  • 位于401000、401005、40100C地址处的3条指令与“SEH安装方法”中将的汇编指令是一样的。
  • 新添加的异常处理器就是位于40105A地址处的异常处理函数。
  1. 继续运行代码到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链表的最后一个结构体。
  1. 运行401005地址处的PUSH DWORD PTR DS:[0]指令。
    在这里插入图片描述
  • 栈中新创建了_EXCEPTION_REGISTRATION_RECORD结构体,继续执行40100C地址处的MOV DWORD PTR FS:[0],ESP指令。
    在这里插入图片描述
  • 栈窗口中出现了新生成的SEH的注释(Next = 0019FF60,Handler = 0040105A),新的异常处理器(40105A)就这样添加到SEH链。
  1. OllyDbg调试器提供了查看SEH链的功能,在OllyDbg主菜单依次选择View-SEH Chain。
    在这里插入图片描述
  2. 如果执行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的位置,存储着发生异常的代码地址。
    在这里插入图片描述
  1. 调试异常处理器
  • 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的代码处。
  1. 删除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选项

  1. OllyDbg调试器提供了调试选项,调试中程序发送异常时,调试器不会暂停,会自动将异常派送给调试者。
  2. 在菜单中选择Options - Debugging options 。
    在这里插入图片描述
  3. Exception包含多个选项卡。
  • 忽略Kernel32中发生的内存非法访问异常。 => 复选Ignore memory access violations in KERNEL32选项后,kernel32.dll模块中发生的内存非法访问异常都会被忽略。
  • 向被调试者派送异常,前面5个已经介绍过了,单击左侧复选框选中后,发生相应异常时OllyDbg调试器就会忽略该异常,并且将其派送给被调试者。
  • ALL FPU exceptions,FPU时专门用于浮点数运算的处理器,它有一套专门指令,与普通x86指令的形态不同。复选后,处理FPU指令过程发生异常时,调试器会无条件将异常派送给被调试者处理。
    在这里插入图片描述
  1. Exceptions选项卡还有一个Ignore alse following custom exceptions for ranges选项,复选该选项后,用户可用直接添加(或删除)其他各种异常,发生这些异常时,调试器会将它们直接派送给被调试者处理。

简单练习

  1. 使用OllyDbg调试器打开seh.exe程序,然后在Exception选项卡中进行相应的设置。
    在这里插入图片描述
  2. 如上设置后,程序在调试运行时发生以上6种异常时,调试器会忽略,将它们直接派送给被调试者。
  3. 在seh.exe程序发生的EXECEPTION_ACCESS_VIOLATION异常会有自身的SEH处理(调试过程不会暂停),按F9键运行程序,直接弹出“Debugger detected”。
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值