逆向工程之Windows异常处理
Aking9 发表于 河南 技术文章 1560浏览 · 2024-05-27 02:47

前言

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习),本文仅作技术研究。
本文涉及到的案例可在附件获取。

导读

逆向工程中的异常处理对于理解程序的行为和结构是非常重要的部分。它可以帮助我们理解在执行过程中如何处理问题或异常情况。
一般的,异常处理步骤包括以下几个部分:

  1. 异常的识别:通过一些异常条件,如非法操作、无效输入或者缺乏资源等等,程序可以识别出具体的异常类型。
  2. 异常的处理:一旦发现异常,程序应该到已经设定的处理段,也称为异常处理程序或者异常捕获代码。这部分代码会处理异常情况,可能涉及到释放已经分配的资源、打印错误信息、或者尝试纠正错误等操作。
  3. 异常的恢复:在处理完异常之后,程序可能会回到正常的执行流,或者可能会进入某种安全模式,避免异常的再次发生。

异常类型

异常是在程序执行过程中发生的事件,需要执行常规控制流之外的代码。
我们可以查看OD调试时,都有哪些异常处理情况

内存访问异常

内存访问异常:当线程中尝试访问没有访问权限的内存的时候会发生该类异常。

例如:一个线程尝试向只具有读权限的内存写入数据的时候就会产生内存访问异常。

我们这里使用CrackMe1.0-Cruehead程序来进行演示

我们导入,对程序入口处修改指令,MOV DWORD PTR DS:[0],0x12345678,并执行

我们发现,程序会中断,OD提示“访问违规,正在写入到[00000000]”。

程序会根据PE头中的相关信息设置区段的初始权限,当然也可以使用诸如VirutalProtect这类API函数在运行时修改权限。

OllyDbg查看PE信息,点击M我们可以转到Memory Map-内存映射窗口

主模块的所在的区段开始于400000,首先是PE头,占1000个字节,PE头中保存了各个区段的名称,长度以及程序运行所必须的一些信息。

这时候我们可以双击PE文件头这一行,在数据窗口中查看。我们也可以在CPU窗口的数据区域中转到PE头地址0x00400000

然后再右键 -- .> 指定 -->PE头,可以解析PE头的各个字段的选项,示出了DOS头的各个字段信息。

我们继续向下看,发现Offset to PE signature,这是PE头的偏移量

我们定位到400100处。这里是关于程序的重要信息:基地址是0x400000,程序入口点是0x00401000.

这里面不过多说明,我们主要看区域的权限信息,我们继续向下翻。

我们可以看到 代码段和数据段的权限是不一样的

对于代码段想让其有可写权限的话,我们可以将Characteristics这个字段的60000020修改为E0000020,这样该区段就具有了所有权限

只需要右键修改,然后保存到文件就可以了。

除0异常

试图除以0时会产生该异常。

这里面,我们使用上一步修改过code权限的文件

我们将前三句汇编指令修改,然后执行。我们可以看到提示整数除0异常。

无效指令或特权指令

当CPU试图执行越权指令的话就会产生该异常。

由于OllyDbg不允许我们输入CPU不可识别的指令,所以我们无法验证。

但是程序员可以自己设计一些处理器并不支持的指令,当执行到指令时显示相应的错误即可。

最为典型就是INT 3指令,INT 3指令会产生一个异常,并且该异常会被调试器捕获到,比如,我们可以设置BPX断点来让程序中断下来,然后就可以对该程序进行相应的控制了。

另外,有一些程序会直接写入INT 3指令,所以说INT 3产生的异常是最常见的。

SEH

SEH( Structured Exception Handling , 结构化异常处理 )

SEH简介

SE它是用来确保该程序可以从错误中恢复,。

  • 如果你没有设置SEH,那么当程序中有异常发生时,会交由系统默认异常处理
  • 如果我们设置了SEH的话,异常处理程序就能够捕获到程序中发生的异常,进行相应的处理后,就会把控制权重新交予程序继续执行,

