
作为一个常年和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还是x64dbg32位的小体积可执行文件我最推荐用原版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等于5ZF会被置为1如果EAX不等于5ZF为0。紧跟在cmp后面的jne指令专门看ZFZF为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继续保持为0je就不会跳成功分支照样可以走到。这样的“临时改指令”是纯内存层面的修改不会写回文件适合快速验证。改完后继续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改成EBjmp的短跳转操作码时要格外注意偏移计算问题。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还是去逆向真实软件中的某段逻辑都会感谢现在愿意一步步走流程的自己。