SEH

tech2024-07-17  59

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

SEH练习示例 #1

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

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

异常

操作系统定义的异常。 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详细说明

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)-> ©的顺序依次传递,知道有异常处理器处理。 异常处理函数的定义 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链的下一个异常处理器。 TEB.NtTib.ExceptionList 通过TEB结构体的NtTib成员可用很容易地访问进程的SEH链,方法非常简单。 TEB.NtTib.ExceptionList = FS:[0]

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

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

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

简单练习

使用OllyDbg调试器打开seh.exe程序,然后在Exception选项卡中进行相应的设置。 如上设置后,程序在调试运行时发生以上6种异常时,调试器会忽略,将它们直接派送给被调试者。在seh.exe程序发生的EXECEPTION_ACCESS_VIOLATION异常会有自身的SEH处理(调试过程不会暂停),按F9键运行程序,直接弹出“Debugger detected”。
最新回复(0)