资讯详情

资讯详情

从reverse3看CTF逆向:Base64编码与字符变换的静态分析全流程

1. 拿到题目后的第一步从文件识别到跑起来1.1 文件类型与基础信息BUUCTF 逆向工程里的 reverse3是很多人的第一道正式逆向题。题目标题写得很直白附件却是一个 32 位 Windows 控制台程序运行起来只干一件事让你输入 flag然后告诉你对还是错。如果你第一次做这类题大概率会在 IDA 里翻半天最后发现核心逻辑其实非常短一个 Base64 编码、一个逐字符累加下标的变换、一个硬编码比较串加起来就是全部考点。这篇文章我就按自己完整的解题路径来写从file和strings这些最基础的检查讲起再到 IDA 定位主函数、识别算法、写 Python 脚本逆推最后复盘几个新手必踩的坑希望能帮你把这一类题彻底吃透。从平台下载附件以后你会发现它没有扩展名大小只有几十 KB。我习惯先丢给file命令看一眼$ file reverse3 reverse3: PE32 executable (console) Intel 80386, for MS Windows从输出里能确认三件事它是 PE 格式目标系统是 Windows架构是 32 位 x86类型是控制台程序没有图形界面。这意味着它大概率就是个简单的scanf 判断程序不会涉及窗口消息循环、资源回调之类的复杂机制非常适合静态分析。很多人会忽略file的输出直接拖进 IDA。我还是建议先花几秒钟看一下因为位数和格式直接决定了后续加载器选项、调试器选型甚至影响你对函数调用约定的判断。比如 32 位程序大多用cdecl调用约定参数通过栈传递看到伪代码里sub_411940(Source)的时候要清楚Source是作为参数传给函数的返回值往往通过eax带回来。另外可以顺手检查一下有没有加壳用upx -t测试一次或者加载进 IDA 后看节表里有没有UPX0、UPX1。大多数 CTF 入门题不会加壳如果strings能正常看到提示字符串基本可以断定没壳。reverse3 属于典型的不加壳题目所以接下来的分析可以直接从静态反汇编开始省掉脱壳这一层麻烦。1.2 运行表现与 strings 里的线索直接运行程序Windows 环境或者虚拟机、Wine 里都行它会打印一行please input the flag:随意输入一个字符串回车结果只有两种正确提示或错误提示。反复输入几组数据后可以确定这是一个确定性的本地校验程序给定输入经过固定变换与某个目标比较然后输出对错。既然逻辑是确定的我们就能通过静态分析把它完整还原出来。接着看字符串$ strings reverse3 ... please input the flag: e3nifIH9b_CndH wrong flag ...这里出现了一个非常可疑的字符串e3nifIH9b_CndH。它既不是正常的英文提示也不像随机的垃圾数据。按经验这类字符串十有八九是校验的目标值或者某种密文。不少新手到了这一步会直接把它当输入试一遍结果当然是错误提示——因为程序在比较之前还会对输入做变换。不过在开始猜测之前我更推荐先把它放进 IDA看看它到底在哪个函数里被引用、和什么代码产生关联。其实strings这一步非常值得做它的价值不在“直接找出 flag”而在于帮你提前建立线索台账。后面在 IDA 里追踪时你会不断回到这个字符串上确认哦原来它就是比较的目标而不是原始输入。新手容易觉得strings太初级实际上它配合file和简单的运行测试已经能筛掉一大半无用信息。1.3 为什么这道题值得认真做一遍reverse3 的定位是给入门者练手它有真实的算法变换有常见的 Base64 编码有一个需要跨函数追踪的数据流还牵扯到“程序内部校验内容”和“平台提交 flag 格式”不一致这种细节。把这些点全部弄清楚以后再去看同类题目会顺畅很多。我的建议是不要一上来就找人要解题报告也不要满足于“跑通脚本拿到 flag”。把 IDA 里每个判断节点、每次函数调用跟一遍能讲清楚“输入进去之后发生了哪几步”这篇题解才算真正吸收。下面我就用最慢、最啰嗦的方式把这几个步骤完整拆开每一步都说明为什么这样做。2. 静态分析主逻辑两个关键函数和一个硬编码串2.1 用 IDA 还原 main 的骨架用 IDA 加载 reverse3等自动分析结束后直接看函数列表找到main或者main_0有些程序用 VC 运行时入口里会包一层main_0。按 F5 得到伪代码整理掉编译器生成的无用变量以后核心逻辑大致是这样int __cdecl main() { char Source[48]; char *v1; char v2[48]; puts(please input the flag:); scanf(%s, Source); v1 (char *)sub_411940(Source); // 第一步对 Source 做编码 sub_411AB0(v1); // 第二步原地修改编码结果 if ( strcmp(v1, e3nifIH9b_CndH) 0 ) puts(right); else puts(wrong flag); return 0; }这里我做了变量重命名先别急着看每个函数内部是怎么实现的先把数据流梳理出来用户输入 →sub_411940返回值 →sub_411AB0原地修改 → 与e3nifIH9b_CndH比较。也就是说输入不是直接和目标比较的中间隔了两层变换。你想要还原出正确的输入就必须把这两层变换一个一个逆回去。这个阶段最重要的不是看懂每一行汇编而是确定调用链和数据流方向。拿到伪代码后我通常会先画出这样一条链路用户输入 -- sub_411940 -- sub_411AB0 -- strcmp 目标串有了这条链路后面每一步都往这个框架里填细节就行。很多新手一上来就钻进某个函数内部逐行读汇编结果读了半天也不知道自己在哪、要解决什么这是非常典型的错误姿势。2.2 认出 Base64 编码函数接下来看sub_411940到底做了什么。翻到.rdata段你很快会看到一个非常眼熟的字符串ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/这就是标准的 Base64 字母表。在二进制里只要出现这张 64 字符的表并且有函数按 3 字节一组、拆成 4 个 6 位索引去查表基本可以断定它实现了 Base64 编码或解码。为了讲清楚后面的判断逻辑我先把 Base64 的基本原理过一遍3 个字节一共 24 个 bit按 6 bit 一组切成 4 组每组的值范围是 0 到 63对应查字母表得到 4 个可见字符。如果原始字节数不是 3 的倍数末尾会补填充。在sub_411940里你能看到类似这样的关键操作伪代码化out[0] table[ in[0] 2 ]; out[1] table[ ((in[0] 3) 4) | (in[1] 4) ]; out[2] table[ ((in[1] 0xF) 2) | (in[2] 6) ]; out[3] table[ in[2] 0x3F ];看到右移 2、右移 4、右移 6配合 3、 0xF、 0x3F这些掩码就可以非常确定它就是 Base64 的实现。这里没必要逐行还原作者的写法只要认得“3 字节进、4 字符出、查表输出”这个模式就够了。这一步的收获是sub_411940把用户输入做了一次标准 Base64 编码。注意它编码的是用户输入的完整字符串包括所有可见字符不区分字母和符号。2.3 认出逐字符自增变换函数接着看sub_411AB0这个函数更短逻辑如下int __cdecl sub_411AB0(char *s) { int i 0; while ( s[i] ! 0 ) { s[i] i; i; } return 0; }用伪代码看其实很直观它就是把字符串里每一个字符的 ASCII 码加上它所在的下标。第 0 个字符加 0第 1 个字符加 1第 2 个字符加 2……直到字符串结尾。下标从 0 开始这一点非常关键后面写脚本时最容易被忽略。现在整个链路就很清楚了用户输入 --Base64编码-- --逐字符加下标-- 与硬编码串比较目标串e3nifIH9b_CndH是经过这两步以后得到的最终形态而不是原始内容。所以解题思路也是对称的先把目标串逐字符减下标还原出 Base64 文本再做 Base64 解码就能拿到程序真正校验的字符串。这一正一反的思路就是这类“编码 变换”题目的通用解法。3. 把加密链路倒过来推导完整解题过程3.1 先逆自增变换再 Base64 解码先把两串摆在一起看你会发现非常直观的对应关系。目标串共有 16 个字符下标从 0 到 15。还原时对每个字符减去它的下标下标0123456789101112131415目标串字符e3nifIH9b_CndH还原后字符e2lfbDB2ZV95b3V9还原出来的e2lfbDB2ZV95b3V9才是真正的 Base64 文本。为什么这一步要先做因为如果直接拿e3nifIH9b_CndH去 Base64 解码里面_和根本不在标准 Base64 字母表里解码器会直接报错。换句话说看到目标串里有字母表之外的字符就该意识到“编码之后还有一层变换”。然后再对e2lfbDB2ZV95b3V9做标准 Base64 解码得到的 12 字节就是程序真正想要的内容{i_l0ve_you}。这串内容带着花括号说明它本身就是一个完整的 flag 内部段不是需要再拼接{}的半成品这一点后面还会重点强调。3.2 用 Python 写还原脚本完整脚本如下我尽量写得直白方便你对照理解import base64 target e3nifIH9b_CndH # 第一步逐字符减下标还原 Base64 文本 b64_text .join(chr(ord(c) - i) for i, c in enumerate(target)) print(还原的 Base64 文本:, b64_text) # 第二步Base64 解码 inner base64.b64decode(b64_text).decode() print(程序内部校验的内容:, inner) # 第三步按平台格式补上 flag 前缀 flag flag inner print(最终 flag:, flag)运行输出还原的 Base64 文本: e2lfbDB2ZV95b3V9 程序内部校验的内容: {i_l0ve_you} 最终 flag: flag{i_l0ve_you}注意为什么是flag inner而不是flag{ inner }因为inner本身就带花括号。很多解题报告到了这一步会顺手写flag{ inner }结果输出变成flag{{i_l0ve_you}}这是我见过的高频错误之一。提示脚本里最关键的一行是enumerate(target)。它自带下标正好对应程序里的i不需要你手动去维护计数器也避免了下标起点错误。能用内置函数表达的循环逻辑尽量不要手写range(len(...))省事且不容易错。如果你不想写脚本CyberChef 也能做同样的操作先按字符减下标再执行From Base64。但脚本更通用毕竟这类题目往后还会遇到掌握 Python 的还原写法几乎是 CTF 逆向的必背技能。3.3 动态调试验证我们的判断脚本跑通了最好还是回到程序本身验证一下。我用 x32dbg 加载 reverse3在strcmp的调用处下断点输入一个长度足够的测试字符串触发断点后观察两个比较参数第二个参数是硬编码串e3nifIH9b_CndH第一个参数是经过两步变换后的结果。然后输入我们在脚本里得到的{i_l0ve_you}程序会返回正确提示说明我们的还原方向完全正确。这一步看似多余但对建立“静态推导 动态确认”的做题习惯非常重要。以后遇到更复杂的题静态分析经常会有多种解释动态调试是消除歧义的最终手段。尤其当伪代码里变量名被编译器搞得难认时直接在内存窗口看变换前后的字节比反复读反汇编快得多。很多老手看一眼寄存器、看一眼内存几秒钟就能确认一个怀疑靠的就是这种两手抓的习惯。4. 新手最容易踩的五个坑4.1 直接对硬编码串做 Base64 解码这是我在讨论区见过最多的错误。看到e3nifIH9b_CndH长得像密文就直接丢给base64.b64decode结果要么报错因为_和不在标准字母表里要么解码出乱码。正确顺序一定是“先减下标再解码”。记住一个规律标准 Base64 的输出只包含 A–Z、a–z、0–9、、/、如果硬编码串里出现这些之外的字符说明后面还叠加了别的变换必须先逆掉最后一层变换。4.2 下标从 0 还是从 1 开始C 语言数组下标从 0 开始所以第 0 个字符加 0等于没变。这就是为什么目标串的第一个字符e还原后的 Base64 串第一个字符还是e。如果你写脚本时习惯性从 1 开始循环第一个字符就会被错误地减 1后面全部错位。判断起点有一个很简单的自检办法还原出来的字符串必须是合法 Base64。如果你从 1 开始算得到的第一个字符就成了d整个串都会乱掉。所以脚本写完后先看中间结果是不是由 Base64 字母表构成这几乎是零成本的检查。4.3 加法和减法的方向搞反程序里s[i] i逆运算当然是s[i] - i这个逻辑本身不难但很多人写着写着就混了尤其当变换函数里还夹着其他代码时。我的建议是先在纸上写出正向变换的伪代码标清楚每一层变换的名字和方向再写逆向脚本。比如这题就三步正向 编码 → 自增逆向 自减 → 解码。每一步对着写别跳步。另一个方向性陷阱是操作顺序。如果你先把e3nifIH9b_CndH里的字符处理成某个自以为的中间态再去做解码顺序错了结果也是错的。必须严格逆着正向链路来最后一步的逆运算最先做最先做的运算最后逆。4.4 程序校验内容与最终 flag 格式不一致这个点在很多解题报告里被一笔带过但新手往往在这里卡住程序实际校验的是{i_l0ve_you}而你在平台上要提交的是flag{i_l0ve_you}。为什么因为题目本身只要求验证花括号内的内容而 BUUCTF 的 flag 提交格式统一是flag{...}。所以解题脚本输出的是flag{i_l0ve_you}直接复制提交即可。反过来这也提醒我们看到答案之后先确认平台要求的格式。有些题目会把flag{...}整体做校验有些只校验花括号内部两者导致的中间结果完全不同不要想当然地套格式。4.5 过于信任 F5 伪代码的变量命名IDA 的 F5 很好用但它生成的变量名如v1、a1、Dest、Source是编译器产物有时和实际语义对不上。我在这题里就见过有人把sub_411940的返回值当成Dest结果绕了半天没绕清楚。正确做法是拿到伪代码后先把调用链在纸上或注释里画出来给每个函数和变量按语义重命名。比如sub_411940→base64_encodesub_411AB0→add_index返回值 →encoded_buf。命名一清楚流程自然就清晰了。这个习惯越早养成越好后面遇到上百 KB 的二进制时变量重命名基本是标配操作。5. 同类题目的举一反三5.1 识别常见编码原语的通用方法从 reverse3 往后你在 CTF 逆向里会反复见到 Base64、Base58、XXTEA、RC4、AES 这类常见原语。识别它们的通用方法可以总结成三条找常量Base64 的 64 字符表是最显眼的线索RC4 和 AES 有固定轮数和特定常数XXTEA 有0x9E3779B9等魔数。字符串表往往能直接定位算法。找结构Base64 的 3 字节进、4 字符出分组RC4 的 S 盒初始化、打乱、加解密三段结构特征都很明显。找模式特定的位移和掩码组合比如 2、 3、|拼接几乎只在 Base64 变体里出现。如果你一时认不出来也别急着逐行分析先用工具尝试常见编码和加密很多情况下能离线完成大半剩下的再用反汇编确认。5.2 变换类题目的通用逆推思路这题的完整链路是一个很好的模板输入 → 编码 → 线性变换 → 比较。做这类题时我的固定流程是从比较点出发找到strcmp或等值判断确定目标串是谁、目标方向是什么。向输入方向回溯列出从输入到比较点之间的每一层变换。反向逐层求逆从最后一层开始写每一步的逆操作。用中间结果自检每一步逆完以后看看数据是否具备该层应有的特征比如还原出合法 Base64、可打印文本、固定长度等。逆向做题和数学证明有点像不要试图一眼看到答案而是把大问题拆成能验证的小步骤。只要每一步的中间结果都符合预期最后答案几乎不会错。5.3 一些值得长期坚持的调试习惯最后说几个我个人长期受益的习惯先静态后动态小题目静态分析足够但遇到混淆或反调试尽早切到动态调试别硬读汇编。多保存现场调试时记录输入、断点位置、寄存器与内存关键值出问题才能回放不靠记忆力。写脚本要含中间输出把每一步还原结果都print出来和预期对照能快速定位到底哪一层走偏了。答题前先确认格式flag{}大小写、内部是否带花括号这些小细节决定了最终能不能提交成功。其实回过头看reverse3 一点都不难难的是把“从比较点回溯到输入”这个思路固定下来。我后来在逆向专区里遇到不少带花指令、反调试甚至自修改代码的题目用的还是这套流程先找比较点再列变换链逐层求逆最后用中间结果自检。如果你能把这篇里的每一步都亲手验证一遍这题的价值就远不止一个 flag 了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →