第25章-异常处理本章,我们将介绍异常处理,这一块通常是初学者的绊脚石,但是如果研究深入一点的话,你会发现它并不难。那么异常是如何产生的呢?当处理器执行了一个错误的操作的时候,程序中就会产生异常。好了,我们来看几个异常例子吧,我们用OD打开cruehead’a的CrackMe。可以看到断在了入口点处,我们在第一行输入会引发异常的指令,这里我们采用MrSilver写的异常例子中指令。内存访问异常:当线程中尝试访问没有访问权限的内存的时候会发生该类异常。例如:一个线程尝试向只具有读权限的内存写入数据的时候就会产生内存访问异常。我们在OD中输入如下指令:这里我们可以看到401057开始的内存单元只具有读取和执行权限,并不具有写入权限。因此如果尝试向该内存单元写入数据的话就会产生异常,我们按F8键。程序会根据PE头中的相关信息设置区段的初始权限,当然也可以使用诸如VirutalProtect这类API函数在运行时修改权限。那么在OllyDbg中我们如何查看各个区段的初始权限呢,还有就是如何修改这些权限呢?我们可以通过单击工具栏中的M按钮来查看各个区段的情况。我们可以看到主模块的所在的区段开始于400000,首先是PE头,占1000个字节,PE头中保存了各个区段的名称,长度以及程序运行所必须的一些信息。我们在数据窗口中定位到PE头。由于PE头开始于400000,所以我们输入400000。其实OllyDbg中有一个可以解析PE头的各个字段的选项,我们可以在数据窗口中单击鼠标右键。我们可以看到显示出了DOS头的各个字段信息。如果我们继续往下看,我们首先会看到OffsettoPEsignature,这是告诉我们PE头的偏移量,我们可以看到偏移量为100。那么起始地址400000加上100就是400100,我们定位到400100处。这里你所看到的,这里是关于程序的重要信息。下面让我们来看看部分字节详细的解释。也就是说基地址400000加上1000就是程序的入口点。如果你想修改入口点的话,例如把入口点修改为2000,我们可以在入口点这一行上单击鼠标右键。这里我们可以输入任意数值,例如:如果你想让程序从402000处开始执行,我们可以输入2000。这个2000是相对于映像基址400000的偏移量。修改完毕以后,我们可以单击鼠标右键选择-Copytoexecutablefile,然后在弹出的窗口中单击鼠标右键选择-Savefile,这样就可以保存到文件了,我们并不修改入口点,只要知道可以这么做就行了。好了,我们继续往下看。下面各个区段的信息,首先我们看到的这个区段起始虚拟地址为1000,注意这里是偏移量,实际上内存地址为401000,并且Characteristics(特征)为CODE,EXECUTE,READ(代码段,可执行,可读)。如果我们想让该区段具有可写权限的话,我们可以将Characteristics这个字段的60000020修改为E0000020,这样该区段就具有了所有权限,嘿嘿,我们来验证一下。好了,我们现在来将修改保存到文件。我们将名称修改为CRACKME3,标识这个文件是修改版。好了,我们现在用OllyDbg打开这个CRACKME3.EXE。现在我们将数据窗口显示模式切换为正常模式。我们按F8键单步,会发生并没有产生异常,EAX的值被成功写入了401057内存单元中了。下面介绍另外一种异常:除0异常:试图除以0时会产生该异常。例如,我们在OllyDbg中输入如下指令:(PS:原作者这个地方讲错了,他是直接DIVECX,ECX这个时候并不为零,EAX为零,EDX指向了7C92E514,所以按照作者的做法,只会产生整数溢出异常,并不会产生除0异常。)寄存器的如下:好了,我们F8单步一次。寄存器的情况如下:这里时候除数ECX为0了。我们继续F8单步。我们可以看到提示Integerdivisionbyzero(整数除0)异常。无效指令,尝试执行特权指令异常:当CPU试图执行越权指令的话就会产生该异常。由于OllyDbg不允许我们输入CPU不可识别的指令,所以我们无法验证。但是程序员可以自己设计一些处理器并不支持的指令,当执行到指令时显示相应的错误即可。最为典型就是INT3指令,INT3指令会产生一个异常,并且该异常会被调试器捕获到,比如,我们可以设置BPX断点来让程序中断下来,然后就可以对该程序进行相应的控制了。另外,有一些程序会直接写入INT3指令,所以说INT3产生的异常是最常见的。其实还有很多其他的异常,这里我们就不一一介绍了。下面我们看一个简单的例子。现在我们只知道该程序会产生异常,但是到底会产生哪种异常呢?我们现在先来看看下面这个示意图:这个图是我从MrSilver的教程中截取出来的,这里我们可以看到一个异常是被处理的流程,首先系统会判断当前进程是否正在被调试。根据上图来看可以知道异常发生后,如果当前程序正在被调试的话,那么此时控制权就会交予调试器。如果调试器的调试选项勾选了跳过对应的异常类型的话,这个时候控制权又会重新归还给当前程序,如果没有勾选跳过对应的异常的话,那么我们就需要按Shift+F9键来跳过该异常并将控制权交予程序了。但是控制权交予程序以后的流程该如何走上图中并没有标注出来。所以我们接着来看下面的流程图。这里我补全了整个流程图,见上图的红色箭头。接下来我们可以看到,当前控制权由调试器归还给程序以后,系统会检查当前程序是否安装了SEH,如果安装了SEH,就转向SEH的异常处理程序执行,如果没有安装SEH,就会调用系统默认的异常处理程序。以上介绍听起来可能有点复杂,其实并不复杂,我们再来详细介绍一下什么是SEH。什么是SEH呢?SEH或者结构化异常处理,它是用来确保该程序可以从错误中恢复,也就是说,如果你没有设置SEH,那么当程序中有异常发生时,程序就会弹出一个错误信息框,告诉我们程序即将关闭。如果我们设置了SEH的话,异常处理程序就能够捕获到程序中发生的异常,进行相应的处理后,就会把控制权重新交予程序继续执行,程序并不会终止也不会弹出那个烦人的错误消息框。此外,我们需要知道每个线程都可以有自己的异常处理程序,如果当前异常处理程序不予处理的话,可以将异常将于SEH链中的其他异常处理程序来处理。如何定位异常处理程序好了,我们用OD重新加载cruehead’a的CrackMe。我们来看看堆栈的情况。这是系统默认安装的异常处理程序,无论什么异常交予该默认异常处理程序处理的话,它都会弹出错误消息框。现在我们来定位到该默认异常处理函数。我们看到,FS:[0]就是指向了当前异常处理程序。我们在数据窗口中定位到FS:[0]。这里我们可以看到FS:[0]指向的内存单元中内容是多少,可能不同的机器上这个值会不一样,我这里FS:[0]指向内存单元中保存的值是12FFE0。从堆栈中我们可以看到,12FFE0指向的是SEH链的最后一个结点,当前异常被交予该结点的异常处理程序的话,就会弹出一个我们熟悉的错误消息框。我们来查看一下SEH链的情况。我们可以看到只有系统默认的异常处理程序被安装了,当有多个异常处理程序的话,当捕获到异常的话,异常会依次由SEH链的顶部向底部传递。由于cruehead’a的CrackMe并没有安装自己的异常处理程序,所以这里我们再看另一个例子smartmouse111。我们用OD加载这个例子。你可以看到程序开始处在安装自己的异常处理程序,OllyBbg中也以注释标注出来了SEhandlerinstallation。我们到达OllyDbg提示SEhandlerinstallation(安装异常处理程序)指令处,首先是一个PUSH4066D8指令,当程序发生异常时,将会调用4066D8地址处的异常处理程序。执行PUSH指令后,4066D8被保存到堆栈中了。接下来一行是将FS:[0]的值保存到EAX中,我们在数据窗口中来看看FS:[0]指向的内存单元中保存的内容是多少。我们在数据窗口中定位到7FFDE000地址处。我们可以看到FS:[0]内存单元中的值为12FFE0。OD提示窗口中也显示了。我们执行这条指令。EAX的值变为了12FFE0,接着这个值被压入堆栈。我们可以看到之所以叫SEH链,因为它是一个链表,这里的12FFE0就指向了上一个异常处理程序。下面一行指令就是将FS:[0]的内容设置为ESP的内容,这样一个异常处理程序就被安装好了。我们执行这一行指令。这样12FFB0处就是我们安装的新的SEH结点了,也就是FS:[0]指向了我们新安装的SEH结点。OllyDbg也标注出来了,提示这是一个SEH结点,首先的4个字节的值指向了老的SEH结点,接下来的4个字节值即当前的异常处理程序入口地址。所以当该程序发生异常后,异常被处理流程如下:判断当前是否被调试,由于当前正在被调试,所以系统将控制权交予调试器,然后如果你勾选了忽略对应异常的选项的话(如果你没有勾选忽略对应异常选项的话,你也可以按Shift+F9键来忽略异常),那么控制权将重新交予程序,如图所示,接着判断是否安装了SEH,这里安装了,所以将会执行4066D8处的异常处理程序。我们来看看SEH链的情况:我们可以看到SEH链的顶部是程序自己安装的异常处理程序,接下来才是系统默认的异常处理程序,如果发生异常,应该是调用4066D8处异常处理程序,而并不是调用系统默认的异常处理程序,我们来手工制造一个异常试试。我们来将该行修改为如下指令:这样会产生一个异常,因为0地址不能写入。我们确保调试选项中忽略各类异常的选项没有被勾选,但是第一个选项还是要勾选的。我们运行起来。OD左下方显示错误,程序将被终止。现在程序继续执行的话可以尝试从错误中恢复。我们在4066D8指向的异常处理程序入口处设置一个断点。我们可以看到OD提示我们,按Shift+F9键可以忽略异常,继续执行程序,嘿嘿。我们可以看到断在了异常处理程序的入口处,我们运行起来看看程序会不会从错误中恢复过来。我们可以看到程序崩溃,弹出了错误消息框,这该程序表明调用了系统默认的异常处理程序。显示这个错误提示框是因为程序自己安装SEH异常处理程序中并没有修复刚刚那个异常,所以异常继续传递,最后交予了系统默认的异常处理程序,将弹出了一个错误消息框,程序就终止掉了。显然,程序自己安装的异常处理程序是用来处理别的类型的异常的,并不能处理向0地址处写入导致的异常。为了能看到异常被成功处理的效果,我们再来看一个例子SDUE1。我们用OD加载该程序,可以看到OD提示说该程序可能被加壳了。我们依然不勾选调试选项中的忽略各类异常的选项,除了忽略第一个异常以外。我们运行起来。我们可以看到发生了异常,断了下来。我们来看看异常处理程序在哪里。在你的机器上,这个地址可能会不一样,因为该地址属于一个动态创建的区段。我们现在来给该异常处理程序设置一个断点。我们定位到了该异常处理程序的入口地址,现在我们该它设置一个断点。我们按Shift+F9键运行起来。我们可以看到断在了异常处理程序的入口处,如果成功从异常中恢复了的话,那么程序将会从刚刚发生异常的指令的下一条指令处继续往下执行。我们给产生异常的指令的下一条指令设置一个断点,然后运行起来。我们可以看到程序继续执行起来了,并弹出提示错误消息框,说明异常已经成功被修复了。其实设置异常处理程序还可以使用SetUnhandledExceptionFilter这个API函数,可以通过其参数来设置异常处理程序的入口地址。好了,本章介绍了我们以后破解过程中会用到的一些知识点,下一章开始我们将介绍VB相关的内容。