汇编中的异常处理
汇编中的异常处理
1 计算机,程序,windows异常
计算机异常:
- 硬件层面:电源异常、时钟异常、I/O异常等、内存页非法访问等
- 操作系统层面: 进程切换、任务调度等
- 进程层面:除零异常、断点异常等
计算机异常:
- Interrupt :用于处理系统I/O产生的中断,通常是硬件异常
- Trap:用于调用系统内核中的功能
- Fault:危害程度较低的系统异常,操作系统将尝试修复,修复失败将转为Abort
- Abort:危害程度较高的系统异常,系统将放弃修复并直接终止程序运行
程序异常:
程序异常是程序运行过程中由于种种原因导致的意外情况,是程序本身可以处理的错误 ,也是程序必须处理、无法忽略的“错误
五种最具代表性的异常,在程序运行过程中最为常见
异常处理:
异常处理机制是用于处理程序异常状况的系统化处理方法,它使得程序代码更加简洁、干净,而且不容易漏掉代码中出现的异常,在常见的异常处理机制实现过程中,程序通过抛出异常 的形式将这些意外情况告诉上级调用者,系统会强制调用者对异常进行处理
为了优化异常处理过程,操作系统引进了异常处理机制,将这些不常见的分支归为异常,进行统一的异常处理,便于程序员在编写程序时将注意力集中于正常情况处理
Windows异常
Windows程序运行分为正常运行模式与调试运行模式两种情况,在不同的运行情况下,操作系统对程序的异常处理流程也有所不同
在程序正常运行的情况下,当异常发生时:操作系统会先将异常抛给进程处理,进程代码中如果存在具体的异常处理代码,则能顺利处理异常继续运行,如果没有,则操作系统启动默认异常处理,终止进程运行
在调试运行的情况下,当异常发生时:操作系统会先把异常抛给调试器进程,由调试人员进一步选择异常处理的方式,调试者在使用调试器处理被调程序异常时有两种方法
- 直接修改代码、寄存器、内存来修改异常
- 将异常抛给被调试程序处理
- 如果这两种方法无法处理异常,则操作系统会使用默认异常处理机制进行处理,终止被调试程序,同时结束调试
2 SEH结构化异常处理机制
在Windows操作系统中,异常处理机制由结构化异常处理机制(SEH)来实现
SEH链
结构化异常处理是Windows操作系统默认的异常处理机制,在程序源码中使用__try、__except等关键字来实现,从数据结构上来看,SEH以链表的形式存在,称为 SEH链
SEH链中的每个节点都是一个_EXCEPTION_REGISTRATION_RECORD结构体,称为异常处理器
- 指明下一个异常处理器的位置
- 提供用于处理异常的代码(Handler成员是一个函数指针,指向异常处理函数,异常处理函数是一个回调函数,由系统调用)
异常处理函数有四个参数,用来传递与异常相关的信息,包括异常类型、发生异常的代码地址、异常发生时CPU寄存器的状态等
异常处理函数返回一个名为EXCEPTION_DISPOSITION 的枚举类型,用于告知系统异常处理完成后程序应如何继续运行
系统在使用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中编写异常处理代码,语法如下所示
汇编语言层面,异常处理器的添加有以下三个步骤:
- 编写异常处理函数
- 构造异常处理器
- 将异常处理器从表头添加到SEH链
首先,将异常处理函数地址压栈,然后,将SEH链表表头地址压栈,此时,ESP指向了新构建的异常处理器地址,因此将表头地址FS:[0]修改为ESP地址
3 SEH反调和逆向
SEH反调:
SEH反调试的两个层次:
-
利用SEH对代码片段的非常规链接方式(相比于跳转和函数调用)实现反调
单纯利用调试者的SEH知识盲点,将有效代码放入异常处理函数中,在调试者不对SEH调试的条件下,实现对有效代码的隐私保护
添加一些混淆和跳转
-
利用程序在正常运行与调试运行的不同工作模式, 实现反调试
基于Windows提供的系统函数实现反调试,在调试运行的模式下,某些异常处理函数不会被访问,实现对有效代码的隐私保护
为避免在调试状态被发现隐藏于异常处理函数中的有效代码,可使用以下两个系统函数:
使用函数UnhandledExceptionFilter可以设置系统默认的异常处理器
该函数只有在非调试状态下才会被调用,将异常处理器设置为系统默认的异常处理器,使异常发生时直接使用默认异常处理器进行处理
使用函数SetUnhandledExceptionFilter设置自定义的异常处理器
该函数只有在非调试状态下才会被调用,该函数将默认异常处理器设置为自定义的异常处理器
实验一:使用c语言添加SEH
在使用关键字添加SEH时,由于编写者没有给出自定义的异常处理函数,编译器会生成一个特殊的函数,这个函数负责将_except中的代码注册为包装后的异常处理函数
因为被包装了,因此在SEH中,使用__try、 __except没有直接显示出添加的异常处理函数,而是显示为外面包着的那个特殊函数
- 编译器不会直接将
__except
块的代码作为异常处理函数,而是将它“包装”到一个特殊的函数中。这个特殊函数是由编译器生成的,用于处理__try/__except
的执行逻辑。 - 这种设计的原因在于,编译器需要:
- 将程序员编写的
__try/__except
代码与底层SEH机制连接起来。 - 在
__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;
}
在编译器层面,可能会被转换为:
- 一个异常处理函数,包含
__except
块的内容。 - 一个底层代码,用于注册和管理异常处理链(SEH链)。
因此,调试器在查看SEH链时,通常不会直接看到__except
块的逻辑,而是看到包装后的特殊函数。这种间接性使得分析变得困难,增加了反调试的效果。
实验二:使用内联汇编块注册SEH
实验要求使用内联汇编块,注册自定义的异常处理函数,在异常处理函数中实现口令匹配功能
因为一般调试人员不调试SEH链,这是利用SEH实现反调功能的一种方式
在利用SEH实现反调功能时,程序真正的功能隐藏在异常处理函数中实现,并故意在主逻辑中触发异常来调用异常处理函数
避免退出:在异常处理函数的代码中,当完成密码比较后,调用了 exit(0);
来正常退出程序。但在一些反调试或防止调试分析的场景中,这种方式可以避免程序直接退出,从而进入一个循环触发异常并进行异常处理。
这是最简单的反调方式之一,一旦分析者对SEH进行分析, 就暴露了,这个简单的隐藏在一般不调试的SEH链中
通常情况下,为了反调,程序员一般不会采用内联汇编方式主动注册异常处理函数,通过内联汇编方式主动注册的异常处理函数会直接显示在SEH链中,很容易被发现
而像使用_try、_except添加的异常处理代码不会直接显示在SEH链中,而是编译器编译后添加到SEH链中,隐藏性高,这样会给调试带来一定的难度,从而达到反调的效果
当我们在代码中使用
_try
和_except
来实现异常处理,编译器会自动将这部分代码转换成更底层的SEH 结构,并将相应的异常处理函数嵌入 SEH 链中。SEH 链是 Windows 用来管理异常处理函数的一个链表结构
为什么__try/__except
的异常处理函数隐藏性更高?
- 使用
__try/__except
时,异常处理函数是由编译器生成的,不是程序员直接定义的。 - 编译器生成的包装函数通常结合了一些底层逻辑,使其不容易被调试器识别为异常处理函数。
- 相比之下,手动注册的异常处理函数(例如通过内联汇编注册)会直接显示在SEH链中,暴露了具体的异常处理逻辑,容易被调试器发现和分析。
4 其它
SEH的核心组件
- 异常记录(Exception Record):描述异常的类型和详细信息。
- 上下文记录(Context Record):保存程序状态(如寄存器值、堆栈指针等),供异常处理程序使用。
- 异常处理程序(Handler):定义如何处理捕获到的异常。
SEH的工作原理
- 每个线程都有一个链式异常处理表,称为SEH链。
- 当异常发生时,系统遍历SEH链,找到最合适的处理程序。
- 如果没有处理程序能够处理异常,则调用默认异常处理程序,通常会导致程序崩溃。
使用内联汇编块注册SEH
内联汇编可以手动设置 SEH,主要涉及对线程的 SEH 链进行操作。以下是通过内联汇编实现 SEH 的步骤:
步骤
- 在栈上定义异常处理程序结构。
- 修改 SEH 链,将自定义异常处理程序注册到链表中。
- 在异常发生后,清理异常处理程序并恢复原始链表。
#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)!