自学内容网 自学内容网

汇编中的异常处理

汇编中的异常处理

1 计算机,程序,windows异常

计算机异常:

  • 硬件层面:电源异常、时钟异常、I/O异常等、内存页非法访问等
  • 操作系统层面: 进程切换、任务调度等
  • 进程层面:除零异常、断点异常等

计算机异常:

  • Interrupt :用于处理系统I/O产生的中断,通常是硬件异常
  • Trap:用于调用系统内核中的功能
  • Fault:危害程度较低的系统异常,操作系统将尝试修复,修复失败将转为Abort
  • Abort:危害程度较高的系统异常,系统将放弃修复并直接终止程序运行

程序异常:

程序异常是程序运行过程中由于种种原因导致的意外情况,是程序本身可以处理的错误 ,也是程序必须处理、无法忽略的“错误

五种最具代表性的异常,在程序运行过程中最为常见

image-20241025145541177

异常处理:

异常处理机制是用于处理程序异常状况的系统化处理方法,它使得程序代码更加简洁、干净,而且不容易漏掉代码中出现的异常,在常见的异常处理机制实现过程中,程序通过抛出异常 的形式将这些意外情况告诉上级调用者,系统会强制调用者对异常进行处理

为了优化异常处理过程,操作系统引进了异常处理机制,将这些不常见的分支归为异常,进行统一的异常处理,便于程序员在编写程序时将注意力集中于正常情况处理

Windows异常

Windows程序运行分为正常运行模式与调试运行模式两种情况,在不同的运行情况下,操作系统对程序的异常处理流程也有所不同

程序正常运行的情况下,当异常发生时:操作系统会先将异常抛给进程处理,进程代码中如果存在具体的异常处理代码,则能顺利处理异常继续运行,如果没有,则操作系统启动默认异常处理,终止进程运行

调试运行的情况下,当异常发生时:操作系统会先把异常抛给调试器进程,由调试人员进一步选择异常处理的方式,调试者在使用调试器处理被调程序异常时有两种方法

  1. 直接修改代码、寄存器、内存来修改异常
  2. 将异常抛给被调试程序处理
  3. 如果这两种方法无法处理异常,则操作系统会使用默认异常处理机制进行处理,终止被调试程序,同时结束调试

2 SEH结构化异常处理机制

在Windows操作系统中,异常处理机制由结构化异常处理机制(SEH)来实现

SEH链

结构化异常处理是Windows操作系统默认的异常处理机制,在程序源码中使用__try、__except等关键字来实现,从数据结构上来看,SEH以链表的形式存在,称为 SEH链

image-20241025161649783

SEH链中的每个节点都是一个_EXCEPTION_REGISTRATION_RECORD结构体,称为异常处理器

  • 指明下一个异常处理器的位置
  • 提供用于处理异常的代码(Handler成员是一个函数指针,指向异常处理函数,异常处理函数是一个回调函数,由系统调用)

异常处理函数有四个参数,用来传递与异常相关的信息,包括异常类型、发生异常的代码地址、异常发生时CPU寄存器的状态等

image-20241025161828336

异常处理函数返回一个名为EXCEPTION_DISPOSITION 的枚举类型,用于告知系统异常处理完成后程序应如何继续运行

image-20241025161858806

系统在使用SEH链时需要知道SEH链的地址,SEH链的头部地址储存在TEB(Thread Environment Block)中

  • TEB是线程描述块,存储着线程运行所需的各种信息,例如线程的空间大小、寄存器状态、堆栈地址等

  • 在TEB的第一个DWORD成员中存储着SEH链表头的地址, 系统就可以通过这个地址找到SEH链

当进程发生异常时,系统首先找出发生异常的线程, 并根据该线程TEB中的信息获得第一个异常处理器的地址

如果异常得到处理则程序继续运行,如果异常抛给操作系统,则系统会终止程序运行

