☰
CTF逆向新手课:从查壳到脚本还原flag的完整分析流程
2026/10/4 3:12:27 网站建设 项目流程

很多新手学逆向,最难迈过的坎不是工具不会用,而是拿到一个exe之后完全不知道该从哪看起。我当年对着IDA的汇编窗口发过好几个小时的呆,后来被朋友点醒,才发现解决这类问题有一套固定的分析流程。眼下这篇就拿攻防世界新手区的经典逆向题“流浪者”完整走一遍这套流程:先查壳、再运行收集信息、用字符串交叉引用定位核心函数、读懂变换逻辑、最后写脚本还原flag。“流浪者”在CTF逆向题库里算是非常典型的新手题,逻辑不复杂,但恰好覆盖了一次完整逆向分析所需要的所有环节。这篇内容适合刚接触CTF逆向、还没有形成自己分析套路的朋友,也适合想快速复盘一遍基础流程的人。

1. 先查壳再运行:确认无壳是静态分析的前提

1.1 运行一遍程序,记下提示信息

拿到陌生程序,第一步不是直接拖进IDA,而是先双击运行。当然我建议在虚拟机里跑,养成隔离执行陌生二进制的习惯。虽然攻防世界这类CTF平台上的题目基本是安全的,但“未知程序先在虚拟机里试”这个习惯能保护你很久。

“流浪者”运行后界面很简单,屏幕上提示输入一串内容。我随手敲了123456,程序立刻给出错误提示。这一步很多人会忽略,但它实际上是性价比最高的一步。运行程序能告诉你三件事:程序的输入输出形式是什么、错误提示长什么样、有没有输出一些额外的格式要求。这些信息会在后面IDA的字符串窗口里变成你的路标。

1.2 用Exeinfo PE查壳,确认无壳

接下来打开Exeinfo PE,或者用DIE(Detect 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,等待自动分析结束,按Shift+F12打开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脚本还原flag

4.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:字符串窗口里找不到目标字符串怎么办

有些题目不会把密文直接以明文形式摆在数据段里,而是运行时动态拼接,或者做了编码存储。这时候Shift+F12的字符串窗口里可能什么都搜不到,或者搜到的全是干扰项。遇到这种情况,别慌,回到逻辑本身:在主函数附近搜索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版本,判断逻辑的骨架都一样:找输入点、找变换逻辑、找比较目标。平台换了,方法论不变。这也是我建议新手认真对待每一道简单题的原因——你攒下的不是一个题的答案,而是一套在任何二进制面前都通用的分析流程。

最后再分享一个小习惯:每做完一道题,我会把关键函数的伪代码截图和解密脚本放进同一个文件夹,命名里标注题号。攒了二十几道题之后回头看,这个文件夹就成了一个“逻辑模式库”——以后再遇到相似的加密结构,一眼就能认出它属于哪一类,定位速度快得惊人。“流浪者”作为你在攻防世界逆向新手区的起步题,把这一套流程吃透,后面的路会好走很多。

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

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

立即咨询