简介

基本说明

SEH 是 Windows 操作系统提供的异常处理机制,在程序源代码中使用__try__except__finally关键字来具体实现。在逆向分析中,SEH 除了基本的异常处理功能外,还大量应用于反调试程序。

基本使用

在 C 语言中使用__try__except__finally关键字就可以很容易地向代码添加 SEH。在汇编语言中添加 SEH 的方法更加简单。

  • 在 C 语言中使用 SEH
#include <stdio.h>
#include <windows.h>

// 自定义异常函数,触发除零异常
void div_zero() {
    int a = 10, b = 0;
    int result = a / b; // 除零异常
    printf("Result: ");
}

int main() {
    __try {
        // 执行除零异常处理
        printf("Trying to divide by zero...");
        div_zero();
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        // 捕获除零异常
        printf("Exception caught: Division by zero!")
    }
    return 0;
}
  • 在汇编语言中使用 SEH
push @MyHandler           ;异常处理器
push dword ptr fs:[0]     ;Head of SEH Linked List
mov dword ptr fs:[0],esp  ;添加链表

在程序代码中使用 SEH 就是指,将自身的异常处理器添加到已有的 SEH 链。从技术层面讲就是自身的EXCEPTION_REGISTRATION_RECORD结构体链接到EXCEPTION_REGISTRATION_RECORD结构体链表。

OS的异常处理方法

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

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

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

调试运行中发生异常时,处理方法与上面有些不同。若被调试进程内存发生异常,OS 会首先把异常抛给调试进程处理。调试器几乎拥有被调试者的所有权限,它不仅可以运行、终止被调试者,还拥有被调试进程的虚拟内存、寄存器的读写权限。需要特别指出的是,被调试者内部发生的所有异常(错误)都由调试器处理。所以调试过程中发生的所有异常(错误)都要先交由调试器管理(被调试者的 SEH 依据优先顺序推给调试器)。像这样,被调试者发生异常时,调试器就会暂停运行,必须采取某种措施来处理异常,完成后继续调试。

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

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

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

  1. 将异常抛给被调试者处理

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

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

若调试器与被调试器都无法处理(或故意不处理)当前发生的异常,则 OS 的默认异常处理机制会处理它,终止被调试进程,同时结束调试。

异常

学习异常处理前,有必要了解操作系统中定义的异常。

在这里插入图片描述

以上异常列表中,我们调试时会经常接触 5 种最具代表性的异常,接下来分别介绍。

EXCEPTION_ACCESS_VIOLATION(C0000005)

试图访问不存在或不具权限的内存区域时,就会发生EXCEPTION_ACCESS_VIOLATION(非法访问异常,该异常最常见)。

在这里插入图片描述

EXCEPTION_BREAKPOINT(80000003)

在运行代码中设置断点后,CPU 尝试执行该地址处的指令时,将发生EXCEPTION_BREAKPOINT异常。调试器就是利用该异常实现断点功能的。

INT3

设置断点命令对应的汇编指令为 INT3,对应的机器指令为 0xCC。CPU 运行代码的过程中若遇到汇编指令 INT3,则会触发EXCEPTION_BREAKPOINT异常。

EXCEPTION_ILLEGAL_INSTRUCTION(C000001D)

CPU 遇到无法解析的指令时引发该异常。比如 0xFFF 指令在 x86 CPU 中未定义,CPU 遇到该指令将引发 EXCEPTION_ILLEGAL_INSTRUCTION异常。

EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094)

INTEGER(整数)除法运算中,若分母为 0(即被 0 除),则引发EXCEPTION_INT_DIVIDE_BY_ZERO异常。

编写应用程序时偶尔会发生该异常,分母为变量时,该变量在某个瞬间变为 0,执行除法运算就会引发EXCEPTION_INT_DIVIDE_BY_ZERO异常。

EXCEPTION_SINGLE_STEP(80000004)

Singel Step(单步)的含义是执行 1 条指令,然后暂停。CPU 进入单步模式后,每执行一条指令就会引发EXCEPTION_SINGLE_STEP异常,暂停运行。将EFLAGS寄存器的TF(陷阱标志)位设置为 1 后,CPU就会进入单步工作模式。

SEH 详细说明

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

异常处理函数的定义

SEH 异常处理函数(SEH 函数)定义如下:

EXCEPTION_DISPOSITION _except_handler
(
	EXCEPTION_RECORD *pRecord,
	EXCEPTION_REGISTRATION_RECORD *pFrame,
	CONTEXT *pContext,
	PVOID   pValue
);

异常处理函数(异常处理器)接收 4 个参数输入,返回名为EXCEPTION_DISPOSITION的枚举类型(enum)。该异常处理函数由系统调用,是一个回调函数,系统调用它时会给出代码中的 4 个参数,这 4 个参数中保存着与异常相关的信息。首先,第一个参数是指向EXCEPTION_RECORD结构体的指针,EXCEPTION_RECORD结构体的定义如下:

typedef struct _EXCEPTION_RECORD
{
	DWORD     ExceptionCode;      //异常代码
	DWORD     ExceptionFlags;     
	struct _EXCEPTION_RECORD *ExceptionRecord;
	PVOID     ExceptionAddress;   //异常发生地址
	DWORD     NumberParameters;
	ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
									//15
}EXCEPTION_RECORD, *PEXCEPTION_RECORD;

请注意该结构体中ExceptionCodeExceptionAddress这 2 个成员,ExceptionCode成员用来指出异常类型,ExceptionAddress成员表示发生异常的代码地址。异常处理函数的第三个参数是指向CONTEXT结构体的指针。

CONTEXT结构体的定义如下:

// CONTEXT_IA32
struct CONTEXT{
	DWORD ContextFlags;
	DWORD Dr0; // 04h
	DWORD Dr1; // 08h
	DWORD Dr2; // 0Ch
	DWORD Dr3; // 10h
	DWORD Dr6; // 14h
	DWORD Dr7; // 18h

	FLOATING_SAVE_AREA FloatSave;
	
	DWORD SegGS; // 88h
	DWORD SegFs; // 90h
	DWORD SegEs; // 94h
	DWORD SegDs; // 98h

	DWORD Edi; // 9Ch
	DWORD Esi; // A0h
	DWORD Ebi; // A4h
	DWORD Edi; // A8h
	DWORD Eci; // ACh
	DWORD Eai; // B0h

	DWORD Ebp;    // B4h
	DWORD Eip;    // B8h
	DWORD SegCs;  // BCh (must be sanitized)
	DWORD EFlags; // C0h
	DWORD Esp;    // C4h
	DWORD SegSs;  // C8h

	BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; // 512 bytes
}

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

异常发生时,执行异常代码的线程就会中断,转而运行 SEH(异常处理器/异常处理函数),此时 OS 会把线程的CONTEXT结构体的指针传递给异常处理函数(异常处理器)的相应参数。CONTENT结构体成员中有 1 个Eip成员(偏移量:B8)。在异常处理函数中将参数传递过来的CONTENT.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 链的下一个异常处理器。

TEB.NtTib.ExceptionList

通过 TEB 结构体的NtTib成员可以很容易地访问进程的 SEH 链,方法非常简单。

如下图所示,TEB.NtTib.ExceptionList成员是 TEB 结构体的第一个成员。FS段寄存器执行段内存的起始地址,TEB 结构体即位于此,所以通过下列公式可以轻松获取TEB.NtTib.ExceptionListd的地址。

TEB.NtTib.ExceptionList=FS:[0]

在这里插入图片描述

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