二进制逆向入门:用x64dbg在crackmes.one挖出硬编码密码
2026/9/17 17:32:09 网站建设 项目流程

记得第一次打开 crackmes.one 的时候,我对着满屏的英文标题和一堆陌生术语发了好长时间呆。作为一个平时写 C/C++ 和 Python 的程序员,"二进制""x64dbg"这些词对我来说完全是另一个世界的东西。但真正上手做完第一道题,我才发现逆向并没有想象中那么高不可攀,关键是找到合适的练习入口,再配上一套顺手的工具。这篇笔记是"二进制破冰录"的第一篇,我想从一个最基础也最典型的场景说起:在 crackmes.one 上下载一个练习程序,用 x64dbg 把它藏在里面的硬编码密码挖出来。

如果你是第一次接触二进制逆向,这篇文章可以当你的第一张地图。我会先解释 crackmes.one 到底是什么、怎么挑题,再带你熟悉 x64dbg 的基本操作,然后完整走一遍"找密码"的实战链路,最后把新手最容易踩的坑提前摆出来。内容不深,但每一步都是可以照着做的。

1. crackmes.one:为什么它是逆向新手最靠谱的练功房

1.1 平台上的 crackme 到底是什么,规则如何

crackmes.one 是一个专门收集"逆向练习程序"的网站,这类程序通常被大家叫做 crackme。简单说,就是作者故意写出来让别人拆解的小程序,程序内部藏着一个或多个"关卡",可能是一个密码、一个序列号,也可能是一套完整的注册验证逻辑。你的任务,就是通过静态分析(看代码)或动态调试(跑起来观察),把藏在里面的秘密找出来。

这个平台和恶意破解软件有本质区别:crackme 的作者在发布时就已经给了你逆向它的许可,程序存在的唯一目的就是被人拆解。很多做漏洞研究、恶意代码分析、游戏保护对抗的人,早期都是靠大量 crackme 刷手感练出来的。平台上的题目会标注运行平台(Windows、Linux、DOS 等)、难度等级、作者说明,还有社区打分和评论,方便你判断题目大概是什么水平。

我在首页翻的时候,最直观的感受是:终于有一个地方可以大胆地拆程序了。平时在公司里,连自己项目的二进制都不能随便折腾,更别说碰第三方软件。但在 crackmes.one 上,你可以随便跑、随便断、随便改内存,拆坏了重新下载一份就行。这种"允许失败"的环境,对新手建立调试直觉非常重要。

1.2 第一道题怎么挑,从难度到平台一个都不能少

新手挑题有几个关键点,我把它们按重要程度排了个序。

第一,看难度。平台会把难度分成多个等级,刚开始一定选最低的那一档。低难度题目的逻辑通常很简单,往往就是一个密码字符串明文写在程序里。选简单题不丢人,恰恰相反,这是建立调试手感和信心的必经之路。我见过太多人上来就想搞"无壳 + 反调试 + 花指令"的题,结果卡在入口处一天,直接劝退。记住,你是来练基本功的,不是来挑战极限的。

第二,看平台和位数。你平时用什么系统、打算用什么调试器,就选什么平台的题。我的建议是从 Windows 平台的 32 位程序开始,因为 Windows 下调试器生态最顺,32 位程序的汇编相对简单,网上能搜到的参考资料也最丰富。平台详情页一般会标明 Architecture 是 x86 还是 x64,下载前多看一眼,免得拿错工具。

第三,看社区反馈。评分高、下载次数多的题目,说明大家玩过之后觉得质量不错,难度标注也相对可信。评论里经常会有"提示:先看字符串""这题用字符串搜索就能过"之类的线索,但如果你不想被剧透,可以先不看评论,自己动手试一遍再说。