进程中的每个线程都有自己的一个TEB。一个进程的所有TEB都以堆栈的方式,存放在从0x7FFDE000开始的线性内存中,每4KB为一个完整的TEB,不过该内存区域是向下扩展的。在用户模式下,当前线程的TEB位于独立的4KB段, 可通过CPU的FS寄存器来访问该段,一般存储在 [FS:0]

在SEH链中添加异常处理器:

C语言中,程序员只需要使用_try将要监视运行的代码包起来,在__except中编写异常处理代码,语法如下所示

image-20241025162656144

汇编语言层面,异常处理器的添加有以下三个步骤:

  1. 编写异常处理函数
  2. 构造异常处理器
  3. 将异常处理器从表头添加到SEH链

首先,将异常处理函数地址压栈,然后,将SEH链表表头地址压栈,此时,ESP指向了新构建的异常处理器地址,因此将表头地址FS:[0]修改为ESP地址

3 SEH反调和逆向

SEH反调:

SEH反调试的两个层次:

  • 利用SEH对代码片段的非常规链接方式(相比于跳转和函数调用)实现反调

    单纯利用调试者的SEH知识盲点,将有效代码放入异常处理函数中,在调试者不对SEH调试的条件下,实现对有效代码的隐私保护

    添加一些混淆和跳转

  • 利用程序在正常运行与调试运行的不同工作模式, 实现反调试

    基于Windows提供的系统函数实现反调试,在调试运行的模式下,某些异常处理函数不会被访问,实现对有效代码的隐私保护

    为避免在调试状态被发现隐藏于异常处理函数中的有效代码,可使用以下两个系统函数:

    使用函数UnhandledExceptionFilter可以设置系统默认的异常处理器

    该函数只有在非调试状态下才会被调用,将异常处理器设置为系统默认的异常处理器,使异常发生时直接使用默认异常处理器进行处理

    使用函数SetUnhandledExceptionFilter设置自定义的异常处理器

    该函数只有在非调试状态下才会被调用,该函数将默认异常处理器设置为自定义的异常处理器

实验一:使用c语言添加SEH

image-20241026113155441

在使用关键字添加SEH时,由于编写者没有给出自定义的异常处理函数,编译器会生成一个特殊的函数,这个函数负责将_except中的代码注册为包装后的异常处理函数

因为被包装了,因此在SEH中,使用__try、 __except没有直接显示出添加的异常处理函数,而是显示为外面包着的那个特殊函数

  • 编译器不会直接将__except块的代码作为异常处理函数,而是将它“包装”到一个特殊的函数中。这个特殊函数是由编译器生成的,用于处理__try/__except的执行逻辑。
  • 这种设计的原因在于,编译器需要:
    1. 将程序员编写的__try/__except代码与底层SEH机制连接起来。
    2. __except块中灵活地实现异常处理,例如根据返回值决定是否继续处理异常。

例如,下面的代码:

c复制代码#include <windows.h>
#include <stdio.h>

int main() {
    __try {
        int *ptr = NULL;
        *ptr = 10; // 引发异常
    }
    __except(EXCEPTION_EXECUTE_HANDLER) {
        printf("Exception caught!\n");
    }
    return 0;
}

在编译器层面,可能会被转换为:

  1. 一个异常处理函数,包含__except块的内容。
  2. 一个底层代码,用于注册和管理异常处理链(SEH链)。

因此,调试器在查看SEH链时,通常不会直接看到__except块的逻辑,而是看到包装后的特殊函数。这种间接性使得分析变得困难,增加了反调试的效果。

实验二:使用内联汇编块注册SEH

实验要求使用内联汇编块,注册自定义的异常处理函数,在异常处理函数中实现口令匹配功能

因为一般调试人员不调试SEH链,这是利用SEH实现反调功能的一种方式

image-20241026113729287

image-20241026113746479

在利用SEH实现反调功能时,程序真正的功能隐藏在异常处理函数中实现,并故意在主逻辑中触发异常来调用异常处理函数

