
前阵子趁着吾爱破解的解题领红包活动我把 Windows 初级题完整走了一遍。这类题说是“初级”其实正好卡在“刚接触逆向”和“能独立解题”的分界线上它不考高深的壳和混淆但要求你懂基本的 PE 结构、会看汇编、能操作调试器。我觉得它是最适合新手拿来练手的一档题所以这篇文章把我从拿到压缩包、查壳、静态分析到动态调试、还原算法、算出 key 的全过程完整复盘一遍。工具可以换但思路是通用的照着走一遍基本就能拿下同类型的题目。先交代一下这类题的典型面貌。Windows 初级题通常是一个很小的 Win32 程序或控制台程序运行后让你输入一串 key 或答案程序内部做一次校验正确就弹窗提示成功错误则提示失败。你要做的不是爆破而是从程序里逆出正确的 key 字符串去提交。相比直接 patch 跳转还原出 key 才是这类题的核心目的这也是它和单纯改写逻辑的 crackme 不一样的地方。1. 先拆解题思路初级题的考点其实很固定1.1 这类题的标准套路与考点既然叫初级题那它的考点就不会是 VMP、Themida 这种重型壳也不会是离谱的虚拟机保护。我自己复盘了近几年的题目发现出题人基本围绕几个固定方向出招PE 文件基本结构能识别程序是 32 位还是 64 位知道入口点、节区、导入表这些概念。加壳识别与脱壳最常见的是 UPX 壳出题人通常会稍微改动一下入口或标志让你不能一条命令脱干净。字符串与导入表定位通过“错误提示”“成功提示”这类字符串反向交叉引用找到校验函数。关键 API 断点GetDlgItemTextA、GetWindowTextA、lstrcmpA、MessageBoxA 这些是重点。简单算法还原字符串比较、逐字节 XOR、加减变换、查表替换属于入门基本功。这些点单独拿出来都不难但组合到一起对刚接触逆向的人来说就是一个完整的小项目。解题的大致流程也很固定我一般压缩成五步黑盒观察、查壳识别、静态定位校验函数、动态跟踪到比较点、还原算法拿 key。后面所有章节都是按这个顺序展开的。1.2 准备一套顺手的工具链选工具的原则是不追求最新最强只要稳定、教程多、能覆盖 32 位和 64 位就行。我这次用的是下面这套组合也是社区里最常见的用途工具说明运行环境VMware 里的 Win7 x86 / Win10 x64题目多数是 32 位Win7 兼容性最好Win10 x64 也能跑注意杀毒误报动态调试x64dbg32 位用 x32dbg、64 位用 x64dbg还在活跃维护插件生态好静态分析IDA Free / Ghidra看反汇编和伪代码IDA 的 F5 对新手很友好查壳Detect It EasyDIE识别壳和编译器信息比 PEiD 更准十六进制HxD / 010 Editor查看数据区、手动 patch 时用行为观察Process Monitor备用看程序是否读写文件、是否联网不需要一上来就全装但虚拟机和 x64dbg 是必须的。为什么强调虚拟机因为在调试过程中要执行来自未知作者的二进制程序而且脱壳、patch 后的文件大概率会被杀毒软件标记在虚拟机里折腾不心疼。我给虚拟机开了快照每次开始调试前恢复一次干净状态这样即使把系统搞坏五分钟就能回来。1.3 开工前的环境准备与题目接收活动题目一般以压缩包形式发布通常带一层解压密码密码写在活动说明里。别嫌麻烦这个密码本身就是规则目的是过滤不看说明的人。我拿到压缩包后的第一件事不是解压运行而是先记下文件的 SHA-256方便后面和别人交流时确认拿的是同一份文件。Windows 下用 certutil 直接算certutil -hashfile 题目.zip SHA256解压之后先看一眼目录里有什么一般是一个 exe可能附带一个说明 txt。把 exe 单独复制到工作目录关闭杀毒软件实时防护或者加入排除项然后双击运行。到这里才算正式开始解题。还有一个细节运行过程全程保持在虚拟机里不要放在常用电脑上跑原因前面说过安全第一。2. 黑盒运行与静态侦察先把入口找出来2.1 先让它跑起来观察反馈双击运行后我的做法是先当普通用户操作一遍。这次遇到的程序是个 Win32 对话框程序界面上有一个输入框和一个确定按钮。我随便输入了 abcdef点确定弹出一个 MessageBox标题是“提示”内容是“答案错误再接再厉”。这个反馈信息非常关键它说明程序一定有完整的“读取输入、比较、弹出结果”链路。读取输入用的多半是 GetDlgItemTextA 或 GetWindowTextA弹窗用的是 MessageBoxA这两个 API 就是后面动态调试的天然断点位置。如果程序是控制台程序对应的就是 scanf / gets 和 printf / puts思路完全一样。另外我建议顺手观察一下程序会不会联网、会不会释放文件。初级题基本不会做这些事但看一眼 Procmon 日志会更安心也免得后面调试时被意外行为带偏。2.2 查壳与识别位数用 DIE 打开 exe先看两个信息打包器和位数。这次的文件 DIE 识别出 UPX右侧显示 32 位。看到 UPX 其实有点开心因为这是最适合新手练手的壳出题人也喜欢用它来默认筛掉完全不懂脱壳的人。处理 UPX 有两条路。第一条是直接命令行脱壳如果程序只是标准 UPX 加壳且没有被修改一条命令就能搞定upx -d 题目.exe但题目往往会做点小改动比如改掉 UPX 标志头这时候 upx -d 会直接报错就要走第二条路手动脱壳。UPX 的入口特征非常明显开头是一条 pushad。用 x64dbg 打开程序停到入口后看到 pushad记下当前 ESP 指向的栈地址在这一地址上下一个硬件访问断点然后 F9 运行。因为 pushad 和 popad 之间的代码不会真正访问这个栈顶位置程序会在 popad 附近停下来这时候再单步几步看到一条大跳转就能跳到真正的入口点 OEP。把这套流程跑顺再用 dump 插件把进程 dump 下来配合 Scylla 修复导入表就能得到一个干净的脱壳文件。新手可能会问不脱壳能不能直接做可以动态调试完全能在带壳状态下进行只是静态分析会很难受。我的建议是UPX 这种壳尽量手动脱一遍这是基本功实在脱不动再退回去选择动态调试。2.3 字符串与导入表里藏着第一手线索脱壳之后把程序丢进 IDA Free 或者 x64dbg 的字符串窗口。我习惯先在 IDA 里按 ShiftF12 打开 Strings 窗口一眼扫过去就能看到刚才运行时的两个字符串“答案错误再接再厉”和“恭喜答案正确”。在“答案错误”上按 X 查看交叉引用会直接跳到引用它的代码位置顺着这里往上翻函数头就能定位到校验逻辑所在的函数。这里要说一下为什么字符串是初级题最快的突破口。Windows 程序要显示文字最终一定会调用文本输出相关的 API而大部分编译器会把字符串直接放在 .rdata 节明文可搜。除非出题人做了字符串加密或者运行时拼接否则你总能通过报错信息找到校验函数。如果搜不到就先检查是不是还有壳没脱干净或者字符串被拆成好几段存储。导入表窗口也值得单独看一眼。如果看到 GetDlgItemTextA 和 lstrcmpA 同时出现基本可以断言校验逻辑是“取出输入和某个硬编码字符串比较”。如果看到 lstrcmpA 这类比较函数直接在其上下断点是最省事的方式。2.4 用 IDA 定位校验函数的思路在 IDA 里跟着字符串交叉引用进入校验函数后按 F5 看伪代码。这一题的校验函数大概长这样int __stdcall sub_401080(HWND hDlg) { char buffer[32]; GetDlgItemTextA(hDlg, 1001, buffer, 32); if (check_key(buffer)) MessageBoxA(hDlg, 恭喜答案正确, 提示, 0); else MessageBoxA(hDlg, 答案错误再接再厉, 提示, 0); return 1; }check_key 是内部函数IDA 显示为 sub_401150 之类的名字。双击跟进去能看到它接收一个指针参数、循环读取每个字节、做一次 XOR、再和一段全局数据比较。看到这样的结构这题就完成 80% 了剩下的事就是把这个 XOR 比较逆向出来把密文字节取出来异或回去得到明文 key。具体怎么逆向放在下一章一起讲。3. 动态调试与算法还原一次完整实战演示3.1 在关键 API 上下断点静态分析已经把校验函数定位到了但纸上谈兵容易错最好还是在调试器里亲手走一遍。我建议从原始文件或脱壳后的文件重新用 x64dbg 打开。如果打开的是脱壳后的文件先确认程序能正常跑起来如果跑不起来就回退用原文件调试。在 x64dbg 的命令行输入bp GetDlgItemTextA bp MessageBoxA按 F9 运行程序在输入框里输入假 key比如 12345678点确定。程序会先断在 GetDlgItemTextA 上。这时候不要急着乱按先按 CtrlF9 执行到返回再按 F8 单步两步回到程序领空。看堆栈窗口和寄存器能找到输入字符串所在的缓冲区地址在数据窗口跟随这个地址就能看到你输入的 12345678。接下来继续 F8 单步程序会调用 check_key。等 MessageBoxA 断点命中时说明比较已经结束马上要弹结果了。所以关键就在于 GetDlgItemTextA 返回之后、MessageBoxA 断下之前这一段代码。如果 F8 太慢可以在 check_key 函数入口直接下断运行过去再单步看内部逻辑。3.2 顺着返回处单步找到关键比较汇编深入 check_key 后代码会变得很有辨识度。我这次看到的汇编片段和下面这个结构类似00401150 mov esi, [ebp8] ; esi 输入缓冲区指针 00401153 xor ecx, ecx ; ecx 清零当作循环计数 00401155 mov al, byte ptr [esiecx] ; 取输入的第 ecx 个字节 00401158 cmp al, 0 ; 判断是否结束 0040115A je short fail ; 提前结束说明长度不够 00401160 xor al, 5Ah ; 与 0x5A 异或 00401163 cmp al, byte_402000[ecx] ; 与固定密文字节比较 00401169 jnz short fail ; 不相等就失败 0040116B inc ecx 0040116C cmp ecx, 8 0040116F jl short 00401155 ; 循环 8 次 00401171 mov eax, 1 ; 全部相等则返回 1看到这段代码解题思路就清晰了。判断条件是(输入每个字符 XOR 0x5A) 固定密文的每个字节。这就是典型的逐字节 XOR 比较。循环次数 8 说明 key 长度是 8 个字符。此时已经把动态和静态的信息对上了静态分析看到的 check_key动态看到的就是这套循环两边互为印证。3.3 一个典型的 XOR 校验从汇编还原到 Python先解释一下为什么出题人喜欢用 XOR因为异或运算自带“对称性”加密和解密是同一操作。程序里做的是 input XOR 0x5A 再和密文比较反过来就是密文 XOR 0x5A 就是 input。这也是很多类似题目的通用解法——把比较条件倒过来就是答案。现在去数据窗口取固定密文字节。在 IDA 里双击 byte_402000 把数据提取出来或者直接在 x64dbg 数据窗口选中 8 个字节复制。假设提取出来是这样的具体值每份题目不一样这里只演示用法1B 18 19 1E 6B 68 69 6E注意直接看到的是十六进制要把它还原成字符。手工先算第一个0x1B XOR 0x5A 0x41也就是字符 A。再算第二个0x18 XOR 0x5A 0x42是 B。全手算容易错写个三行脚本更稳enc [0x1B, 0x18, 0x19, 0x1E, 0x6B, 0x68, 0x69, 0x6E] key .join(chr(b ^ 0x5A) for b in enc) print(key)跑出来就是 ABCD1234。我这里用作演示的密文组合刚好能解出这个可读字符串实际题目里的密文可能对应任意字符但方法就是这一条把密文数组取出来逐字节 XOR 回去得到的就是正确 key。3.4 手工验证与提交让结果闭环算出 key 后别急着提交先回程序里验证一遍。运行程序输入 ABCD1234点确定这次弹出的是“恭喜答案正确”。到这里解题闭环就完成了。如果你用的是脱壳后的文件验证建议再用原始文件验证一次防止因为脱壳修改数据导致 key 失效虽然这种情况很少见但确认一下总没错。提交的地方是活动页面要求填的正是这个 key 字符串。有些年份的规则是把 key 用于解开下一题有些年份是直接按 key 发放红包不管哪种key 都是核心交付物。顺手说一下这题如果想爆破过关只需要把 check_key 结束时的 jnz 改成 jz或者直接 nop 掉失败分支程序就会对所有输入弹正确。但解题领红包的规则通常不接受 patch 结果必须提交正确的 key。所以在练习时我建议把“还原出 key”作为目标而不是只会 patch。4. 常见卡壳点与排错实录新手最容易踩的坑4.1 断点打不中、程序闪退怎么办这一节全是实际操作里最常见的坑。第一个坑断点下在 GetDlgItemTextA 上怎么输入都不命中。先确认用的调试器位数对不对——32 位程序要用 x32dbg64 位程序要用 x64dbg这个错了断点必然有问题。第二个坑程序一开起来就闪退根本没机会下断。这种情况通常是程序在启动阶段做了反调试检测或者因为 ASLR 每次加载地址都变。x64dbg 默认会处理 ASLR停在系统断点时用 CtrlG 跳到模块入口先在那里下断再运行。如果程序检测调试器初级题一般就是检测 IsDebuggerPresent你可以在该函数头部把返回值改成 0或者直接把调用它的地方跳过去。实在不行还有一个保底思路在 ExitProcess 上下断程序退出前会停一下这时候再往回找是谁调用了退出往往能摸到反调试逻辑的源头。4.2 脱壳后跑不起来的两个基本操作脱壳后文件双击没反应是新手最容易卡住的地方。原因九成是导入表坏了还有一成是重定位表问题。UPX 壳的导入表是压缩的dump 出来之后必须重建。操作流程用 x64dbg 打开带壳原始程序按前面讲的路径跑到 OEP停下来后不要关闭调试器直接用 Scylla 插件附加当前进程点 IAT AutoSearch再点 Get Imports正常情况下会列出所有 API最后点 Fix Dump选择刚才 dump 出来的文件Scylla 会生成一个修复后的新文件。跑这个新文件大概率就正常了。如果这步还不行还有一个不脱壳的替代方案直接用原始带壳文件做动态调试。虽然静态分析看不痛快但只要断点能命中照样能拿到 key。刚开始学的时候不必死磕脱壳修复先把题解出来建立信心更重要。4.3 遇到花指令和反调试的保底打法初级题不排除加一点花指令像下面这种模式很常见00401170 push eax 00401171 pop eax 00401172 jmp 00401173 00401173 db 0xE8 ; 干扰数据花指令的目的就是让反汇编器出错静态看起来混乱。我的处理方法是动态调试时不要在这种代码块里单步太久直接找代码块末尾的真实跳转或者干脆跳过整段无意义指令。如果静态分析被干扰得没法看就完全以动态调试为准反正真正校验的地方就那么几行找到比较指令就赢了。反调试也一样初级题用的基本就是 IsDebuggerPresent、PEB 的 BeingDebugged 标志这类老手段。知道了机制就不慌要么 patch 掉检测要么动态调试时留意寄存器里可疑的返回值手动改掉。4.4 新手误区速查表把新手最容易犯的错整理成一张表解题前扫一眼比踩坑后再看性价比高得多误区后果正确做法拿到文件直接开调试器没先运行观察不知道程序预期行为断点乱下先跑一遍记录提示信息字符串搜不到就怀疑题目太难可能漏了壳或字符串加密先确认脱壳是否完整再找交叉引用只盯寄存器不看堆栈和数据窗口找不到输入缓冲区和密文数据寄存器、堆栈、数据窗口三连看在入口点反复单步浪费时间效率低容易绕晕直接对关键 API 下断一步到位在实机跑未知样本有安全风险全程在虚拟机里操作算出 key 不验证就提交可能因大小写或空格错误被拒先在程序里验证再提交这张表我会在每次解题前扫一遍习惯之后基本能避开整个新手期的绝大多数弯路。5. 复盘与延伸解完这一题下一步学什么5.1 把解题过程当成一次“体检”解完题后我通常会花一点时间做复盘而不是急着庆祝。复盘的方式很简单把解题过程按时间线过一遍标出每一步花了多少时间。如果查壳和找字符串只花了五分钟说明流程熟悉如果卡在某一步超过半小时那一步就是你的短板。比如我自己早期经常卡在“从 GetDlgItemTextA 返回到 check_key 之间的那十几条指令”后来发现是因为不熟悉常见 Win32 API 的调用约定于是专门补了 stdcall 和 fastcall 的知识。复盘之后再针对性复习汇编指令不熟就背常用指令表PE 结构不熟就把节区、导入表、重定位表过一遍API 不熟就查微软文档。这种“做题、复盘、补课”的循环比单纯刷题有效得多。5.2 初级题的常见变种提前练手这一题的套路是 XOR 校验但初级题的变种也值得提前了解免得下次换个形式就懵变种一换壳。把 UPX 换成 ASPack、NSPack或者手工改过的壳。应对思路一样先识别壳再决定命令行脱壳还是手动脱壳。变种二换算法。把 XOR 换成加减、乘除、查表替换或者先用某个固定字符串做几次迭代运算。应对方法是从比较指令往前推把每一步操作逆回去。变种三内存注册码。程序不直接存储密文而是在运行时动态生成正确答案然后用 lstrcmpA 去比较。这时直接给 lstrcmpA 下断点断下来后去内存里找它第二个参数指向的字符串那就是答案。变种四多处校验。程序在校验通过后还会做二次校验patch 第一处不够。建议还是顺着逻辑把所有校验函数都还原出来一次拿全 key。提前把这些变种在脑子里过一遍下次遇到类似题目你已经知道大概往哪个方向走。5.3 最后的提醒边界感与长期主义最后说一点题外话。会逆向之后很多朋友会手痒想拿商业软件练手。我的态度很明确这类社区活动题和 CTF 题是用来学习的但破解商业软件、绕过授权验证是另一回事不仅有法律风险也不符合大部分逆向学习者的初衷。学习逆向的价值在于理解程序运行的底层逻辑能读懂二进制能在合法授权下做安全研究、漏洞分析、恶意代码分析。把这份能力用在正道上路会越走越宽。我建议把每年的解题领红包初级题当作年度体检代码分析能力退步没有、工具链熟不熟、汇编有没有生疏一题就测出来了。保持这个习惯比临时抱佛脚有效得多。最后分享一个我自己的心得做这类 Windows 初级题真正花时间的从来不是“调不明白”而是“不知道方向对不对”。一旦你确认程序读取了输入、找到了比较指令、能把比较条件倒过来剩下就是体力活。新手阶段最容易提高的其实就是这个“找方向”的能力——多解几题形成自己的解题 SOP后面会越来越快。我现在回头再看这题从打开压缩包到算出 ABCD1234总耗时不到二十分钟但第一次做的时候光在入口点发呆就花了半小时。所以别急照着这个顺序多走几遍你的第二题、第三题会快得多。