
很多新手学逆向最难迈过的坎不是工具不会用而是拿到一个exe之后完全不知道该从哪看起。我当年对着IDA的汇编窗口发过好几个小时的呆后来被朋友点醒才发现解决这类问题有一套固定的分析流程。眼下这篇就拿攻防世界新手区的经典逆向题“流浪者”完整走一遍这套流程先查壳、再运行收集信息、用字符串交叉引用定位核心函数、读懂变换逻辑、最后写脚本还原flag。“流浪者”在CTF逆向题库里算是非常典型的新手题逻辑不复杂但恰好覆盖了一次完整逆向分析所需要的所有环节。这篇内容适合刚接触CTF逆向、还没有形成自己分析套路的朋友也适合想快速复盘一遍基础流程的人。1. 先查壳再运行确认无壳是静态分析的前提1.1 运行一遍程序记下提示信息拿到陌生程序第一步不是直接拖进IDA而是先双击运行。当然我建议在虚拟机里跑养成隔离执行陌生二进制的习惯。虽然攻防世界这类CTF平台上的题目基本是安全的但“未知程序先在虚拟机里试”这个习惯能保护你很久。“流浪者”运行后界面很简单屏幕上提示输入一串内容。我随手敲了123456程序立刻给出错误提示。这一步很多人会忽略但它实际上是性价比最高的一步。运行程序能告诉你三件事程序的输入输出形式是什么、错误提示长什么样、有没有输出一些额外的格式要求。这些信息会在后面IDA的字符串窗口里变成你的路标。1.2 用Exeinfo PE查壳确认无壳接下来打开Exeinfo PE或者用DIEDetect It Easy把exe拖进去。结果很明确这是一个32位PE程序没有加壳从编译特征看像是VC 6.0的产物。这个结论非常重要因为它决定了后续可以用纯静态分析的方式来做。为什么要先查壳壳的本质是一段先于主程序运行的代码负责解密自身。有壳的程序入口点执行的是壳的代码你在IDA里看到的第一屏汇编全是壳在忙着还原真正的程序逻辑原始代码还处于加密状态。新手要是在有壳的情况下直接读汇编等于跟一个穿着雨衣的人较劲本质内容全被挡住了。这道题无壳所以IDA能直接看到干净的伪代码少掉一层麻烦。1.3 静态分析工具IDA还是Ghidra工具选择上静态分析首选IDA Pro。它对新手最重要的功能就是一个按键F5可以把汇编代码转成近似C语言的伪代码。如果你暂时没有IDA环境Ghidra也完全够用它由官方开源跨平台反编译插件叫Decompiler同样可以一键出伪代码。动态调试器x64dbg留作备用当静态分析看不懂某个分支时动态下断点看寄存器和内存变化理解起来会直观很多。提示不要迷信工具品牌。能帮你看到函数调用关系和伪代码的工具就是好工具。“流浪者”这个难度IDA和Ghidra随便一个都能轻松搞定。2. 定位核心函数的捷径从字符串交叉引用一路按图索骥2.1 打开Strings窗口找到刚才看到的提示文本把exe拖进IDA等待自动分析结束按ShiftF12打开Strings窗口。窗口里列出了程序里所有明文字符串我在列表中很快找到了刚才运行程序时看到的错误提示文本双击跳转到数据段里对应的字符串地址。为什么这个操作对新手题这么有效因为程序判断输入正确与否最终一定要给用户一个反馈。这个反馈字符串在二进制里是以明文形式存着的。我们要做的就是从结果反推过程找到这个反馈字符串逆着交叉引用就能定位到引用它的代码而那基本就是核心校验逻辑所在。2.2 按X查看交叉引用跳到校验函数在字符串地址上按X键或者右键选择Jump to xref to...IDA会列出所有引用了这个字符串的代码位置。双击跳过去就到了调用该字符串的函数。这里要提醒一句跳过去以后新手面对一大段汇编很容易再次懵掉。我的建议是先别管汇编直接按F5让IDA翻译成伪代码然后再看整体结构。汇编是给机器看的伪代码才是给人看的。至少在CTF新手阶段F5伪代码完全可以作为主战场。2.3 F5读整体结构输入、变换、比较三段式F5之后的伪代码逻辑结构非常清晰是CTF逆向题最经典的“三段式”接收用户输入的字符串对输入字符串调用一个变换函数把变换后的结果跟一个写死在程序里的字符串常量比较相等则走成功分支否则输出错误提示。到这里问题已经缩小了一大半。我们只需要做两件事把那个字符串常量抄出来它就是加密后的目标密文再读懂中间那个变换函数。密文已经在伪代码里躺着了剩下的就是攻破变换函数。3. 伪代码里的人话翻译那个变换函数到底在算什么3.1 把伪代码转成数学规则三种字符三种偏移变换函数的伪代码大致可以整理成下面这段逻辑以我这次在伪代码里看到的取值为例for ( i 0; i strlen(input); i ) { if ( input[i] 64 input[i] 90 ) { input[i] (input[i] - 51) % 26 65; // 大写 A-Z } else if ( input[i] 96 input[i] 122 ) { input[i] (input[i] - 79) % 26 97; // 小写 a-z } else if ( input[i] 47 input[i] 57 ) { input[i] (input[i] - 47) % 10 48; // 数字 0-9 } }别被这些数字吓到我们来做个简单的算术字符集合ASCII范围伪代码等价公式等价偏移量大写字母65~90(c - 65 14) % 26 6514小写字母97~122(c - 97 18) % 26 9718数字48~57(c - 48 1) % 10 481拿大写字母举例(input[i] - 51) % 26 65因为65 - 51 14所以等价于(input[i] - 65 14) % 26 65。翻译成人话就是大写字母的每个字符在字母表里往后走14位超过Z就绕回A继续数。小写字母往后走18位数字往后走1位。这就是为什么这道题叫“流浪者”——每个字符都在自己的字符集里“流浪”了若干步。注意不同平台发布的题目版本偏移量可能有差异。我这里写的是分析时读到的51、79、47这几个减法常量请以你从自己IDA里读到的数值为准。关键是掌握换算方法用对应的基础ASCII值65、97、48减去减法常量就是你需要的偏移量。3.2 为什么取模字符要在自己的“圆盘”里循环新手看到% 26和% 10通常会有个疑问为什么要取模道理很简单为了让变换后的结果仍然落在原来的字符集合里。字母表只有26个位置如果不取模Z往后走4位会变成]数字9往后走1位会变成:这就打乱了输出字符的可读性。取模之后字符就像钟表指针一样走到12点就绕回1点始终不会超出表盘。这个“有限集合循环位移”的思路在后面的逆向题里会反复出现。遇到类似题目时先判断出那个“有限集合”是什么——字母表、数字表、可见ASCII字符表都有可能——再找出步长算法基本就透明了。取模的值对应集合的大小这个关系要记牢。3.3 边界条件决定了解密时哪些字符不能动看这段伪代码新手最容易忽略的是那三个if分支的边界判断。input[i] 64 input[i] 90正好覆盖大写A-Z 96 122正好覆盖小写a-z 47 57正好覆盖数字0-9。而不满足任何分支的字符会走else原样返回。边界条件决定了哪些字符会被变换、哪些不会被变换。如果flag里存在下划线、花括号这类特殊字符它们不在任何分支里就不会被变换。写解密脚本时遇到这类字符应该原样保留不能强行套偏移。很多新手跑出来的flag出现乱码就是因为他把花括号也拿去移位了。这不是算法问题是边界条件没看全。4. 顺着加密逻辑反向推写Python脚本还原flag4.1 解密公式把“往后走”改成“往前走”加密规则是“字符在字符集内往后走n格”解密自然就是“往前走n格”。用数学表达就是大写c_dec (c_enc - 65 - 14 26) % 26 65小写c_dec (c_enc - 97 - 18 26) % 26 97数字c_dec (c_enc - 48 - 1 10) % 10 48这里在取模前加上26或10是为了保证被减数非负避免负数取模带来的困扰。实际上Python的%运算结果永远是非负的直接写(c_enc - 65 - 14) % 26 65也完全正确我下面脚本里就用的这种写法。4.2 完整脚本target串填进去就能跑假设我从IDA里复制的目标字符串存在变量target里target 这里粘贴你从IDA里复制出来的目标字符串 def decrypt_char(c): # 对应加密的大写分支往后走14解密就往前走14 if ord(A) c ord(Z): return (c - ord(A) - 14) % 26 ord(A) # 对应加密的小写分支往后走18 if ord(a) c ord(z): return (c - ord(a) - 18) % 26 ord(a) # 对应加密的数字分支往后走1 if ord(0) c ord(9): return (c - ord(0) - 1) % 10 ord(0) # 特殊字符不参与变换原样保留 return c flag .join(chr(decrypt_char(ord(ch))) for ch in target) print(flag)把target替换成你在IDA里看到的目标字符串直接运行。如果一切顺利输出出来的就是程序真正期望的flag。这里我故意没有把目标字符串写死在示例脚本里就是想让你亲自动手从那道题的伪代码里把常量捞出来——这个过程本身就是逆向训练的一部分。4.3 验证闭环把flag重新输回程序跑出flag之后我的习惯是立刻把它重新输入到“流浪者”程序里验证。程序判定成功才说明整个分析闭环了。这一步省不得。见过不少这样的场面脚本跑出来一串字符新手开心得不行结果拿回去一验证是错的。方向搞反、偏移量算错、特殊字符被强行变换任何一个环节出错都会得到看似合理的乱码。验证不花多少时间却能帮你把错误拦截在提交之前。4.4 没有Python时用Excel甚至手算也能推如果你手边暂时没有Python环境用Excel也能完成同样的工作。拿大写字母举例Excel公式可以写成CHAR(MOD(CODE(X) - 65 - 14, 26) 65)把“X”替换成目标密文里的具体字符逐个套用、拼接即可。我大学刚接触这类题目时甚至干过拿纸笔一个字符一个字符推的傻事。笨是真的笨但推完一个完整字符串取模和字符集这两个概念就彻底刻进脑子里了。5. 新手最容易卡住的三个地方顺带聊聊APK版本怎么处理5.1 坑1字符串窗口里找不到目标字符串怎么办有些题目不会把密文直接以明文形式摆在数据段里而是运行时动态拼接或者做了编码存储。这时候ShiftF12的字符串窗口里可能什么都搜不到或者搜到的全是干扰项。遇到这种情况别慌回到逻辑本身在主函数附近搜索strcmp、memcmp、strncmp这类比较函数的交叉引用再向上回溯比较的参数来自哪个函数、哪个静态数据最终一定能找到密文的来源。“流浪者”这道题用不上这么费劲的手段但“找比较函数”这个方法进入新手期之后的进阶题目后你会经常用到。5.2 坑2伪代码变量名太乱不知道读哪些IDA给伪代码变量起名毫无感情v1、v2、v3函数名也都是sub_401000这样的地址。新手在这种代码面前很容易丧失信心。我的建议是忽略名字只抓三样东西循环结构for语句里i从哪开始、到哪结束决定变换作用在哪些字符上分支结构if/else按什么条件分类决定字符类型被如何区分算术运算加减乘除和取模决定每类字符被怎么变换。把这三样东西圈出来剩下的赋值、拷贝、函数调用基本都是“搬运工”不改变核心逻辑。圈完以后整个函数就是一张规则表写解密脚本就是照着这张表逐行翻译。5.3 坑3加密和解密方向搞反还有一类常见错误解密脚本里直接调用加密逻辑把密文又“加密”了一遍。这相当于从起点往东走两步到达B点然后又往东走两步指望能回到起点——当然不可能除非加密操作碰巧是可逆的对称变换典型例子是异或同一个值。判断方向有个土办法加密函数如果有偏移解密就应该是-偏移加密如果有查表解密就应该是反查表加密如果有异或解密仍然是异或同一个值。动笔写脚本之前先明确回答自己一个问题这个变换的逆运算是什么想清楚了再写代码方向错误导致的返工会省掉很多。5.4 扩展如果在攻防世界下载到的是APK版本攻防世界很多逆向题会出移动端版本“流浪者”这种“输入→变换→比较”的逻辑如果做成APK处理方法几乎一样只是工具链不同。拿到APK后先别急着用IDA打开用jadx或JEB直接把APK反编译成Java源码在代码里搜索关键字符串或者定位MainActivity大概率能看到和PE版如出一辙的校验逻辑。如果核心逻辑被放在native层.so文件那还是回到IDA分析so文件套路完全不变。MT管理器在安卓端可以用来快速查看APK的smali、dex和资源文件做小规模改动很顺手但完整解题我更推荐jadx静态分析加Frida动态验证的组合。无论PE版本还是APK版本判断逻辑的骨架都一样找输入点、找变换逻辑、找比较目标。平台换了方法论不变。这也是我建议新手认真对待每一道简单题的原因——你攒下的不是一个题的答案而是一套在任何二进制面前都通用的分析流程。最后再分享一个小习惯每做完一道题我会把关键函数的伪代码截图和解密脚本放进同一个文件夹命名里标注题号。攒了二十几道题之后回头看这个文件夹就成了一个“逻辑模式库”——以后再遇到相似的加密结构一眼就能认出它属于哪一类定位速度快得惊人。“流浪者”作为你在攻防世界逆向新手区的起步题把这一套流程吃透后面的路会好走很多。