每个线程都可以有自己的异常处理程序,如果当前异常处理程序不予处理的话,可以将异常将于SEH链中的其他异常处理程序来处理。

在微软的Windows平台上诸如访问冲突(access violations)整数除以零(integer divide by zero)非法指令(illegal instructions) 等程序异常由结构化异常处理程序(SEH, Structured Exception Handling) 来处理。

SEH结构

_EXCEPTION_REGISTRATION_RECORD

_EXCEPTION_REGISTRATION_RECORD结构体是Windows中一个关键的数据结构,用于维护一种叫做Structured Exception Handling (SEH)的异常处理机制链表。每个线程都拥有自己的SEH链表,当异常发生时,逆序遍历该链表,并调用相应的异常处理程序。

//0x8 bytes (sizeof)
struct _EXCEPTION_REGISTRATION_RECORD
{
    struct _EXCEPTION_REGISTRATION_RECORD* Next;                            //0x0
    enum _EXCEPTION_DISPOSITION (*Handler)(struct _EXCEPTION_RECORD* arg1, VOID* arg2, struct _CONTEXT* arg3, VOID* arg4); //0x4
};
  • Next:指向下一个 _EXCEPTION_REGISTRATION_RECORD 结构的指针,形成一个链表。

  • Handler:指向一个异常处理函数的指针。当异常发生时,这个处理函数会被调用。

    这个函数接收四个参数(分别是一个_EXCEPTION_RECORD指针,一个VOID指针,一个_CONTEXT指针和一个VOID指针),返回一个_EXCEPTION_DISPOSITION枚举值。

简写

typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PVOID Handler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

通常,程序利用 _EXCEPTION_REGISTRATION_RECORD 结构在堆栈上建立SEH链表。当触发一个异常时,系统将首先调用SEH链表顶部的Handler,如果无法处理异常,就将异常传递给下一个Handler,直到找到可以处理该异常的Handler或者遍历完整个链表。

_EXCEPTION_DISPOSITION

_EXCEPTION_DISPOSITION是一个枚举类型,用于表示异常处理程序(Exception Handler)处理完成后的返回状态

//0x4 bytes (sizeof)
enum _EXCEPTION_DISPOSITION
{
    ExceptionContinueExecution = 0,
    ExceptionContinueSearch = 1,
    ExceptionNestedException = 2,
    ExceptionCollidedUnwind = 3
};
  • ExceptionContinueExecution:如果异常处理程序修复了异常,会返回这个值,然后继续执行产生异常的那条指令。
  • ExceptionContinueSearch:表示异常处理程序无法处理当前的异常,系统应继续向下搜索异常处理程序链表,寻找可以处理当前异常的处理程序。
  • ExceptionNestedException:在执行异常处理程序的过程中,又发生了异常,并且这个新的异常的优先级更高。
  • ExceptionCollidedUnwind:在处理异步异常的过程中,遇到了一个或者多个帧的终止处理程序。

_CONTEXT结构体

_CONTEXT是Windows编程中一个重要的结构体,用于描述一个线程的完整(或部分)执行环境,主要记录了CPU所有寄存器的状态。

//0x2cc bytes (sizeof)
struct _CONTEXT
{
    ULONG ContextFlags;                                                     //0x0
    ULONG Dr0;                                                              //0x4
    ULONG Dr1;                                                              //0x8
    ULONG Dr2;                                                              //0xc
    ULONG Dr3;                                                              //0x10
    ULONG Dr6;                                                              //0x14
    ULONG Dr7;                                                              //0x18
    struct _FLOATING_SAVE_AREA FloatSave;                                   //0x1c
    ULONG SegGs;                                                            //0x8c
    ULONG SegFs;                                                            //0x90
    ULONG SegEs;                                                            //0x94
    ULONG SegDs;                                                            //0x98
    ULONG Edi;                                                              //0x9c
    ULONG Esi;                                                              //0xa0
    ULONG Ebx;                                                              //0xa4
    ULONG Edx;                                                              //0xa8
    ULONG Ecx;                                                              //0xac
    ULONG Eax;                                                              //0xb0
    ULONG Ebp;                                                              //0xb4
    ULONG Eip;                                                              //0xb8
    ULONG SegCs;                                                            //0xbc
    ULONG EFlags;                                                           //0xc0
    ULONG Esp;                                                              //0xc4
    ULONG SegSs;                                                            //0xc8
    UCHAR ExtendedRegisters[512];                                           //0xcc
};

