16.4 异常处理机制

异常就是在程序运行过程中产生的错误。OllyDBG利用异常机制捕获调试程序在运行过程中产生的异常,对异常进行排查,从而实现断点功能,使程序暂停运行。OllyDBG将异常处理过程放置在一个大消息循环中,具体如代码清单16-9所示。

代码清单16-9 异常处理过程分析—IDA分析


loc_439077:;异常处理循环起始地址

;部分与异常处理无关的代码分析略

loc_439616:

00439616 push 0

00439618 push offset DebugEvent;DEBUG_EVENT结构指针,记录异常信息

;等待调试事件,用于捕获调试进程的异常信息

0043961D call WaitForDebugEvent

00439622 test eax, eax;检查异常信息

00439624 jnz short loc_43966A;若成功,则跳转

;部分代码分析略

loc_43966A:;地址标号,异常处理部分

0043966A push offset DebugEvent;压入异常信息结构体指针

0043966F call sub_496B4C;调用插件异常处理函数

00439674 pop ecx

00439675 mov ecx, DebugEvent.dwProcessId;获取异常类型

0043967B cmp ecx, dword_4D5A70;检查是否为被调试程序所抛出的异常

00439681 jz short loc_4396D9;如果是,则跳转到异常处理部分

;非调试程序的异常信息,重新设置相关的异常信息

00439683 mov eax, DebugEvent.dwProcessId

00439688 push eax

00439689 mov edx, DebugEvent.dwDebugEventCode

0043968F push edx;arglist

00439690 lea ecx,[esi+0D86h]

00439696 push ecx;format

00439697 push 0;char

00439699 push 0;int

0043969B call_Addtolist

004396A0 add esp,14h

004396A3 cmp DebugEvent.dwDebugEventCode,1;检查是否为异常调试事件

004396AA jnz short loc_4396BC;如果不是,则跳转

;等待状态宏:STATUS_WAIT_0

004396AC cmp dword ptr DebugEvent.u+50h,0;检查等待状态是否为0

004396B3 jz short loc_4396BC;如果是等待状态,则跳转

;异常不忽略宏:DBG_EXCEPTION_NOT_HANDLED

004396B5 mov ebx,80010001h;设置异常状态为:不忽略

004396BA jmp short loc_4396C1

loc_4396BC:;地址标号,异常忽略宏:DBG_CONTINUE

004396BC mov ebx,10002h;设置异常状态为:忽略

loc_4396C1:;检查异常是否被忽略处理

004396C1 push ebx;dwContinueStatus

004396C2 mov eax, DebugEvent.dwThreadId

004396C7 push eax;dwThreadId

004396C8 mov edx, DebugEvent.dwProcessId

004396CE push edx;dwProcessId

004396CF call ContinueDebugEvent;继续执行调试程序

004396D4 jmp loc_439077;跳转回循环起始处,继续检查调试事件

loc_4396D9:;OllyDBG异常断点处理部分

;异常信息检查部分略

loc_439764:;地址标号

00439764 xor eax, eax;int

00439766 xor edx, edx;int

00439768 mov dword_4D8130,eax

0043976D lea ecx,[ebp+var_54];int

00439770 mov byte_4E3A20,0

00439777 mov dword_4E3B54,edx

0043977D push ecx;int

0043977E call sub_42EBD0;此函数为OllyDBG的三种异常断点处理部分

;其余代码分析略


根据代码清单16-9对异常循环处理过程的粗略分析,最终找到了对三种断点产生的异常进行处理的函数sub_42EBD0。sub_42EBD0函数运行前所需工作流程如下:

进入消息循环,这里的分析略。

利用WaitForDebugEvent函数捕获异常信息,如果捕获失败,则回到循环起始处。

捕获到异常,率先由OllyDBG插件进行异常处理。

检查是否为调试异常,如果不是,则继续执行程序,回到循环起始处。

如果是调试异常,则进行相关检查,进入断点异常处理函数中。

当进入最后一步时,程序已经被成功断下,调试程序处于挂起状态,等待调试者的处理。函数sub_42EBD0完成断点触发过程,将这个函数重新命名为BreakpointDebugEvent,分析如代码清单16-10所示。

代码清单16-10 函数BreakpointDebugEvent分析—IDA分析


BreakpointDebugEvent proc near;函数入口

;局部变量标号、参数标号分析略

0042EBD0 push ebp

0042EBD1 mov ebp, esp

0042EBD3 add esp,0FFFFF004h

0042EBD9 push eax

0042EBDA add esp,0FFFFF500h

0042EBE0 push ebx

0042EBE1 push esi

0042EBE2 push edi

0042EBE3 mov esi, DebugEvent.dwThreadId

