☰
abexcm5 CrackMe逆向实战:从OllyDbg调试到Patch爆破
2026/10/9 8:21:46 网站建设 项目流程

作为一个常年和OD、x64dbg打交道的二进制爱好者,CrackMe系列一直是我觉得性价比最高的练手材料。新160个CrackMe里前几个题目都很适合拿来当“热身操”,其中002号abexcm5更是经典得不能再经典。别看它体积小到只有几个KB,里面塞了完整的PE加载逻辑、导入表解析、API调用和条件跳转,能把它啃下来,你基本就告别“只会拖进OD看字符串”的新手阶段了。这篇文章不打算泛泛而谈原理,而是直接带你走一遍完整的逆向流程:从载入到定位关键分支,再一路到手工Patch和可执行文件保存,中间穿插我在实际操作中踩过的坑和总结的经验。

1. 项目概述与逆向目标

1.1 这个CrackMe是什么

abexcm5是“新160个CrackMe”逆向练习序列中的第2个程序。它由一位ID为abex的作者编写,目标极其纯粹:检查程序运行环境中的光驱是否存在。说是检查光驱,程序本身并不读取光盘上的任何数据,它只是调用Windows的API来获取当前磁盘类型,如果结果正好等于“CD-ROM”类型,程序就展示一段成功提示;否则就弹出一个错误对话框。你不需要输入注册码,也不需要找算法,整个破解过程完全围绕着“如何欺骗这个判断”展开。

我见过不少初学者刚拿到这个文件时有点懵,因为它既不弹NAG窗口,也不需要爆破跳转去跳过某个烦人的计时器,程序流程一眼就能看完。但正是因为简单,它能让你在没有任何噪音的情况下看清楚一个Windows GUI程序从入口点开始经过了哪些API调用,以及一个条件跳转是如何决定整个程序命运的。说白了,这就是一个“用汇编写成的教学切片”。

1.2 为什么要拿它开刀

CrackMe这玩意儿的价值不在于“难度越高越牛”,而在于难度阶梯设置得是否合理。abexcm5正好处在一个黄金位置:它比Hello World级别的程序多一点API交互,又比那些带反调试、带虚拟机壳的商业程序简单到令人发指。对刚接触逆向的人来说,它有四个非常具体的训练价值。

  • 第一,训练动态调试的基本功:载入程序、单步跟踪、观察寄存器变化,这套动作在这里可以完整走一遍。
  • 第二,训练定位关键判断的逻辑:在一段不分函数的单薄代码里找到“影响成败的那个跳转”。
  • 第三,训练静态修改字节码的能力:用工具直接改掉一小段机器码,让程序行为发生变化。
  • 第四,训练PE文件入口点的理解:毕竟这小文件的代码段和入口点距离极近,你很快就能建立起“程序是从某个固定地址开始执行”的空间感。

只有把这四点练扎实了,后面碰到的算法注册机、反调试、加密壳之类的问题才不会显得那么可怕。

2. 环境准备与工具选型

2.1 运行环境与系统兼容性

做这类练习,很多人第一反应是“能不能直接在Win10/Win11上跑”。实话说,abexcm5作为老程序,在64位Windows上运行一般没大问题,因为它只调用了MessageBoxA和GetDriveTypeA这类老牌API,不涉及驱动、不涉及特殊权限。不过不同机器上系统盘的返回值会有差异,程序最终弹出的错误对话框内容也可能有细微区别,这属于正常现象。

为了调试体验更好,我个人的建议是准备一台Windows 7虚拟机,或者至少在虚拟机里再放一份XP镜像。原因很简单:老程序在老旧系统上行为更“规矩”,而且OllyDbg在Windows 7虚拟机里挂载这种小程序几乎零兼容性烦恼。如果你是纯新手,不想折腾虚拟机,直接在Windows 10物理机的“兼容模式”下运行通常也可以。万一遇到程序起不来的情况,Priority指向的往往不是代码问题,而是系统的DEP或UAC做了额外干涉,关闭UAC或右键属性里调整兼容性多半能解决。

2.2 调试器选择:OllyDbg还是x64dbg

32位的小体积可执行文件,我最推荐用原版OllyDbg 1.10或基于它的OllyDbg 2.01变体。理由很朴素:它对这个体量的文件处理速度快、界面不花哨、单步快捷键顺手,F7进入Call、F8步过、F2下断点的肌肉记忆一旦建立,后面好处多多。