第四,下载后不要直接双击运行。这是我比较坚持的一个安全习惯:任何从网上下载的可执行文件,先丢到 VirusTotal 上扫一遍。crackme 虽然绝大多数是安全的,但互联网下载渠道鱼龙混杂,没法保证每个文件都干净。我自己的做法是:在 VMware 虚拟机里专门开一个 Windows 测试环境,把下载好的 crackme 放进去运行和调试。虚拟机的好处是随便折腾,快照一拍,坏了就回滚,和日常系统互不污染。

提示:第一次做逆向练习,强烈建议全程在虚拟机里进行。这不只是防病毒,也是防止自己手滑把系统环境弄乱——调试器修改内存、注入指令这类操作,一旦挂错进程,后果很难收拾。

2. x64dbg 调试器:上手前要搞清楚的四个关键点

2.1 为什么选 x64dbg,而不是 OllyDbg 或 IDA

市面上常见的逆向工具挺多,OllyDbg、x64dbg、IDA Pro、Ghidra、WinDbg、radare2 各有各的强项。对纯新手来说,x64dbg 是最合适的动态调试起点,原因有三个。

第一,它开源、免费、还在持续更新。OllyDbg 算是老前辈,但主要支持 32 位,很多年没有大更新,对新系统、新指令集的支持越来越吃力。x64dbg 则同时提供 x32dbg 和 x64dbg 两个可执行文件,分别调试 32 位和 64 位程序,插件生态也比较活跃。

第二,它的界面设计对动态调试非常友好。CPU 窗口同时显示反汇编、寄存器、数据、堆栈四个子窗口,打开就能看全局。断点窗口、内存布局、模块列表这些常用面板也都能快速调出来。对新手来说,"一眼能看到自己在哪"是最重要的。

第三,学习资源多。网上关于 x64dbg 的教程、插件、讨论数量,比其他调试器多得多,遇到问题容易搜到答案,不至于卡在一个小细节上耽误半天。

需要说明的是,x64dbg 和 IDA/Ghidra 不是替代关系。x64dbg 是动态调试器,重点在程序跑起来之后再观察,看实时寄存器、内存内容和每条指令的执行效果;IDA 和 Ghidra 偏静态分析,适合做整体代码脉络梳理。两者经常搭配使用,但最开始,先用好动态调试器就足够解决大量简单题了。

2.2 四个核心窗口的分工与常用快捷键

x64dbg 启动后,最显眼的是 CPU 窗口,它由四个子窗口组成,很多人第一次看到会觉得眼花,其实只要记住每个窗口的职责就清楚了。

左上角是反汇编窗口,显示当前指令的地址、机器码、汇编助记符和注释,这是你主要盯的地方。右上角是寄存器窗口,显示通用寄存器、标志位、指令指针等当前值,相当于 CPU 实时状态的仪表盘。左下角是数据窗口,显示内存内容,可以切换成 ASCII、Unicode、十六进制等不同显示方式。右下角是堆栈窗口,显示当前函数调用栈,包括返回地址、局部变量等信息。

四个窗口是联动的。你在反汇编窗口选中某个地址,数据窗口会跟着显示这个地址的内容;程序每执行一步,寄存器和堆栈都会实时变化。动态调试的乐趣就在这里,你像外科医生一样,一刀一刀解剖程序的运行过程。

常用快捷键不用背太多,先记熟这几个就够了:

按键功能备注
F9运行一直跑到下一个断点或程序结束
F2设置/取消断点在反汇编窗口当前行切换断点
F7单步进入进入汇编指令内部(如 call)
F8单步步过执行当前指令,但不进入子函数
F12暂停程序程序跑飞时用来强制中断
Ctrl+G跳转到地址/表达式输入地址或符号名
Ctrl+F在当前窗口搜索可用在反汇编、数据等窗口
Ctrl+B二进制搜索在内存区搜索二进制内容

从我的经验来看,F9、F2、F7、F8 这四个键覆盖了八成以上的日常操作。遇到复杂逻辑,再用 F12 暂停、用 Ctrl+G 跳转。不要一上来就背快捷键,边用边记效率最高,等题做多了,手指自然会记住。

