第十二章:一个难啃的NOOB例子
一、简介
本章我们将研究一个有点更具挑战性的程序。它叫ReverseMe1,我写的。我也会讨论一个Olly的插件“ASCII码表”。可以在
工具页下载它。这个ReverseMe是用来说明为什么LAME补丁方式通常就是那么lame(烂)的一个极好的例子。
二、准备开始
运行下程序看看:(p1)
我们能看到它说还没有注册,需要序列号。那就给它一个:(p2)
点“Check Serial”:(p3)
我们看到我们是错的(再一次)!Olly载入应用,用咱们信得过的“搜索字符串”:(p4)
好哇,看起来前途光明呀。咱们来检查下“That is not correct”字符串:(p5)
咱们来到了问题的核心。因为每一个都是单独的方法,我们需要看看哪里调用了它们,所以咱们要这么做:(p6)
Olly弹出了References窗口:(p7)
我们能够看到有一个对该函数的调用。咱们双击它,看看它是啥样的:(p8)
这里,我们能看到坏消息是在401078处调用的,并且我们马上就能看到40106A处有个跳转指令跳到这里:(p9)
向上滚几行,我们就能看到一个CALL,用来检测 程序/比较/跳转 ,和我们前面看到的一样。从这里我们能够猜到,主要的检测程序是在4010FC,401063处调用了它。在返回后,EAX寄存器被检测其值是否是0,如果不是就跳到坏消息。(p10)
测试下我们的假设,在40106A处设置断点,然后重启应用。在输入一个序列号以后(我输入的还是“12121212”),我们断在了调用序列号校验的那个CALL后面的跳转处:(p11)
现在咱们帮Olly走正确的路,所以我们不能让跳转实现(直接到调用好消息的CALL那):(p12)
点一下运行:(p13)
耶,so easy(妈妈再也不用担心我破不了了)!!点OK:(p14)
噢,艹%$@,这里他爸的发生了啥,你个阿西吧$$$%^#!!!!!很明显,我们的程序没有注册成功。这说明我们肯定错过了啥。
三、进一步分析
重启应用,输入序列号,让Olly再次断在40106A:(p15)
看看这个,如果我们阻止Olly跳到坏消息那,直接执行40106C的那个CALL,就是调用4010AA。沿着那条路往下走,我们能看到它是相当的标准:它弹出一个显示“That is correct”的消息框,然后将主窗口的标签修改成“This program is registered!”。(p16)
等等!一旦我们从那个CALL返回了,在401071还有另一个CALL等着咱:(p17)
该CALL调用的是401130,所以咱们看看那个子程序。首先,我们注意它调用了SetDlgItemTextA,不过有一个看起来很奇怪的字符串。咱们来一行一行的执行。401130有个CALL调用了4010FC。往上看,我们看到这是一个序列号校验子程序。然后EAX自身做了OR操作看是否为0,如果不是,它执行了许多看起来很怪异的玩意儿:(p18)
到目前为止,我们从这些收集到的信息是,在我们给程序打了补丁后它显示了好消息,然后另一个CALL执行了,在这个CALL里,又有一个CALL再次执行了序列号校验子程序,对结果做了同样的分析。这是一个备份检测点!现在我们来看看如果我们在这个备份检测点失败的话会怎样(这里我们是可以让它检测失败的,因为我们只给那个跳转打了补丁):(p19)
首先,ECX被设置值为1F(十进制是31)***对不住了,被切掉了一点(译者注:指的是上面图片中MOV ECX,1F那行)***。然后ESI被赋值为0,EAX被清0。然后就进入一个循环。咱们一步一步执行这个循环。第一行从ESI+403070拷贝了一个字节到AL寄存器中,我们知道ESI等于0,所以地址实际上就是403070。咱们看看内存中这个地址里是什么。右键并选择Follow in dump->constant,或者就右键dump窗口,选择goto并输入地址403070。(p20)
如果仔细看的话,就会发现这就是上面传给SetDlgTextItemA的字符串参数。所以它就是将那串奇怪的字符串的第一个字符拷贝到AL中。
***有件事你应该知道,许多汇编语言指令会按默认的使用方式使用某些寄存器,例如ECX被用来作为计数器,ESI被用来作为源地址,EDI被用来作为目的地址。本例中就是这样的。***
接下来,我们将该字符与2C进行XOR,然后再将其存回原来的地址中:(p21)
最后,给ESI(源址寄存器)加1,再做LOOPD操作。LOOPD意思是ECX寄存器减1,然后循环直至ECX为0。也就是说,我们原来给ECX赋的值,十进制的31,就是循环的次数。
总的来看,该循环遍历奇怪字符串的每一个字符,将它们与2C进行XOR操作,再保存回原内存。这些操作将持续到ECX等于0,或31次。单步执行一次LOOPD指令后回到顶部,然后看看数据窗口:(p22)
你会发现,字符串的第一个数字已经变了。原来的字符被执行XOR操作后变成了“T”。如果你单步执行这个循环几次的话,会看到数据窗口中的字符串的变化。你也会发现传给SetDlgTextItemA的参数也变了:(p23)
单步执行完这个循环,就会看到最后生成的消息,看起来相当熟悉呀,“This program is not registered!”。这和程序事实上还没有注册时主窗口中显示的消息是一样的:(p24)
可以看到这个字符串变成了传递给SetDlgTextItemA的值,事实上用之前在那里的坏消息替换了已注册的好消息:(p25)
下面就是主窗口中显示的:(p26)
所以,现在我们知道了,给该应用打补丁的巧妙的方法是进入到序列号检测子程序,确保它总是返回正确的值,因为它不只是在第一次检测时被调用,而且在显示成功后再次被调用。再提醒你一下,序列号检测的相关CALL被调用,然对eax进行0测试。如果不是0,就跳到坏消息,所以我们想让子程序返回0!然后,序列号检测子程序再次被调用,如果它再次返回0,那么我们的第二次检测就通过了:(p27)
那么,咱们去序列号检测子程序那,看看能对它做些什么。子程序的开始调用了GetDlgItemTextA,我们猜它就是获取我们输入的序列号。你可以在401101(它指向的是放置文本的buffer)的参数上右键,在数据窗口中跟随它:(p28)
我们单步步过GetDlgItemTextA指令后,就能在buffer中看到我们的序列号了:(p29)
在它被保存到buffer后,该buffer的起始地址被拷贝到EAX中,随后该地址中的内容被拷贝到EAX中。就是将我们密码的前四个字节拷贝到EAX中。然后这几个字节与3334进行比较,如果不匹配,EAX就被填充为1(坏消息),否则就填充为0(好消息):(p30)
我们可以看到,做主要决定的是401121的JNZ指令:(p31)
这一行决定了在返回前,EAX到底是0还是1。所以我们要做的就是保证EAX总是等于0:(p32)
所以现在,代码将总是直接给EAX赋0值,然后直接跳转到返回处。运行下程序看看:(p33)
注意在对序列号检测子程序调用后,我们自然而然的就跳转到好消息那了:(p34)
在第二个检测点,我们也跳到了好消息那:(p35)
我们现在已经找到了一个注册该程序的补丁,无论你输入什么序列号都行file:////tmp/wps-yysniper/ksohtml/wpsVPPSCD.jpg。
祝贺你!
四、ASCII码表插件
你需要做的一件事是找出密码是什么(或对密码有什么样的要求)。给你些帮助,下载并安装“Ascii Table”插件,将其拷贝到插件目录。重启Olly后,选择“plugin”->“Ascii table”就会显示一个表格。尽管它还有很多地方需要改进,不过它能让你快速查询ASCII值:(p36)
***如果有人想要主动更新或重做这个插件,我将永远感激。第一,那些文本不应该被选中,也不应该可编辑(我为什么要编辑ASCII码表?)。第二,让窗口大小可变真是件好事。如果有人做了,请告诉我,我欠你一辈子。***
本文PDF文件下载(已排版):
本文相关附件下载地址(国外链接,不是一直好用):
吐槽:本来昨天就将这一章翻译完毕,我用的是Google Doc在线编辑,翻译完后怎么都下载不了,前后下了20多次才下载完。然后是排版等等,来发帖子,帖子编辑完了,发现52的网站又出了问题打不开,我艹了都。