避免退出:在异常处理函数的代码中,当完成密码比较后,调用了 exit(0); 来正常退出程序。但在一些反调试或防止调试分析的场景中,这种方式可以避免程序直接退出,从而进入一个循环触发异常并进行异常处理。

这是最简单的反调方式之一,一旦分析者对SEH进行分析, 就暴露了,这个简单的隐藏在一般不调试的SEH链中

通常情况下,为了反调,程序员一般不会采用内联汇编方式主动注册异常处理函数,通过内联汇编方式主动注册的异常处理函数会直接显示在SEH链中,很容易被发现

而像使用_try、_except添加的异常处理代码不会直接显示在SEH链中,而是编译器编译后添加到SEH链中,隐藏性高,这样会给调试带来一定的难度,从而达到反调的效果

当我们在代码中使用 _try_except 来实现异常处理,编译器会自动将这部分代码转换成更底层的SEH 结构,并将相应的异常处理函数嵌入 SEH 链中。SEH 链是 Windows 用来管理异常处理函数的一个链表结构

为什么__try/__except的异常处理函数隐藏性更高?

  1. 使用__try/__except时,异常处理函数是由编译器生成的,不是程序员直接定义的。
  2. 编译器生成的包装函数通常结合了一些底层逻辑,使其不容易被调试器识别为异常处理函数。
  3. 相比之下,手动注册的异常处理函数(例如通过内联汇编注册)会直接显示在SEH链中,暴露了具体的异常处理逻辑,容易被调试器发现和分析。

4 其它

SEH的核心组件

  1. 异常记录(Exception Record):描述异常的类型和详细信息。
  2. 上下文记录(Context Record):保存程序状态(如寄存器值、堆栈指针等),供异常处理程序使用。
  3. 异常处理程序(Handler):定义如何处理捕获到的异常。

SEH的工作原理

  1. 每个线程都有一个链式异常处理表,称为SEH链
  2. 当异常发生时,系统遍历SEH链,找到最合适的处理程序。
  3. 如果没有处理程序能够处理异常,则调用默认异常处理程序,通常会导致程序崩溃。

使用内联汇编块注册SEH

内联汇编可以手动设置 SEH,主要涉及对线程的 SEH 链进行操作。以下是通过内联汇编实现 SEH 的步骤:

步骤

  1. 在栈上定义异常处理程序结构。
  2. 修改 SEH 链,将自定义异常处理程序注册到链表中。
  3. 在异常发生后,清理异常处理程序并恢复原始链表。
#include <windows.h>
#include <stdio.h>

// 自定义异常处理程序
LONG WINAPI MyExceptionHandler(EXCEPTION_POINTERS *ExceptionInfo) {
    printf("Exception caught! Code: 0x%X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
    return EXCEPTION_EXECUTE_HANDLER; // 继续异常处理流程
}

int main() {
    __asm {
        // 在栈上定义异常处理结构
        push MyExceptionHandler     // 指向自定义异常处理程序的指针
        push fs:[0]                 // 保存旧的 SEH 链表头
        mov fs:[0], esp             // 设置新的 SEH 链表头
    }

    // 触发异常
    int *ptr = NULL;
    *ptr = 10;

    __asm {
        // 恢复旧的 SEH 链表
        mov eax, fs:[0]             // 获取当前 SEH 链表头
        mov fs:[0], [eax+4]         // 恢复到旧的 SEH 链表头
        add esp, 8                  // 清理异常处理结构
    }

    return 0;
}

清理异常处理程序并恢复原始链表

SEH链的管理

  • SEH链是一个栈式结构,每次注册异常处理程序时,新的处理程序会作为链表的头部。
  • 异常处理完毕后,需要将当前异常处理程序从链表中移除,以恢复到之前的状态。

原文地址:https://blog.csdn.net/LH1013886337/article/details/143835207

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!