☰
Crackme逆向分析001:静态分析、动态调试与序列号算法还原
2026/10/19 6:26:58 网站建设 项目流程

最近开始整理一个长期逆向学习计划,计划把常见的Crackme做成一个系列,暂定叫“Crackme逆向分析365例”,001号是块很顺手的入门练手石。这个Crackme是Windows 32位图形界面程序,界面简单:一个用户名输入框、一个序列号输入框,外加一个“Check”按钮。点击校验时,如果序列号不对,会弹一个“Try Again”的对话框。正经做逆向的老手可能觉得这题目太基础,但对于刚接触逆向、想从零开始建立分析框架的人,这一例能把静态分析、动态调试、算法还原整条链路完整走一遍。我大概花了一下午,从查壳、看区段到断点下位置、跟寄存器,最后把序列号生成算法完全还原,全程没有用任何花哨技巧。如果你也刚开始碰这类题,这篇流程可以直接照着抄。

1. 拿到Crackme之后的第一步——信息收集与识别

1.1 文件类型与架构识别

很多初学者拿到一个exe就直接丢进调试器,结果折腾半天连入口点都找不到。正确做法是先做基础鉴定,搞清楚这个程序是32位还是64位、用什么编译器生成、有没有加壳、子系统是窗口还是控制台。我习惯先用一个工具做基础探测,常见的有DIE(Detect It Easy)、Exeinfo PE,甚至命令行版file也能提供基础信息。

这个Crackme我一开始用Exeinfo PE打开:

  • 文件类型:PE32 executable (GUI) Intel 80386
  • 子系统:Windows GUI
  • 编译器/打包器:Microsoft Visual C++ 6.0
  • 区段:.text、.rdata、.data、.rsrc,数量正常
  • 壳:未识别到

看到VC6编译,说明这是老程序,内部逻辑相对简单,不会使用太复杂的运行库。看到GUI子系统,可以预料界面会通过Win32 API创建。看到没有壳,就可以直接进行静态分析,省去了脱壳的步骤。如果是UPX加壳,第一件事就会变成脱壳,但这里不需要。

另外,需要留意入口点地址。通常VC6程序入口是0x00401000左右,文件映像基址0x400000。后来用IDA加载,确认入口点停在0x004010D0附近,符合预期。这一步虽然简单,但价值很高:知道了架构和编译器,你就能预判程序可能调用的API类型,比如用户输入大概率会走GetDlgItemTextA/W,错误提示大概率会走MessageBoxA/W。

1.2 加壳与保护检测

加壳检测要专门拿出来说,因为它直接决定后面主攻静态还是动态。许多刚入门的人一上来就在IDA里按F5,看伪代码看不明白就卡死,其实是壳干扰了反汇编。可以用DIE的“通用扫描”看一下熵值,如果几个区段的熵都接近8.0,基本就是加壳或者加密节区。

这个CrackMe001的熵值:

  • .text 熵 6.1 左右,符合普通代码特征
  • .rdata 熵 4.5,基本是常量字符串
  • .data 熵 2.8,普通全局数据

没有异常高熵,说明没有加密or压缩外壳。同时,我用搜索工具扫了字符串,没有看到IsDebuggerPresent、GetTickCount、rdtsc等常见反调试特征,也没有定时检测或完整性校验。这类带反调试的Crackme往后在365例里会碰到,001号故意去掉这些干扰,适合拿来建立信心。

但不要因此掉以轻心。即便是无壳无反调试,程序仍可能用内存比较、非标准算法、甚至把关键字符串拆开拼接来干扰字符串定位。所以接下来静态分析时,我的策略还是从字符串交叉引用和导入表入手,而不是指望一眼看到明文密码。

2. 静态分析的突破口——定位关键代码

2.1 字符串与导入表定位

用IDA打开无壳VC6程序,通常会看到系统Stub后进入WinMain,WinMain再调用DialogBox或者CreateWindow,然后进入消息循环。对于对话框程序,最常用的是通过“DialogBoxParamA”创建模态对话框,然后在对话框过程函数里处理WM_COMMAND消息。

我打开IDA后,没有直接看反汇编,而是先切换到Strings窗口(Shift+F12),搜“Try Again”和“Correct”等提示。结果找到了两处字符串:

  • “Correct!”
  • “Try Again”

双击“Try Again”,IDA会自动跳到引用这个字符串的代码片段。这个代码片段通常是一个MessageBoxA/MessageBoxW调用。找到后,按X查看交叉引用,可以看到它被一个跳转指令引用,而跳转指令的上面就是比较指令。于是整个校验逻辑的轮廓就出来了。

