
1. 从一道“游戏题”说起Reverse方向到底在考什么攻防世界Reverse方向的题目我断断续续刷了不少Game这道题算是非常典型的一道入门级逆向题。之所以说“典型”是因为它覆盖了逆向解题最核心、最高频的三个动作识别文件类型、静态分析定位关键逻辑、动态调试验证思路。不管你是刚开始接触CTF逆向还是已经在刷题但总觉得卡在某个环节这道题都值得认真走一遍。先说结论Game的核心逻辑并不复杂程序伪装成一个有交互界面的小游戏表面上让你按规则玩实际上flag就藏在某个判断逻辑里。你不需要真的通关只需要定位到那个判断函数理清它的输入输出关系就能把flag提取出来。全程用到工具也不多一个exeinfo PE一个IDA Pro或者Ghidra再加上x64dbg足够。这道题适合什么人如果你已经能独立用IDA打开程序、会看伪代码、知道怎么下断点那这道题对你来说更偏向“查漏补缺”。如果你纯粹是第一次接触逆向那这道题的价值就更大——因为它不需要你掌握花指令、反调试、虚拟机保护这些进阶对抗手段纯粹靠耐心和基本工具操作就能拿下非常适合建立逆向分析的完整流程感。接下来我按自己的解题顺序从拿到文件到最后提取flag全程记录一遍。其中包含我对每一步“为什么这么做”的理解以及踩过的坑希望能让你少走弯路。2. 拿到文件之后先别急着双击运行2.1 第一步永远是确认文件属性而不是运行很多人拿到题目压缩包第一反应是解压后直接双击exe跑一下看看界面。这个习惯在CTF里其实不太好原因有三个第一你并不知道这个程序是不是加了壳如果加了壳双击运行看到的只是壳解压后的运行效果对静态分析没有直接帮助。第二有些题目程序会检测调试器、检测虚拟机运行前不做好信息收集后面动态调试时候容易被反调试机制干扰。第三少数题目甚至会在运行时释放恶意代码或者做网络请求虽然CTF环境相对安全但养成先分析再运行的习惯长期来看对你的安全从业素养有好处。所以拿到Game这个文件后我做的第一件事是查看它的文件信息。这里是Windows平台下的一个exe我用exeinfo PE打开看到的结果如下文件类型PE32可执行文件32位程序编译器特征Microsoft Visual C 6.0大概率是VC6编译不是加壳特征节区信息.text、.rdata、.data等标准节区没有出现UPX、ASPack、Themida之类的异常节区名这一步最关键的信息就是“没有壳”。如果显示UPX那好办直接upx -d脱壳如果是VMP或者ASProtect这类强壳那就得当硬仗来打。Game这道题直接把裸奔的PE给你了暗示得很明显重点不是对抗壳而是分析逻辑。顺便提一句除了exeinfo PE也可以用DIEDetect It Easy来做同样的事。DIE对编译器版本和加壳特征的识别更细致一些两个工具可以互为备份。2.2 运行观察但要带目的确认无壳后我仍然运行了程序。这一步的目的不是“玩”而是收集三类信息程序是控制台程序还是GUI程序这决定了主函数入口长什么样程序运行时是否打印了提示文字这些文字会直接影响后续静态分析时搜索字符串的方向程序是否有明显的输入点比如getchar、scanf、ReadFile这类函数Game运行后出现的是一个类似走迷宫的小游戏界面有方向键控制角色移动的引导文字界面是用字符画出来的不是真正的图形窗口。这让我心里有底了逻辑大概率是在控制台应用程序框架里配合一些游戏循环的代码核心flag逻辑要么在通关判断里要么在某个特定输入分支里。运行时的观察记录也很重要我一般会记下程序显示的台词、按钮、反馈信息这些文字几乎100%会出现在程序的静态字符串表里是静态分析的切入点。3. 用IDA Pro做静态分析从main函数到关键判断3.1 先找字符串再找引用比一口吞下main函数更快把文件拖进IDA Pro后我很少直接按F5看main函数除非题目特别简单。更高效的做法是看字符串列表ShiftF12快捷键从字符串入手反推关键逻辑。Game的字符串窗口里除了游戏操作提示和绘制界面的ASCII字符我注意到几条很关键的字符串比如类似“you win”“flag is”“Right!”“Wrong”之类的反馈信息。尤其是和flag相关的字符串它的交叉引用位置往往就是关键逻辑所在。双击“you win”这类字符串跳转到它在.rdata节区的位置然后按X查看交叉引用IDA会带我到引用这个字符串的代码位置。在这个位置附近的函数就是控制通关和flag输出的函数。对于Game这道题这个函数通常叫sub_401040、sub_401070这类默认名字因为原始符号没有保留但代码逻辑非常清楚。3.2 F5伪代码里怎么一眼看穿核心逻辑跳到关键函数后按F5切换到伪代码视图。这一步是个人最依赖的操作它把汇编翻译成类似C的伪代码减轻阅读负担。Game关键函数的伪代码结构大致是这样的输出游戏提示绘制界面字符判断输入的方向键值更新角色位置检测角色是否到达终点如果到达终点进入flag输出分支通常是一个基于若干变量计算拼接字符串的过程真实代码中那个计算flag的分支往往长这个样子我用类似风格描述具体变量名以实际为准if ( v1 18 v2 32 ) { strcpy(flag, flag{); for ( i 0; i 24; i ) flag[5 i] key[i] ^ v4[i]; flag[29] }; puts(flag); }也就是说当你把游戏里的角色移动到某个特定坐标比如第18行第32列程序就会用一段固定的key数据异或一段固定数据生成flag字符串并打印出来。看到这种结构你根本不需要真的靠键盘把角色走过去。你需要做的是两件事第一搞清楚key数组是多少第二搞清楚参与异或的数据是多少。这两组数据都在程序里写死了静态分析可以把它们逐个提取出来。3.3 提取静态数据的小技巧在IDA里当你看到伪代码中是这种形式v4[i] ^ byte_4099B0[i]把鼠标放在byte_4099B0上IDA会显示这段数据的十六进制预览。你可以直接手动复制出来也可以选中后按ShiftE导出为C数组格式。这个导出功能非常实用能省去手工转录的麻烦还能避免抄错数据。不过手动记录时需要小心有些数据可能不止一个函数引用导出的是整段数据你需要按偏移切分。例如只用到前24字节就只取前24字节别多取否则异或出来全是乱码。3.4 Game这类题的“静态解题”路线图总结一下纯静态分析解决Game这类题目的路线是用exeinfo PE确认程序无壳、32位/64位、编译特征用IDA打开ShiftF12查看字符串找flag/win/success/correct类似的引导词交叉引用引导词进入关键函数F5伪代码理清核心判断逻辑确认flag的生成算法提取常量数据用脚本Python最方便完成异或/拼接等计算得到flag如果静态分析顺利从拿到文件到算出flag通常不超过20分钟。但现实情况是很多人在第4步会卡住因为伪代码里变量名是编译器生成的v1、v2、v3这种各种数据类型混在一起看半天理不清逻辑。这时候就需要动态调试来辅助验证了。4. 动态调试让程序自己告诉你答案4.1 为什么静态分析完还要动态调试有人会问如果静态分析已经算出flag了为什么还要动态调试我的习惯是“动态调试验证而非依赖”。因为伪代码毕竟是反编译出来的结果编译器优化、类型推断不准都可能导致伪代码和真实执行逻辑有细微差别。尤其是像Game这种涉及到数组下标、指针偏移的代码差一个字节结果就完全不对。动态调试的核心价值在于你不需要在脑子里模拟程序执行而是让程序跑给你看。配合断点你可以直接看到某个时刻寄存器是什么值、内存里存了什么数据、某个比较指令实际跳没跳这些都是板上钉钉的事实不掺杂反编译器的猜测。4.2 用x64dbg下断点观察关键比较Game是32位程序我喜欢用x64dbg它同时支持32位和64位程序来做动态调试。把程序加载进x64dbg后先在IDA里找到关键比较指令的地址然后在这个地址上下断点。操作路径是这样的在IDA里找到关键函数中执行坐标比较的那条指令记录下来比如0x4018C7: cmp eax, 18在x64dbg中按CtrlG跳转到0x4018C7按F2下断点F9运行程序程序会停在断点处按F8单步观察寄存器窗口中EAX的值如果你是第一次用x64dbg需要适应一下它的窗口布局左上角是反汇编窗口右上角是寄存器窗口左下角是内存窗口。重点看的是EIP当前指令指针、EAX大多时候是函数返回值或比较的左操作数、ZF零标志位决定跳转是否成立这几个。4.3 修改内存和寄存器跳过游戏过程当你停在关键比较指令前发现程序还没走到终点坐标那么最简单的办法是直接修改寄存器的值让它误以为角色已经到达终点。比如比较指令是cmp eax, 18而EAX此时的值为3代表角色当前行号是3期望行号是18。你可以双击寄存器窗口中的EAX把它改成18然后按F8继续单步。如果后续还有列号比较同样改掉。这样程序就会走到flag输出分支。有时候更粗暴的办法是直接修改EIP让它跳到打印flag的函数入口。但这个方法比较危险前提是你确定跳转目标的地址是对的而且函数没有依赖前面代码设置的局部变量。对Game这种结构简单的程序来说修改寄存器值已经足够不需要直接跳EIP。动态调试在Reverse中的定位永远是“验证”和“辅助”不是必须步骤。但如果你能在刷题时养成“静态分析出思路动态调试验思路”的习惯后面遇到反调试、混淆的进阶题时会从容得多。5. 关键算法拆解flag为什么是这样生成的5.1 异或运算CTF逆向里最常见的加密原语Game这道题的flag生成算法核心是一段异或运算。异或XOR是CTF逆向出镜率最高的一种运算没有之一。原因很简单异或具有对称性A XOR B C则A XOR C B。也就是说同一个异或操作既能用来加密也能用来解密加密解密完全对称不需要额外的逆函数。在Game中flag的生成方式类似这样flag[i] key[i] ^ data[i]key是程序里一个固定数组通常是一些看似随机的十六进制字节data也是固定数组两者异或后得到可见字符拼起来就是flag。重要提示在提取这类数据时第一优先级是确认你要异或的字节数。如果flag结尾是}那参与异或的字节数通常等于flag中间部分的长度。取数据时多一个字节少一个字节都会导致最后的flag不完整。5.2 用Python一键算出flag我习惯用Python来做最终计算比手算快得多也避免了按错计算器的风险。下面是配合Game这类题目的模板脚本key bytes.fromhex(1D 2C 3B 4A 5F 6E 7D 8C 9B A0 ...) data bytes.fromhex(7B 5A 49 38 27 16 05 74 63 52 ...) flag bytes([k ^ d for k, d in zip(key, data)]) print(flag{ flag.decode(utf-8) })如果数据在IDA里已经导出为C数组格式你也可以直接粘贴稍微改成Python语法就行。有些情况下你不需要把整段数据都手工填进去可以写个小脚本从程序文件里按偏移读取但对Game这种短数据来说手工填写加核对就够了。5.3 确认flag的快速方法比对格式CTF里的flag常见格式因赛题而异但攻防世界平台通常使用统一的提交格式而且题目描述里一般会说明flag的格式。在提取结果之后快速核对一下是否以flag{开头是否以}结尾中间部分是否全是可打印字符字母、数字、下划线如果满足这三个条件基本可以确定算出来的就是正确答案。如果不满足回查数据提取那一步大概率是数据取错偏移了。5.4 如果静态数据和动态调试对不上怎么办这种问题我遇到过一次。原因是程序不是一次性把所有flag数据算好而是在运行过程中通过某个算法动态生成数据表。这时候你从静态看数据段看到的可能是初始值或未解码状态和实际用来异或的数据完全不一样。解决办法是动态调试在flag输出函数入口下断点程序运行到此处时打开内存窗口查看参与异或的数据区域把当前内存中的真实值导出来再替换到你的脚本里计算。这是静态和动态配合最有价值的地方纯静态会卡住纯动态会变成瞎试两者结合才能高效解题。6. 常见问题与排错记录我第一次做Game时踩过的坑6.1 用错工具32位程序用了64位IDA这是新手最容易踩的坑。Game是32位程序你如果双击的是IDA 64位版本它会提示无法识别这个文件格式或者加载出来反汇编全是乱码。解决办法很简单用IDA Pro的32位版本ida.exe打开32位程序用64位版本ida64.exe打开64位程序。这个匹配关系一定要记牢固。6.2 字符串搜得到但交叉引用看不到有时候你搜索“flag”能搜到但是选中后按X查交叉引用IDA弹出来的窗口里是空的。造成这种状况的原因通常是程序使用了动态构造字符串的方式——字符串不是直接在代码里被引用而是经过计算拼接后再打印的。换一个思路不要死磕这个字符串的交叉引用改成在关键函数附近往前翻翻代码看哪个函数调用了输出相关的API比如printf、puts、WriteConsole等从调用处往上反推。或者干脆用动态调试在输出函数下断点然后看栈回溯调用栈窗口直接找到是哪个函数调用了它。6.3 改了寄存器值但程序还是没输出flag如果你在比较指令处修改了寄存器值单步执行后程序仍然走上错误分支原因很可能是跳转条件还依赖其他标志位。比如比较指令是cmp eax, ebx后面跟着jz你改了EAX但EBX不等于你改的值ZF标志位没变化跳转还是不成立。更稳妥的方法是把比较的两个操作数都看清楚同时修改。遇到cmp指令看它比较的是谁和谁如果是cmp [eax], 18这种形式你得修改的是内存中的值而不是寄存器值需要用内存窗口去改。6.4 逆向上手者的两个常见心理误区第一迷信工具。有人觉得IDA是万能的事实上工具只是辅助逆向的核心能力是阅读逻辑和推理。Game这种题你甚至可以用十六进制编辑器打开文件直接搜索可见字符串再结合文件结构的规律去猜flag这条路也能走通。第二死磕一条路。静态分析卡住了非要继续抠伪代码动态调试卡住了非要一行一行单步。其实逆向解题最忌讳钻牛角尖该换思路就换思路该用脚本就用脚本甚至可以去查一下C标准库函数的行为这些都能帮助你突破。下表是Game这类入门逆向题的常见问题与排查方向速查现象可能原因排查方向IDA打不开或反汇编乱码32位程序用了64位IDA改用32位版本IDA打开字符串搜到了但无交叉引用字符串动态拼接或引用被优化在下断点处检查调用栈从输出API反推修改寄存器后仍走错分支跳转依赖内存值或其他标志位同时修改参与比较的所有操作数脚本算出的flag是乱码提取的数据偏移或长度不对回IDA核对数据区导出范围检查是key还是data弄错程序运行时报缺少DLL运行环境缺VC运行库安装对应运行库或换一台完整环境的机器调试6.5 一个小技巧用“对比法”快速确认代码改动动态调试时如果你不确定某处改动是否影响了流程可以在改动前先记录寄存器和内存的原始值改动后如果结果不对立刻恢复原始值重来。不要硬着头皮往下走。我早期调试就是改一个寄存器发现结果不对继续改另一个最后改得面目全非也不知道程序原本该是什么样。后来学聪明了每一处改动都写在纸上或者用注释记录下来随时可以回退。7. 从Game延伸出去的三个进阶方向7.1 进阶一遇到加壳程序怎么处理Game没有壳但不代表Reverse题目都不加壳。常见的壳包括UPX压缩壳、ASProtect加密壳、TheMida商业保护壳、VMProtect虚拟化壳等。遇到这些需要先脱壳再分析。UPX可以直接用工具脱ASProtect和TheMida需要手动寻找OEP原始入口点VMProtect则通常意味着你要面对虚拟化混淆代码难度直接上一个台阶。建议的顺序是先把Game这类无壳题刷熟练再学UPX脱壳再学手动找OEP最后再去碰VMProtect千万别跳级。7.2 进阶二反调试与反虚拟机有一些题目会在运行前检测当前是否处于调试器环境中检测手段包括但不限于查PEB中的BeingDebugged标志、调用IsDebuggerPresent、比较时间戳、检测特定窗口类名。Game没有这些机制但了解这些对抗手段很重要因为它们是真实恶意软件和商业保护软件的常见手法也是CTF高级逆向题的常客。7.3 进阶三从脚本小子到代码分析很多初学者觉得逆向就是“找flag”这个理解没错但格局可以再大一点。CTF逆向的底层能力是代码分析能力是“看到一段陌生代码能在有限信息下推断出它干了什么”的能力。这个能力在漏洞挖掘、病毒分析、软件破解、协议逆向等领域都是通用的。Game这道题的完整分析流程本质上就是一次迷你版的软件行为分析先观察文件特征再静态梳理逻辑再动态验证关键路径最后还原出核心算法。这套流程我在工作中真的用过很多次只不过分析对象从CTF题目变成了真实的恶意样本或者商业程序。最后说点个人体会。Game这道题不是什么宏伟的作品但它给了我一个很关键的启发逆向分析最重要的事情不是工具多熟练而是思路清晰。先确认目标flag在哪里再定位关键路径哪个函数控制flag输出然后选择合适的手段静态还是动态最后提取数据完成计算。这套思路一旦建立你会发现后面刷题的速度会有一个明显的提升因为所有题目的框架其实都差不多差别只在于细节的复杂度。如果你现在正卡在Game或者类似的入门题上我的建议很简单从字符串开始找到关键判断读明白那个判断然后写脚本把数据算出来。不要贪多求快也不要因为一道题卡了两小时就崩溃。逆向这东西做多了自然就顺了。