x64dbg当然也可以,而且它对现代Windows系统的适配更好,如果你本来就习惯x64dbg,完全可以用它来练这个案例。只是有一点要注意:x64dbg在某些系统API的单步表现上会偏向反汇编分析,对新手来说信息量稍大,不如OllyDbg干净。我这个案例里用的是OllyDbg,下文所有按钮和地址描述都基于它,x64dbg用户请自行对照。

除了调试器,还建议准备一个十六进制编辑工具,比如HxD或者010 Editor。调试器里虽然能直接改字节码,但有时用十六进制编辑器直接打开文件改几个字节,对理解“文件存储形态”更有帮助。因为CrackMe的整个代码段十分短小,你甚至可以在Hex编辑窗口里肉眼扫到关键跳转指令对应的机器码,这种直观感是调试器给不了的。

3. 静态拆解:入口点与关键结构

3.1 入口点分析

用OllyDbg打开abexcm5.exe后,默认会停在系统断点处,按一下F9运行到用户入口点,你会看到程序停在00401000附近。这个地址非常靠近PE文件头,意味着代码段几乎挤在文件最前面。如果一个程序入口点紧挨着节表区域,通常说明它是用汇编直接写的小程序,没有经过编译器额外布局。编译型C程序一般不会这样安置入口代码,它们往往会把启动函数放在代码段的更深偏移处。

入口点这里可以看到一个很典型的Win32汇编程序骨架:先给MessageBoxA的各个参数依次压栈,然后调用一个系统API。参数顺序从右往左压入,所以最后的push NULL是在给MessageBoxA传hWnd,前面的push字符串地址分别对应Text和Caption。看到这样的结构基本就能判断,这个程序在它自己的WinMain逻辑里没有做太多初始化,直接就准备弹框了。

这里要特别注意一个细节:程序调用MessageBoxA之后通常紧接着就是调用ExitProcess退出,但在这个CrackMe里,弹框之前还有一个GetDriveTypeA的调用。也就是说程序在执行流程里先做了一次环境检测,再根据检测结果决定弹出哪个对话框。检测和弹框这两件事被硬生生写在了同一个代码流中,并没有分成独立函数。这种“没有函数边界”的代码结构在逆向小样本时很常见,你得适应它。

3.2 导入表与关键API

一个小型Win32程序在磁盘上会额外携带一块“导入表”数据,里面记录了程序运行时需要从系统动态库里引入的函数名字和所在DLL。你可以用任何PE工具查看,也可以在OllyDbg的“查看->导入表”菜单里直接看到。abexcm5的导入表非常精简,基本就两个核心函数:MessageBoxA和GetDriveTypeA。这俩都是kernel32和user32里的地摊级API,没有任何一层多余封装。

这告诉我们一个判断程序行为的好方法:当你在调试器里看到一个程序导入函数极少时,它的逻辑就会相对直白。你完全可以沿着这几个API调用点逐个下断点,用“断到API再往回调”的思路反推程序的执行路径。说白了,逆向的第一步不一定非要从汇编指令里抠字眼,扫一眼导入表往往就能预判程序的意图。

另外,GetDriveTypeA这个API从名字就能看出它和磁盘类型检测有关。它的输入参数是一个以反斜杠结尾的根目录字符串,比如“C:\”,返回值是UINT类型,代表当前设备的类别。我们只需要记住其中0是未知,2是可移动磁盘,3是固定磁盘,5是光驱CD-ROM这几个值就够了。对这个CrackMe来说,程序想要看到的“理想类型”就是5。

4. 动态调试全过程

4.1 第一步:单步到关键Call

现在开始正式的单步调试。程序停在00401000入口点后,我习惯按住F8一直步过,同时眼睛盯着右侧信息区的指令变化。别小看这一口气的F8,它能让你快速体会到程序从入口点经过一段“压栈-调用-比较-跳转”的流动感。

单步到调用GetDriveTypeA的那条指令时,你会看到在它上方不远处的栈区或寄存器里提前准备好了字符串参数。不同版本的文件里参数可能是“.\”或“C:\”,这都不要紧,你需要在意的只是这个参数最终作为root path传入API,然后API根据这个磁盘根路径返回类型值。按一次F8跨过这个Call后,反汇编窗口的下一条指令会显示cmp eax, 5一类的判断,而寄存器窗口的EAX此时就是你获得的第一手环境信息。