导入表也是重要线索。在左侧Imports窗口查看,发现程序导入了GetWindowTextA、GetDlgItemTextA、MessageBoxA、wsprintfA等。这里透露了一个信息:程序很可能从控件中读取输入,用ANSI字符串处理,最后格式化结果或展示错误。没有导入加密库,所以算法大概率是手工汇编实现的,不会太复杂。

字符编码也要留意。如果导入的是GetDlgItemTextW,计算时就必须考虑宽字符(UTF-16LE),每个字符占2字节。001号导入的是GetDlgItemTextA,所以算法直接处理单字节ASCII码,这给还原降低了难度。以后做Crackme时,先看API列表再猜算法形态,是一种很有效率的工作模式。

2.2 算法识别与关键跳转

定位到关键代码区域后,我在IDA里把反汇编整理出来。程序在Check按钮的WM_COMMAND分支里,依次做了四件事:

  1. 调用GetDlgItemTextA获取用户名
  2. 调用GetDlgItemTextA获取用户输入的序列号
  3. 计算一个校验值
  4. 将校验值和序列号转换出的整数比较

核心反汇编很典型,整理成带注释的样子大致像下面这样:

00401350 mov esi, offset name_buf 00401356 call GetDlgItemTextA 0040135B mov esi, offset key_input 00401360 call GetDlgItemTextA ; 初始化累加器 00401365 xor edi, edi ; edi = 0 00401367 xor ebx, ebx ; ebx = 0 (位置计数器) ; 用户名循环开始 00401370 movzx eax, byte ptr [esp+ebx+name_buf] 00401375 test eax, eax 00401378 je short 004013A0 ; 碰到字符串结尾就跳出 0040137A inc ebx ; 位置索引+1 0040137B imul eax, ebx ; eax = 字符ASCII * 位置序号 0040137E add edi, eax ; 累加到edi 00401380 movzx eax, byte ptr [esp+ebx-1+name_buf] 00401386 jmp short 00401370 ; 循环结束后对edi做混淆 004013A0 imul edi, edi, 0xC5A5 ; edi *= 0xC5A5 004013A7 xor edi, 0x1D872B41 ; edi ^= 0x1D872B41 004013AD mov [esp+calc], edi ; 把用户输入的字符串转成整数 004013B1 push 10h ; 十进制基数 004013B3 push offset key_input 004013B5 call ds:lstrtol? 或 strtol? ; 实际可能是 CountLeadingOrTrailingSpaces 或 CRT的strtol 004013BB mov [esp+user_key], eax ; 比较 004013C0 mov eax, [esp+calc] 004013C3 cmp eax, [esp+user_key] 004013C7 jnz short loc_TryAgain 004013C9 push offset CorrectString 004013CE call MessageBoxA 004013D3 ret

注意,上述反汇编是经过风格整理的,实际地址和指令顺序可能略有差异,但核心算法很清晰:

  • 遍历用户名,按字符ASCII码乘以其位置序号(从1开始),累加求和
  • 累加后乘固定常数0xC5A5,再异或固定常数0x1D872B41
  • 把输入序列号以十进制解析成整数
  • 两者相等则通过,否则失败

这个算法第一眼看上去比简单累加多了一道混淆,但本质还是一个线性路径,没有任何分支嵌套,非常容易还原。如果程序直接拿字符串比较,那更简单;但那种题通常仅在很基础的Demo中出现。001号选择“输入序列号解析为整数”的方式,是为了让你练习参数进出的跟踪。

关键跳转是004013C7的jnz loc_TryAgain。如果理解了这个跳转,就可以直接在这里改为jz或者nop实现爆破。不过我的目的是还原算法,不是单纯破解,所以后面进入动态阶段进一步验证这个静态结论。

3. 动态调试实战——从反汇编到破解过程

3.1 下断点与动态跟踪

静态分析只能“猜”,动态调试才能“验证”。我使用x64dbg加载CrackMe001(64位系统也能调试32位程序,但需要用32位版本或启动WoW64支持)。OllyDbg也能做,但x64dbg在新系统上稳定性更好,快捷键逻辑也更顺手。

动态调试第一步,给关键API下断点:

  • 在命令框输入bp GetDlgItemTextA
  • 在命令框输入bp MessageBoxA
  • 也可以直接对004013C7的jnz下F2断点,因为我们已经从静态分析得知地址

