CM14是指Crack Me第14号。Crack Me是指作者以被破解为目的而编写出来的程序。这是程序作者给破解者们的考题,也是破解者对自身技术验证和提升的机会。这篇文章主要内容是对Crack Me 14程序的破解和逆向分析,同时也是一篇入门级的教学。如果您只是被本文标题所吸引无意中点进来,可考虑略过此文,去寻找更快乐的文章。
在分析CM14的时候,会用到IDA——一个神奇的静态分析的软件。和OD一样,它以汇编语言反编译出程序的代码,但有一点最大的区别——它可以为程序生成具有全局性的概览图。这使得我们可以更直观和全面的看到整个程序各处跳转的情况。正如本文标题所描述的一样,CM14如同迷魂阵一般,身处其中却不知为何处,又如同盲人摸象,不知为何物。但当我们用IDA来俯瞰整个程序的时候,感慨不过如此,同时也无不为作者分散变量的做法感到惊讶,直呼内行!
尽管本文是写给有基础的读者观赏的,但本文也写的十分详细。因为笔者意在传达一种逆向的思想:即逆向的追代码,建立假设。顺向的看代码,验证假设。
传达一些逆向方法:系统call,能跳就跳,避免在系统call中打转。
1.查壳工具——exeinfope
2.ollydbg——动态的反编译软件
3.IDA PRO——静态的反编译软件
就像为了撑起牌面而又不让本应贫穷的我们雪上加霜,在去高档茶楼吃饭的时候心里要有一张菜谱和价格上限。而破解程序我们心里也要有一定的量度和流程,也就是【怎么做】和【流程】。
这是以下笔者自己的规则:
流程:
1.先找爆破点,爆破后程序必须正常运行且无论怎样都显示注册成功。
2.逆着分析代码,从爆破点开始,追着关键的值,寻找其来源。
3.从来源处顺着单步运行,观察关键值的变化,得出其变化机制即算法
怎么做:
1.对于系统函数的调用,尽量不进入函数的本身,只留意函数的输入和输出,揣测其作用。
2.追码过程中可进入系统函数内部,直至又出到本程序名空间截止。
知己知彼方能百战不殆,再说了程序都不运行就直接拖去破解是个什么原理,搞不好你连程序都运不起来。我们第一步首先必须的就是将程序的关键流程试一次:
看到这里有经验的破解者就会想到字符串断点和弹窗API断点了,其实程序的流程就能或多或少的提供爆破点的信息。
如同高档茶楼吃饭前的开胃菜一样,我们破解程序在享用主程序前,要先吃点开胃菜
这就是查壳,脱壳,程序修复。
既然都叫盲人摸象了,我们首先必须是以盲人的身份来摸象。这绝不是开玩笑,有些分析文章可能会一开始就告诉读者这个这个函数是什么功能,用那个那个软件打开然后怎么怎么做。但笔者认为,这些文章都极度缺少了作为探索者的视角,所谓分析必然是从无到有,从粗到细的,最后才所谓的分析。所以,刚开始我们一定要以探索者的角度去探索程序,而笔者作为探索者最开始惯用的手法就是直接OD载入,然后想办法断点爆破:
笔者这里先用了最简单的字符串搜索,毕竟肯定一切操作要从简,能简单就简单,何必搞那么复杂。不过这里提一嘴字符串搜索的时机一定的是在OD载入后但程序没运行起来之前因为程序一旦运行起来,会在内存中产生大量的数据,使得字符串搜索结果变得异常复杂。见下图:
回到上一张简洁的图中,我们能看到“Sorry try again”的字样,这是我们输入错误序列号后,弹窗所提示的字符串,我们点进去,按照以往的经验,在附近就能找到判断序列号正确与否的关键跳转了吧。
从上图中可以清楚的看到,字符串所在的代码段被jump夹断,和我们以往看到的条件跳转有些少不同。尽管有点特别,但被jump夹断也意味着这段代码必定是由其他地方跳转过来的,否则这段代码是不会被运行的。所以我们现在要找到跳转过来的地方:选中第一个jump下面的代码xor ebx, ebx
从上图红框可以看到,这跳转来自两个地址。两个地址!这是什么原理,难道一个序列号还要判断两次,还是不一样的判断吗?我们利用命令bp+地址来迅速给这两个地址下断点。再走正常的流程
bp 004038AD bp 00403A04
然后发现,程序根本没断下来!!说实话我当时真的是一脸黑人问号,呆滞了许久。难道是根本上的错误吗?难道这是段假代码,真正的代码在别的地方?正当我准备用弹窗API来断点的时候,突然想起来我找跳转的地方是第一句jmp的下面一句,为什么呢?难道就不可以是任意一句吗?于是我顺着jmp往下找:
当我找到jump下面第二句的时候,发现了一个新的跳转,同样用bp命令迅速打上断点并运行:
断下来了,断下来了!我心中呐喊着,感觉自己似乎已经完全掌握了这个披着狼皮外衣的CM14。当我把“关键跳JNZ”改成JZ并运行后,发现程序断在了之前第一次设置断点的地方:
第一个断点并没有跳转,这个跳转是跳向我们错误弹窗的地方,既然没有跳我们先不管,让程序继续运行,这时程序来到了第二个跳转:
第二个断点正是跳向我们的错误弹窗!难道他就是...他就是!不多说,直接改反后运行,皆大欢喜。
皆个屁,这程序不但没有弹出恭喜你注册成功的字样,反而又断在了第一个断点的地方,继续运行又跳到了第二个断点,如此反复跳转,久而久之便形成了世间之绝技:
啊这,我已经无法思考了,便跟着反复疯狂按下了F9运行,直到程序不再中断,弹出了注册成功的弹窗:
这..应该成功了吧,但是为什么我丝毫感觉不到破解的成就感,却感到满是疑惑甚至是一丝忧伤?这心中按耐不住的感觉,难道是名为劝退的喷泉即将喷发吗?
从之前的分析可以看到,一共三个跳转!三个跳转可以跳到错误弹框的地方,同时还有一个谜一样的循环在这三个跳转间徘徊,如果我们要分析序列号的生成方式,应该从哪个跳转入手?难不成一个一个去试吗,那这个循环怎么办,每次循环都要看一遍吗?
基于上述问题你当然可以这么做,一个个的去分析,或许你可以从堆栈数据区中找到什么线索从而成为你的突破口。我们思考是否可以这么做:既然我们分析序列号是从跳转入手的,那我们是否可以把程序代码的所有跳转关系用线来表示,循环也当做跳转用线来表示,将代码块一个一个连起来。最后我们能得到一幅程序代码跳转关系图。根据关系图来锁定关键的跳转,然后分析。但毕竟把所有关系画出来得花费大量时间和精力,况且这只是个CM程序——以被破解为目的而编写出来的。如果我们真的花费了那么多时间,就算最后分析成功了,从被破解的意义来说,作者已经赢了。那有没有什么工具可以生成这个程序代码跳转关系图呢?有,那就是IDA。
当我们用IDA载入程序后我们可以看到:
左侧是函数列表区,我们可以通过在代码区右键新建函数来增加。
右则是代码区,也是我们最主要看的地方。
那我们如何生成一个关系图呢?首先我们得在代码区中划分出一个函数,我们可以把这个函数当做是个主函数,从而生成关系图:
我们从载入IDA最开始的位置可以看到有很多dd声明,一直往下拉,直到我们熟悉的汇编代码出现为止。来到这里,我们右键,创建函数。
上图可以看到左边红框多了个函数,右边红框处会多了这样的虚线,证明这个函数是从这里开始划分的。新建函数后我们右键创建关系图:
这就是我们程序的关系图,每一个框代表一个代码块,线则是代表跳转的方向。这样我们就能俯瞰程序找到关键的地方。但问题是,这关键的地方在哪啊?怎么代码块里连错误提示的相关代码都没有?
我们可以放大来看到每个代码块里的汇编代码,很明显,这不是我们要的,只因这图没有指示出跳转到错误框。我个人猜测,造成这样的原因是,我们划分的函数是自成体系的。它没有参与到我们判断序列号的代码块中。当然这只是假设,笔者本人也是刚用IDA没多长时间,如果知道原因的可以留个言,帮忙解惑。
既然我们这次划分的函数没有参与到主要流程,那我们就按ESC返回到代码区,继续划分新的函数,直到我们看到这个:
下面的【成功】和【错误】正是注册成功弹出框和错误弹出框的代码。当然了,软件是不会标出来的,这个是我自己写的注释,至于为什么能分辨的出来,是因为这两个代码块中引用了弹出框的相关字符:
这下看清楚了吧!我们甚至可以通过数指向弹出框的箭头数量,来了解到有多少个关键的跳转。显而易见的,指向错误弹出框——2个,指向成功弹出框的——1个。通过点击指向他们的箭头,我们可以找到跳转他们的代码块,通过分析,笔者得出以下结论:
跳到成功弹窗的只有2号线;而跳到错误弹窗的却有1号和3号两根线。当代码全部运行,这三根关键跳的执行顺序是这样的:1(错误)——2(成功)——3(错误),刚好和我标的数字顺序一样呢~所以我们追码的时候也要按照这个顺序来追。
1号线到了1号线到了,下一站是深圳西站。不知道为啥越说越像地铁线。咳~我们回到正题。为了来到1号线的代码地址,我们可以将关系图再次转化为代码,再查看相应代码的地址了。
通过代码区的地址,笔者找到了1号线的位置。这次我们打开我们熟悉的OD,重行加载程序(因为要确保撤掉之前对程序的修改),然后右键——转到——表达式,输入找到的地址,确认后代码区自动跳到1号线的地址:
接下来我们就是要分析一号线的判断方法和判断依据了。
cmp si ,bx then jnz address
很明显这里是对比si和bx的值,若不相等则执行跳转。si
和bx
两个值了,所以接下来我们就要对这两个值进行追码,找到最后影响目标值的代码。目标值有两个,我们先寻找si
的来源,因此我们运行的途中要留意ESI寄存器的变化情况。
经过第一轮的运行我们发现,离判断最近的影响esi值的语句是mov esi ,ecx
。这是赋值语句,将ecx的值赋给esi,为了追溯其源,自然而然我们就转到了对ecx的追踪;找到影响ecx最近的语句:neg ecx
。是ecx取补自身,用自己影响自己,还行。那我们继续向上找:setne cl
。意味取反ZF标志位并放到cl中。这里ZF标志位对ecx的值产生影响,那废话不多说,找最近的影响ZF标志位的代码:cmp eax, 0x9
。对比eax和9,相同ZF标志位为1,不同则为0。因为9是固定数据,那影响标志位的就是eax,所以又转到eax的追寻。这时候断点明显已经不在能影响eax的范围内了,我们把断点往上移,移到上面的push eax
。运行后发现,当程序走过call dword ptr ds: [&MSVBVM60.__vbaLenBstr]
的时候,eax发生了变化。证明是这个call改变了eax的值。那我们是不是应该进到call里看个究竟呢——当然可以,但前提必须得是你不知道这个call是干嘛的。这里的call明显就是测出push eax
中eax字符串的长度。就算我们不知道,也可以自己假设,查看call的参数和结果来证实。实在不行也可使用百度大法,因为前辈们已经总结出了一些常用的逆向函数了。
好了,说到这里,这整个过程无非就是顺着看运行的程序,逆着去找关键的影响值。如此反复迭代,我们最终就能找到目标值的来源。笔者在这里如此啰嗦,只是想传达这种方法。
言归正传。我们已查明esi
的来源。从逆向来看:ecx -> ZF标志位 -> eax -> 名为vabLenBstr的call -> call的参数eax
事实上,只有我们控制了call的参数eax,才有改变esi的可能。那这个参数eax要如何改变呢?改变成什么样呢?别急,我们现在是要将esi和ebx对比,所以得先去看看它的竞争对手——ebx。
我们用同样的方法去追bx的值,明显的,之前断点的范围内没有影响bx的代码,我们把断点放远点:
运行下来发现只有这一句改变了ebx的值,而这一句恰好是常用的清零操作,让ebx的值变为0。这意味ebx是个定值0。
ebx是定值0,esi是个可以通过call参数eax改变的变值,参数eax是输入的序列号。而关键跳的地方是cmp si, bx
只有当si和bx相同才不会跳转。所以现在目的很明显:通过输入的序列号,改变si的值,使其和bx相同,即为0。那我们输入的序列号要怎样才能满足这个条件呢?我们这次不妨顺着看影响esi的流程:输入的序列号eax->调用名为vabLenBstr的call->序列号长度存储在eax->cmp eax, 9 改变ZF标志位 ->setne cl 取反ZF存在cl -> neg ecx ecx中取补-> move esi, ecx将ecx赋值给esi
其实较为精通汇编的读者,一看就知道这序列号得怎么输入了。如果实在不是很懂,甚至不知道汇编相关指令(和笔者一样),可以把不知道的指令百度(当然理解这些指令,你甚至可能再去理解什么是寄存器什么是标志位,这也是一个类似追码的过程,层层叠进,只不过在生活上有一个更好听的名字——“学习”)。
对于不是很精通汇编,但只懂得一点点的笔者来说,并不是将这一过程的每一句代码都拿去百度,而是先把程序运行到关键跳,查看变值的结果(这里的esi最后为FFFFFF),然后也是类似逆向找,顺向看。找到变值的结果出处(此处是neg ecx)。如此反复便能明白整个值的算法。
说到这里,此处关键跳的相关判断和算法已经全然不重要,重要的是分析的过程,接下来就交给诸君了。
本来寻思这篇文章的目的是描述CM14动静分析并破解的全过程,写给有破解基础的读者看的。可没想到在构思文章的途中却发现了笔者从CM0到CM14一路破解过来的思路和规则。这些都是笔者破解的时候不假思索的做法和习惯,平日也没太大的留意。只不过在写此文时,不断的构思和总结,才发现了这么个东西。所以说啊,写文章看似只是耗费时间进行技术的分享,实则也是对自己长期以来做的工作的总结,这次真的是让我对写文一事刮目相看了,看来以后有必要保持这样的习惯。
后续CM14的破解过程也不打算写出来了。局部的方法无非就是逆着找,顺着看的迭代过程。全局的就是寻找划分函数区域,生成跳转关系图并进行分析的过程。如果文章反响的好,并有读者认为有必要写出后续破解过程的。笔者必当犬马之劳。
笔者也是个刚入门学习破解的人,以前就对破解产生兴趣,在吾爱破解论坛下了点教材,加以学习、实践,便有了今天的此文和彼我。如文章有错误,还请路过高手多多包涵,我也当耳提面命,诚心请教。
在写本文的时候也发现了一件趣事:如果我们在追码的过程不断寻根溯源,一定要寻找值的真正来源,那将会是个怎样的结果?或许看到这里读者可能有些疑惑。“什么叫不断的寻根溯源?找到真正的来源不就是我们追码的目的吗?”确实,我们追码是为了寻找值的来源。笔者在这里描述的是这样一个场景:当我们追码发现一个寄存器(假设是eax)的值来源是我们通过键盘输入的序列号时,我们想去验证这个eax是否真的是我们通过键盘输入的序列号,尽管他看起来和我们输入的序列号完全一致,但毕竟只有亲自目睹其键盘输入才能确信。理所当然的,为了弄清这个事实,我们又去追eax。随后又会发现为了证明eax是由键盘输入的值又要去证明另一个值...如此反复,无穷无尽。
其实并不是在破解的时候才会出现这般尴尬的状况,在数学上也是如此。为了证明一个定理,而又要去证明另一个定理,周而复始,无穷无尽。所以我们必须得规定一些原子不可分定理,以这些定理去证明或建立其他新的理论,这样才能走出这个循环的怪圈。破解也一样,我们要规定一些原子不可分规则,当我们逆向遇到的时候,就不必继续追码,而是转而分析和下结论。"