以我经常调试到的情况来说,如果程序在Windows 10物理机上运行,系统盘基本都是固定磁盘类型,EAX大概率等于3。如果放在某些U盘启动的“Windows To Go”环境里,EAX还有可能等于2。无论它等于什么,只要不是5,程序就会走错误分支。

4.2 第二步:观察返回值与标志位

关键的一刻发生在cmp指令之后。cmp eax, 5的意思是把EAX里的值与5做减法比较,比较结果不写回EAX,而是反映到标志寄存器的ZF、CF这些位上。如果EAX等于5,ZF会被置为1;如果EAX不等于5,ZF为0。紧跟在cmp后面的jne指令专门看ZF:ZF为0就跳转到错误提示分支;ZF为1就不跳转,继续沿成功分支执行。

很多人第一次调试时看不懂“jne不跳才是成功”是什么意思,这里有个更直观的生活类比:JNE就是“如果不一样才走”,而程序希望得到“一样”的结果,所以只有EAX恰好是5,才不触发这个跳转,成功代码才会执行。说白了,这个程序把“验证是否等于5”和“不等于5就弹错误”写在了一条跳转里。理解了这个,你手里就已经攥着破解的全部钥匙了。

不过要注意一个容易忽略的点:cmp执行后ZF发生了变化,但随后如果执行了任何其他影响标志位指令,ZF就会改变。在OllyDbg单步时,你一旦跨过jne本身,它要么跳了要么没跳,之后标志位再变也无所谓,因为分支已经决定了。这也是为什么我们要在jne指令上先停下来,看当前ZF的值,并想清楚接下来该不该让它跳到错误分支。

4.3 第三步:验证成功分支

为了确认成功分支里到底有什么,我们不需要一开始就修改程序。最简单的方法是把jne的条件临时“反过来”:在OllyDbg中选中jne指令,按空格弹出汇编修改窗口,把jne改成jmp(无条件跳转)显然不行,这会直接跳到错误框;你该改的是把jne改成一句“无操作”指令NOP,或者把jne改成je(等于就跳转)。把jne改成je后,EAX等于3时永远不等于5,所以ZF继续保持为0,je就不会跳,成功分支照样可以走到。

这样的“临时改指令”是纯内存层面的修改,不会写回文件,适合快速验证。改完后继续F8单步,你会穿过成功指向的指令,最终看到MessageBoxA的参数被压栈,再按一下F8或F9,屏幕上就弹出了成功提示框。到了这一秒,破解目标在逻辑上已经被拿下了。剩下的问题只是“如何让修改永久生效”。

5. Patch实战:让程序永远认为光驱存在

5.1 方案A:修改条件跳转

把程序从“jne的错误跳转”改成“永远不跳”,是最直观的Patch方式。在OllyDbg的汇编窗口里右键选择“复制到可执行文件”前,你首先要弄清目标指令所在地址和原始字节。

通常jne指令的机器码是75开头,紧接着一个字节是相对偏移量。例如“75 1A”意思是当前位置跳转0x1A个字节。如果你把它改成90 90(两个NOP),那么CPU在执行完cmp后不会再被jne导向错误分支,而会自然落到成功路径上,行为等同于“无论光驱存在与否,都认为存在”。如果想更“优雅”一点,也可以把jne改成jmp,让程序强制执行成功分支,但这就失去了判定的意义,显得不够自然。

如果你用的是十六进制编辑器直接改文件,找到对应偏移处把75改成EB(jmp的短跳转操作码)时要格外注意偏移计算问题。75后面的偏移量是相对jne下一条指令来算的,改成jmp后仍然相对jmp下一条指令来算,偏移值本身通常不用动。也就是说假设原始代码是75 0E,把它改成EB 0E就能从jne变成jmp,而且目标地址还是同一个。这里最关键的提醒是:修改前一定要确认这个jne是不是你真正想要跳过的那个,改错一个字节就可能把程序搞挂。

5.2 方案B:覆盖返回值

除了动跳转指令,你还可以在GetDriveTypeA调用返回后立刻清空EAX并赋值为5。具体做法是找到call指令后的那一行,在cmp eax, 5之前插入一条mov eax, 5或push 5/pop eax。这样就算系统报告的真实类型是固定磁盘,程序看到的也是CD-ROM,后面的cmp和jne就跟真的一样被执行,整个流程从结果上看仍然走向成功分支。