实际操作时,我先给GetDlgItemTextA下断,再运行程序。填写用户名“abc”,序列号填“12345”,点击Check。程序立刻断在GetDlgItemTextA处,第一次是读取用户名,第二次是读取序列号。单步往下走,一直到00401365处,然后打开寄存器窗口观察edi的变化。

在动态调试中,我关注几个值:

  • 用户名字符串:abc,三个字节:0x61、0x62、0x63
  • 循环结束后EDI的值:0x00CE5D18(对应上文算法)
  • 常数0xC5A5乘以0x1D872B41异或以后的结果

用计算器手动算一遍:

  1. 累加:0x61*1 + 0x62*2 + 0x63*3 = 97*1 + 98*2 + 99*3 = 97 + 196 + 297 = 590
  2. 乘0xC5A5(50597):590 * 0xC5A5 = 590 * 50597 = 29,852,230,十六进制为0x01C76646
  3. 异或0x1D872B41:0x01C76646 XOR 0x1D872B41 = 0x1C404907,十进制473099015

然后程序将输入序列号“12345”解析成整数12345,比较,失败。如果输入用户名“abc”,正确序列号应该是473099015。我在对话框里输入这个数字,点击Check,果然弹出“Correct!”。这确认了静态分析完全正确,同时也验证了动态调试中寄存器EDI的走向。

这一步的实操意义在于:不要只在纸面上看算法,要盯着寄存器实际变化。很多Crackme在关键位置会有常量微调,比如把ASCII和位置索引做异或、把长度额外加入等。动态单步执行能把每次累加都看得清清楚楚,比静态猜测更靠谱。

3.2 序列号算法还原

现在把算法整理成伪代码,方便日后自己写“注册机”或学习其他题目:

#include <stdio.h> int main() { const char* name = "abc"; unsigned int edi = 0; int i = 0; while (name[i] != 0) { unsigned char c = (unsigned char)name[i]; i++; edi += c * i; // 位置从1开始 } edi *= 0xC5A5; edi ^= 0x1D872B41; printf("%u\n", edi); return 0; }

这就是标准注册机逻辑。当然,真正的Crackme还会允许用户名长度变化,有些会把前两个字符单独处理,有些会先用小写统一字符,有些会限定用户名长度必须大于等于5。001号没有这些限制,输入任意单字符也能运行。但单字符时位置索引只有1,没有太大区别。

写注册机时要特别注意整数溢出。unsigned int在32位环境下是32位,乘法溢出时取低32位。我用C语言直接算出显式值;如果用Python,需要加& 0xFFFFFFFF模拟溢出。很多人拿Python验证算法时,忘记模拟无符号整数的溢出,结果怎么都对不上。我在实测中就用Python写过一版,后来加了& 0xFFFFFFFF才一致。

另外,程序把序列号当作十进制整数解析,这就要求输入的序列号不能带负号、不能带小数。如果算法生成的数值超过4294967295,实际上由于溢出大部分情况不会超过。使用push 10当基数,可以从反汇编确认。动态调试中如果在004013B5的调用处下断点,查看栈上数据也可以看到字符串“12345”和参数10,这能验证你的静态判断。

4. 常见问题与排查技巧实录

4.1 动调中的反调试与绕过

001号没有反调试,但不代表以后碰不到。很多时候你明明在关键函数下好断点,程序却跑飞或弹异常,这是反调试在捣乱。常见反调试手段包括:调用IsDebuggerPresent、读取PEB的BeingDebugged标志、用rdtsc计算指令时间差、检测窗口标题、用SetUnhandledExceptionFilter做SEH混淆。

如果遇到IsDebuggerPresent,最简单的绕过方式是在调试器里改标志位,或直接patch掉call IsDebuggerPresent,让它返回0。更稳妥的办法是在调试器命令行里用bp IsDebuggerPresent下断,让函数返回时修改EAX为0。也可以用ScyllaHide这个插件自动隐藏调试器,但建议不要过度依赖插件,至少要知道原理。

对于001号,没有这些干扰,反而要养成“先字符串定位,再关键跳转,再动调验证”的习惯。遇到反调试时,先静态找反调试调用点,再动调单步过,一个个禁用。

4.2 写注册机的思路

算法还原后,不光要会用指定用户名算序列号,还要具备批量生成的能力。我通常写一个简单的控制台脚本,输入用户名,输出序列号。

