DS:[0] 前言
CM14是指Crack Me第14号。Crack Me是指作者以被破解为目的而编写出来的程序。这是程序作者给破解者们的考题,也是破解者对自身技术验证和提升的机会。这篇文章主要内容是对Crack Me 14程序的破解和逆向分析,同时也是一篇入门级的教学。如果您只是被本文标题所吸引无意中点进来,可考虑略过此文,去寻找更快乐的文章。
在分析CM14的时候,会用到IDA——一个神奇的静态分析的软件。和OD一样,它以汇编语言反编译出程序的代码,但有一点最大的区别——它可以为程序生成具有全局性的概览图。这使得我们可以更直观和全面的看到整个程序各处跳转的情况。正如本文标题所描述的一样,CM14如同迷魂阵一般,身处其中却不知为何处,又如同盲人摸象,不知为何物。但当我们用IDA来俯瞰整个程序的时候,感慨不过如此,同时也无不为作者分散变量的做法感到惊讶,直呼内行!
尽管本文是写给有基础的读者观赏的,但本文也写的十分详细。因为笔者意在传达一种逆向的思想:即逆向的追代码,建立假设。顺向的看代码,验证假设。
传达一些逆向方法:系统call,能跳就跳,避免在系统call中打转。
DS:[2] 准备工作
相关工具
1.查壳工具——exeinfope
2.ollydbg——动态的反编译软件
3.IDA PRO——静态的反编译软件
心里逼数
就像为了撑起牌面而又不让本应贫穷的我们雪上加霜,在去高档茶楼吃饭的时候心里要有一张菜谱和价格上限。而破解程序我们心里也要有一定的量度和流程,也就是【怎么做】和【流程】。
这是以下笔者自己的规则:
流程:
1.先找爆破点,爆破后程序必须正常运行且无论怎样都显示注册成功。
2.逆着分析代码,从爆破点开始,追着关键的值,寻找其来源。
3.从来源处顺着单步运行,观察关键值的变化,得出其变化机制即算法
怎么做:
1.对于系统函数的调用,尽量不进入函数的本身,只留意函数的输入和输出,揣测其作用。
2.追码过程中可进入系统函数内部,直至又出到本程序名空间截止。
程序流程探索
知己知彼方能百战不殆,再说了程序都不运行就直接拖去破解是个什么原理,搞不好你连程序都运不起来。我们第一步首先必须的就是将程序的关键流程试一次:
看到这里有经验的破解者就会想到字符串断点和弹窗API断点了,其实程序的流程就能或多或少的提供爆破点的信息。
程序预处理
如同高档茶楼吃饭前的开胃菜一样,我们破解程序在享用主程序前,要先吃点开胃菜
这就是查壳,脱壳,程序修复。
查壳,没壳,就相当于这顿饭其实你是在吃大排档好吧。
DS:[4] 盲人摸象
既然都叫盲人摸象了,我们首先必须是以盲人的身份来摸象。这绝不是开玩笑,有些分析文章可能会一开始就告诉读者这个这个函数是什么功能,用那个那个软件打开然后怎么怎么做。但笔者认为,这些文章都极度缺少了作为探索者的视角,所谓分析必然是从无到有,从粗到细的,最后才所谓的分析。所以,刚开始我们一定要以探索者的角度去探索程序,而笔者作为探索者最开始惯用的手法就是直接OD载入,然后想办法断点爆破:
OD载入的字符搜索,看上去还挺简单的。
笔者这里先用了最简单的字符串搜索,毕竟肯定一切操作要从简,能简单就简单,何必搞那么复杂。不过这里提一嘴字符串搜索的时机一定的是在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运行,直到程序不再中断,弹出了注册成功的弹窗:
这..应该成功了吧,但是为什么我丝毫感觉不到破解的成就感,却感到满是疑惑甚至是一丝忧伤?这心中按耐不住的感觉,难道是名为劝退的喷泉即将喷发吗?
DS:[6] 纸上谈兵
从之前的分析可以看到,一共三个跳转!三个跳转可以跳到错误弹框的地方,同时还有一个谜一样的循环在这三个跳转间徘徊,如果我们要分析序列号的生成方式,应该从哪个跳转入手?难不成一个一个去试吗,那这个循环怎么办,每次循环都要看一遍吗?
基于上述问题你当然可以这么做,一个个的去分析,或许你可以从堆栈数据区中找到什么线索从而成为你的突破口。我们思考是否可以这么做:既然我们分析序列号是从跳转入手的,那我们是否可以把程序代码的所有跳转关系用线来表示,循环也当做跳转用线来表示,将代码块一个一个连起来。最后我们能得到一幅程序代码跳转关系图。根据关系图来锁定关键的跳转,然后分析。但毕竟把所有关系画出来得花费大量时间和精力,况且这只是个CM程序——以被破解为目的而编写出来的。如果我们真的花费了那么多时间,就算最后分析成功了,从被破解的意义来说,作者已经赢了。那有没有什么工具可以生成这个程序代码跳转关系图呢?有,那就是IDA。
生成程序代码跳转关系图
当我们用IDA载入程序后我们可以看到:
左侧是函数列表区,我们可以通过在代码区右键新建函数来增加。
右则是代码区,也是我们最主要看的地方。
那我们如何生成一个关系图呢?首先我们得在代码区中划分出一个函数,我们可以把这个函数当做是个主函数,从而生成关系图:
我们从载入IDA最开始的位置可以看到有很多dd声明,一直往下拉,直到我们熟悉的汇编代码出现为止。来到这里,我们右键,创建函数。
上图可以看到左边红框多了个函数,右边红框处会多了这样的虚线,证明这个函数是从这里开始划分的。新建函数后我们右键创建关系图:
这就是我们程序的关系图,每一个框代表一个代码块,线则是代表跳转的方向。这样我们就能俯瞰程序找到关键的地方。但问题是,这关键的地方在哪啊?怎么代码块里连错误提示的相关代码都没有?
我们可以放大来看到每个代码块里的汇编代码,很明显,这不是我们要的,只因这图没有指示出跳转到错误框。我个人猜测,造成这样的原因是,我们划分的函数是自成体系的。它没有参与到我们判断序列号的代码块中。当然这只是假设,笔者本人也是刚用IDA没多长时间,如果知道原因的可以留个言,帮忙解惑。
既然我们这次划分的函数没有参与到主要流程,那我们就按ESC返回到代码区,继续划分新的函数,直到我们看到这个:
下面的【成功】和【错误】正是注册成功弹出框和错误弹出框的代码。当然了,软件是不会标出来的,这个是我自己写的注释,至于为什么能分辨的出来,是因为这两个代码块中引用了弹出框的相关字符:
这下看清楚了吧!我们甚至可以通过数指向弹出框的箭头数量,来了解到有多少个关键的跳转。显而易见的,指向错误弹出框——2个,指向成功弹出框的——1个。通过点击指向他们的箭头,我们可以找到跳转他们的代码块,通过分析,笔者得出以下结论:
跳到成功弹窗的只有2号线;而跳到错误弹窗的却有1号和3号两根线。当代码全部运行,这三根关键跳的执行顺序是这样的:1(错误)——2(成功)——3(错误),刚好和我标的数字顺序一样呢~所以我们追码的时候也要按照这个顺序来追。
DS:[8]过五关斩六将
1号线
1号线到了1号线到了,下一站是深圳西站。不知道为啥越说越像地铁线。咳~我们回到正题。为了来到1号线的代码地址,我们可以将关系图再次转化为代码,再查看相应代码的地址了。
通过代码区的地址,笔者找到了1号线的位置。这次我们打开我们熟悉的OD,重行加载程序(因为要确保撤掉之前对程序的修改),然后右键——转到——表达式,输入找到的地址,确认后代码区自动跳到1号线的地址:
接下来我们就是要分析一号线的判断方法和判断依据了。
分析
- 判断方法:
cmp si ,bx then jnz address
很明显这里是对比si和bx的值,若不相等则执行跳转。
- 判断依据
从判断方法来看,依据就是si
和bx
两个值了,所以接下来我们就要对这两个值进行追码,找到最后影响目标值的代码。
过程
首先目测代码,找到能影响目标值的代码,并下断。就一般来说不会在太远的地方下断点,因为我们是逆向分析,所以先顺着代码走找到离判断地点最近的影响处,再逐步逆着向上找,如此反复我们就能找到目标值的来源。
si
目标值有两个,我们先寻找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的值,明显的,之前断点的范围内没有影响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)。如此反复便能明白整个值的算法。
说到这里,此处关键跳的相关判断和算法已经全然不重要,重要的是分析的过程,接下来就交给诸君了。
DS:[10] 后续
本来寻思这篇文章的目的是描述CM14动静分析并破解的全过程,写给有破解基础的读者看的。可没想到在构思文章的途中却发现了笔者从CM0到CM14一路破解过来的思路和规则。这些都是笔者破解的时候不假思索的做法和习惯,平日也没太大的留意。只不过在写此文时,不断的构思和总结,才发现了这么个东西。所以说啊,写文章看似只是耗费时间进行技术的分享,实则也是对自己长期以来做的工作的总结,这次真的是让我对写文一事刮目相看了,看来以后有必要保持这样的习惯。
后续CM14的破解过程也不打算写出来了。局部的方法无非就是逆着找,顺着看的迭代过程。全局的就是寻找划分函数区域,生成跳转关系图并进行分析的过程。如果文章反响的好,并有读者认为有必要写出后续破解过程的。笔者必当犬马之劳。
笔者也是个刚入门学习破解的人,以前就对破解产生兴趣,在吾爱破解论坛下了点教材,加以学习、实践,便有了今天的此文和彼我。如文章有错误,还请路过高手多多包涵,我也当耳提面命,诚心请教。
DS:[12] 趣事
在写本文的时候也发现了一件趣事:如果我们在追码的过程不断寻根溯源,一定要寻找值的真正来源,那将会是个怎样的结果?或许看到这里读者可能有些疑惑。“什么叫不断的寻根溯源?找到真正的来源不就是我们追码的目的吗?”确实,我们追码是为了寻找值的来源。笔者在这里描述的是这样一个场景:当我们追码发现一个寄存器(假设是eax)的值来源是我们通过键盘输入的序列号时,我们想去验证这个eax是否真的是我们通过键盘输入的序列号,尽管他看起来和我们输入的序列号完全一致,但毕竟只有亲自目睹其键盘输入才能确信。理所当然的,为了弄清这个事实,我们又去追eax。随后又会发现为了证明eax是由键盘输入的值又要去证明另一个值...如此反复,无穷无尽。
其实并不是在破解的时候才会出现这般尴尬的状况,在数学上也是如此。为了证明一个定理,而又要去证明另一个定理,周而复始,无穷无尽。所以我们必须得规定一些原子不可分定理,以这些定理去证明或建立其他新的理论,这样才能走出这个循环的怪圈。破解也一样,我们要规定一些原子不可分规则,当我们逆向遇到的时候,就不必继续追码,而是转而分析和下结论。
DS:[14] 庖丁解牛
为了让更新的内容更好的被读者们关注到,本应放到DS:[8]章节中的内容,特意新开此庖丁解牛章节来记录。本节主要解刨程序的算法部分,并会提出笔者的一些新的主张。
之前我们分析了1号线,事实上笔者还没完全说出它的作用——判断输入字符串长度是否为9位。想必如果是跟着文中方法去分析的读者也已经知道了吧。那么接下来笔者来为大家揭露剩下2、3号线的神秘面纱吧。
二号线
好!我们已经看到了二号线始发地的代码!还很短,是不是很简单呢?我们赶紧分析吧!先别急。如果是有些少破解经验的读者们可能会发现一个异常:二号线始发地的另一条线路竟然不是指向错误弹框!这其实不难理解,就一般而言,一个序列号的判断逻辑应该是这样:
如果等于序列号则跳到成功弹框
否则跳到错误弹框
按照这个逻辑,2号线始发地的另一条线应该指向错误弹框。然而事实是另一条线指向了其他的代码块而非错误弹框。这里就不得不提出笔者的另一个主张:错误优先。什么是错误优先?就像我们遇到的这种情况一样,我们应该转向去分析错误弹框引用的地方,也就是说我们应该先放弃2号线的研究,转而去研究3号线。为什么要这么做呢?笔者认为,破解就是一个排错的过程,当错误全部被排除了,自然而然的,也就破解了程序。再者,我们逆向去破解程序,都是根据错误信息去逆向的。可见,错误对一个程序是多么重要。而往往,错误也会为破解打开一个入口。因此这里我们先去研究3号线,当把3号线研究透彻了,想必2号线也迎刃而解了吧。
3号线
3号线到了,3号线到了,他的目的地仍旧是深圳西站,只不过这个深圳西站看上去更加离奇复杂。让我们顺着蓝色的3号线来找到它的始发站。
竟然只有两行代码,也太简单了吧。我们还发现了它头上被指着两个绿色的箭头。这意味着什么呢?意味着简单的人容易被绿啊...咳,意味着它被两处代码块所跳转,让我们分别顺着两个绿色的箭头,来找到它们的始发站吧!
在这里,笔者为了方便描述,分别将两个始发站称为A和B。同样的,为了弄清楚到底是A还是B跳到了我们的蓝色始发站,我们再次顺着绿色的箭头,分别找到相应的地址,在OD上使用命令打下断点,然后运行:dd address
运行后发现两个断点都成功把程序断下来了。若我们仔细看跳转,发现只有B才跳到了蓝色箭头的始发地。显然,B将会是我们这次分析的目标。
B
分析
-
判断方法:
test di, di then jnz address
jnz的跳转是根据ZF标志位的,而ZF标志位是进行某些代码操作后变化的,其表示操作对象是否为0,若为0则ZF为1,否则ZF为0。而这里的test操作便是如此。熟悉汇编的读者或许会知道,test 两个相同的变量往往是检查其是否为空。因此这里的代码可以翻译为:若di不为空则跳转。
还是建议刚入门的读者熟悉一下常用的跳转指令,能影响他们的标志位,以及能影响标志位的代码操作。
-
判断依据
既然这里根据di的值是否为空进行跳转,那么依据就是di的值了。
过程
这次的分析我们先不着急打开OD,让我们使用目测法。所谓目测法就是在IDA上选中需要追码的变量,找到最近的影响目标值的代码操作。
首先我们用鼠标选中目标值di,随后ida会把用到的相同的字符串用黄色底色标志出来,没错,就是这样,如同下图那样。
向上找,找到黄色的底色,发现是一句赋值语句mov edi, eax
。显然这里把eax的值赋给了edi,那我们就转而追eax。这时候有萌新又要发问了:下面有那么多调用call的操作,即使没有见到edi的相关赋值,指不定call里面偷偷改变了我们的目标值edi了呢?没错,说的对,说的妙。笔者能直接定位到那里其实还是凭借一点点的经验的。其实,刚开始我就像和这位萌新一样有这样的疑问,我们不妨从质疑的地方断点,然后一句一句的运行,观察edi的值。我们就会发现,即便下面运行了那么多代码,edi也不会改变。所以说啊,有时候连自己都不相信的地方,就好好断点单步去试试,试多了自然就会有经验。
话说回来,接下来我们追eax的值。从图里可以看出来,eax头上紧挨着一个叫__vbaVarTstNe
的call。eax一般都是call的结果保存处,所以笔者推测这里的eax来源是这个call的结果。而要分析call我们就要留意call前面忽然出现的push代码。一般来说,为了向call里传输参数都会用push把参数保存到堆栈区。我们暂时假设这两个被push进去的ecx和edx是参数好了。我们通过这样的假设,就获得了一个较为清晰的模型:用ecx和edx作为参数,调用call之后产生结果并存在eax。接下来让我们回到OD里,验证我们的假设。
首先,我们同样通过IDA找到这个call的地址并下断点:
OD中TstNe
可以看到ecx和edx分别存着两个奇怪的东西。在这里,笔者尝试右键数据追随,想看看它们是否为一些数的地址。箭头所指的便是笔者跟随ecx来到的数据窗口。第一行就是ecx所指的地址记录的相关的值。从图中可以看出,噢不,图中什么都看不出来,只有一些奇怪的数字。
接着按F8单步运行,当代码过了call语句后,eax的值变成了FFFFFFFF。这足以证明是这个call改变了eax的值,而eax又被赋值到了目标值edi中。接下来的任务很明确了,找到这个call的作用,看看他究竟是如何产生eax值的。
还记得笔者在前面的章节说过的原则之一吗?
对于系统函数的调用,尽量不进入函数的本身,只留意函数的输入和输出,揣测其作用。
我是挺想猜的,因为进入函数本身会让事情变得复杂,会让你面临一堆又一堆的看不明白的代码。但正如读者们所看到,现在我们连参数是什么都没弄清楚,而且这个参数还是个假设。所以这里笔者一方面为自己的菜而感到无奈,一方面也只能乖乖听话,进入它内部。
__vbaVarTstNe系统函数的探索
系统函数第一层
一进去就看到这代码还挺少的,然而我并高兴不起来,因为“无限”的call正等着我们。这里的方法还是一样,找到影响目标值最近的语句分析。这里由于call过多,笔者就不一一列出来了,这是一个需要耐心的过程。
最后我们来到系统函数里的这么一个地方:
奇偶
从上图中可以看出来,这里用了几个FPU的协处理器的指令。什么是FPU协处理器,可以理解成专门处理小数的处理器,并且它有自己的一套指令。在OD里最直观的就是右边红框有小数的部分了。
左边红框中的fcom st(1)
是拿右边红框的1.0000和3.0000做对比,指令执行后会改变下面红框FST的值。接下来的fstsw ax
指令会把FST赋值到ax中。最后用test ah, 0x5
来改变PF奇偶标志位,jpe指令跳转时根据PF标志位跳转。说实话,这里一顿操作猛如虎 ,但说实话笔者不太懂这里的fcom之后,test的意思。希望有识之士可以在下面留言帮忙解惑。
没关系,既然明白这里是对比红框的ST0和ST1的值,我们尝试鼠标双击,改变任意一个值,让它们在执行fcom指令前彼此相等:
小数
ohhh,修改后一直单步运行程序,直到我们回到可蔼可亲的叫__vbaVarTstNe的call。此时发现eax变成了0。还记得之前eax的值是FFFFFFFF吗?我们通过修改对比的小数,让eax值发生了变化。这足以证明这里的关键代码操作是对比两个小数的值是否相等。
通过猜测并修改关键值的方法我们就让函数的功能原形毕露:对比两个数是否相等,相等eax则为0,不相等则为FFFFFFFF。既然函数的功能已经明了,接下来就是得找出这两个拿去对比的数的由来了。
让我们再此回到那个调用fcom指令来对比1.000和3.000的地方。这次我们要从这里开始,逆向找到3.0000和1.0000两个值的由来。别忘了逆着找,顺着看。这里笔者选择先追寻3.0000的出处。找到影响它最近的代码:
fld
经运行可发现,当代码运行红框处时,右边就出现了3.000。而这里的代码又是使用了fld指令。天呐,fld指令又是什么,谁来救救萌新。
之前章节里说过了,不懂的指令就百度嘛,不然你以为咧。
fld是把指定的数据放进st(0)协处理器当中,可以理解成push吧。
所以这里其实就是把ss:[ebp-0x8]的值放进了我们的st(0)中。接下来理所当然的转向对地址ss:[ebp-0x8]记录的值的追踪。为了方便我们能看到ebp-0x8的字样,我们右键右下角的堆栈区域,选择地址->相对于ebp,使堆栈区的地址表示形式变更为以ebp为基址的表现形式。同时我们在此右键,选择锁定堆栈,使其固定位置。
锁定堆栈
为什么要固定位置呢?因为固定位置可以从视图上使这里的EBP-8的位置和意义不会发生变化。EBP-8的意义是什么?如你所见这里的意义就是我们刚刚追码发现存储着3.000的地方。千万别忘了我们其实还是在追码途中。那么它为什么会变化呢?这就得牵扯到ebp的概念了。ebp寄存器里存着的是当前堆栈的底部。而每次进入一个call或从里面出来,当前堆栈的意义都有可能就会发生变化。其实仅从代码上来看,ebp的值是由改变它的一些代码来决定的。如进入call后经常能看到这样的代码:
//call 开始没多久
push ebp
mov ebp,esp
...
//call 快要结束了
leave //恢复ebp esp的值
retn //call 结束了
这就是为啥笔者会说call改变ebp的意义。因为在call开始的地方经常有这样的操作。如果想要深入了解,建议还是通过百度,毕竟笔者也是一知半解,怕误人子弟。
除了在堆栈区中锁定目标值外,我们还可以在左下方的数据窗口跟随目标值:
数据跟随
跟随后就能在下面的HEX数据区看到[ebp-0x8]地址所记录的值了。当然这里的地址写的是0018F388,数据区和堆栈区不一样,不能切换成以ebp为基址进行表示。
有一点奇怪的是,我们都已经知道[ebp-0x8]所记录的值是3.0000,而HEX数据区和堆栈区却都显示的是00000000。如上图所示,橙框为[ebp-0x8]地址所记录的值,两个红框分别为HEX数据区和堆栈区记录的值。
刚开始笔者也是百思不得其解,以为是破旧的OD出了BUG。再一番猛如虎地疯狂到处右键后,笔者终于发现了这个奥义:
反复右键
在HEX数据区右键->浮点->64位双精度后,奇迹出现了!让我们歌颂奇迹再现吧。
歌颂奇迹
终于!3.000出来了。现在我们左青龙右白虎,佛挡杀佛。相信在左边数据区和右边堆栈区的结合,能斩杀一切障碍吧。就照这个架势冲了。
记住哦,我们得从最外面的call开始进入,也就是从我们发现那个叫__vbaVarTstNe
的call开始。一边单步一边观察值的变化。随后发现当经过一个叫VariantChangeTypeEx
的call后值发生了变化,变成了3.0000!
variant
同样的,我们为了避免进入call内部使问题变得复杂,尝试从push进去的参数里找出蛛丝马迹。但是完全看不出有什么和3有关系的数据,因此需要继续深入。
继续深入
倘若顺利的话,当运行完图中橘色框的代码时,下面红色框所示的目标数据区就会如上图一样变成3.000,目标值改变了,意味着我们得追movs doword ptr es:[edi],dword ptr ds:[esi]
了。这里也是赋值语句,只不过用了movs,在赋值之后相应的edi和esi都会增加4个字。说白了这四句其实是这样的:
move doword ptr es:[edi], doword ptr ds:[esi]
move doword ptr es:[edi+4], doword ptr ds:[esi+4]
move doword ptr es:[edi+4+4], doword ptr ds:[esi+4+4]
move doword ptr es:[edi+4+4+4], doword ptr ds:[esi+4+4+4]
为了方便理解,给大家说说字是个啥。所谓的字就是这个HEX数据里的一个16进制(2个数字/字母),每四个字组成一个dword双字。可以看到第一行每一个双字的起始位置分别为0,4,4+4,4+4+4。而上面的那四句代码无非就是把esi里第一行里的所有数据全部赋值到edi中嘛。
所以接下来的目标转向了esi了。这里从目测就看到esi值的来源是ebp-0x20,我们用之前同样的方法,跟随这个ebp-0x20。
如果要在堆栈区里显示出ebp-0x20,一定要记住,先解开对堆栈的锁定,单步运行后,堆栈内容发生变化后再锁定,最后再去找ebp-0x20。因为如果不解开锁定,ebp在视图上的含义就会不变,依旧是上一次我们锁定前的ebp。
又来了,又发现是一个call改变了目标值。啊这,这得跟到什么时候。所以笔者才会说系统函数能不跟就不跟。这次我们来到了一个叫oleaut32.VarR8FromStr的函数:
varR8
我们还是先根据参数和结果来猜函数。这次我们在它的参数里发现了有个字符串“3”。
天哪,难道是根据这个字符串生成的小数3.0000吗?为了验证这个假设,笔者把这个字符改成了6。
6
注意了,这个参数记录的是“3”这个字符串的地址。为此我们还需要右键,跟随DWORD,随后就能看到3的十六进制33。双击后即可修改。
运行后,之前的目标值确实从3.000变成了6.000。那接下来就追这个参数的起源。笔者在这里就不重复描述了。应该可以说,追码的方法已经大致说的差不多了,在这里还是推荐大家多练。不然干看着也没什么用。
一顿分析后,我们竟然回到了最开始见到的叫__vbaVarTstNe
call的参数。
从图中可以看出来,edx的值为0018F4E8
,跟随到数据窗口后发现,该地址的第三个双字处,事实上记录这之前字符串"3"的地址005FDFA4
(红框处)。
请注意一个双字(四对数字/字母)是从右往左读的,而一个字(一对数字)则是左往右。
也就是说edx是记录了记录3的地址。这么说可能有点绕口,这里直接右键跟随DWORD就能看到“3”了。其实如果各位读者在阅读本文时也按着流程去跟程序,相信是很容易理解的。
edx内容
还记得之前的关键处是拿1.000和3.000对比的吗?既然3.000是由call的edx参数传进来,那么1.000会不会也是call的另一个参数ecx传进来的呢?当然!那是非常可能的。
ecx初见
同样我们追随参数ecx来到数据窗口,却发现这里有几个可疑的参数。分别是第一个双字的8005,第二个双字的01,还有诡异的第三个双字00000000。看到了那么多个0不知道各位读者有何感想,那当然是马上右键切换成64位浮点数看看。
ecx内容
如果大家对这个1.000的出现抱有质疑,当然可以用回溯3.000时相同的方法,一路尾随。大家肯定会说,跟到什么时候,太麻烦了啊。对,说的不错,其实笔者刚学的时候就是一路跟,跟完一个又重新跟,虽然这有助于提高个人的阅读代码和回溯能力,但效率极其的差,时间久了还容易产生烦躁的情绪,感觉自己浪费时间。那有什么快速的验证方法呢?当然有。
不知道大家是否还记得这个call的作用——对比1.000和3.000,如果相同则eax为0,不相同为FFFFFFFF。所以现在我们只要把1.000的值更改成3.000,让他强制相同,再观察eax的值。就能验证我们的推测了。其实到后面愈发熟练的情况下,笔者大多都是用这种方法。只有搞不清楚参数,才会进入系统内部的call。反正还是那句话:
系统的函数,能不进就不进。
到目前为止,笔者演示了如何层层递进,找到影响跳转依据的值的代码。最后又如何层层递出,将这种影响又回溯到相关的call的参数上。我们的回溯必定是最终回到程序本身,而不是系统空间。
不知道大家发现了没有,参与call运算的,都是其参数对应地址的第三个双字,甚至还在里面记录了关键值的地址。说实话,笔者也不知道这是为什么。有可能这是编写这个程序的编程语言的特征吧。以后看到一个call就可以留意一下它参数的第三个双字了。如果我们一开始就知道是第三个双字的位置记录了对比的值,我们就不用那么辛苦的追码了。
既然我们发现了对比参数是edx和ecx里面的相关内容,那接下来我们就来对他们追码吧!
对比参数之一ecx
首先,先追ecx的来源;ecx是文中的1.000来源,刚开始笔者还是用目测法吧:
ecx的ida
可以看到ecx里的值来源于[ebp+var_D0]。同样选中,变成黄色底色,发现上面有个mov的赋值语句。我们到OD里验证一下:
ecx被赋值
按照设想,红框的内容应该变成1.000,结果发现毫无变化。单步再运行一次,发现运行完接下来的fstp
语句后,才变成了1.000。
fstp
这是为什么呢?这是因为1.000这个目标值事先就存在了协处理器——fstp
指令是把协处理器的浮点数弹出,并赋值给代码里指定的内容。而右边黄框内容则是1.000曾存在协处理器里的证据。因此我们这次追码要找和浮点运算相关的指令,这种指令在OD里都是红色开头的。
fsub
这里是一个fsub
的减法指令,它把协处理器中的49.0000和ds:[0x4010D8]中的值即48.0000相减,最后得到我们要的1.0000。
ds:[0x4010D8]中的值是个定值,这个我们在IDA里可以很容易的查得到:
定值
因此接下来就是找协处理器的49.0000的来源。既然这个代码操作挨着一个叫__vbaR8Str
的call,那我们要搞清楚这个call到底是何方神圣。
目前进度:对比参数ecx——>__vbaR8St
接下来笔者给大家演示一个如何用函数猜测法迅速跳过遇到的call。虽然之前进入系统内部call的时候已经用过一次了,不过笔者在这里用更加清晰的话语总结一下。
49
- 看参数:
eax——“49”
- 看结果:
FPU多了个小数49.000
- 看call名字:
R8Str,Str是String的缩写,和字符串有关
- 建立假说:
将字符串“49”转换为小数49.00
- 验证:
更改eax中的“49”,查看结果是不是对应的小数。
- 自我矫正:
验证失败后看看是不是参数或结果找错了。技穷的时候只能进入系统call。
以上便是笔者遇到call的解决步骤。进入系统call仅作最后的手段。在建立假说的时候,call的名字犹然重要。为什么笔者知道Str是String的缩写,而String是指字符串呢?那当然是因为这也是编程知识的一部分咯。
简单的逆向可能未必需要太多的计算机和编程知识。当如果想进一步提升效率和进阶自己的技巧,这两种知识是必不可少的。
接下来找参数eax “49”字符串的来源吧。
49来源
这次的主角是个叫 msvbvm60.rtcStrFromVar
的call。
目前进度:对比参数ecx——>__vbaR8St——>rtcStrFromVar
- 参数:
ecx——似乎是一个地址,第三双字上存着一个十六进制的数据31
- 看结果:
结果为“49”字符串
- 看名字:
StrFromVar。String是字符串,Var,估计是varchar,和ascii有关。
- 建立假说:
将参数地址中的记录的十六进制31转化为ascii对应的数据。
- 验证:
查ascii码,并且更改参数,观察结果值相应变化。
ascii
有点计算机常识还是很重要的,它能够提高我们对数据的敏感度。下面为了简单描述,遇到call就不做过多的笔墨了。只要各位读者心中有这个框架,遇到call也会不慌不忙的吧。
经验证这里的call的作用就是笔者猜测的那样把十六进制换算成十进制。下面该找到它的参数ecx的出处了。
lea
非常明显的看到,ecx首先是被用了一个lea
指令赋值,由于lea
指令的特殊性,ecx的值是ebp-0x50这个地址,而不是地址里记录的值。随后一个赋值指令mov
把ax的值赋给了距离ebp-0x50只有2个双字的ebp-0x48。赋值后就能看到我们期待的值31在底下的黄框内出现了。
实际上这里31的值是由ax决定的,而这次ax又紧挨着一个叫rtcAnsiValueBstr
的call。老样子,继续跟。
目前进度:对比参数ecx——>__vbaR8St——>rtcStrFromVar——>rtcAnsiValueBstr
rtcAnsiValueBstr
参数eax的值是字符1,结果是1对应的16进制31。很明显这是一个把字符转换成16进制的call。参数eax又挨着一个叫msvbvm60.__vbaStrMove
的call,下一个决定就是你了。
然而并不是它,因为这个call并没有改变eax。这意味着改变eax的值,在这个call的前面,而前面刚好有一个叫rtcMidCharBstr
的拦路虎。嗯,是它了。
目前进度:对比参数ecx——>__vbaR8St——>rtcStrFromVar——>
rtcAnsiValueBstr——>rtcMidCharBstr
rtcMidCharBstr
红框里的为函数参数,内容对应右边三个黄框。经检验,该call的作用是截取eax参数字符串的某一指定位置。这里因为位置已经被赋值固定为1,所以永远取字符串第一个。验证过程就留给各位读者了。
目前进度:对比参数ecx——>__vbaR8St——>rtcStrFromVar——>
rtcAnsiValueBstr——>rtcMidCharBstr——>输入的内容
当我们的回溯,回溯到了我们输入的内容,基本来说就可以适可而止了。截止目前,我们通过对函数结果的观察,再对参数的追码,反复循环,弄清各个call的作用后,我们终于可以下结论:
对比参数之一的ecx是我们输入字符串的第一位。也就是文中1.000的来源。
对比参数之二edx
第二个对比参数edx才是重中之重,因为它事实上就是本CM的算法所在。然而,笔者觉得这文章实在太长了。说实话,作为一篇入门级的文章来说,只要传授了方法,剩下的可能就只有重复和枯燥了。所以笔者这里就不再累赘重复,只放些少结论。
当我们回溯edx时就会发现,3.000这个关键对比数是来源于一句异或代码:
xor ax, 2
将ax中的值和2进行异或,结果存在ax中。什么是异或?其实就是某种运算。更为直观的,如果读者的电脑是WIN10系统,可以打开系统自带的计算器并切换成程序员模式:
点击hex即可切换成16进制运算,把1和2进行xor异或运算,即可得到结果3。3再进行相关的转换,就会变成小数3.000。
程序首先会把1和2进行异或,然后是2和2,再然后是3和2...以此类推,一直到9和2的异或。最后将这些结果组合起来便是本程序的真正的序列号了。整个流程其实就是一个循环不断调用异或运算,每次循环将需要异或的数字加1,直到循环次数为9。这点从IDA里可以更清晰的看到:
循环体:
参数与2异或
参与异或的参数ax+1
判断循环次数是否达到9,否则继续
上图中青色线便是循环的线,他指向了程序较为开头的地方。
右边红框代码中的[ebp+var_18]便是参与异或的参数,运算后会加上1,并且根据循环跳转回到程序较早的地方。
既然序列号是1到9和2的异或运算,那我们就可以用计算器算一下,最后再把结果全部拼接在一起得出这样的字符串:
30167451011
然后把它输入进去,最后再激动的按下确定按钮,你就可以满怀期待的看到:
错误弹框。是否还记得序列号要求是9位的字符。然而从1到9的异或拼接起来的字符长度大于了9,自然就不行咯。
那怎么办呢?其实答案我们追码的过程中。这里就把悬念留给看到这里的你吧。
二号线呢
我们曾遵循着错误优先的原则,跳过了只指向成功弹窗的二号线。先前说过,只要解决了三号线,二号线就迎刃而解。
如何,想到了吗?给个提示:和循环有关。别的就不说了,一切尽在不言中。
DS:[16] 后记
这是笔者第一次写破解有关的文章,也是第一次写教程。笔者也知道,写教程本身要有比较丰富的经验,否则弄不好误人子弟。笔者也承认文中可能存在有很多纰漏,或许在熟练的人看来还过于幼稚。但笔者写此文的目的在于,把自己在破解的感受和想法传达给他人,产生碰撞和火花,从而反馈得到更多的经验和知识。
教程文章真的很难写,要衡量阅读者的技术水平。如:是否应该假设阅读者OD使用不太成熟,这样是不是就得把一些OD上的操作说的更加清楚;又或者应该假设读者编程知识缺乏,这样是不是就得解释每一句代码的含义。要把量这个度真的很难,因为写起来根本不知道要详细到怎样的程度才合适。这也使得在某些读者眼里看来文章废话连篇。
或许以后也不会出那么详细的教程了,如果读者真的要入门,必须得了解一些基础的知识和掌握一些技术,以下是笔者能想到的一些内容:
一些常识,如ascii码,16进制等
程序的壳的含义,壳是什么。
工具的使用方法,如OD和IDA
一些关于汇编的知识
掌握一门编程语言
当然,这些内容也没说要一次性看完再去搞破解,到那时候恐怕连兴趣都提不上了。所以笔者还是建议边学边破解,多问为什么,遇到不明白的多问问,多百度。
如果读者还对文章内容有什么异议,或者想学习交流的,可私信或留言。本文到此结束,期待和大家下次再见。