2.3 加载程序后先停在入口点,别在系统断点里迷路

x64dbg 加载一个 exe 文件时,默认会停在系统断点(System Breakpoint)处。所谓系统断点,是程序被系统加载器刚拉起、还没执行任何程序自身代码时的位置,此时你看到的是 ntdll.dll 里的系统代码。如果在这里直接按 F9 运行,程序会一口气跑完所有逻辑,可能直接弹出控制台窗口等待输入;如果在这里反复按 F8 单步,又会一头扎进系统 API 内部,越走越深,很快迷路。

我的建议是,在加载对话框里勾选"入口断点"(Entry Breakpoint)。这样 x64dbg 会在程序入口点自动暂停。程序入口点是操作系统把程序加载好之后,开始执行开发者代码的第一条指令,从这里开始调试,视野最干净,整个控制流都在你眼前展开。

如果你已经把程序加载了,但没勾入口断点,可以这样补救:在命令栏输入bp EntryPoint设置一个临时断点,按 F9 运行,程序会在入口停下。或者在"符号"面板里找到当前主模块,展开后找到 EntryPoint 标号,右键设置断点并跳转过去。

提示:一个干净的入口点是新手调试的前提。最常见的迷路场景,就是在系统断点处反复按 F8,结果一头扎进 ntdll 的初始化逻辑里,完全找不到自己在哪。记住,先把程序拉到入口点再开始干活。

3. 实战:从观察程序行为到挖出硬编码密码的完整链路

3.1 先当普通用户跑一遍,记录行为

下面用一个典型的第一题来讲完整流程。假设我从 crackmes.one 下载了一个 Windows 32 位程序,文件名按平台习惯暂时叫 ch01.exe。这题难度最低,作者描述就一句话:"find the password"。

拿到程序,我不会上来就开调试器,而是先把它当普通软件跑一遍。在虚拟机里打开命令行窗口,执行 ch01.exe,屏幕上出现一行提示:

Enter the password:

我随手输入一个错误密码123456,回车,程序立刻回了一句:

Wrong! Try again.

然后退出。这就是关键的第一步:记录行为。通过一次正常运行,我已经知道程序做了三件事:显示提示、读取输入、输出判断结果。那么它内部一定有一个"比较输入和正确答案"的逻辑,而正确答案要么是硬编码字符串,要么是从某段数据里推导出来的。对新手而言,大部分低难度题目的密码就是硬编码字符串,所以下一步直接用字符串搜索来定位。

可能有读者会问,为什么不直接在入口点单步调试,一条一条指令跟过去?理论上可以,但一个哪怕只有几 KB 的程序,从入口到输入判断点之间也会经过大量初始化逻辑、库函数调用,纯单步跟下去效率太低,而且对汇编不熟的人很容易跟丢。字符串搜索相当于先给地图标了"宝藏地点",再直接跑过去,远比全图扫描高效。

3.2 用字符串引用定位关键代码区域

在 x64dbg 中加载 ch01.exe,勾选入口断点,按 F9 停在入口位置。此时不要急着开始单步,先在反汇编窗口点击右键,找到菜单里的"搜索"相关入口,然后依次选择"当前模块"→"字符串引用"(String references)。

x64dbg 会打开一个新的引用面板,列出当前模块里所有可读的 ASCII 和 Unicode 字符串。在我这道题里,能看到这些内容:

  • Enter the password:
  • Wrong! Try again.
  • Congratulations! You found it.
  • %s

前三个字符串都在程序运行过程中出现过,尤其Wrong! Try again.Congratulations! You found it.,它们就是两条分支的结果提示。双击Wrong! Try again.这一行,x64dbg 会自动跳到反汇编窗口中引用这个字符串的指令位置。

这里有个小技巧:双击引用列表里的字符串,跳转到的不是字符串本身所在的内存地址,而是代码中引用该字符串的那条指令。然后再往上翻几行,就能把整个"判断区域"的上下文看得明明白白。