需要注意几个细节:

  • 字符编码。如果程序用GetDlgItemTextW,那么name[i]每次取2字节,必须按宽字符处理,而不能用普通char。
  • 大小写敏感性。001号算法对大小写敏感,因为ASCII码不同。但有些Crackme会先调用CharLowerA或者CharUpperA统一大小写,如果你忽略这一步,算出的序列号永远不对。
  • 溢出模拟。C语言中unsigned int会自动溢出,Python需要手动& 0xFFFFFFFF。
  • 字符串结尾。调试时如果字符串缓冲区里有残余字符,可能把自己坑了。动态时一定要确认循环到00就停,别把栈上垃圾值算进去。

我通常先把算法写成C语言再交叉验证一两个用例,然后改成Python或任何语言。001号用“用户名abc”验证出序列号473099015,用“xyz”再算一个,实测通过。交叉验证是保证脚本可用的重要步骤。

4.3 断点失效的排查

新手最常问的是“为什么我按F2下断点跟的时候,程序根本没断”。这个问题在动态调试中经常出现,主要原因有:

  • 地址不对。比如你直接手动输入了一个错误地址,没有看反汇编,断点落在其他指令中间。解决:重新用IDA或x64dbg的反汇编窗口定位,确认地址处确实是指令边界。
  • 代码自修改。有些Crackme运行时会把代码段解压到其他地方,静态地址不是最终的运行地址。解决:先在printf或MessageBoxA下断,再通过调用栈回溯到实际调用点。
  • 多线程。程序可能在另一个线程执行校验逻辑,你在主线程下断没效果。解决:对所有线程下断点,或者用“中断当前进程”的方式查看调用栈。
  • 优化导致地址不固定。无壳程序通常固定地址,但如果加载时启用了ASLR,可能每次运行地址不同。解决:在x64dbg里关闭随机化,或者使用IDA附加时重新定位。

001号没有上述问题,但了解这些排查路径,下一例遇到时不会懵。

5. Crackme系列练习的方法论

5.1 从静态到动态的闭环

很多人学逆向只看书不实操,或者只会F5看伪代码。建议拿到每个Crackme都强制自己走完整闭环:先查壳、再看导入表和字符串、然后静态还原大致逻辑、接着动态验证关键寄存器、最后写注册机。这个流程可能在一开始很慢,但每做一例,速度都会提升。

001号正是这个闭环的最小化模板。如果只爆破,一分钟就完事;但还原算法,你就能学到“常数+CMP”这种最基础校验模式。以后遇到更复杂的题,通常是叠加更多混淆、加入用户名合法性检查、使用复杂加密算法,但底层思路都是找出输入到输出的映射关系。

做这个365例的初衷,是想把常见模式都过一遍:简单累加、异或、CRC32、MD5、AES、自解密、反调试、反虚拟机、网络验证等。001号是“累加加混淆”的代表。

5.2 工具选择的个人心得

工欲善其事,必先利其器。我的建议组合:

  • 文件识别:DIE
  • 静态分析:IDA(或Ghidra)
  • 动态调试:x64dbg
  • 辅助工具:Python计算器、010 Editor看二进制、Process Monitor看文件/注册表行为

不一定要全套正版插件,很多开源的足够。IDA的伪代码偶尔会误导,因为编译器优化后的C源码还原出来很抽象,不如直接看汇编,搭配动态寄存器变化更准确。001号用IDA看伪代码也能看明白,但使用imul edi, edi, 0xC5A5这样的汇编指令后,反而更容易理解溢出。

如果一个Crackme的算法无法从静态恢复,我建议动态跟踪关键循环,记录每轮寄存器的值,用Excel或Python列出来,通常能在几十行内找到规律。这个“归纳法”在遇到自定义加密算法时特别好用。

写在最后

Crackme001这一例整体不算难,但它让我体会到一件事:很多看似高大上的“逆向分析”,落到具体题目上其实就是找字符串、找比较、还原参数三个动作。只要愿意跟着调试器一步步走,哪怕一开始慢,也会越走越顺。我在操作中遇到最大的一次弯路,是误以为程序使用了宽字符,直到动态断下才发现是ANSI字符串,浪费了十几分钟。后来我习惯性先看导入表再决定字符处理方式,这类低级错误就很少再犯。

如果你也在做这个系列,拿到一个Crackme后别急着破解掉就完事,试着把算法用代码写出来,再试着换一个用户名测试,看能不能稳定生成。这个过程才是逆向分析能力的真正积累。下一题我计划把目标放在带反调试的Crackme上,到时候再分享一套从查杀反调试到还原算法的完整思路,这题就先到这里。

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

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

立即咨询