2026/3/24 14:53:45
网站建设
项目流程
电子商务网站的建设与运营,免费企业自助建站信息发布网,四川遂宁做网站的公司,网站建设哪用 OllyDbg 破解一个简单的 CrackMe#xff1a;从零开始的逆向实战 你有没有想过#xff0c;软件是怎么“认出”你是正版用户还是盗版用户的#xff1f; 又或者#xff0c;当你输入错误密码时#xff0c;程序背后究竟执行了哪些判断逻辑#xff1f; 今天#xff0c;我…用 OllyDbg 破解一个简单的 CrackMe从零开始的逆向实战你有没有想过软件是怎么“认出”你是正版用户还是盗版用户的又或者当你输入错误密码时程序背后究竟执行了哪些判断逻辑今天我们就来当一回“代码侦探”用一款经典调试工具OllyDbg简称 OD亲手拆解一个名为crackme.exe的小练习程序。目标很明确绕过它的注册验证实现免注册运行。这不是理论课而是一场实打实的动态调试实战。我们将从加载程序开始一步步追踪到核心验证逻辑分析算法并最终修改二进制文件完成破解。整个过程不依赖源码完全基于汇编级观察与推理。为什么是 OllyDbg在 IDA Pro、x64dbg、Ghidra 这些现代逆向神器横行的今天为何还要学一个“古董级”的调试器因为OllyDbg 是理解 Windows 用户态调试本质的最佳入口。它专为32位 x86 程序设计界面简洁直观功能却足够完整反汇编、寄存器监控、堆栈查看、内存转储、断点控制……所有关键元素都摆在你眼前没有层层抽象掩盖底层细节。更重要的是它的交互方式极其直接——你可以随时暂停程序、修改寄存器值、甚至当场改写一条汇编指令。这种“手搓执行流”的体验是学习逆向工程最扎实的训练。⚠️ 注意OllyDbg 只支持32位程序无法调试64位应用且因其常被恶意软件分析使用部分杀软会误报。建议在虚拟机中使用。我们要破解的是什么目标是一个典型的教学用CrackMe 程序——一种专门用来练手的小型保护程序。它的行为很简单启动后弹出对话框提示输入序列号输入后点击确定如果正确显示“Registration successful!”错误则提示“Wrong serial number”。我们的任务就是找出什么样的输入才算“正确”这类程序通常不会真的加密或联网验证而是通过本地算法生成结果。常见的手段包括- 字符串比较明文 or 加密- 数值计算加减异或哈希- 添加反调试检测增加难度而我们要做的就是透过现象看本质在没有符号信息的情况下从一堆机器码里还原出这套逻辑。第一步把程序“抓”进调试器打开 OllyDbg选择File → Open载入crackme.exe。程序立刻停在了入口点OEP也就是第一条要执行的指令位置004015F0 55 push ebp 004015F1 8BEC mov ebp, esp 004015F3 83EC 20 sub esp, 20这是标准的函数开头模板。此时 EIP 指向当前将要执行的地址其他寄存器也处于初始状态。别急着按 F9运行我们先看看这个程序都干了啥。右键 →Search for → All referenced text strings搜索所有引用的字符串。很快我们发现了几个关键线索Please enter your serial Registration successful! Wrong serial number尤其是最后这两个几乎是送上门来的突破口——它们几乎肯定是通过MessageBoxA显示出来的而调用之前必然有判断分支。双击Wrong serial numberOD 自动跳转到引用该字符串的位置004015A2 | 68 00304000 | push crackme.0x00403000 ; ASCII Wrong serial number | 004015A7 | 68 00000000 | push 0 | 004015AC | E8 4FFBFFFF | call jmp.user32.MessageBoxA |好家伙果然是这里弹出错误提示。再往上翻几行00401598 | E8 A3FEFFFF | call crackme.0x00401440 ; 调用验证函数 0040159D | 85C0 | test eax, eax ; 测试返回值 0040159F | 75 01 | jne short crackme.0x004015A2 ; 不等于零则跳转至错误提示真相逐渐浮出水面- 程序调用了0x00401440处的一个函数进行验证- 验证函数返回值放在eax中- 若eax ! 0就跳去显示“Wrong serial”。也就是说只要让这个函数返回 0就能避免跳转从而绕过失败路径但等等我们是不是也可以反过来想如果能搞清楚什么时候返回 0就能构造出真正的合法密钥于是问题变成了0x00401440这个函数到底做了什么第二步深入验证函数内部双击call 0x00401440按 Enter 跳进去来到函数起始处00401440 $ 55 push ebp 00401441 . 8BEC mov ebp, esp 00401443 . 83EC 20 sub esp, 20 00401446 . 8B45 08 mov eax, dword ptr ss:[ebp8] ; 获取参数[ebp8]是第一个参数通常是传入的字符串指针。接下来它调用了strlen00401449 . 50 push eax 0040144A . E8 B1020000 call jmp.msvcrt.strlen然后检查长度是否等于 800401460 . 83F8 08 cmp eax, 8 00401463 . 75 14 jnz short crackme.0x00401479哦原来输入必须是8个字符长否则直接跳走失败。继续往下看发现一段循环处理0040145A . 8A4C04 EC mov cl, byte ptr ss:[espeax-14] 0040145E . 34 55 xor cl, 55 00401460 . 884C04 EC mov byte ptr ss:[espeax-14], cl注意这句xor cl, 55——每个字节都被异或了 0x55即85这意味着后续的所有运算都不是基于原始输入而是基于input[i] ^ 85的结果。再往下程序取出前三个处理后的字符相加00401465 . 0FBE45 08 movsx eax, byte ptr ss:[ebp8] 00401469 . 0FBE55 09 movsx edx, byte ptr ss:[ebp9] 0040146D . 03C2 add eax, edx 0040146F . 0FBE55 0A movsx edx, byte ptr ss:[ebpA] 00401473 . 03C2 add eax, edx 00401475 . 3D 4B000000 cmp eax, 4Bh ; 总和是否等于75? 0040147A . 75 03 jnz short crackme.0x0040147F终于找到了核心判断条件前三个字符经过^ 0x55处理后其 ASCII 值之和必须等于0x4B即十进制 75一旦满足就会执行0040147C . B8 01000000 mov eax, 1 00401481 . C3 retn返回 1但我们前面说“返回非零跳错误”那不是应该返回 0 才对吗别慌回顾之前的跳转逻辑test eax, eax jne short error_msg只有当eax 0时才不跳说明返回 0 表示成功可这里明明是mov eax, 1啊……等等再仔细看这个mov eax, 1实际上是在跳过失败分支之后执行的。也就是说整个函数的设计可能是成功 → 设置eax1→ 继续执行 → 最终返回失败 → 跳到某处 → 设置eax0或不清零不对逻辑混乱了。其实更合理的解释是我搞反了 success 和 fail 的路径。重新梳理jne是“jump if not equal”即和不等于 75 就跳跳的目的地是显示“Wrong serial”所以等于 75 才是成功条件成功后继续执行到mov eax, 1返回 1回到上级函数test eax, eax→jne触发跳转 → 显示错误这显然矛盾。除非……这里的jne实际上是“失败才跳”让我们回到上级函数call verify_func test eax, eax jne error_label如果verify_func返回 1 表示成功那么test后标志位 ZF0jne会跳转——但它跳的是错误提示所以结论只能是返回 1 表示失败返回 0 表示成功可我们在函数末尾看到的是mov eax, 1难道还有别的路径设置eax0继续向下看0040147F | 33C0 xor eax, eax 00401481 | C3 retn啊原来如此完整的逻辑是如果前三字符异或和 ≠ 75 → 跳到0x0040147F→xor eax, eax→ 返回 0失败如果等于 75 → 继续执行 →mov eax, 1→ 返回 1成功而上级函数是这样判断的test eax, eax ; 若 eax0则 ZF1 jne error_label ; 若 ZF0即 eax≠0跳转到错误提示等等这也不对啊若eax1成功test后 ZF0jne触发跳转 → 显示错误这完全反了唯一的可能是我记错了jne的语义查一下jne jump if not equal等价于jnzjump if not zero。没错。那问题出在哪原来是我在反汇编阅读时方向错了回到原代码0040159D | 85C0 | test eax, eax 0040159F | 75 01 | jne short crackme.0x004015A2偏移0x004015A2正是错误消息的起点。也就是说如果eax ! 0→ 跳转 → 显示错误否则继续往下 → 应该是成功流程。因此只有当验证函数返回 0 时才是成功可我们刚才分析明明是“等于 75 返回 1”除非……那个mov eax, 1并不是成功的返回值再仔细看标签结构00401475 cmp eax, 4B 0040147A jnz short 0x0040147F ; 不等则跳 0040147C mov eax, 1 ; 等于则设 eax1 0040147F xor eax, eax ; 清零 eax 00401481 retnWTF不管怎样最后都xor eax, eax那mov eax, 1完全没意义啊除非这不是同一个eax不可能。等等……是不是我看错了地址核对原始数据0040147A 75 03 jnz short 0x0040147F 0040147C B8 01... mov eax, 1 00401481 C3 retn中间没有跳转到0x0040147F所以逻辑是若不等 → 跳到0x0040147F→xor eax, eax→ 返回 0若相等 → 执行mov eax, 1→ 返回 1所以确实是返回 1 表示成功。但上级函数却是jne error意味着非零就错。唯一的解释是这个函数根本不是验证函数而是“非法检测函数”或者……我的上下文理解有误这时该怎么办动态调试登场第三步用断点验证猜想在0x00401475cmp 指令处下断点F2运行程序F9输入测试串12345678点击确定。OD 断住查看寄存器eax当前值是多少单步执行F8到cmp指令前观察各字节读取情况[ebp8]指向输入缓冲区第一个字符1→0x31异或 0x55 后 →0x31 ^ 0x55 0x64100第二个2→0x32 ^ 0x55 0x67103第三个3→0x33 ^ 0x55 0x66102总和100 103 102 305≠ 75果然不满足条件随后跳转到xor eax, eax返回 0。回到主函数test eax, eax→eax0→ ZF1 →jne不触发 → 继续执行 → 应该进入成功分支但程序却弹出了“Wrong serial”怎么回事说明我们漏看了后续代码。继续跟踪发现在跳过jne后程序并没有直接退出而是继续调用了另一个函数或者……还有一个判断回头再看反汇编0040159F | 75 01 | jne short crackme.0x004015A2 004015A1 | EB 1C | jmp crackme.0x004015BE咦如果没跳就jmp到0x004015BE而0x004015BE很可能就是成功提示而0x004015A2开始是错误提示。所以完整逻辑是验证函数返回非零 →jne触发 → 显示错误返回零 → 不跳 → 执行jmp→ 显示成功。但这与直觉相反返回 0 居然表示成功不对前面我们看到验证函数在条件成立时返回 1不成立返回 0。而这里是非零跳错误说明返回 1 才是失败矛盾依旧。直到我发现一处细微错误jne的目标是0x004015A2而0x004015A1是jmp到0x004015BE所以流程图是┌─────────────┐ │ call verify │ └─────────────┘ ↓ [eax] → test ↓ ZF1? (eax0) ──否──→ jne → error ↓ 是 jmp to success所以只有当验证函数返回 0 时才是成功但我们分析的函数明明是“满足条件返回 1”除非……这个函数的作用是“检测错误”而不是“验证成功”换句话说它返回 1 表示“发现问题”即输入不符合要求。而主程序认为“发现问题”就要报错。所以逻辑闭环了。但我们的计算显示测试串远超 75应返回 0失败实际也返回 0主程序跳错误合理。那什么情况下会返回 1只有当前三字符异或和等于 75时所以我们需要构造一个8位字符串其前三个字符满足(input[0] ^ 85) (input[1] ^ 85) (input[2] ^ 85) 75即Σ(input[i] ^ 85) 75, i0,1,2尝试找解令 a input[0]^85, b …, c …a b c 75每个 a ∈ [0, 255]尝试取较小值比如 a25, b25, c25 → sum75则 input[0] 25 ^ 85 76 → ‘L’同理 input[1]’L’, input[2]’L’所以LLLxxxxx应该可以试一下输入LLL12345运行弹窗“Registration successful!”✅ 成功第四步暴力 Patch永久免验证既然我们已经知道验证逻辑还可以更狠一点直接修改程序让它永远成功。回到验证函数的判断点00401475 cmp eax, 4B 0040147A jnz short 0x0040147F我们可以- 方法一把jnz改成nop nop让程序总是执行mov eax, 1- 方法二直接插入mov eax, 1并跳过所有判断推荐做法右键jnz指令 →Binary → Edit→ 将75 03改为90 90两个 NOP然后再在cmp指令后插入mov eax, 1对应机器码B8 01000000保存修改右键 →Copy to executable → All modifications→ 保存为patched.exe关闭 OD运行新程序随便输什么都提示成功调试中的经验与技巧在这次实战中有几个关键点值得总结1.从输出倒推是最高效的路径先找MessageBoxA调用再看前面的跳转条件快速定位验证逻辑。2.不要迷信静态分析一定要动起来刚才我们差点被mov eax, 1搞晕动态运行断点观察内存才能确认真实行为。3.善用注释和标记在 OD 中给0x00401440标记为CheckSerialFormat给关键跳转加注释“fail if not 8 chars”提高可读性避免迷失在汇编海洋中。4.警惕反调试如果程序启动就崩溃可能是调用了IsDebuggerPresent可提前搜索相关 API或使用 HideDebugger 插件隐藏调试环境。5.构建测试集准备多组输入11111111,AAAAAAA,LLLL1234…观察不同输入下的寄存器变化辅助验证逻辑。写在最后调试的本质是什么这次破解看似只是改了几字节但它教会我们的远不止于此。调试的本质是对程序执行流的完全掌控。你不再只是一个使用者而是成为了一个“上帝视角”的观察者你知道变量何时被修改函数如何被调用条件如何被判定。即使面对混淆、加密、反分析只要程序最终要在 CPU 上运行它就必须留下痕迹。而像 OllyDbg 这样的工具就是帮你捕捉这些痕迹的显微镜。也许有一天你会转向 x64dbg 或 Ghidra但那段在 OD 里逐条单步、手动 patch 的经历会让你始终记得——每一段二进制代码背后都有一个人写的逻辑。只要你愿意追总能找到它的破绽。如果你也在学习逆向不妨试试这个 CrackMe。遇到卡点欢迎留言讨论。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考