在我这个例子里,跳转后看到的是这样一段逻辑(地址和寄存器和实际题目不同,但结构相似):

00401150 lea eax, [string "Enter the password:"] 00401156 push eax 00401157 call <printf> 0040115C lea ecx, [input_buffer] 0040115F push ecx 00401160 call <scanf> 00401165 lea edx, [string "s3cr3t_p4ss"] 0040116B push edx 0040116C push ecx 0040116D call <strcmp> 00401172 test eax, eax 00401174 jne loc_wrong 00401176 lea esi, [string "Congratulations! You found it."] 0040117C call <printf> 00401181 ret

最亮眼的,就是lea edx, [string "s3cr3t_p4ss"]这一行。x64dbg 的反汇编注释直接显示出了引用的字符串内容,也就是说,密码s3cr3t_p4ss已经清清楚楚地写在这里了。

3.3 在比较指令处挖出密码并验证

既然已经看到了s3cr3t_p4ss,我直接把它记下来,回到命令行重新运行 ch01.exe,输入这个密码:

Enter the password: s3cr3t_p4ss Congratulations! You found it.

控制台输出成功提示,第一题完成。整个过程不到十分钟,甚至可能更短。

但只挖出密码还不够过瘾,我会回到调试器,把这段逻辑彻底弄明白。程序调用scanf读取输入,把内容存进input_buffer,然后调用strcmp把输入缓冲区与硬编码字符串s3cr3t_p4ss比较。strcmp的返回值存在eax里:如果两个字符串相等,eax为 0;后面的test eax, eaxjne loc_wrong组合在一起,就实现了"相等则落入成功分支,不相等则跳到 Wrong 分支"的判断。

这里还有一个特别直观的动态调试操作:在jne loc_wrong那一行按 F2 设置断点,重新运行程序,输入任意错误密码。程序执行到这个跳转指令时会自动暂停。这时候,你可以在寄存器窗口里直接双击 ZF 标志位,把它从 0 改成 1,让跳转条件不成立,然后按 F9 继续运行,程序就会走进成功分支,打出Congratulations!。也可以在反汇编窗口按空格,把jne loc_wrong改成jmp强制跳转。这是动态调试最直观的魅力:你不只是看懂了代码,还能亲手篡改程序的执行路径。

提示:用 F2 下断点后,记得通过重启程序(Ctrl+F2)避免旧断点影响新的调试流程。如果想清空所有断点,可以在断点窗口里一键移除。新手经常忘了这一点,导致改完代码后程序异常,还以为是环境问题。

4. 找到的不只是密码,更是二进制的数据形态

4.1 字符串常量在 PE 文件里如何存在

刚做完第一题的时候,我其实有个疑问:为什么字符串搜索这么好用?x64dbg 凭什么能在一堆二进制数据里精准地列出我关心的字符串?

要回答这个问题,得从编译过程说起。用 C 语言写一个最简单的判断程序:

#include <stdio.h> #include <string.h> int main() { char input[64]; const char *password = "s3cr3t_p4ss"; printf("Enter the password: "); scanf("%63s", input); if (strcmp(input, password) == 0) { printf("Congratulations! You found it.\n"); } else { printf("Wrong! Try again.\n"); } return 0; }

编译成 Windows 常见的 PE 格式可执行文件后,代码和数据会分门别类放进不同的节(Section)。指令代码一般在.text节,只读常量数据包括字符串在.rdata节,可读写的全局变量在.data节。这些字符串常量在文件里就是一段连续的字节序列,比如s3cr3t_p4ss对应的十六进制是73 33 63 72 33 74 5F 70 34 73 73 00,最后的00是 C 字符串的结束符\0

那么程序怎么知道字符串在哪呢?编译器在生成指令时,会把字符串所在的内存地址直接编码到指令里。上面例子里的lea edx, [string "s3cr3t_p4ss"],就是让edx寄存器指向这个字符串在内存中的地址。理解了这一点,你就能明白为什么 x64dbg 在反汇编注释里能直接显示出字符串内容——因为它读出了该地址处的字节序列,发现是一段可打印字符。

