
我玩了四五个小时的攻防世界Reverse方向Game这道题算是我印象比较深的一道。原因很简单它的逆向难度其实不高但入口藏得比较深你要是上来就盯着main函数硬看很容易绕远路。这道题适合刚接触逆向、想练一练“动静结合”分析思路的人因为它既需要你用静态分析理清逻辑又需要你实际动调去验证假设整个过程非常典型。这篇文章我就按我实际做题的路径来写包括我怎么收集信息、怎么发现关键函数、怎么定位验证逻辑、以及最后怎么把flag抠出来。中间会穿插一些我踩过的坑希望能帮你少走点弯路。1. 开局准备先搞清楚你面对的是一个什么样的程序拿到题目压缩包之后我习惯先做三件事查文件类型、看保护机制、跑一下看行为。这三件事花不了两分钟但能让你对接下来的分析方向有个基本判断。在Linux环境下我最先用的命令是file看这个程序的架构和链接方式。Game这个题目给的是一个x86-64的ELF文件动态链接不是静态编译这意味着程序运行时依赖系统的共享库。继续用checksec看保护机制发现NX栈不可执行是开启的但PIE是关闭的。PIE关闭意味着程序加载到内存中的基址是固定的这给动态调试省了不少事你下断点的时候可以直接用ELF文件里的地址。接下来我直接运行了一下这个程序看一下它的交互逻辑。程序起来之后会打印一些提示信息看起来像是某个猜数字或者猜拳的小游戏。我随便输了一些数字进去试了试程序反馈了输赢结果然后退出。这时候我大致能确认这是一个带交互逻辑的小游戏程序flag大概率藏在“通关”之后才会输出的逻辑分支里。还有一个非常重要的信息收集手段是strings直接拉出程序里的可见字符串。这一步能让你快速了解程序内部有哪些关键输出。Game这个程序里能看到类似“Input your number”、“You are win”、“You are lose”、“flag{...}”之类的字符串。看到flag字样出现的时候基本就可以确定只要让程序走到“win”的分支flag就会被打出来。不过这里我要多说一句strings看到flag字符串不代表flag就在那摆着的。很多时候你看到的只是一个fake flag真正的flag需要你在内存里去捞。所以不要把strings的结果当结论只当线索。2. 静态分析找到游戏的核心逻辑藏在哪个函数里确认了程序的基本情况之后我把它丢进了IDA Pro。这一步基本上没有任何悬念你需要在反汇编视图里找到程序的入口然后跟着start一路找到main函数。Game的main函数写得不算复杂但它在打印完欢迎信息之后核心逻辑并不在main本身而是调用了一个子函数。这个子函数就是整个游戏的主体逻辑我暂时把它命名为game_loop。你跟着调用关系进到这个函数内部能看到它做了这样几件事打印提示信息要求玩家输入一个数字通过scanf或者类似的输入函数读取玩家输入把输入值和一个内存里某个全局变量的当前值做比较根据比较结果决定跳转到“win”分支还是“lose”分支这个结构是典型的C语言switch或者if-else逻辑编译到汇编之后变成cmp加jz/jnz的跳转。我们在IDA里可以按F5直接看伪代码Game这个程序没做什么混淆F5出来的伪代码可读性很高。核心逻辑大致是程序内部有一个计数器或者状态值你输入的数值如果和它的某个期望值匹配计数器加一加到一个阈值之后进入“win”状态如果中途有一次不匹配直接跳到“lose”。这里有个关键点值得展开说这种“累加计数到阈值”的设计会让你误以为需要连续猜对很多次才能赢。但实际上你仔细看伪代码会发现计数器加一的操作发生在匹配成功之后而匹配失败会直接跳到结束并不影响你动态调试时直接改计数器或者改跳转条件。我还发现这个函数里调用了一个生成随机数或者从某处读取值的子函数。如果你盯着这个子函数去分析它的算法试图算出每次期望的输入值那你可能要花很多时间。我一开始就是从分析这个子函数入手试图推导出一个序列生成公式结果绕了一大圈浪费了不少时间。后来我才意识到这个子函数的返回值在内存里有缓存它的算法逻辑根本不用完全逆向你只需要在动态调试的时候用断点把它实际算出来的值dump出来就行。这一段是我踩的最大的一个坑后面我会细说。3. 动态调试用断点验证假设而不是猜静态分析能帮你搭出程序的骨架但真正要确认行为和拿到flag还是得靠动态调试。我这边用的是gdb配合pwndbg插件操作起来比裸gdb顺手很多。我调试的核心思路很简单在游戏主循环的比较指令处下断点然后观察程序实际拿了什么值来和我的输入做比较。gdb做这件事非常方便。实际操作中我首先用IDA里分析的地址在gdb里下了个断点位置设在比较指令之前。然后运行程序随便输入一个数字断点命中之后我用x/gx查看内存中那个关键全局变量的值再用info registers查看寄存器的当前状态。几步下来程序每次比较时的真实数据就能看得很清楚。为了更直观我还做了一次“观察者模式”调试——不急着改任何东西连续多次运行记录每次程序实际期望的数值。跑了几次之后发现这个数值每次都不一样说明它确实是程序运行中动态计算出来的。但是如果我在比较指令的地方用set命令直接修改内存值让比较一定成功那么后续的逻辑就能一直往下走。这里有一个关键的技巧别只盯着数据要盯着数据从哪来。我在比较指令前面的调用处多下了一个断点单步进去观察它计算返回值的具体过程。这样我能看到程序是读了哪块内存、做了什么运算生成了期望值。多看了几次之后我发现期望值其实是从一个固定种子生成的序列。种子在程序启动时被写入内存然后就再也没变过。也就是说如果你能获取或推算出这个种子理论上你是可以算出完整序列的。但对于拿flag这个目标来说完全没必要走这条路——直接改内存让计数器跳到阈值比算序列快得多。不过我也要说一下直接改跳转属于“暴力通关”如果你只是想拿到flag那没问题。但如果你是练习我建议你还是花点时间把序列生成逻辑逆清楚这对提升逆向分析能力帮助很大。我自己的做法是先用暴力改流程的方式把flag拿了一遍确认目标可达然后回头又逆了一遍序列生成逻辑算出了它的生成规则。两道工序都做完整个题才算真正拿透了。4. 关键分歧点计数器在内存里的位置直接改写它既然确认了核心机制是“匹配成功累加计数达到阈值即win”那最简单粗暴的做法就是找到这个计数器在内存里的位置直接给它改成阈值然后随便输一个数字触发判定。怎么找计数器的位置我的做法是这样的在IDA里定位到计数器加一的那条指令比如add [rbpvar_4], 1记录下这条指令在IDA里的地址。然后在gdb里同样地址下断点先正常输一次正确数字让断点命中再用x/gx查看计数器当前的内存值然后继续执行几次观察计数器是如何变化的。确认这个内存地址之后我就可以直接修改它了。修改方法也很简单在gdb里用p或者set命令set {long}(address) 99把计数器改成99之后再随便输一个数字程序判断的时候发现计数器已经达到阈值直接走进win分支把flag打印出来。这里有一个细节需要提醒有的人会想直接把jz改成jnz不也一样吗确实一样但改跳转指令有风险——如果程序后面还有别的跳转也用了同一个条件码可能引发连锁反应。改计数器则是一个更精准的做法它只影响计数逻辑不影响其他流程。我试过两种做法都成功拿到了flag。但实测下来改计数器的操作更稳因为它符合程序本身的逻辑走向——你不是在打破规则你是在让自己“看似”完成了足够多的正确匹配。5. 从内存里抠flag的几种姿势flag打印出来之后你以为就完了没有Game这道题有个比较有意思的地方flag其实在“win”分支被触发之前就已经存在于内存里了。你在字符串表里看到的那个“flag{...}”只是一个占位符真正的flag是被程序动态拼出来的。我第一次拿flag的时候是直接盯着程序输出窗口把flag抄下来的。后来我又试了另一种玩法在程序还没走进win分支之前直接在内存里搜索flag。用gdb的search命令gdb-peda$ search flag{这种搜索能直接把内存里以flag开头的内容全部dump出来。你会发现在程序运行过程中的某个时刻内存里已经出现了完整的flag字符串只是还没有通过输出函数打印出来。这说明程序提前就把flag准备好了只是放在了一个等待被输出的缓冲区里。关于在内存中搜索和提取信息我再补充几点经验搜索时最好把字符串特征值写全比如直接搜flag{而不是搜flag否则可能出现大量无关命中。如果你在调试器中看到的内存内容不是完整的可打印字符串通常是因为程序还在组装它的过程中这时候你可以给程序发送一个触发信号让它走完剩下的逻辑再搜。如果你用的是IDA的动态调试Remote GDB Debugger也支持直接在调试器里Search - Sequence of bytes操作更直观。这些技巧在以后做其他逆向题的时候经常会用到养成“不仅在输出端看结果也在内存里看状态”的习惯能帮你解决很多看起来莫名其妙的题目。6. 复盘Game这道题真正想让你练的是什么题目本身做完了但我觉得应该花点时间复盘一下。Game这个题名看起来像个不起眼的小游戏但它实际上把逆向工程里几个核心的思维习惯都练到了第一个是“别被流程带偏”。程序表面的逻辑是猜数字、判断胜负但你分析的核心不应该是这个“游戏体验”而是背后的状态变化规律。很多时候你在逆向一个程序时会被它表面的业务逻辑干扰忘了你真正要找的是那个关键的“状态点”——计数器、标志位、缓冲区指针、跳转条件等等。Game这道题把这一点体现得非常清楚。第二个是“动静结合”。你光看静态代码可能会陷进序列生成算法里拔不出来你光动态调试又可能对整个程序的结构缺乏整体感。最有效的路径是先用静态分析搭骨架理清关键函数和分支结构再用动态调试去验证和定位具体数据变化。这套打法在真实世界的漏洞分析和软件逆向里一样适用。第三个是“fuzz不一定比人工分析快”。我之前也试过用自动化爆破的方式去猜那个数字序列因为觉得这个题的游戏逻辑很简单也许跑个几次就能自动通关。实际测下来发现并不可行因为每次运行程序时随机种子的变化会导致期望序列变化你没法预先构造一个通吃的答案。还不如直接看逻辑、下断点、改数据一分钟搞定。这是一个很重要的心态问题在逆向中工具是辅助理解是核心。7. 一个容易被忽略的小坑输出缓冲区和终端编码最后说一个我做题时遇到的环境问题可能也有人会碰到。我用gdb运行Game的时候发现程序打印提示信息和我输入的内容顺序有时候是乱的甚至程序在等待输入时提示信息还没显示出来。这是Linux终端和程序的输出缓冲区不同步导致的典型现象。解决这个问题有两个办法一个是在程序外面通过工具强制让标准输出变成行缓冲或者无缓冲比如用stdbuf命令运行Gamestdbuf -o0 ./Game另一个办法是在调试时直接忽略这个现象因为你反正会在断点处观察寄存器和内存不需要依赖交互界面的顺序。但如果你的调试目标完全依赖程序的标准输出那输出顺序错乱可能会误导你的判断所以还是建议用stdbuf跑一下。还有一个问题更隐蔽终端编码。如果你发现输出的flag里有字符显示成乱码不要急着怀疑题目有问题先确认一下终端是不是UTF-8编码。Game这个题的flag是纯ASCII字符理论上不会出现编码问题但我见过有人用Windows的命令行工具去解Linux的ELF导致字符串显示乱掉的案例。逆向题就是这样一个域工具链和环境的选择本身就占三分之一的解题时间。8. 给新手的几条实在建议如果你刚开始接触攻防世界的Reverse方向Game这道题算是一个不错的节点。它难度不高但涉及的技能栈很全。我基于自己做这道题的经验给你几条实在的建议第一不要看到ELF就想着脱壳。Game没有加壳它是一个普通的ELF文件你直接拉进IDA就能看伪代码。很多时候新手一上来就问“怎么脱壳”其实大部分题根本不需要脱壳先查一下checksec和file确认有没有加壳再说。第二F5伪代码可以看但要会对应回汇编看。IDA的F5插件很方便但它不是万能的复杂的控制流有可能被它美化掉导致你忽略真正的逻辑。Game这道题的F5结果很简单但你至少也应该能看懂cmp和jz之间的对应关系否则遇到真正的混淆题目会完全束手无策。第三遇到随机数不要慌。很多新手看到程序里有随机数就觉得自己没法分析了但你要知道程序里的随机数几乎都是伪随机。它一定有种子而种子的来源可能是时间、可能是固定值、也可能是某个文件内容。找到种子的来源你就能复现这个“随机数”的生成结果。Game这道题甚至不用复现——你只需要在程序生成了期望值之后从内存中把它dump出来就行。第四做题一定要记笔记。我做完Game之后回头整理这篇复盘如果没有当时的断点记录和寄存器截图我很难回忆起每一步的具体操作。不用写很正式的文档但至少把你断点下在哪个地址、修改了哪些内存值、flag是从哪里捞出来的这些关键过程记录下来以后遇到类似的题可以直接复用你的方法论。第五动手做别只看别人的题解。题解能帮你理清思路但看懂了和会做了之间的距离只有你自己实际操作才能填上。Game这道题值得你独立做一遍然后不看任何题解再用动调的方式重做一遍。两遍下来你对“内存中修改数据”这件事的手感会完全不一样。