0042EBE9 push esi

;此函数完成线程环境信息的获取,获取线程信息的API为GetThreadContext

;存放线程信息的结构为CONTEXT,详情可查看MSDN帮助文档

0042EBEA call sub_42E44C;此函数分析略

0042EBEF mov edi, eax

0042EBF1 mov eax,[ebp+arg_0]

0042EBF4 pop ecx

0042EBF5 mov[eax],edi

0042EBF7 mov edx, DebugEvent.dwDebugEventCode;获取调试状态

0042EBFD cmp edx,9;检查switch边界,一共9个case语句块

0042EC00 ja loc_4313F4;default语句块的首地址

0042EC06 jmp ds:off_42EC0D[edx*4];获取case地址表中的case块地址,并跳转

;case地址表中的各个地址标号,每一个标号对应各种调试事件的处理代码首地址

off_42EC0D:

0042EC0D dd offset loc_4313F4;default语句块首地址

0042EC0D dd offset loc_42EC35;EXCEPTION_DEBUG_EVENT

0042EC0D dd offset loc_430CFF;CREATE_THREAD_DEBUG_EVENT

0042EC0D dd offset loc_430DD7;CREATE_PROCESS_DEBUG_EVENT

0042EC0D dd offset loc_430F3F;EXIT_THREAD_DEBUG_EVENT

0042EC0D dd offset loc_431037;EXIT_PROCESS_DEBUG_EVENT

0042EC0D dd offset loc_43112D;LOAD_DLL_DEBUG_EVENT

0042EC0D dd offset loc_4311B7;UNLOAD_DLL_DEBUG_EVENT

0042EC0D dd offset loc_431276;OUTPUT_DEBUG_STRING_EVENT

0042EC0D dd offset loc_4313C7;RIP_EVENT

;以上为调试状态检测,这里只关心EXCEPTION_DEBUG_EVENT

;异常的处理工作将在此语句块内完成,进入case语句块中,代码如下

loc_42EC35:;地址标号,EXCEPTION_DEBUG_EVENT对应case块

0042EC35 mov ecx, dword_4E360C;jumptable 0042EC06 case 1

0042EC3B xor eax, eax

0042EC3D mov[ebp+var_14],ecx

0042EC40 mov dword_4E360C, eax

0042EC45 mov[ebp+var_5C],offset DebugEvent.u

0042EC4C test edi, edi;检查主线程中是否存在当前寄存器的信息

0042EC4E jnz short loc_42EC5D;若存在,则跳转

;部分代码分析略

loc_42EC5D:

0042EC5D mov eax,[edi+2Ch]

0042EC60 mov[ebp+arglist],eax;eax中保存当前eip

0042EC63 cmp[ebp+var_14],0;检查异常标识

0042EC67 mov ebx,[edi+10h]

0042EC6A jz short loc_42EC7F;跳转到异常类型检查

;部分代码分析略

loc_42EC7F:;地址编号,异常类型检查

0042EC7F mov eax,[ebp+var_5C];获取异常类型

;检查异常类型是否为EXCEPTION_BREAKPOINT

0042EC82 cmp dword ptr[eax],80000003h;INT3断点检查

0042EC88 jz short loc_42EC91;跳转到INT3断点处理


根据代码清单16-10的分析,异常处理首先要检查调试事件类型,如果调试信息为异常,则进入异常处理部分,判断异常类型,先判断异常是否为INT3断点所产生的,如果是,则通过跳转指令执行地址标号short loc_42EC91所对应的代码。因此,首先对INT3断点的捕获过程进行分析,如代码清单16-11所示。

代码清单16-11 INT3断点捕获过程—IDA分析


loc_42EC91:

0042EC91 push 2;char

0042EC93 push 1;n

0042EC95 mov ecx,[ebp+arglist]

0042EC98 dec ecx;在ecx中保存eip信息,执行减1操作,使执行指令回退1

0042EC99 push ecx;arglist

0042EC9A lea eax,[ebp+src];保存读取信息

0042EC9D push eax;src

0042EC9E call_Readmemory;读取eip指向的地址处的内存信息

0042ECA3 add esp,10h

0042ECA6 cmp eax,1;检查是否读取成功

0042ECA9 jz short loc_42ECB2;若读取成功,则跳转

0042ECAB xor edx, edx

0042ECAD mov[ebp+var_24],edx

0042ECB0 jmp short loc_42ED0A;跳过检查,执行INT3断点处理

loc_42ECB2:;地址标号,检查是否为INT3断点

0042ECB2 xor eax, eax

0042ECB4 mov al,[ebp+src];获取eip指向的地址处的机器代码