4.2 为什么明文密码这么容易暴露

明文密码容易暴露的根本原因是:程序要在运行时和用户交互,它必须把提示语、错误信息、成功信息这些常量打包进二进制;这些常量一旦以连续可读字节的形式存在,任何扫描器都能轻松发现。x64dbg 的字符串搜索功能,本质上是把模块内存中所有满足"连续可打印字符且长度达到一定阈值"的字节序列列出来。

这也解释了为什么很多 crackme 题目会刻意对字符串做处理:用 XOR 加密字符串、把密码拆成几个部分再拼接、运行时动态生成字符串,目的就是避开这种直接的字符串搜索。这是后面更进阶的内容。对新手来说,理解"程序为了和用户交互,必然留下字符串足迹"这一点,是建立二进制直觉的第一步。

从防御角度反过来看,这也给写程序的人提了个醒:如果你在开发一个需要校验密码的应用,就不该把密码明文写死在代码里,更不该用strcmp把输入和密码直接比较。正确的做法是存哈希值、加盐哈希,校验时比较的是哈希结果而不是明文。即便攻击者拿到了哈希,也不能像拿到明文那样直接输入通关。crackme 是故意留漏洞的练习程序,和真实产品的安全设计是两个方向,但这个对比能帮你理解为什么很多"看起来很简单"的判断,在实际系统里会做得绕来绕去。

4.3 拿到密码之后,下一层该看什么

当你能轻松找出第一个硬编码密码之后,就不要再满足于"拿到 flag、截图、下一题"了。我自己的习惯是,每做完一题,强制自己回来做两件事:写 writeup,换一种思路重新求解。

写 writeup 不是写给别人看的,而是把"我从哪里入手、看到了什么、为什么在这里下断点、程序整体逻辑是什么"完整记录下来。这个习惯帮我建立了非常清晰的排查框架,之后再遇到更复杂的题,我能很快回忆起以前处理过的模式。很多新手做了几十题还是水平原地踏步,缺的就是这个复盘动作。

换思路求解怎么理解?第一题我用字符串搜索找到了密码,那我再试试别的动态调试路径,比如:

  • 不在入口处搜索字符串,而是在scanf返回后紧跟的指令处下断点,单步跟踪strcmp的两个参数,观察寄存器和栈上的数据变化;
  • 在硬编码字符串的地址上设置内存断点,看它什么时候被访问,从访问点反向定位逻辑;
  • 尝试修改jne跳转指令,注入新的指令序列,让程序始终走"成功"分支。

几种做法里,尤其推荐理解"内存断点"的思路。你先在数据窗口里跳转到s3cr3t_p4ss的地址,右键设置一个内存断点,然后重新运行程序并输入错误密码;当代码读取这个字符串、准备和输入比较时,调试器会瞬间拦住你,把你带到访问该字符串的那条指令。这种"从数据反查代码"的思维,在以后追查复杂数据流、脱加密壳时会非常有用,建议多练几次。

5. 新手最容易踩的坑,以及我的避坑心得

5.1 在系统 DLL 里打转:入口点的坑

第一个最常见的坑,我在第二章提过,就是加载程序后没有停在入口点,结果在 ntdll 或系统 DLL 里一顿步进,完全找不到自己的程序代码在哪儿。出现这种情况,别慌,最快的解决办法是:在符号面板里找到主模块(比如 ch01),展开后找到入口点地址,右键选择"转到入口点",或者直接在命令栏输入bp EntryPoint下断点,再按 F9 运行,让程序停在自己家里。

另外注意,x64dbg 文件夹里有两个可执行文件,x32dbg.exe 用来调试 32 位程序,x64dbg.exe 用来调试 64 位程序。虽然它们的界面几乎一样,但用错版本会导致加载失败或者寄存器显示异常。我见过不少新手拿 64 位调试器和 32 位程序强行配对,折腾半天发现是版本选错了。

