资讯动态

Acid_burn CrackMe:Name/Serial算法还原与注册机编写

发布时间:2026/10/19 20:03:53 来源:尧图企业网站定制
最近整理硬盘翻出早年练逆向的笔记重新把160个CrackMe这个经典入门题库过了一遍顺手把Acid_burn.exe的完整分析过程沉淀成文。这个词在逆向圈基本不用多解释CrackMe就是作者故意写出来给学习者练手用的带验证小程序而Acid_burn是这套题里最常被拿来当开胃菜的一个无壳、界面清爽、验证逻辑不算复杂却有两条独立注册通道。这篇先讲第一通道Name/Serial的完整破解与算法还原再顺带把爆破思路和环境配置一起交代了适合刚接触逆向、想在真实小目标上练断点和读汇编的朋友。提前声明一句CrackMe本身就是作者公开给安全学习者练功的分析过程也就停留在学习研究层面别想着拿去对付商业软件。1. 项目概述与整体破解思路1.1 为什么选Acid_burn作为入门目标CrackMe这个传统在国外逆向论坛流传了很多年后来有人整理了160个由易到难的合集里面既有随手写着玩的也有堆了反调试、虚拟机壳、自修改代码的硬茬。Acid_burn在整个序列里属于非常靠前的一档它没有加壳编译特征明显主界面就两个输入窗口和两个验证按钮对新手来说压力小很多。我见过不少人一上来就找带壳或者带反调试的题目练结果连壳都没脱掉就放弃了其实这违背了练习的初衷。入门阶段最重要的是建立“定位关键代码 - 观察数据变化 - 还原算法”这条闭环思维而不是追求打穿多难的壳。Acid_burn恰好把这三步都覆盖了第一次跑完会很有成就感适合建立信心。另外它是个对话框程序标题栏写着Acid Burn运行后能看到两个区域一个要求输入Name和Serial另一个要求只输入Serial。这种UI设计本身就暗示作者放了两道验证逻辑后面分析时会发现两条路径完全不相关这也是这个题目比较厚道的地方。把第一道验证完整走通你就掌握了对付大多数简单CrackMe的通用套路。1.2 先跑起来界面观察与信息收集拿到程序别急着往调试器里扔第一步永远是先运行它观察行为。双击Acid_burn.exe弹出的主窗口上有提示文本底部有两个按钮分别对应两种验证模式。随便输入一个用户名和一串数字点按钮后会弹窗提示注册码错误具体文本大概是提示请输入正确的序列号之类。这个弹窗就是后续逆向的第一个突破口。所有对话框程序无论用什么框架最终都要调用MessageBox相关API来显示提示而错误提示字符串一定以明文或编码形式存在于程序里。所以我们后续的思路就是用工具找到字符串 - 回溯到弹窗代码 - 往上翻找到跳转条件 - 吃透比较逻辑。还要观察程序关闭、置顶、拉伸等行为确认它是标准Win32对话框程序。另外顺手用文件属性看一下版本信息虽然不一定有但有些作者会在资源里留彩蛋。这些前期信息收集动作看着不起眼实际能帮你少走很多弯路。现场记录里我写下了程序运行时的窗口句柄、按钮ID这类信息等会儿动态调试时要用。2. 环境准备与工具链选型2.1 查壳与识别DIE和PEiD逆向的第一步不是打开反汇编器而是先查壳。壳的作用是压缩或加密原始代码有壳的话你看到的入口点根本不是程序真正的逻辑。Acid_burn这种老程序大概率没壳但养成查壳习惯非常必要。我用的是DIEDetect It Easy比老牌的PEiD识别更准界面也更友好。把exe拖进DIE立刻看到结果Microsoft Visual C 6.0无壳。这意味着所有的程序代码都是原始机器码可以直接静态分析。顺便看了下区段信息标准PE结构没有可疑的自校验区段不需要处理重定位和IAT的问题。如果你机器上只有PEiD结果也一样就是看“编译器/加壳工具”那一栏有没有类似“UPX”、“ASPack”之类的内容。有壳的话就要先脱壳常见的UPX用工具一键脱其他壳可能要手动找到原始入口点。Acid_burn省了这一步直接把精力放在逻辑分析上。查壳做完后记下目标程序的MD5或SHA-1值方便后面一旦改坏了还能跟原始文件对比。老逆向工程师的习惯是建一个专门的工作目录原始文件、备份、调试脚本分开放别嫌麻烦等程序被改崩的时候就知道这步有多值了。2.2 调试器选型x64dbg还是IDA对于这种32位的老程序我首选x64dbg的32位版本也就是x32dbg。它是目前Windows平台上最顺手的开源调试器界面信息密度大断点、单步、内存窗口操作流畅还自带强大的字符串搜索功能。新手不需要一上来就啃IDA虽然IDA的静态反编译对还原逻辑帮助巨大但图形模式和反编译结果容易让人忽略底层的执行细节。实际我的工作流是先用x64dbg动态跑定位关键地址和指令再用IDA对关键函数做静态分析两边对照着看。如果你只装了IDA免费版一样够用只是没有F5反编译纯汇编阅读也能完成这个题目因为Acid_burn的验证函数并不长。OllyDbg作为老牌工具依然能用但对新系统兼容性和插件生态不如x64dbg。我这里全程用x32dbg演示除了调试器本身还准备了一个ScyllaHide插件虽然Acid_burn没有明显反调试但预防总比临时找工具强。再有就是虚拟机里跑一个干净的Windows系统既能防止真实机被恶意代码污染也方便随时恢复快照。2.3 环境配置细节有些CrackMe会故意检测调试器一旦发现调试环境就改变行为或直接退出。Acid_burn这个版本我没有遇到这类问题但从练习角度出发所有反调试特性都值得你留个心眼。我的建议是在虚拟机里装好系统后先打个快照调试中遇到问题随时还原。x64dbg自带的设置里也建议把“系统断点”停用这样程序运行行为更接近真实环境。另一个细节是调试器的符号加载。老程序没有PDB调试符号不可能看到函数名所以我们要靠API调用和字符串引用来推断代码意图。x64dbg会把常见的Windows API名称标出来比如GetDlgItemTextA、MessageBoxA这些API名称本身就是极强的线索。把断点下在API上能看到程序在哪个时机调用了它们这是逆向的核心手法之一。还有一点很多人会忽略把系统字体、DPI缩放调整好因为老程序在缩放系统下界面可能错位影响观察。虚拟机里建议保持默认96 DPI这样调试窗口和程序窗口的坐标、尺寸对照起来更方便免得在不同环境之间换算把自己绕晕。3. 静态分析从弹窗文本到关键分支3.1 字符串搜索定位弹窗代码把Acid_burn.exe拖进x32dbg程序会停在入口点。这时先别急着运行直接右键菜单里找“搜索字符串”或“引用”不同版本名字略有差异。x32dbg会在整个模块的可读段中搜索ASCII和Unicode字符串结果列表里通常能看到大量可读文本。在搜索结果里找刚才运行时见过的错误提示文本比如“Make sure you enter a valid serial”这类错别字带感叹号的文本版本很多不同镜像里的措辞可能略有出入。我的调试记录里是直接搜到了明文。双击这条结果x32dbg会跳到引用这个字符串的那条指令上通常是压栈操作准备把这串文本作为MessageBox的参数。从这里往下看能看到一串调用序列压入句柄、压入文本指针、调用MessageBoxA。而在这段代码往上翻几行一定有一个比较或跳转指令决定程序是走成功弹窗还是失败弹窗。找到那个跳转指令就找到了整个验证逻辑的核心分岔点这是几乎所有简单CrackMe的共同结构。新手容易犯的错是在字符串窗口里看到结果就直接双击跳过去之后忘记看上下文结果被大量无关代码淹没。正确做法是先记录这个字符串所在的内存地址比如0x00401A2B然后跳过去把视野扩展到前后二三十条指令看清来龙去脉。3.2 回溯上一层找到决定成败的跳转在错误弹窗的代码块上方我看到了典型的比较逻辑。首先是调用某个函数处理用户输入然后得到返回值紧接着是test或cmp指令最后是jz/jnz分支跳转。具体的指令形态经常是00401A00 call 00401380 ; 处理用户名和序列号的验证函数 00401A05 test eax, eax ; 检查返回值是否为0 00401A07 jz short 00401A20 ; 结果为0则跳去错误弹窗 00401A09 push 00401B30 ; 压入成功提示文本 00401A0E call MessageBoxA这里eax保存的就是验证函数的返回值0表示验证失败非0表示成功。jz指令的意思是在ZF标志为1时跳转而test eax, eax在eax为0时会把ZF置1所以逻辑是验证失败 - 跳去错误弹窗。如果我们把jz改成jnz或者把test改成xor eax, eax再配合nop掉跳转就能让程序无论验证是否成功都走成功分支这就是最原始的“爆破”。把这个位置记到书签里后面写爆破补丁时要用。这里先不急着手改因为爆破会让我们错过算法本身。对于以学习为目的的CrackMe来说爆破只是练手真正有价值的是把验证算法逆出来写成注册机。3.3 定位Name/Serial验证函数从错误弹窗往上翻看到call 00401380这就是验证函数的入口。在x32dbg里输入地址直接跳过去查看函数开头发现它首先调用GetDlgItemTextA获取编辑框里的用户名和序列号。具体代码是这样的00401380 push ebp 00401381 mov ebp, esp 00401383 sub esp, 40 00401386 push 40 ; 缓冲区长度 00401388 push offset buffer ; 接收文本的缓冲区 0040138D push 1 ; 控件ID 0040138F push hWnd ; 窗口句柄 00401395 call GetDlgItemTextA ; 读取用户名看到控件ID、窗口句柄、缓冲区长度的压栈顺序基本就能确认这是在读取用户输入。继续往下还有一次GetDlgItemTextA调用参数里控件ID不同这次读取的是序列号输入框。两个字符串都读出来之后程序把它们传给真正的算法逻辑。现在静态层面已经有了完整地图界面层读输入验证函数处理输入返回结果控制弹窗。后面动态调试只需要聚焦在00401380这个函数内部观察它如何变换用户名和序列号。静态分析到这里其实已经够写一个爆破版了但我们要的是注册机所以继续往深处挖。4. 动态调试还原Name/Serial注册算法4.1 在关键函数入口下断点动态调试之前先在00401380这个验证函数入口下一个断点。方法是在x32dbg的地址栏输入00401380回车跳过去按F2下断点。然后重新运行程序等界面出现后随便输入用户名和序列号比如用户名填“hello”序列号填“123456”点验证按钮。程序会在进入验证函数时停下这代表我们成功拦截到了关键逻辑。这时候先看寄存器窗口尤其注意EAX、ECX、EDX的值再看堆栈窗口能看到返回地址和压入的参数。按F8单步往下走很快会看到GetDlgItemTextA执行之后缓冲区里就出现了我们输入的字符串。x32dbg的数据窗口可以跟随寄存器或固定地址显示。把缓冲区地址输进去能看到ASCII形式的“hello”和“123456”。这就像在手术台上找到了病灶之后每一步操作都清清楚楚。初次跟这种函数我的建议是按F8一行行走遇到call不要盲目按F7钻进去先看call的目标地址是不是系统API。如果是user32、kernel32里的API放心让它跑完如果是模块内部函数再考虑是否要进入。4.2 算法推导从汇编还原注册码生成规则整个验证函数的核心在一个循环里。从我的调试记录看代码大致是这样004013C0 xor ecx, ecx ; 累加器清零 004013C2 mov edx, offset name_buf ; 指向用户名字符串 004013C8 movzx eax, byte ptr [edx] ; 取一个字符 004013CB test eax, eax ; 判断字符串是否结束 004013CD je short 004013E0 ; 结束则跳走 004013CF add ecx, eax ; 累加字符ASCII码 004013D1 inc edx ; 指向下一个字符 004013D2 jmp short 004013C8 ; 继续循环这段逻辑就是把用户名字符串的每个字符ASCII码累加到ECX。循环结束以后又看到了几条关键指令004013E0 imul ecx, ecx, 0x2F ; 累加结果乘以常数 004013E3 add ecx, 0xAABB ; 再加上一个常数 004013E9 xor ecx, 0x1234567F ; 异或 004013EF mov eax, ecx 004013F1 xor edx, edx 004013F3 mov ecx, 0x1F31 ; 除数 004013F8 div ecx ; 除以常数取余数 004013FA mov [serial_buf], eax ; 把结果转成字符串看到这里算法基本清楚了。它先对所有用户名字符求和再乘常数、加常数、异或常数最后对一个大数取模得到最终的序列号数值。接下来程序会把这个数值转换成十进制字符串然后和用户在序列号框里输入的内容做字符串比较。如果一致返回成功标志。注意不同渠道下载的Acid_burn镜像在常量和细节上可能不一样我这个记录基于自己虚拟机的调试现场。CrackMe练习的重心是掌握还原流程与其纠结具体某个常量不如把“从循环跟到数据处理再从数据处理跟到字符串比较”这套方法论练熟。动手验证时务以你自己调试器里看到的指令为准。4.3 字符串比较与返回值的确定在验证函数的尾部看到了把计算出的整数转成字符串的代码。通常是用wsprintfA或自己写的一段十进制转换循环。转换完成后程序调用lstrcmpA或一个内部比较函数对比两个字符串。比较指令是典型的00401430 call lstrcmpA ; 比较生成的序列号和用户输入的序列号 00401436 test eax, eax 00401438 jne short 00401442 ; 不相等则返回失败 0040143A mov eax, 1 ; 相等则返回成功 0040143F ret 00401442 xor eax, eax ; 失败返回值 00401444 retlstrcmpA的返回值在eax0表示两串相等非0表示不相等。jne在比较结果非0时跳走直接返回0所以整个验证函数的成功条件就是程序自己算出的序列号字符串和用户输入的完全一致。到这里算法完全展开了。我们把这段逻辑写成伪代码后面注册机就是它的直接翻译。顺带说一下如果你想偷懒只做内存补丁那在00401436把jne改成je同样可以爆破成功但少了理解算法的过程。我建议至少跟完一遍循环再看注册机会更容易理解。5. 注册机编写与完整验证5.1 Python脚本实现注册机算法已经还原注册机无非是把汇编里的运算翻译成高级语言。由于算法基于名字字符的ASCII累加所以用户名取什么注册机就能算出对应的序列号。我这里用Python写了一个简单脚本直接对应刚才的调试结果。def generate_serial(name): total 0 # 对应汇编里的字符累加循环 for ch in name.encode(ascii): total ch # 对应汇编里的乘常数、加常数、异或 total total * 0x2F total total 0xAABB total total ^ 0x1234567F # 对应汇编里对常数的取模 serial total % 0x1F31 return str(serial) if __name__ __main__: username input(请输入用户名: ) print(序列号为:, generate_serial(username))把脚本保存成keygen.py在命令行运行输入和调试时一样的用户名“hello”脚本输出的序列号就是程序计算的结果。整个脚本不过十几行因为验证函数本身就不复杂这正体现了逆向工作中“算法还原”取代“暴力破解”的价值一旦知道规则任何用户名都能在一秒内生成合法序列号。如果手边没有Python用C语言写也完全没问题逻辑一模一样。甚至可以在Excel里用公式模拟出来本质就是一个数学函数。从这个角度说CrackMe里的序列号验证本质上就是认证算法的比拼作者试图用各种运算增加逆向难度而我们做的是通过调试把运算链还原出来。5.2 联调验证与C语言版本思路算法要经过程序实测才算数。回到Acid_burn程序在Name框输入“hello”在Serial框输入刚才Python算出的序列号点击验证弹出了成功提示。这一步验证了我们对算法的理解完全正确。如果你觉得Python每次还要跑到终端里复制序列号麻烦可以把脚本改成交互式GUI甚至加个读取名字文件的批处理模式。但作为教学示例简洁是第一位的。C语言版的代码也很直接#include stdio.h #include string.h int main() { char name[128] {0}; unsigned int total 0; unsigned int serial 0; printf(Enter name: ); scanf(%s, name); for (int i 0; name[i]; i) { total (unsigned char)name[i]; } total total * 0x2F; total total 0xAABB; total total ^ 0x1234567F; serial total % 0x1F31; printf(Serial: %u\n, serial); return 0; }用惯了哪些语言就用哪个实现重点是理解运算顺序。这里有个细节值得注意汇编里用的是32位寄存器所以C语言里的变量类型应该是unsigned int而不是int否则负数或溢出会干扰最终结果。即使在Python里也要注意取模运算的返回值是否符合汇编语义。联调成功之后整个“定位目标 - 追踪算法 - 编写注册机 - 实测验证”的流程就走通了。这也是做一切CrackMe乃至软件逆向的标准路径。6. 常见问题与避坑清单6.1 字符串搜索找不到引用怎么办不是所有CrackMe都把错误提示明文存放在数据段里。有的程序会动态拼接字符串有的用Unicode编码有的甚至把字符串加密运行时才在内存中解密。如果你在x32dbg的字符串搜索里一无所获可以尝试以下办法先在程序里随便输入错误数据并让它弹窗然后在x32dbg里对MessageBoxA下断点重新运行并触发弹窗。当程序停在MessageBoxA的调用处时看堆栈里的参数第二个参数就是文本内容的指针顺着指针跳到数据窗口就能看到真实字符串。这个办法对所有用MessageBox显示错误消息的程序都有效。对于Unicode字符串注意x32dbg的搜索选项里要勾选Unicode。有些老版本默认只搜ASCII漏掉中文或宽字符串的情况很常见。Acid_burn用的基本都是ASCII所以不会遇到这个问题但坚持用API断点兜底的习惯很重要。6.2 单步跟丢、断点失效与反调试干扰单步跟踪时最怕跟到系统DLL内部或者在某个call里绕了半天出不来的情况。Acid_burn本身没有反调试但如果你遇到有反调试的程序常见的特征包括调用IsDebuggerPresent、读取时间戳计算运行时长、用int 2D指令触发异常。处理手段不外乎三个一是用ScyllaHide这类插件隐藏调试痕迹二是换静态分析直接读汇编代码而不运行程序三是忽略反调试逻辑找到它判断调试器的地方改成相反分支。对新手来说遇到明显反调试可以直接换下一个CrackMe没必要死磕等经验积累够了再回头收拾它。单步跟丢的另一个常见原因是按了F8但遇到loop循环程序在几十条指令里反复跳新手容易看得眼花。这时候不要继续跟直接看到循环结构后分析循环体确定累加器和循环结束条件再跳到循环结束后的下一条指令。把大循环当做一个黑盒子处理只在循环开始和结束处下断点效率会提高很多。6.3 爆破后程序异常怎么办爆破的本质是修改机器码一旦改错位置或者改出越界指令程序可能直接崩溃。我见过不少新手把jz改成jnz之后保存结果程序一运行就报非法指令。原因是jz和jnz指令长度相同但跳转偏移不同如果没改全后续字节指令边界就乱了。安全的做法是只修改跳转条件为目的的字节比如把74改75或者把0F 84改0F 85改动幅度最小。如果想把跳转变成无操作建议用90NOP填充而不是删除指令。修改前备份原始文件用十六进制编辑器确认修改位置的字节内容改完后再用哈希比对确认自己到底动了哪些字节。爆破文件测试时也要小心有些CrackMe有内存自校验运行时会重新计算自身文件的校验值发现与存储值不一致就用假成功弹窗迷惑你。Acid_burn没有这种机制但养成“爆破后用重新运行程序验证”的习惯总没错。6.4 算法还原时容易犯的三个错误第一个错误是把累加器的初始值记错。很多验证函数加载局部变量时有复杂的偏移计算比如mov eax, [ebp-0x8]你以为是0实际之前已经赋过值。建议在每个循环开始前记录寄存器状态再在循环结束后对比。第二个错误是忽视数据宽度。汇编里movzx、movsx、push的缓冲区长度都影响数据读取范围。字符串比较时一个字节的差异都可能导致序列号错误。注册机里尤其要注意类型宽度C语言里int和unsigned int的差异在取模运算时非常明显。第三个错误是过早跳到结论。看到加法就以为只有加法看到异或就以为只有异或实际算法往往是一整串复合运算。最好的习惯是把每一条算术指令都记下来最后分别验证每一步的结果确保没有遗漏任何一条指令。写在后面练习方法和下篇预告这套160个CrackMe的题目每做一道能学到的东西都很实在。我的个人经验是从一开始就用一个文本文件记录每个程序的调试过程包括断点地址、关键指令、算法推导、注册机代码做完一个整理一个。回头翻看这些笔记能清楚看到自己分析方法的变化这种进步比单纯刷题数量重要得多。Acid_burn的第二种验证入口“Serial Only”还没分析那个入口的逻辑和Name/Serial完全不同涉及的比较方式也更有意思。下一篇我会继续写那个入口的完整分析过程再加上一些针对反调试和程序自校验的实战处理。先把Name/Serial这条路径练透工具操作形成肌肉记忆再往后走会顺利很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