以下是这些字段的一些基本的描述:

  • ContextFlags:用于标识该CONTEXT数据内包含哪些有效的上下文信息。
  • DriDr7:硬件断点寄存器。
  • SegGsSegFsSegEsSegDs:表示对应的代码段或数据段寄存器。
  • EdiEsiEbxEdxEcxEax:通用寄存器。
  • Ebp:基址指针寄存器。
  • Eip:指令指针寄存器。
  • SegCs:代码段寄存器。
  • EFlags:标志寄存器。
  • Esp:堆栈指针寄存器。
  • SegSs:堆栈段寄存器。
  • ExtendedRegisters:包含一系列其他寄存器(特定CPU架构的)的状态信息。

在捕获异常或调试过程中,你可以使用_CONTEXT结构体来获取线程挂起时的状态,进行进一步的分析。

SEH注册与卸载

注册

push @_except_handler    ;异常处理器
push dwod ptr fs:[0]     ;取出 SEH链表头
mov dwod ptr fs:[0],esp  ;添加链表
  1. 首先需要创建一个_EXCEPTION_REGISTRATION_RECORD的结构实例。这个结构主要包含两个指针,一个是下一个结构的地址,另一个是对应的异常处理函数的地址。
  2. 然后,新创建的_EXCEPTION_REGISTRATION_RECORD实例将被添加到异常处理程序链表的最前面,也就是链表的头部。
  3. 进行这个操作时,Next字段被设置为当前FS:[0]指向的值(这个值指向链表的当前头部),然后将_EXCEPTION_REGISTRATION_RECORD实例的地址写入到FS:[0]。这样这个新的异常处理程序就添加到了头部,变为了链表的第一个处理程序。
  4. 最后,当异常发生时,操作系统就会根据这个链表依次调用每个异常处理器,直到某个处理器处理了这个异常。

卸载

pop dword ptr fs:[0]    ;还原链表头
add esp,4    ;删除 异常处理器

SEH处理流程

  1. 当一个异常产生时(例如,访问无效的内存地址),操作系统会首先查找当前线程的结构化异常处理器链(SEH链)。这个链表的入口点是存在于线程环境块(Thread Environment Block, TEB)中,可以通过 FS:[0] 来访问。
  2. 异常处理器链实际上是一个_EXCEPTION_REGISTRATION结构的链表。这个结构主要包含两个字段:一个是链表中下一个节点的指针,另一个是处理程序(handler)的指针。链表的第一个节点(即最后注册的异常处理器)的地址存储在FS:[0]位置。
  3. 操作系统会按照链表的顺序,逐个调用每一个处理程序,直到找到一个处理程序能够处理当前的异常。在调用处理程序的过程中,操作系统会传入一个_EXCEPTION_POINTERS结构,这个结构包含了异常的信息以及产生异常时的上下文状态(如寄存器的值等)。
  4. 在处理程序中,可以通过检查传入的异常信息来决定是否处理这个异常。如果决定处理这个异常,那么处理程序需要修复(或者恢复)产生异常的上下文状态,并明确告诉操作系统这个异常已经被处理,操作系统就会恢复程序的执行。如果处理程序不处理这个异常,那么操作系统就会继续调用链表中的下一个处理程序。
  5. 如果所有的异常处理程序都不能处理当前的异常,那么操作系统会调用默认的异常处理程序,通常情况下这意味着终止正在执行的程序,并显示一个错误信息。

定位异常处理程序

默认异常处理

程序导入OD,查看堆栈与FS:[0]信息