5.2 断点位置的坑:下得太早或太晚

断点下得太早,比如程序还没读取输入,你就把断点下在strcmp调用处,那程序根本不会触发;断点下得太晚,比如已经输出了判断结果,你想看的比较逻辑早就执行完了。新手最常遇到的疑问就是:"我明明在关键位置下了断点,怎么程序一直没有反应?"

解决办法通常有两个方向。一是先用字符串搜索把目标代码区域缩小,再决定断点下在哪条指令上;二是用"靠行为定位"的方法,比如在scanf调用返回后的那一行下断点,输入完密码后程序必然停在那里,然后你就可以用 F8 一步步跟上程序后面的判断逻辑,亲眼看到寄存器里发生了什么。调试经验说白了就是"猜一个位置,验证一个位置",多试几次,断点的手感自然就有了。

还有一个容易被忽略的点:有些 crackme 会在开头调用IsDebuggerPresentNtQueryInformationProcess这类反调试 API。如果你发现程序在调试器里的行为和正常运行明显不同,比如直接退出、输出乱码,先别怀疑自己操作错了,很可能是题目故意设置的保护。这时候可以通过字符串区里的输出提示缩小判断区域,或者翻一下题目评论,看看作者是不是留了一层小机关。

5.3 环境与安全的坑:虚拟机、壳、责任边界

环境上的第一个坑,是直接在真实物理机上双击陌生 crackme。这个我在第二章强调过,再做一次提醒:强烈建议在虚拟机里跑,快照是你最好的后悔药。

第二个坑,是没有检查程序有没有加壳。如果一个 crackme 加了 UPX、ASProtect 或 VMProtect,那么入口点看到的已经不是正常逻辑代码,而是脱壳和解压逻辑。对新手来说,第一题一定不要选带壳的题目,等熟悉了 PE 结构和常见保护机制之后,再专门去学脱壳也不迟。判断有没有壳,可以用 Detect It Easy 这类工具扫描一下,几秒就能出结果。

责任边界这一点,虽然听起来像说教,但我还是必须认真讲。crackme 是作者明确许可别人逆向的练习程序,对它的所有操作都限定在练习范畴。同一个调试器、同一套技术,如果拿去分析收费软件的注册算法、绕过商业授权、破解在线服务,那就超出了合法边界,也违背了逆向学习的基本伦理。我给自己定的规矩是:只逆向自己拥有源码或得到明确授权的程序,商业软件、闭源服务的破解一概不碰。技术本身是中性的,关键看人怎么用。

在实际操作中,还有一个安全习惯值得养成:把用于逆向练习的 Windows 虚拟机和日常工作的系统分开,快照命名写清楚,不要混用账号和数据。这样就算某个 crackme 带恶意行为,损失也控制在虚拟机里,随时可以回滚到干净状态。

最后再聊两句个人的体会。做完第一道题之后的那几分钟,我确实有点兴奋——不是觉得"破解"了什么,而是第一次清楚地看见,程序在底层原来是这个样子。一个简单的strcmp、一串明文字符串,在我眼前从朦胧的机器码变成了可以理解的语言。这种"看见底层"的感觉,会推动你去学更多东西:寄存器是干嘛的、栈是怎么增长的、PE 文件头里藏了什么、函数调用约定为什么这么设计。

如果你也想入门,我的建议是:别一上来就啃厚厚的逆向教材,太容易劝退。先去 crackmes.one 挑一道最简单的题,用 x64dbg 把第一个硬编码字符串挖出来。这个小小的正反馈,比任何课程都能让你留下来。下一篇我打算写更常见的场景——当密码不是明文,而是通过 XOR 之类的算法隐藏起来时,怎么用动态调试一层层把它剥开。中间会用到更多寄存器分析和内存操作,希望到时候你已经拿到了自己的第一个"找到密码"的记录。

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

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

立即咨询