0042ECB7 cmp eax,0CCh;检查机器代码是否为0xCC

0042ECBC jnz short loc_42ECC7;若机器代码不等于0xCC,则跳转

0042ECBE mov[ebp+var_24],1;设置调试程序指令的回溯长度为1

0042ECC5 jmp short loc_42ED0A;跳过检查,进行INT3断点处理

loc_42ECC7:;地址标号,检查是否为INT3断点

0042ECC7 cmp eax,3;检查获取的机器代码是否为0x03

0042ECCA jz short loc_42ECD3;如果是,则跳转

0042ECCC xor edx, edx

0042ECCE mov[ebp+var_24],edx;设置调试程序指令的回溯长度为0

0042ECD1 jmp short loc_42ED0A;跳过检查,进行INT3断点处理

.loc_42ECD3:;循环起始点地址标号

0042ECD3 push 2;char

0042ECD5 push 1;n

0042ECD7 mov ecx,[ebp+arglist]

0042ECDA sub ecx,2;在ecx中保存eip信息,执行减1操作,让执行指令回退2

0042ECDD push ecx;arglist

0042ECDE lea eax,[ebp+src];保存读取信息

0042ECE1 push eax;src

0042ECE2 call_Readmemory

0042ECE7 add esp,10h

0042ECEA cmp eax,1;检查读取结果

0042ECED jnz short loc_42ECFC;读取失败跳转

0042ECEF xor edx, edx

0042ECF1 mov dl,[ebp+src]

0042ECF4 cmp edx,0CDh;检查读取结果是否为0xCD

0042ECFA jz short loc_42ED03;若读取结果为0xCD,则执行跳转

;检查INT3断点失败,进入流程loc_42ED0A,非INT3断点EIP无需调整

;设置eip回溯值为0,此段代码分析略

loc_42ED03:;地址标号,调整eip的回溯值为2

0042ED03 mov[ebp+var_24],2

loc_42ED0A:

0042ED0A mov eax,[ebp+var_24];获取eip的回溯值

0042ED0D sub[ebp+arglist],eax;回溯指令码,得到正确的断点地址

0042ED10 mov edx, dword_4D5708

0042ED16 cmp edx,[ebp+arglist];检查当前保存的断点是否正确

0042ED19 jnz short loc_42ED23;如果正确,则不跳转;如果不正确,则修正

;以上代码的功能为获取正确的断点地址,此时调试程序已经被断下,部分代码分析略


经过代码清单16-11的处理后,OllyDBG将调试程序停留在正确的INT3断点处,在显示反汇编代码的过程中,没有直接显示断点处机器码0xCC或0xCD,而是通过查找断点信息表中所对应的原机器码的信息来进行显示,以防止因修改指令造成的指令混乱。

在调试人员对OllyDBG发出再次运行的指令后,OllyDBG将会先修复INT3断点处的内存数据,然后再次运行修复后的指令代码。INT3断点处的指令被执行后,此处将会被再次设置为INT3断点,其代码分析略。

前面分析了INT3断点的异常捕获过程,接下来分析内存断点的异常捕获过程。如果检查INT3断点失败,则会开始内存断点的异常检查,具体分析如代码清单16-12所示。

代码清单16-12 内存断点异常捕获—IDA分析


loc_42ED39:;地址标号,异常类型检查

0042ED39 mov edx,[ebp+var_5C]

0042ED3C mov ecx,[edx]

0042ED3E cmp ecx,0C000008Fh;EXCEPTION_FLT_INEXACT_RESULT

;由于内存断点通过修改内存属性来制造异常,因此可以直接查找到内存访问异常处

;部分异常比较代码分析略,由于之前对ecx执行了sub ecx,80000001h操作

;此处实际是在检查异常类型EXCEPTION_ACCESS_VIOLATION=0xC0000005

0042ED76 sub ecx,40000001h;内存读、写错误

0042ED7C jz loc_42FF94;进入异常处理部分

;部分代码分析略

loc_42FF94:;地址标号,内存访问异常处理

0042FF94 mov eax,[ebp+arglist]

0042FF97 push eax

0042FF98 call_Findmodule;查找模块信息

0042FF9D pop ecx

0042FF9E mov[ebp+var_54],eax;获取模块首地址

0042FFA1 mov eax,[ebp+var_5C]

0042FFA4 mov edx,[ebp+var_5C]

0042FFA7 cmp dword ptr[eax+10h],2;检查模块中的ExceptionFlags是否大于2

0042FFAB mov edi,[edx+18h]

0042FFAE jb loc_430419

0042FFB4 cmp dword_4D8140,0;检查内存断点长度是否为0

0042FFBB jz loc_430419

0042FFC1 cmp dword_4D5700,0

