CrackMe字符串比对逆向入门:从静态分析到动态调试的完整实战 CTF 的逆向题里CrackMe 属于最友好的入口没有之一。你拿到的不是需要对抗的某个商业软件而是一个专门写出来让你解的谜题程序接收输入内部做一次字符串比对结果决定输出恭喜还是报错。字符串比对恰恰是这类题目里最常见、也最适合练手的逻辑所以从它下手等于用最小成本把整条逆向分析流程串起来。这篇文章就用一个我们内部复刻的入门题 crackme0 做例子从静态分析一路走到动态调试把每一步为什么这么做、工具怎么用、踩了什么坑都讲透。适合刚接触逆向、能敲点 Linux 命令行、想打 CTF 但拿到 ELF 文件不知道从哪下刀的同学。1. 先搞明白 CrackMe 这类题到底在考什么1.1 一句话理解 CrackMe 的运行逻辑CrackMe 是专门设计出来给逆向学习者破解的小程序逻辑通常非常简单先是打印一句“请输入密码/输入 flag”然后读入你的输入再和程序内部某个“预期值”做比较相等就走成功分支不相等就走失败分支。比较的位置就是整个题目的题眼。你可能会问这不就是个带密码的玩具吗对它的价值恰恰在于“玩具”。因为程序是故意给你看的所以编译时不会上太强的保护函数符号可能还留着比较函数也大概率是公开的 libc 函数。逆向的本质就是阅读程序并还原它的行为CrackMe 把“读程序”这件事压缩到一个函数里非常适合入门。打个比方这就像拿到一个透明表壳的密码锁厂家还贴心地附上了说明书你的任务是找出那个能打开锁的数字。虽然理论上你甚至可以暴力试一万遍但更优雅的方式是直接看锁芯结构一次就得出答案。1.2 为什么字符串比对是逆向入门第一课CrackMe 家族的题五花八门但字符串比对几乎是出镜率最高的基础款原因很实在第一strcmp是动态库函数。只要程序是动态链接的反汇编里就会有一个非常显眼的call strcmpplt哪怕你不懂汇编光凭函数名都能猜到这一行在干什么。第二strcmp的返回值语义固定相等返回 0不等返回非 0。这个语义直接催生了汇编里最经典的一段流程——test eax, eax配上jne或jz。你只要见过三次这种组合第四次闭着眼都能认出来这是在根据比较结果跳分支。第三它是后面一切变体的源头。把strcmp换成逐字节循环就是“单字节比对”在比之前先 XOR 一下就是“编码比对”先检查strlen就是“长度校验型”。这些都不是新东西都是在字符串比对的基本盘上加料。所以我认为字符串比对类题目是性价比最高的练习素材它强迫你把文件识别、字符串提取、反汇编阅读、动态调试这几个基础技能各用一遍但难度又刚好不会让你劝退。1.3 工具选型够用就行别一上来就上重炮做这类题我的原则是“先用命令行小工具再用重型武器”。命令行的file、strings、objdump、readelf都是 binutils 自带任何 Linux 发行版都有不需要额外安装零成本起步。动态部分主力是gdb再加一个ltrace作为快速旁路。很多教程一上来就让你装 IDA 或者 Ghidra我不是反对图形工具而是觉得入门阶段命令行工具反而更能逼你理解指令级的东西。比如 Ghidra 会把test eax, eax; jne反编译成if (iVar1 ! 0)看着爽但你少了一次“自己读汇编、自己总结规律”的训练。等到你开始做稍微大一点的题或者遇到符号被 strip 的情况再上 Ghidra 或 radare2 不迟。工具是手段读懂程序才是目的。后面我会在实操环节说明哪些场景下图形工具反而更省力。2. 静态分析先不动代码把纸面线索翻干净2.1 拿到二进制先验明正身所有逆向题的第一步都是同一个动作file。这一步花不了十秒钟但信息量极大。$ file crackme0 crackme0: ELF 64-bit LSB executable, x86-64, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped一行输出至少能读出四件事ELF 64-bit这是 Linux 下的 ELF 格式不是 Windows 的 PE所以后面的工具链全按 Linux 思路走。x86-64架构是 64 位寄存器要用rdi、rsi、rax这种名字不能套 32 位的那套eax为主的规则。dynamically linked程序依赖动态库说明strcmp这类函数会走 PLT反汇编里可以一眼看到调用点。not stripped符号表还留着意思是main函数可以直接按名字定位省掉一大段找入口的功夫。如果看到stripped也不用慌后面第 4 节我会讲怎么在没有main符号的情况下找入口。保护属性方面入门题一般也就开个 NXPIE 都不一定开但养成习惯总是好的可以用checksec工具或者readelf -l快速扫一眼。2.2 strings 先把纸面上的线索翻出来接下来是最有“捡漏”感的一步strings。它会把二进制里所有连续可打印字符串打出来很多入门题在这一步直接白给。$ strings crackme0 /lib64/ld-linux-x86-64.so.2 ... Please input your password: Congratulation! The flag is: Wrong, try again.从这些字符串我们能锁定三件事程序会用Please input your password:提示输入成功和失败两条分支都有对应输出正确答案本身并没有以连续字符串的形式暴露在文件里。如果它直接出现在 strings 输出里这题基本就是送分的直接./crackme0输进去就能拿 flag。我们这个例子里答案没直接露出来原因很可能是作者把关键字符串拆成了单个字符或者一段一段存放用来给新手设一个小绊子。这正是需要动分析手段的情况。需要注意的是strings只提取“连续可打印字符序列”如果目标字符串被 XOR 编码过、或者按字节拆开存放它就看不见。所以你应当把 strings 当成情报收集工具而不是唯一答案来源。2.3 反汇编定位核心比较逻辑在静态阶段真正硬核的动作是反汇编。先用objdump -d把main的片段翻出来$ objdump -d crackme0 | grep -A 30 main:看到的节选大致长这样我做了简化方便阅读lea rdi,[rip0x2a7a] # Please input your password: call printfplt lea rsi,[rbp-0x40] # 输入缓冲区 lea rdi,[rip0x29c2] # %s call __isoc99_scanfplt ... lea rsi,[rip0x29d5] # 指向某个全局区域 lea rdi,[rbp-0x40] # 刚才的输入 call strcmpplt test eax, eax jne 0x4012ab # 不相等跳到失败 lea rdi,[rip0x29b4] # Congratulation! jmp 0x4012b7读这段汇编不需要懂太多抓住两个重点第一scanf之后程序把输入缓冲区地址放进了rdi把另一个全局地址放进了rsi然后调用strcmp。在 x86-64 的 System V 调用约定里函数参数从左到右依次放在rdi、rsi、rdx所以这一行strcmp的意图就是“拿我的输入和某个固定值做比较”。第二test eax, eax配合jne是经典的判断套路。strcmp返回 0 表示相等test指令检查结果是否为 0jne不相等则跳就会在返回值非 0 时跳到失败分支。换句话说只要让这个jne不跳程序就会走进成功分支。看到这里题眼已经定位了那个被放在rsi里的全局地址就是我们要找的答案所在。2.4 静态阶段的收尾工作因为 strings 没直接显示答案我们得去对应的内存区域把内容扒出来。用objdump的-s参数直接 dump 数据段$ objdump -s -j .data crackme0假设看到这样一行403050 53 33 63 72 33 74 5f 4b 33 79 21 00 00 00 00 00 S3cr3t_K3y!.......右边的 ASCII 列已经把答案写出来了S3cr3t_K3y!。把十六进制字节挨个翻译成字符结果完全一致。这个动作本质上就是在“读”程序内部存的那个固定值。静态分析到这里题目其实已经解完了。你可以直接运行程序输入这串字符大概率会看到恭喜信息。但我还是建议继续做动态调试原因很简单静态分析给的是“纸面证据”动态调试给的是“运行时的实锤”而且真实比赛里很多题单靠静态看不出来比如运行时会解码、会动态拼接字符串。所以下一节我们把 gdb 用起来验证一下刚才的判断。3. 动态调试让程序亲口把答案说出来3.1 静态之后为什么还要动态有人可能会想静态都解出来了还费劲调什么我一开始也这么觉得直到遇到反调试和自解密题目才明白静态分析经常建立在对代码的“推测”上而动态调试能看到程序真实执行时的状态。具体到字符串比对题动态调试能帮你解决三个实际问题验证你静态推断的答案对不对当答案被拆成多段或者动态生成时从内存里拿到拼接后的完整字符串观察比较前后有没有隐藏的预处理比如先 XOR 再比对、比之前先检查长度。可以说静态分析负责“缩小范围”动态调试负责“一锤定音”。两个都要会缺一个都会在后面的题里吃瘪。3.2 三个 gdb 命令解决绝大多数字符串比对打开 gdb直接对strcmp下断点$ gdb ./crackme0 (gdb) break strcmp (gdb) run程序跑起来后会要求输入随便敲几个字符比如AAAA回车。由于动态链接的程序首次调用strcmp时会先解析到 libc 的实际实现断点会在比较真正发生的那一刻命中。这里有个细节break strcmp断的是所有strcmp调用如果程序内部在别处也调用了strcmp你可能会先断到不相关的位置。处理办法很简单多按几次continue直到停在我们的比较上。判断依据是看参数里有没有出现你刚输入的内容。如果想更精准可以先用静态分析里找到的地址下断点。比如从objdump里查到call strcmpplt这条指令的地址是0x401230就执行(gdb) break *0x401230这样只会在这个调用点停下不担心误入其他strcmp。3.3 从寄存器里把答案“读”出来断在strcmp入口时rdi和rsi正好是函数的前两个参数也就是将要比较的两个字符串。用x/s命令按字符串模式查看内存(gdb) x/s $rdi 0x7ffffffde70: AAAA (gdb) x/s $rsi 0x403050: S3cr3t_K3y!第一行是我们的输入AAAA第二行就是程序拿来比对的固定值S3cr3t_K3y!。答案直接写在眼前比任何静态分析都直观。有人会问为什么rsi才是答案而不是rdi这取决于源码里调用顺序。例子里程序是strcmp(buf, secret)按调用约定buf进rdi、secret进rsi。如果作者闲得没事写成strcmp(secret, buf)那答案就跑rdi里了。所以别死记“第二个参数是答案”一定要结合反汇编里参数是怎么放的来判断。确认完参数后我们还可以看看返回值验证一下“不相等返回非 0”的语义(gdb) finish Run till exit from #0 __strcmp_sse2 ... (gdb) p $eax $1 -120-120是非 0 值对应的正是test eax, eax; jne跳向失败分支。现在退出 gdb重新运行程序输入S3cr3t_K3y!就能看到成功输出。到这里整个“静态定位 动态验证”的闭环就完整了。3.4 更快的旁门ltrace 一句话出答案如果说 gdb 是正规军那ltrace就是侦察兵出手极快。它的作用是拦截程序对动态库函数的调用并把字符串参数直接打印出来$ ltrace ./crackme0 ... strcmp(AAAA, S3cr3t_K3y!) -120输入一个错误字符串ltrace会把strcmp的两个参数原样打出来答案S3cr3t_K3y!直接暴露。整个过程连反汇编都不用做几秒钟结束。这条路线对“输入→字符串比对”的题目几乎是绝杀连调用顺序都不用管因为它把两个参数都打印了。不过别高兴太早ltrace只对动态链接的库函数有效如果程序自己写了个循环比对逻辑或者用了静态链接ltrace就看不到任何有价值的东西这时候还是得老老实实回 gdb。我的习惯是看到疑似字符串比对的题先花十秒跑一次ltrace没结果再上静态分析。这个顺序帮我省过不少时间。3.5 一条暴力路线直接 patch 掉比较分支有些场合你的目标只是拿到 flag不在乎“正常通过验证”那就可以用更粗暴的手段修改比较分支让它无论输入什么都走进成功分支。原理很简单我们发现跳转逻辑是test eax, eax; jne fail其中jne的机器码通常是75 xx。只要把75改成74指令就变成了je fail语义反了过来只有相等才跳失败不相等反而走进成功分支。更彻底的做法是用90NOP把jne指令直接抹掉让流程无视比较结果直接往下走。具体操作可以用objdump找到那条jne指令的文件偏移然后用二进制编辑工具改写对应字节。这个动作在 CTF 里叫“patch 二进制”是合法且有价值的练习你是在处理主办方明示授权的赛题。但我要补一句很关键的话这套技术只应用在你拥有权限的目标上比如 CTF 赛题、自己写的程序拿去处理别人没有授权的商业软件就是另一回事了那是底线问题。4. 常见问题与排查技巧实录4.1 典型问题速查表我整理了一张对照表基本上覆盖了字符串比对入门题里最容易踩的坑。现象可能原因解决方法file显示不是 ELF或工具直接报格式错可能是 Windows PE、ARM/MIPS 等其他平台确认目标平台Windows 用对应工具链ARM 用qemu模拟运行找不到main符号程序被strip过gdb 里info functions找入口或在 Ghidra 里从字符串交叉引用反推每次断点地址都不一样开启 PIE 随机化gdb 里执行set disable-randomization on或用符号断点而不是绝对地址打断strcmp却一直断不到目标位置库里内部也有strcmp调用用 objdump 找到具体call地址下break *0x...输入正确答案还是失败比较前有长度检查、前缀拼接或编码先用任意输入跑一遍观察是否真的走到了strcmp检查比较前有没有strlen或 XOR 循环答案在 strings 里不完整字符串被拆段或编码用objdump -s看原始字节动态调试时看rsi指向的完整内存文本正常但比较失败字符串可能是宽字符中间夹0x00别用x/s改用x/40bx看十六进制原始字节4.2 三个实战排查思路场景一秘密被拆成多段存储。strings 只看到一半比如只有S3cr后面接的内容在另一个数据区里。这种题静态也能解但效率低。更好的办法是直接断在strcmp处x/s $rsi会把运行时拼接好的完整字符串显示出来。动态调试在这种场景下明显优于静态。场景二长度校验先行。有的题会先比较输入长度长度不对直接返回失败根本不会调用strcmp。表现是你输入任何字符串gdb 里打断点却等不到比较发生。排查思路是先找strlen调用或者找反汇编里有没有立即数比较比如cmp $0x9, eax; jne。长度对上之后strcmp才会出现。场景三PIE 随机化导致手工算地址失败。如果你习惯用 objdump 拿到偏移再在 gdb 里用“基址 偏移”手工算断点地址遇到 PIE 会非常痛苦因为每次运行基址都变。最省心的做法是set disable-randomization on关闭随机化或者干脆用符号断点让 gdb 自己去处理符号解析人类少算一步就少一个出错机会。4.3 练习方式与边界想练这类题目现在渠道很多不少 CTF 训练平台和学校的内部靶场都会长期挂 CrackMe 存档还有专门的逆向练习仓库把几千道题按难度排好从签到题一路到加壳混淆。我的建议是精做二十个入门字符串比对题再往上走别贪多。每一个题都坚持走完“file → strings → 反汇编 → gdb 验证”这条完整流程速度自然就上来了。关于边界我多啰嗦一句分析 CTF 赛题、分析你拥有授权的样本这是正当的安全研究和学习但同样的技术拿去破解没有授权的商业软件、绕过别人的收费验证性质完全不同。逆向这个技能本身是中性的用在哪里决定了它的价值。入门阶段养成只在授权范围内动手的习惯对你将来的安全从业之路只有好处。5. 复盘与一个实用小习惯最后说说我个人复盘这套流程时的体会。字符串比对类 CrackMe 的最佳套路其实是固定的先用file和strings摸清底细再用反汇编找到那个strcmp最后用 gdb 或ltrace把比对双方拉到眼前。整个过程从“猜”变成“读”从“黑盒”变成“白盒”。我刚开始接触这类题时最常犯的错误有三个拿到二进制就急着运行不看file输出看到strcmp但没去确认两个参数的顺序用 gdb 打断点时傻等不知道怎么用continue绕过库内部的无用调用。这几个坑我都踩过所以特意在第 3 节和第 4 节写得很细希望能帮你少走几步冤枉路。如果你想把这一套练得更扎实我建议你做一个逆向工程里特别有效的动作拿最简单的 C 源码自己编译一个 CrackMe故意把答案拆成几段存再故意开一次 PIE。编译完自己尝试用今天这套流程去解你会发现“出题人视角”和“解题人视角”一旦切换过再看到陌生二进制时思路会清晰很多。逆向这东西练的是手感和路径今天这篇文章就是给你铺的第一段路。