我学习逆向工程已经有段时间了,个人认为crackme真是练手的好东西,它是逐级提高的,这就使得我们的学习可以很轻松的进行,对于我们这些刚开始学习逆向工程的来说这是一件好事,我们不用担心一开始就遇到一个很变态的逆向对象,花费了太多的时间却仍然是搞不懂,从而变得一蹶不振,白白浪费了时间和精力不说,更重要的是打击了我们这些人的学习积极性,使得我们对逆向工程失去了兴趣,这才是最致命的。
Crackme玩法的精髓就在于算法和反调试(对于我的粗陋见解请高手们不要见笑),算法要涉及到数学知识,我是典型的数学白痴,小学数学还算掌握的不错,但对于MD5、SHA、CRC等高级算法就是一窍不通了;相对于算法来说,反调试就有趣的多了,它能让你了解更多的系统底层知识,了解软件开发者是如,何想方设法去阻挡黑客来破解软件的,这次给大家带来的就是我与一个Anti-debug crackme斗智斗勇的故事,希望大家能够喜欢。
首先我们还是使用PEiD来查一下软件是否加了壳,根据PEiD的显示,程序没有加壳,是使用Borland C++编写的,接下来我们开始收集信息吧,打开crackme,随便输入一些内容,点击下面的“Try”,结果弹出了提示对话框。
这个crackme是有对话框的,上面还带有提示信息,接下来我们当然是使用字符串参考来看看能不能找到提示信息,但可惜的是没有找到,既然字符串参考不能使用,那我们就给显示对话框的函数下断点吧!在命令行里输入“bp MessageBoXA”下断,然后运行程序,随便填写内容,点击“Try”,我们断下来了吗?很抱歉,我们并没有断下程序,而且更悲剧的是程序自动退出了,我们的OD也消失了,这是怎么回事啊?难道程序长了眼腈,看到我们正在使用调试器调试它,因此它自己退出了?
一、干掉杀手线程
程序当然没有长眼睛,它只不过是利用了某种方法检测出了我们的调试器,为了防止被调试就退出来了。如果我们知道程序使用的是哪种检测方法,那么就可以绕过它,这样程序就不会退出了,但可惜的是我们也不知道,那么该如何防止程序退出呢?既然程序要退出,那么它就必定要调用实现退出功能的函数,可供程序作者使用的这类函数并不多,我们来看一下,它到底都调用了什么函数。点击右键,选择“搜索”——“当前模块的名称”,结果我们找到了这个函数,这个函数的作用就是终止进程,我们在这个函数上下断点,单击右键,选择在导入表上切换断点,这样就成功下断了。按F9运行程序,还是随便填写内容,点击“Try”,这回程序被成功的断下来了。
这里我们不能取消断点,然后按ALT+F9(执行到用户代码),那样的话程序就会退出来,我们仔细观察堆栈区,寻找是哪个函数调用这个函数的,应该是00401DD5这里的函数,我们前往表达式00401DD5,得到了如下的代码:
我们直接nop掉这5行代码,然后保存就可以解决程序退出的问题了。
二、拆除除零异常
解决掉前面的问题后,我们继续在命令行里输入"bp MessageBoXA”下断,并希望程序会被断在弹出对话框处,好把我们带入到关键代码处,然而结果却再一次的出乎了我们的意料,程序又出现了新的问题,程序出现了除零异常,也许你会这么想:“是不是我们刚才的修改导致了程序异常呢?”但答案是否定的,为什么呢?我们从软件开发者的角度来考虑这个问题,软件开发者在开发软件的时候必须要考虑软件的健壮性,也就是说他们要保证软件尽可能的不出现运行错误和异常,因为软件是要给客户使用的,如果你编写的软件经常出现问题那谁还会使用啊!但事实上谁也不能保证软件不出现异常,即便是软件的开发者,因此软件开发者就编写了异常处理机制,当软件出现异常的时候就调用这个异常处理机制来修复异常,以确保程序能够继续运行。这也就是说,即使程序出现了异常,程序自己也可以处理,不用我们来处理的,然而在这里程序却抛出了异常来让我们处理,很显然这是程序编写者的诡计,企图阻止我们调试的脚步,该如何解决这个问题呢?我们应该找到出现异常的代码,然后绕过它。如何寻找出现异常的代码呢?难道我们要按F7,F7,F7……一个小时,一整天,甚至更多的时间吗?这绝对不是一件有趣的事情,当然不可以,我们来寻找一个省时又省力的方法吧!
我们来考虑一下整个异常处理过程,程序出现了异常。然后执行被中断,调用异常处理机制来处理异常,异常被修复,程序从中断的地方开始继续执行。我们的问题也来了,程序是如何识别中断的地方呢?它是如何知道要从中断的地方继续执行呢?我从书上找到了答案,原来程序在将要处理异常时会将寄存器压入堆栈,等到异常处理完后,寄存器被恢复,程序继续执行。知道了这点后,我们也就清楚如何来寻找出现异常的代码了,因为EIP也被压入了堆栈,EIP指向的代码也就是出现异常的代码,因此我们就到堆栈中来寻找吧!等一下,你确定这样就可以找到吗?堆栈中的数据错综复杂,仅知道这点是不可能正确的找到出现异常的代码的,我们还需要做一件事,就是观察调试器的寄存器窗口,看看里面储存的数据都有什么特点。
通过仔细观察,我们发现段寄存器FS、ES、CS、SS、DS、GS中的数据是保持不变的,我们可以根据这个特点到堆栈中来寻找压入其中的异常代码,结果很轻松的就找到了我们想要的东西,这里的00401ACF就是被压入堆栈的EIP,也就是我们要寻找的出现异常的代码,我们前往表达式00401ACF就得到如下的代码:
这里程序先将ECX清零,然后进行除法运算,除零异常也就产生了,可以看出程序并不是偶然产生异常的,很显然是程序的编写者精心设计的,目的就是阻止我们调试程序,我们直接NOF掉00401ACC处这句代码然后保存,就可以绕过这里的异常了。
三、除掉ISDEBUGGERPRESENT
在处理掉前面的问题后,我们还没有解决掉所有的反调试,因为新的问题又出来了,这次程序的表现是不停的弹出对话框。这样疯狂的举动还真是少见,对于这个问题应该很好解决的,既然弹出了对话框,那我们就下断“bp MessageBoXA”,然后追踪是哪里调用了这个函数。
程序成功的断下后,取消断点,按ALT+F9(执行到用户代码)后,得到了如下的代码:
上面的这个CALL会调用显示对话框的函数MessageBoXA,我们把代码向上翻,看能不能找到绕过这个CALL代码。
00401ACF处的代码是不是很熟悉,它就是我们上面定位到的出现异常的代码,我们看到程序会执行call <jmp.&KERNEL32.lsDebuggerPresent>这句代码,那么IsDebuggerPresent这个函数都有什么作用昵?从它的英文字面意思来看,来者不善,我们还是查一查吧!根据我的查证,这个函数是微软封装提供给软件开发者用来检测调试器的,让我们再深入一点,了解一下它的内部原理,首先我们需要知道在进程结构中,有一个BOOLEAN变量,这个变量在这里很重要,因为它标志着当前的进程是否处于被调试状态。IsDebuggerPresent函数就是通过查看这个变量来检视调试器的,理解清楚了它的原理后,我们就来绕过这里的检测吧,只要把00401AED处的jnz改为无条件跳转jmp,就可以轻松的绕过了。
到此为止,我们就解决了所有的反调试问题,现在终于可以进行算法的分析了,这个crackme的算法非常简单,这里我就不啰嗦了。虽然我们突破了所有的反调试,但是我们弄清楚所有的问题了吗?如果你希望自己的技术能够快速的提高,那么就绝不能满足于突破了所有的反调试环节,关键在于我们还耍弄清楚所有的问题,这里很显然还遗留了一个问题,就是我们在分析程序为什么会莫名其妙的退出时所提出来的“程序是如何检测到我们的调试器的呢?”,在解答这个问题前,让我们来一起看看,看到堆栈窗口中的字符串“OLLYDBG.EXE”和反汇编窗口注释栏中的字符串“CreateToolhelp32Snapshot”、“Process32First”、“Process32Next”了吗?这就是程序检测调试器的经典方法,叫做检查父进程法。
因为一般情况下普通的win32进程的父进程都是Explorer.exe,如果不是的话,则很可能是被调试器加载了。这里程序先调用函数CreateToolhelp32Snapshot得到所有进程的列表快照,然后调用函数Process32First和Process32Next遍历进程,判断是否为自己的进程,再调用函数GetModuteFileNameEx得到父进程名,比较当前父进程是否为Explorer.exe,如果不是Exploner.exe,则就是调试器,程序就会调用TerminateProcess退出。本文中的这个crackme程序不仅采用了这种方法检测OLLYDBG.EXE,还检测了TRW.EXE、Winlce.exe、DeDe.exe等调试。现在我们总算是把问题都弄清楚了,也学习到了许多新知识,文章到这儿就该结束了,最后奉上我真诚的祝福,祝愿所有朋友们,早日都能成为高手,一起努力吧!本文内容所提及均为本地测试或经过目标授权同意,旨在提供教育和研究信息,内容已去除关键敏感信息和代码,以防止被恶意利用。文章内提及的漏洞均已修复,作者不鼓励或支持任何形式的非法破解行为。