0042FFC8 jz loc_430419

0042FFCE cmp edi, dword_4D8144;异常地址值低于断点内存页首地址值

0042FFD4 jb loc_430419

0042FFDA cmp edi, dword_4D8148;异常地址值高于断点内存页尾地址值

0042FFE0 jnb loc_430419

0042FFE6 or dword_4D5774,20h

0042FFED lea edx,[ebp+buffer]

0042FFF3 push edx;dest

0042FFF4 or dword_4D5710,2

0042FFFB mov ecx,[ebp+arglist]

0042FFFE push ecx;arglist

0042FFFF call_Readcommand;读取调试程序异常处内存数据

00430004 add esp,8

00430007 mov[ebp+var_38],eax

0043000A cmp[ebp+var_38],0;检查成功读取到的内存数据长度

0043000E jbe short loc_430038;若等于0,则进入错误处理

;部分代码分析略

;将读取的机器码数据进行反汇编分析,转换成对应的汇编代码

0043002B call_Disasm

00430030 add esp,1Ch

00430033 mov[ebp+var_C],eax;保存反汇编数据长度

00430036 jmp short loc_43003D;跳过读取内存错误处理部分

;错误处理分析略

loc_43003D:;地址标号

.0043003D cmp[ebp+var_C],0;检查反汇编数据长度

.00430041 jle loc_4301E1;若为0,则进入错误处理

.00430047 mov ecx,[ebp+arglist];获取异常首地址

.0043004A add ecx,[ebp+var_C];异常首地址加异常断点长度

.0043004D cmp ecx, dword_4D813C;dword_4D813C中保存了内存断点的首地址

;异常地址是否低于内存断点地址,若是,则跳到异常处理

.00430053 jbe loc_4301E1

00430059 mov eax, dword_4D813C

0043005E add eax, dword_4D8140

00430064 cmp eax,[ebp+arglist]

;比较内存断点范围是否小于等于异常地址,若是则跳到异常处理

00430067 jbe loc_4301E1

;检查内存断点标记,跳转到响应处理流程

0043006D cmp dword_4D8138,0

00430074 jz loc_4301BD;进入错误处理

;相关模块信息检查分析略

004300C0 jz short loc_4300CC;跳过错误处理

004300C2 mov eax,2;设置返回值

004300C7 jmp loc_431425;结束处理

;部分代码分析略

loc_43012F:;地址标号

0043012F push 0

00430131 push 0

00430133 push 0

00430135 call_Setmembreakpoint;清除内存断点

0043013A add esp,0Ch

0043013D cmp[ebp+var_54],0;检查是否清除成功

00430141 jz short loc_4301B4;若清除失败,则进入错误处理

;部分代码分析略

loc_430183:

00430183 cmp dword_4D920C,0

0043018A jz short loc_4301B4

0043018C mov ecx,[ebp+var_54]

0043018F test byte ptr[ecx+8],4

00430193 jz short loc_4301B4

00430195 push 0

00430197 mov eax,[ebp+var_54]

0043019A mov edx,[eax+0Ch]

0043019D push edx;检查断点地址

0043019E call_Finddecode;查找断点所在代码区的位置

;部分代码分析略

loc_4301B4:;地址标号

004301B4 xor eax, eax

004301B6 mov dword_4D8138,eax;设置断点表的第一个变量为0

004301BB jmp short loc_4301D2;跳转到short loc_4301D2调整优先级

;设置优先级,结束函数调用部分的代码分析略


代码清单16-12展示了内存断点的触发过程。回顾内存断点的设置过程,其实现原理为通过修改内存属性来达到触发异常的目的。因此,内存断点的触发便是内存访问类错误,其处理流程如下:

(1)得到线程信息;

(2)跳转到相应的异常处理分支中;

(3)若得到线程信息,则根据线程信息的eip进行赋值,否则根据异常地址进行赋值;

(4)得到异常所处的模块的信息,并解析反汇编信息,以进行相关检查;

(5)若模块为自解压(SFX)模式,则进行相应的检查以及错误处理;

(6)检查内存断点是否在kernel32.dll中,弹出提示窗口,并将断点去除;

(7)最后调整优先级并退出。

硬件断点的捕获过程是由调试寄存器来完成的,因此OllyDBG没有捕获处理过程。到此,三种断点的触发过程就分析完了。本节只是对断点异常处理的过程进行了简略分析,处理过程中的许多细节并没有给出详细的分析和讲解,大家应亲自动手分析,以便加深对这些知识的理解。

掌握了断点的设置与捕获流程,就可以实现MyOllyDBG的基本功能。但是,如何加载程序并进行调试分析呢?这将是16.5节要讲解的内容。