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节要讲解的内容。