这个方案的优点是代码改动“更符合逻辑”,程序没有破坏原始跳转结构,看起来只是把返回值替换成了预期的值。缺点是你要多插入一条指令,处理不好可能挤掉接下来的指令。在OllyDbg里用空格键把cmp eax, 5改成mov eax, 5是不行的,因为cmp占用字节数和mov不同,后面指令会错位。正确做法是在call后腾出空间插入几条NOP,或者干脆在内存窗口中另找空地写代码再jmp回来。对于这个案例,我建议新手还是优先用方案A,不要自找麻烦。

5.3 保存修改后的文件

动态调试时的所有改动只存在于内存里,想让修改生效必须把改动复制回可执行文件。OllyDbg的流程是:右键选中刚才修改过的指令,选择“复制到可执行文件”,在弹出的窗口里再点“全部修改”,最后在右侧可执行文件窗口里右键选择“保存文件到新路径”,输入一个新文件名。

这里有个我踩过无数次的坑:千万不要直接保存覆盖原文件,除非你另存了备份。因为一旦保存错了或改坏了,原文件就没了。当年我刚开始接触逆向时,把原程序覆盖得连实验对象都没了,只能重新下载,非常狼狈。正确的习惯是统一把Patch后的文件命名成“abexcm5_patched.exe”之类的名字,保留原始样本不动,后续调试还有原始版本能对照。

保存完成后,直接运行这个新文件。如果一切正常,它应该不依赖光驱型号、不依赖系统环境,二话不说弹出成功提示框。到这一步,你的爆破就算正式完成。

6. 常见问题与排查速查表

6.1 调试中常见的坑

这个CrackMe虽然简单,但实际操作中遇到问题的朋友不少。最常见的现象是,明明在jne处把指令改成了NOP,运行后还是弹错误框。出现这种情况,八成是改错了位置:你改的是进入错误分支后某个位置的NOP,真正的关键jne还在原处,又或者你在静态编辑器里改的偏移和OllyDbg显示的地址没有正确对应,把75改成了别的字节,导致程序直接崩溃。

另一个高频坑是分不清DRIVE类型和盘符字母。有人看到一个盘符“D:”就以为它是光驱,其实人家可能就是一块普通硬盘分区。GetDriveTypeA识别的是磁盘物理类型,跟盘符本身没有绝对绑定关系。调试时盯着EAX的值就对了,别靠脑补“D盘应该是什么”。

第三个坑是X64系统上运行老文件弹错或闪退。XP时代写的汇编程序有时会因为系统API重定向或栈对齐问题行为异常。如果你发现程序入口点还没跑到就报错,先试着把文件放进虚拟机环境里跑,或者用OllyDbg的“按F9运行后断在系统断点”功能确认文件有没有被系统加载起来。

6.2 问题速查表

现象可能原因解决方向
找不到GetDriveTypeA调用静态分析时没有跟踪到API在OD命令框输入bp GetDriveTypeA下断点重跑
EAX返回值与自己预期不符系统环境不同导致磁盘类型不同不必纠结,只要不是5就会走错误分支
Patch后文件打开报错修改字节导致指令错位或PE校验失败恢复原始文件,仅修改jne字节,不要动其他区域
成功框弹不出来跳转改成jmp但目标地址算错对比改动前后的字节长度,确认跳转偏移没变
程序运行即退出入口点未被正确调试,或系统API异常在入口点下断,使用F9重新载入

这张表并不是大而全的排查手册,但覆盖了初学者最常撞上的几道墙。真遇到问题时,放下鼠标先问一句“我到底改了哪个字节、有没有影响到周边指令”,答案往往就在眼前。

7. 我的实操体会与建议

就我个人而言,abexcm5这个CrackMe带给我的收益比很多复杂程序都大。它把逆向最初的几个核心动作压缩在了一次完整的调试流程里,让我意识到一件事:所谓“破解”很多时候根本不是玄学,而是“找到那个影响到最终结果的判断点”这种简单的工程问题。拿到任何程序,先看入口API,再盯比较指令和跳转指令,最后想办法改变判断结果,这套方法论可以说是放之四海而皆准。

对于新手,我的建议是做完这个案例后,不要立刻去碰高难度的壳,先把修改后的样本与未修改样本做一次二进制对比,用文件对比工具看看自己到底改了哪些字节,再对着Windows API文档把GetDriveTypeA的所有返回值都查一遍。这些看似琐碎的动作,能帮你把“条件跳转”“磁盘类型返回值”“PE指令存储”这些概念彻底焊死在脑子里。后面无论你去解其他CrackMe,还是去逆向真实软件中的某段逻辑,都会感谢现在愿意一步步走流程的自己。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询