堆栈中的是 这是系统默认安装的异常处理程序,无论什么异常交予该默认异常处理程序处理的话,它都会弹出错误消息框

我们发现FS:[0](12FFE0)指向的是SEH链的最后一个结点,即系统默认异常处理程序,

我们再次确认,点击查看 -->SEH链,看到只有系统默认的异常处理程序被安装了

如果当有多个异常处理程序的话,当捕获到异常的话,异常会依次由SEH链的顶部向底部传递。

SEH实验一

我们调试另一个程序查看

可以看到程序开始处在安装自己的异常处理程序,OllyBbg中也以注释标注出来了

我们执行SEH安装指令,进行追踪

  1. 首先是一个PUSH 4066D8指令,当程序发生异常时,将会调用4066D8地址处的异常处理程序
  2. 将·FS:[0](即 0x0012FFE0)的值赋值给EAX,压入栈
  3. FS:[0]的内容设置为ESP的内容,这样一个异常处理程序就被安装好了

因此0x0012FFB0处就是我们安装的 新的SEH结点了,也就是FS:[0]指向了我们新安装的SEH结点。

查看SEH链!

当出现异常的时候,会先执行0x004066D8的异常处理程序,若无法成功处理异常,则在执行0x7C839Q90的系统默认异常处理程序

我们安装好了SEH,我们查看EIP,因此我们只要在安装指令后边制造异常就要可以了。

我们在0x404B1F处,设置汇编指令MOV DWORD PTR DS:[0],EDX,这样会产生一个异常,因为0地址不能写入。

我们确保调试选项中忽略各类异常的选项没有被勾选,但是第一个选项还是要勾选的。

我们执行后,会发现,在OD左下角提示:访问违规: 正在写入到 [00000000],使用shift + F7/F8/F9可以忽略异常

我们在4066D8指向的异常处理程序入口处设置一个断点。

Shift +F9运行会运行到,我们SEH断点,我们继续F9运行,发现无弹窗

这是因为,系统默认异常处理函数:UnhandledExceptionFilter(),,会检查注册表:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug

Auto表项,为1,不弹出对话框,为其他,弹出对话框

我们修改后重新 执行。

SEH实验二

我们进行第二个实验SDUE1

我们用OD加载该程序,可以看到OD提示说该程序可能被加壳了

我们依然不勾选调试选项中的忽略各类异常的选项,除了忽略第一个异常以外。

我们点击是后,发现OD进行了解码,我们点击删除分析

F9运行,发现程序中断,在OD左下方提示访问违规,在堆栈区我们可以看到SEH异常处理函数地址为0x009F0054

我们在当前异常指令写一条语句设置断点。

我们转到0x009F0054,在这个函数入口设置断点

Shift +F9运行,我们可以看到断在了异常处理程序的入口处,如果成功从异常中恢复了的话,那么程序将会从刚刚发生异常的指令的下一条指令处继续往下执行。

我们继续F9运行看看程序是否会在我们前面设置的断点中断

我们可以看到程序继续执行起来了,并弹出提示错误消息框,说明异常已经成功被修复了。

我们继续慢慢F9,发现有很多的访问异常,我们都Shift +F9运行,最终我们发现程序成功运行。

异常处理流程

当出现异常后的异常处理流程

当前控制权由调试器归还给程序以后,系统会检查当前程序是否安装了SEH,如果安装了SEH,就转向SEH的异常处理程序执行,如果没有安装SEH,就会调用系统默认的异常处理程序。

当系统默认异常处理流程未解决异常,则会弹出异常处理窗口。(系统默认异常处理函数为UnhandledExceptionFilter())

注意,SEH处理异常未解决异常时

  • 用户设置过SetUnhandledExceptionFilter()函数设定的进程异常处理

  • 用户没定义进程异常处理,则调用系统默认异常处理UnhandledExceptionFilter()

    UEF会根据注册表信息决定是否弹出报错对话框

附件:
0 条评论
某人
表情
可输入 255