ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

逆向新手实战:从“流浪者”看字符串查表变换与脚本还原

逆向新手实战:从“流浪者”看字符串查表变换与脚本还原 一个周末我在攻防世界逆向题库里翻到一道叫“流浪者”的题。以前我只会在签到题里找 flag一直想找一道能真正把“静态分析、动态调试、写脚本”串起来的题目练手。这道题用完给我的感受是难度很适合逆向新手关键逻辑不复杂但它非常典型——程序会要求你输入一串内容经过一个字符变换循环之后再和内存中的目标字符串比较。你把这条变换链捋清楚反推回去flag 自然就出来了。这篇文章我会把从下载文件到最后拿到 flag 的完整过程记录一遍包括文件类型识别、IDA 定位核心函数、动态调试验证、Python 逆向脚本求解以及新手特别容易卡住的地方。如果你刚开始接触 CTF 逆向或者已经在攻防世界刷了几道题但总觉得“懂答案不懂思路”这篇内容应该能帮你在脑子里立起一个解题框架。1. 题目拆解先搞清楚“流浪者”在考什么很多人拿到逆向题第一反应是双击运行然后瞎点一通最后到网上搜答案。这也不是不行但你会漏掉最重要的东西题目到底在哪里拦截了你它又是拿什么和你输入的字符做比较的。CTF 逆向题本质上就是“给一个处理过的程序让你还原它的算法或者绕过它的校验”所以第一步不是盲目操作而是把题目信息整理清楚。1.1 攻防世界的逆向区怎么练攻防世界的逆向题目分很多档从纯签到题到 VM 保护、反调试、花指令难度跨度很大。“流浪者”属于典型的入门进阶题它不是那种你一眼就能在字符串里看到 flag 的送分题但也没有加壳没有混淆没有反调试逻辑还集中在一个小函数里。对新手来说这种题最适合用来建立“按流程分析”的习惯。在攻防世界做题有个好处每道题有固定的 flag 格式做完之后可以立刻验证答案。你不需要担心题目环境被破坏也不需要准备复杂的比赛环境一个 Windows 虚拟机和几个常用逆向工具就够了。和那些几十 MB 的大型商业软件逆向不同CTF 题目的输入输出非常明确逆向目标通常就是“找到那个决定正确与错误的比较点”所以非常适合新手反复练习。我第一次做“流浪者”的时候把它当成了一个普通的 CrackMe 来分析。它的行为很直接运行起来后要求输入一串字符如果输入不正确程序会提示错误。这个交互模式就是后面所有分析的主线去程序里找到“提示错误”的那段代码顺着它向上回溯就能看到校验逻辑。1.2 拿到压缩包后的第一轮检查先别急着双击运行。我一般会在虚拟机里用几个小工具做一次快速侦察花两分钟确认这是什么东西。攻防世界下载下来的通常是一个压缩包解压后是一个 exe。我把它放到一个专门的“逆向练习”文件夹然后打开终端用file命令看一眼文件类型$ file Wanderer.exe Wanderer.exe: PE32 executable (GUI) Intel 80386, for MS Windows这说明它是一个 32 位的 Windows GUI 程序PE 格式没有附带“console”字样也就是说它在 Windows 上运行时会开一个窗口而不是命令行。看到这里我心里大概有数了IDA 用 32 位模式打开动态调试用 x64dbg 或者 OllyDbg 都行。接着用 Exeinfo PE 或者 Detect It Easy 查一下壳。这一步很关键如果有壳我们得先脱壳再分析那又是另一套流程。Exeinfo 打开后显示类似Microsoft Visual C的编译信息没有 UPX、没有 Themida、没有 VMProtect。也就是说这是一个没壳的普通程序可以直接拿 IDA 分析。这里我想多说一句很多新手容易忽略这个环节上来就拖进 IDA 狂按 F5。实际上“识别文件类型 查壳 判断编译语言”这三个动作能在后面帮你省下大量时间。比如你知道它是 MSVC 编译的 32 位程序那么在动态调试时下断点、看调用约定、找 main 入口都会更有针对性。工具的使用顺序本身就是逆向基本功的一部分。1.3 从解题框架理解目标做逆向题永远要记住一个骨架输入 - 处理 - 比较 - 输出。程序不可能平白无故判断你对不对它一定会把你输入的内容做某种变换然后和一个固定字符串或者固定数值做比较。我们的任务就是把这个流程还原出来。不管是“流浪者”还是其他题我建议在动手前先在纸上画一条数据流用户输入经过什么函数变成什么中间值最后和哪个常量比较。很多新手盯着 IDA 的汇编代码看半天觉得每一行都不懂就是因为没有先立起这个框架。有了框架之后你看到的每条指令都可以归类到“处理”或者“比较”这两个筐里一下子就不乱了。这道题的目标就是找到那个固定比较串然后反推出合法输入。听起来简单但实操中会有两个难点第一固定比较串可能不是明文而是经过变换后的结果第二变换逻辑可能在多个函数里来回跳转新手容易跟丢。下面我会结合工具详细讲一遍我是怎么走完这条线的。2. 准备工具与环境静态和动态两条腿走路我经常看到有人问“逆向用 IDA 还是 Ghidra”“OllyDbg 是不是过时了”。我的建议很朴素工具没有绝对好坏关键是同一道题你最好同时用静态分析和动态调试这两个视角看一遍。静态分析效率高动态调试准确两者互为印证。下面是我做“流浪者”时用的工具组合。2.1 静态分析用 IDA重点看 F5 伪代码IDA Pro 是逆向的老牌工具F5 插件能把汇编代码转成类似 C 的伪代码极大降低阅读门槛。新版 IDA 对 PE32 文件支持很好打开程序后它会自动识别入口点、导入表、字符串还会帮你标注库函数。对新手来说学会“找字符串 - 交叉引用 - F5 看伪代码”这三部曲就能解决相当大一部分题目。如果你没有正版 IDA也可以先用 Ghidra 替代。Ghidra 是免费开源的同样有反编译功能只是界面和快捷键需要适应一下。我个人建议新手优先用 IDA因为攻防世界的大多数题都是老题网上 writeup 几乎都用 IDA 描述地址和函数名你跟着做不会产生“版本不同导致界面对不上”的困扰。“流浪者”这道题在 IDA 里打开后我第一件事不是看汇编而是按ShiftF12打开 Strings 窗口。这里能看到程序里所有字符串包括中文字符串。如果有中文提示说明程序模块的编码方式可能是 UTF-8 或者 GBKIDA 新版本一般能正常显示。看到类似“请输入正确内容”“恭喜”这类字符串后双击它再按X查看交叉引用就能跳到引用它的代码处那通常就是校验函数所在的位置。2.2 动态调试用 x64dbg验证猜想要靠断点静态分析能给你一个“程序大概是这么走的”结论但有时伪代码不明显或者编译器优化让逻辑变得别扭这时就需要动态调试。x64dbg 是现在 Windows 平台上主流的 32 位/64 位调试器对于 32 位程序用 x32dbg界面清晰断点管理也方便。动态调试的核心动作是下断点。在“流浪者”这道题里最常用的断点位置是strcmp或者memcmp。当你输入一串测试字符串后程序走到字符串比较函数时会被断下来这时候你可以直接观察两个参数一个是你输入内容经过变换后的结果另一个是程序期望的常量串。两个参数放在一起校验逻辑就完全暴露了。有的新手会问既然静态分析已经看到伪代码了还有必要动态调试吗我的体会是非常有必要。伪代码有时会把常量隐藏在数据段里你看不清它的最终形态而动态调试时寄存器、栈、内存中的实际字节都摆在眼前你会非常直观地看到“哦原来这个数组里存的就是目标字符串”。另一个原因是动态调试能帮你确认真实流程防止因为 IDA 反编译错误而走偏。2.3 一定要用虚拟机隔离环境做逆向题我建议使用虚拟机Windows 10 或者 Windows 7 都行。原因有两个第一很多题目来自比赛不排除作者有意或无意夹带的奇怪的代码在虚拟机里运行比较安全不会影响宿主机器第二调试器与系统环境、杀毒软件之间有时候会互相干扰虚拟机能提供一个干净可控的环境。你不需要给虚拟机分配太高配置2GB 内存、单核 CPU 足够。把 IDA、x64dbg、Exeinfo PE、Python 都装进去然后对虚拟机打一个快照。每当分析完一道题或者系统出了奇怪问题直接还原快照省得到处折腾环境。反向程序如果带有反调试在虚拟机里也能更方便地观察它的行为不会被杀毒软件拦腰截断。对于“流浪者”这道题环境要求并不高。我用的就是一台 Win10 x64 虚拟机在里面跑 x32dbg 和 IDA非常稳定没有遇到兼容性问题。如果有的程序在 Win10 下表现异常你可以换成 Win7 虚拟机再试。CTF 老题很多都基于 XP/Win7 时代的技术栈Win7 虚拟机往往是最稳妥的选择。3. “流浪者”解题实操从字符串找到核心算法接下来是重头戏我会把从打开 IDA 到写出逆向脚本的完整过程拆开来讲。这道题的核心逻辑可以归纳成一句话输入字符串经过一个“字符变换循环”变成新字符串然后和固定串比对。下面按实际操作的顺序来。3.1 在 IDA 中定位关键函数我用 IDA 打开Wanderer.exe后按ShiftF12打开 Strings 窗口。一眼就看到了几个和校验相关的字符串比如输入提示、错误提示、正确提示。错误提示字符串的交叉引用最值得看因为程序往往只在“校验失败”分支引用它。我双击错误提示字符串按X查看交叉引用IDA 把它带到了一个函数内部。这个函数就是整个程序的核心校验函数。进入函数后按F5IDA 会生成一段伪代码。伪代码里通常先有scanf或者对某个缓冲区的读取操作然后有strlen对输入长度做检查接着是一个for循环对每个字符做变换最后是一个strcmp或memcmp比较函数。如果程序特别简单可能还会直接看到硬编码字符串出现在比较函数附近。有一点我必须强调很多新手看到strcmp就不往下看了认为比较的字符串就是 flag这是最大的误区。比较函数的两个参数一个可能是你输入内容变换后的结果另一个是关键常量但它不一定等于最终 flag。flag 通常是你需要提供的合法输入也就是说你要反推的是“变换前的内容”而不是“变换后对比用的字符串”。理解清楚这个方向这道题的逻辑就清晰了一半。3.2 读取核心校验伪代码在我分析的这个版本里IDA 给出的伪代码大致是这样的结构int verify(char *input) { char buf[64]; int len strlen(input); char *table 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ; if (len ! expected_len) return 0; for (int i 0; i len; i) { buf[i] table[input[i] - 0]; } return strcmp(buf, 这里的常量串需要从内存/伪代码中提取) 0; }注意上面这段是我根据这道题反编译结果整理出的逻辑骨架不同比赛版本可能稍有出入但“查表”这个套路在 CTF 入门题里非常常见。所谓查表就是程序准备了一张固定顺序的字符表然后把输入字符的数值作为下标取表中对应位置的字符作为输出。它本质上就是一种替换加密把输入中的每一个字符替换成表中某个字符。咱们把这段逻辑翻译成人话程序先检查你的输入长度是否符合要求然后逐字把输入字符变成表格里的某个字符最后拿这一串结果和它预先存好的目标字符串做比较。由于它用了一个table[...]操作你在逆向时只要把表格内容、下标计算方式、目标字符串这三样东西找到整个问题就收束成一个映射逆运算。当时我看完伪代码后直接在数据段里找到了那张表又找到了目标字符串。紧接着我意识到不需要去理解程序为什么这样写也不需要去逆向它的字符串拼接逻辑我只需要把“目标字符串中的每个字符”在表中对应的下标找出来再把下标还原成输入字符即可。这个思路对所有查表类逆向题目都适用。3.3 用 Python 还原输入既然核心是查表那么解密脚本的核心也是建表、索引、映射。我们可以完全照着程序的逻辑写一个“反向版本”。假设程序的映射关系是输入字符 c - 下标 n c - 0 - 输出字符 table[n]那么反过来就是目标字符 ch - 找到它在 table 中的下标 n - 输入字符 chr(n ord(0))用 Python 实现就是# 从 IDA 数据段复制出来的表格 table 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ # 从 IDA 伪代码 / 动态调试中提取的目标字符串 # 注意这里不直接贴完整目标串留给你自己从题目中提取 enc xxxxxxx # 建立反查表: 字符 - 下标 pos {ch: i for i, ch in enumerate(table)} flag [] for ch in enc: if ch in pos: flag.append(chr(pos[ch] ord(0))) else: flag.append(?) # 如果目标字符不在表中说明漏掉了什么 print(.join(flag))把从 IDA 里看到的目标字符串填入enc运行脚本就能得到合法的输入内容。这道题最后要求的答案一般就是flag{...}格式你只需要把脚本输出内容按照题目要求包好即可。这里我想强调一个脚本设计细节我特意加了一个if ch in pos的判断如果目标字符串里某个字符不在表中就输出一个?而不是让程序崩溃。这个设计很朴素但在实战中特别有用。因为有时候你复制的目标字符串会带着不可见字符、换行符或者截断符号直接忽略它们会掩盖问题输出?能帮你快速发现哪一步没做对。如果你的目标字符串比表格中所有字符的集合大那就说明真正的算法可能不只是简单查表可能还做了加减法、异或、大小写变换等操作。这时候你就得回到伪代码里多看几行。比如有些题会写成buf[i] table[(input[i] 3) % 62]这时反向操作就要先找下标再减去偏移量。3.4 动态调试复核脚本算出来的结果对不对不能光靠脑子想最好在调试器里验证一遍。我会用 x32dbg 打开程序先找到核心校验函数的地址在strcmp那行下断点。如果比较函数是strcmp通常在导入表里能看到直接在导入表里对它下断即可如果被内联优化了那就得先根据 IDA 伪代码找到调用点在调用点下断。动态调试步骤大致是这样用 x32dbg 加载Wanderer.exe。在strcmp或核心校验函数入口下断点F2。运行程序输入一个测试字符串比如flag{test}。程序运行到断点时自动暂停查看栈窗口能看到两个参数。点进第二个参数对应的内存地址用十六进制或者 ASCII 方式查看字符串内容。当你看到目标字符串和脚本中的enc一致时就说明静态分析没有跑偏。然后你再用脚本算出的结果作为输入重新运行程序看到“正确”提示这道题就闭环了。动态调试还有一个很妙的用法你可以在断点处直接修改比较函数的返回值把strcmp结果改成 0强行让校验通过。这个操作可以用来验证“程序确实只是字符串比较”不过它只是绕过而不是真逆向用来辅助确认逻辑可以但拿 flag 通常还是要把合法输入还原出来。4. 逆向过程中的坑与排查技巧下面这些坑是我在自己做题和带身边朋友练习时经常遇到的。它们不一定都出现在“流浪者”这道题里但只要你开始刷攻防世界逆向区大概率会撞上其中一个。我按“症状 - 原因 - 对策”整理成表格方便你以后遇到问题快速对照。症状常见原因对策IDA 字符串窗口看不到中文程序使用 GBK/UTF-8IDA 未正确识别编码按A修改字符串编码或用 UE 等工具查看十六进制F5 伪代码显示函数名混乱符号被去除只有 sub_xxxx不用管函数名重点看参数传递和字符串引用动态调试时断在系统 DLL 里断点下在导入表函数程序还没走到用户代码等程序运行到WinMain或关键函数后再下断找到的字符串里没有 flag程序把 flag 经过算法隐藏了找字符串的交叉引用往上回溯处理逻辑直接搜索字符串没有结果目标串可能被拆分、加密或动态生成检查循环、异或操作用调试器在比较点观察内存程序一运行就闪退缺少动态库或环境不兼容用虚拟机且安装对应运行库查看导入表杀毒软件报毒没加壳的样本被静态查杀误报关掉实时防护或放到虚拟机白名单目录4.1 中文文本在 IDA 里显示成乱码怎么处理很多 PE 程序为了方便界面显示会把中文字符串以 UTF-16 或者 GBK 形式存放在数据段。IDA 的 Strings 窗口默认可能按单字节 ASCII 扫描这样中文就显示成乱码。遇到这种情况先别急着认为程序没有提示字符串。我常用的办法是到Options - General - Strings里调整字符串扫描选项或者直接在 IDA 的 Hex View 里看数据。如果是 GBK中文数据表现为两个字节一组每个字节都大于 0x80如果是 UTF-16则会出现很多0x00间隔。识别出编码后可以把数据转成明文再搜索。这个考察频率不高但做老题时常遇到。在“流浪者”这道题里我用的 IDA 版本比较新中文显示正常省了不少事。但如果你用的版本比较旧强烈建议升级一下或者安装插件。工具版本太旧很多时候不是技术问题而是浪费时间在环境上。4.2 F5 出来的伪代码看不懂怎么办新手最常见的问题是在 IDA 里按了 F5弹出来一大段 C 代码里面有v3、v4、a1、dword_405018这种变量名完全不知道在看什么。我的经验是不要试图一次看懂所有代码只盯三样东西。第一输入数据是从哪里来的第二输入数据在哪个循环里被改写了第三最后和哪个固定值比较。其他无关函数调用、堆栈初始化、异常处理逻辑统统先放一边。你可以把伪代码里和这三件事无关的行用注释或者高亮标记暂时忽略。如果 F5 的结果太乱还有一个备选方案回到汇编窗口找到调用strcmp或者memcmp的地方往前看几条指令。汇编虽然难读但“数据从哪里加载、存在哪个寄存器、传给哪个函数”这个模式比 C 伪代码更直接尤其在编译器做了优化时。4.3 断点下错位置导致调试效率极低动态调试很容易踩的坑是在导入表函数上下断结果程序每次调用都中断明明没走到我们想要的校验分支也停下来。比如你在printf上下断程序打印提示信息也会触发烦得很。更稳妥的做法是先用 IDA 找到核心校验函数的首地址然后切到 x32dbg按CtrlG跳到那个地址按F2下断。这样只有在程序真正执行到校验逻辑时才会断下来。如果你连目标地址都还没找清楚那说明静态分析的功夫还没到位建议先回 IDA 把函数定位做完再开调试器。动态调试还有一个技巧在断点命中后不要急着一点一点单步过循环先看循环次数。如果循环次数和输入字符串长度一致那你多半就站在变换循环里。这时可以配合 x32dbg 的“自动步过”功能快速拉完整个循环直接看最后的比较结果。4.4 这题需要脱壳或者对抗反调试吗“流浪者”这道题目前没有壳也没有明显反调试逻辑所以新手可以把全部精力放在算法还原上。但是我要提醒一点攻防世界里的题并不都这么友好你后面一定会遇到带 UPX 壳的、检测IsDebuggerPresent的、甚至带花指令的题目。如果遇到 UPX 壳最简单的办法是下载一个 UPX 解壳工具命令行一行就能脱壳如果遇到反调试则可以在 OD/x64dbg 里找到IsDebuggerPresent等 API 的位置把返回值改成 0或者直接在入口处打补丁跳过检测。懂得这些技能后你才能说逆向基本入门。不过现阶段“流浪者”的价值就在于干干净净地展示“输入变换比较”这串核心流程。先把这一套走顺再去碰各种烤壳技巧会从容很多。5. 复盘做完这道题我应该带走什么做一道题不只是为了一个通过记录。每次做完我都喜欢留几分钟复盘问自己如果换一道新题我会不会还是毫无头绪下面三件事是我从“流浪者”里提炼出来的也是很多进阶题都通用的底层思路。5.1 逆向的第一性原理是追踪数据流程序再复杂最终都要完成“输入 - 处理 - 比较 - 输出”这条数据流。逆向新手容易陷在工具按钮和快捷键里忘了最核心的任务是回答问题“我输入的数据变成什么样子了它和什么东西做了比较”只要把这条数据流的每一段都找到flag 基本就浮出水面了。具体到“流浪者”数据流是用户输入字符串 - 用查表循环逐个替换字符 - 得到变换后的字符串 - 和内存常量串比较。我顺着错误提示字符串找到校验函数顺着校验函数找到循环顺着循环找到表再顺着表反推输入。每一环都清晰可查这就是一条标准的数据流追踪链。5.2 静态和动态必须配合不能只靠一招静态分析能快速定位代码位置但伪代码只是 IDA 的猜测不是 100% 准确动态调试能看到程序运行时的真实状态但如果你连目标代码在哪里都不知道动态调试也无从谈起。两者是互补关系不是替代关系。我建议你在练习时给自己定一个规矩每道题至少做一遍静态定位再在关键比较点做一次动态确认。这个习惯养成后即使遇到 IDA 反编译结果不理想的情况你还会有第二双眼睛去验证。5.3 刷题顺序比盲目刷题更重要攻防世界逆向区有不少适合新手的题。我的推荐顺序是先做Hello, CTF和open-source它们让你熟悉最基本的 flag 查找和逻辑阅读再做流浪者这类带字符变换的题让你理解“输入不是直接比较”这个概念然后可以挑战game、no-strings-attached这类涉及简单加密或逻辑链的题最后再去碰需要动态调试和脱壳技巧的题目。这个顺序的合理性在于它每次都只引入一个新变量。做“流浪者”时你新增的变量是“字符变换循环”但文件本身没壳、流程简单这样你的认知负担被控制在一个合理范围内不会一下子涌进来太多新概念。很多新手觉得逆向难不是因为智商不够而是一开始就被塞了太多复杂知识点导致根本没建立起最基本的分析模型。最后说一个我自己的小习惯对于查表类算法我从来不会肉眼看表去猜映射而是直接写成脚本用代码反查。原因很简单人的眼睛适合看规律但不太适合做几十个字符的精确映射而程序一行循环就能把活干完。做完“流浪者”之后我在后面很多题里都用同样的方式处理了类似逻辑效率明显高了不少。希望这篇记录也能帮你把这条思路内化成自己的工具。
返回列表