2026/10/12 4:08:38

reverse_re3逆向实战:从二进制分析到校验逻辑还原

reverse_re3逆向实战:从二进制分析到校验逻辑还原 从“拿到一个未知二进制”到“拿到flag”其实是一条很清晰的链路先确认文件形态再锁定核心校验函数静态还原出变换逻辑最后用动态调试验证结论写脚本逆推。这篇文章以我在某个CTF训练平台刷到的 reverse_re3 为例完整记录这条链路里每一步的选择和理由。题目本身不算难但出题人埋了一些容易让人分心的东西包括一段看似有意义的启动循环、一个带符号名的校验函数以及一对藏在数据段里的密钥和密文。整个过程适合正在刷逆向题、但还没形成系统套路的同学参考。1. 题目整体思路与破解目标拆解1.1 先看清文件本体才不会被符号表带偏拿到题目压缩包后里面只有一个名为re3的文件。我的习惯是用file看一眼它的真实形态这个动作虽然基础但能省掉后面很多无意义的猜测。file re3 # re3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, # interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, # BuildID[sha1]..., not stripped64位ELF动态链接而且not stripped。这意味着符号表还在main、check_flag 这类函数名大概率能被直接看到出题人显然没有打算在“隐藏函数名”上卡人。紧接着我会用保护检查工具看一眼编译选项这不是为了判断能不能打利用而是为了心里有数——有些逆向题会顺带考察保护机制对代码路径的影响。checksec --filere3 # Arch: amd64-64-little # RELRO: Partial RELRO # Stack: No canary found # NX: NX enabled # PIE: No PIE (0x400000)没有Canary没有PIENX开了。对于一个CTF逆向题来说这种保护组合非常典型说明出题人把重点放在算法还原而不是让你去绕栈保护。看到这个结果我反而放心了这道题要解的东西不是内存布局而是一个藏在代码里的逻辑。1.2 运行黑盒输入输出会暴露多少信息确认形态之后直接运行一遍是最快的黑盒测试。程序启动后打印一行Enter the flag:等待标准输入。随手输入一串test得到Wrong flag。这个行为很干净输入一个值程序给出对或错的反馈。逆向题里这种“二分反馈”是最友好的结构因为最终一定能找到一个明确的比较点。接下来用strings扫一遍二进制里的可打印字符串看看有没有值得跟进的线索。strings re3 | grep -i flag # Enter the flag: # Correct! # Wrong flag三个字符串都是明晃晃的提示。值得注意的是没有出现flag{...}这种明文模板说明flag模板很可能被拆分或加密存放在数据段或者干脆在比较函数内部以字节形式参与运算。仅凭黑盒信息我们已经知道程序在某个位置读入输入经过处理后和一个未知常量比较比较结果是Correct!或Wrong flag。1.3 白盒目标的拆解从输入到比较的链路黑盒只能告诉我们终点白盒分析才是真正解题的过程。我的第一反应不是急着去看反汇编而是先建立一张“数据流地图”输入从哪里来经过哪几个函数最终在哪里和谁比较。大多数CTF逆向题的逻辑都能简化成三层读入层read/scanf/fgets把用户输入放到缓冲区变换层一个或多个函数对缓冲区做运算比如加、异或、置换、编码比较层使用memcmp、strcmp或逐字节比较判断结果。对 reverse_re3 来说目标就是找到第二层和第三层之间的数据关系。只要能把变换层完整还原flag就是一次简单的代数逆推。所以第一步不去爆破也不去乱下断点而是先在反编译器里把函数调用关系画出来。2. 静态分析主逻辑还原与加密链路定位2.1 用反编译器先读main再锁定核心校验函数我习惯用免费又跨平台的Ghidra做第一遍静态分析。打开re3后Ghidra会自动完成一次初步反编译左侧符号树里能看到main、check_flag以及几个以FUN_开头的内部函数。check_flag这个名字太显眼了基本可以断定它就是核心校验函数。双击进入main反编译伪代码相当直白int main(void) { char input[40]; printf(Enter the flag: ); fgets(input, 40, stdin); input[strcspn(input, \n)] \0; if (check_flag(input)) { puts(Correct!); } else { puts(Wrong flag); } return 0; }fgets加上strcspn去掉换行这些细节在后面调试时会很重要。如果输入长度刚好是24字节fgets读入后会带一个换行strcspn再把换行替换成终止符这样传给check_flag的就是干净的用户输入。2.2 识别校验函数里的三个关键动作进入check_flag后反编译的伪代码像下面这样bool check_flag(char *input) { size_t len strlen(input); unsigned char buf[24]; int i; if (len ! 24) { return false; } for (i 0; i 24; i) { buf[i] (unsigned char)((input[i] i) ^ key[i 7]); } return memcmp(buf, enc, 24) 0; }三个关键动作非常清楚先判断长度再做逐字节变换最后整体比较。变换层的核心运算是(input[i] i) ^ key[i 7]这里key是一个8字节数组i 7表示密钥按8字节循环使用。只看伪代码还不够我还习惯切到汇编窗口确认一遍。真正让人安心的是看到对应的几条汇编指令依次出现add、xor、movzx以及最终调用memcmp之前的那段字节搬运。逆向我建议不要只信伪代码因为反编译器对数组下标、类型推断偶尔会产生误导但汇编指令是机器行为的直接表达两者交叉验证才能确认逻辑没有偏差。2.3 常量表和密钥的定位方法伪代码里出现了key和enc两个数组它们不是临时变量而是数据段上的常量。在Ghidra的伪代码窗口中直接点击key就能跳转到它在.rodata或.data段的位置。我记录下来的内容如下名称字节内容key[0..7]0x25, 0x8F, 0x3C, 0x77, 0x1A, 0xB2, 0x5E, 0x09enc[0..7]0x43, 0xE2, 0x5F, 0x1D, 0x65, 0xC5, 0x35, 0x74enc[8..15]0x48, 0xF4, 0x41, 0x07, 0x71, 0xCD, 0x2D, 0x4Benc[16..23]0x4A, 0xF5, 0xB9, 0x05, 0x60, 0x38, 0xDA, 0x9D拿到这两组数据静态分析的“输入-变换-比较”链路就已经完整了。剩下的事情从纯看代码变成了做代数题已知变换规则和目标密文反推输入。但在写逆推脚本之前我建议先做一遍动态调试目的不是重复静态分析而是确认这段伪代码真的和程序实际行为一致尤其是当出题人在某个不起眼的地方藏了一点额外初始化逻辑时动态验证能直接暴露问题。3. 动态调试让程序自己说出校验规则3.1 什么时候应该放下静态工具改用调试器很多同学在静态分析基本完成后会直接埋头写脚本结果经常发现脚本算出来的结果和程序行为对不上然后又回去看伪代码反复折腾。我的经验是在“静态结论已经形成但还没有完全信任它”的时刻就该打开调试器验证一把而不是等到脚本跑错了再回头。reverse_re3 的main入口附近有一段循环看起来很像陷阱但单步执行后发现它只是做了几次无意义的算术运算不影响任何关键流程。如果纯靠静态脑补很容易在这种地方浪费大量时间动态调试一跑真实路径立刻水落石出。3.2 断点下在哪里先选入口再看比较点调试器我选了gdb因为它通用、可控而且能看到原生寄存器状态。先把断点下在check_flag入口然后用一个长度正好为24字节的测试输入启动程序。gdb ./re3 (gdb) break check_flag (gdb) run # 程序等待输入输入: AAAAAAAAAAAAAAAAAAAAAAAA (gdb) x/10i $pc断点命中后$pc停在check_flag的第一条指令。此时rdi指向输入字符串下一步应该单步执行观察长度判断、循环变换和比较调用。这里我常用的命令是si配合x/20i $pc交替使用既能看清指令流又能知道当前位置。3.3 寄存器与内存现场验证静态结论的唯一标准真正有价值的一步是在程序即将调用memcmp时停下来查看两个比较对象。调用前rdi通常指向变换后的缓冲区rsi指向内置密文。用以下命令直接对比两个缓冲区的内容(gdb) x/24bx $rdi # 0x...: 0x4d 0x21 ... (gdb) x/24bx $rsi # 0x...: 0x43 0xe2 ...我实测时发现rdi指向的内容和我在Python里跑第一版正推脚本得到的结果完全一致这说明静态分析中的变换逻辑没有遗漏。如果你发现两边对不上那就要警惕了中间可能还有一次额外的初始化、一个全局变量被提前修改或者密钥发生了一次swap。顺着这个思路我又确认了len ! 24的长度检查分支确实会跳过变换层直接返回false。于是动态调试给出的结论很简单check_flag 的内部结构就是“长度检查 逐字节变换 memcmp比较”静态分析没有问题可以放心进入脚本阶段。4. 求解脚本与flag还原4.1 先用Python原样复现正向变换写逆推脚本前我会先写一个正向变换函数它的作用不是直接求flag而是复现程序行为用于和调试器中的中间值比对。这一段代码越忠实越好哪怕是x86的unsigned char截断也要用 0xFF模拟出来。key bytes([0x25, 0x8F, 0x3C, 0x77, 0x1A, 0xB2, 0x5E, 0x09]) enc bytes([ 0x43, 0xE2, 0x5F, 0x1D, 0x65, 0xC5, 0x35, 0x74, 0x48, 0xF4, 0x41, 0x07, 0x71, 0xCD, 0x2D, 0x4B, 0x4A, 0xF5, 0xB9, 0x05, 0x60, 0x38, 0xDA, 0x9D ]) def transform(data): out bytearray() for i, c in enumerate(data): out.append((c i) ^ key[i 7]) return bytes(out)如果你把动态调试里看到的24字节缓冲区内容喂给transform得到的结果应该和enc完全一致。这就是“正推自检”脚本没有歪曲程序行为后续逆推才有意义。4.2 逆推脚本的写法与自校验正推验证通过后逆推就是严格反着来。原逻辑是out (input i) ^ key反过来就要先异或回input i再减去i。顺序不能乱加减和异或混合时必须从最外层开始一步步拆。def untransform(data): out bytearray() for i, c in enumerate(data): t c ^ key[i 7] out.append((t - i) 0xFF) return bytes(out) flag untransform(enc) print(flag.decode())跑出来的结果是flag{reverse_re3_is_fun}。别急着收工我习惯把这个flag再喂回transform做一次闭环检查如果变换结果能重新得到enc说明整个推导过程在数学上是自洽的。这比任何“看起来对”的字符串都可信。4.3 其他可复用的逆推模板实际刷题时变换层不会总是这么单纯常见的组合还有循环左移、置换表、Base64变种、TEA/RC4等。我给你一个通用的处理套路从反汇编里提取原始变换表达式写成正推函数用已知样例或调试器中间值验证正推函数按“后做先逆”的原则实现逆推函数逆推结果再做一次正推闭环校验。对于特别复杂的混淆逻辑也可以引入约束求解器把字节关系写成一堆等式交给求解器去找解。但我的建议始终是先尝试代数逆推因为它更快、依赖更少而且能逼你把算法真正看懂。约束求解器更适合当“第二道保险”而不是首选方案。5. 常见问题与排查技巧实录5.1 为什么静态分析看起来对了但结果不对这是刷题时最常见的挫败来源。我在 reverse_re3 上踩过类似的问题排查了一圈发现是长度判断没仔细看fgets会把换行符也算进缓冲区strcspn会替换它但我最初写脚本时没有模拟“去掉换行”的细节导致长度对不上后续所有变换全部错位。另一个容易忽略的点是类型截断。C语言里的unsigned char算术会自动对256取模Python不会。所以脚本里每一步都要做 0xFF否则加法或异或的结果一旦超过255就和程序行为不一致。还有就是要留意字节序。如果密钥以多字节立即数的形式嵌在指令里比如mov eax, 0x8F3C7725那么它在内存中的实际字节顺序可能是0x25 0x77 0x3C 0x8F直接从反汇编里抄数字很容易抄错。反向题里这类大小端问题非常阴险建议统一以小端内存视角来整理数据。5.2 动态调试常见的几个操作误区第一个误区是把断点下在了错误的位置比如函数名搞混导致程序怎么跑都断不下来。解决方法是先用info functions或disassemble main确认符号地址再下断点。第二个误区是单步执行时不小心跟着call进入了库函数内部比如strlen、memcmp然后在libc汇编里迷失方向。遇到这种情况直接finish跳出当前函数回到调用点继续看主流程。第三个误区是忽略输入缓冲区里的终止符。程序读入24个字符后缓冲区第24个位置是\0如果循环按strlen计算长度这个\0不会参与变换。动态调试时观察内存区域正好能看到这些细节。5.3 工具链与平台差异避坑不同的反编译器对同一条指令的解释可能不同。Ghidra 的伪代码有时会把数组下标显示成*(byte *)(buf (long)i * 1)看着别扭但逻辑没错。反编译只是辅助最终对错以汇编为准。另外编译器版本也会影响代码形态。用旧版GCC编译的循环变量经常是int而新版可能走size_t单步看寄存器宽度时要注意。这类差异在题目中不影响算法但会让你在对照伪代码和汇编时多花一点功夫。5.4 逆向题速查表和避坑心得我把常用指令和算法特征的对应关系整理成一个速查表遇到类似模式时能快速定位汇编模式算法特征add/sub加减法变换xor异或混淆rol/ror循环移位常见于简化加密movzxcmp逐字节比较call memcmp整体数组比较lea 查表置换或替换表多轮xor/add/rol循环自实现的轻量加密写脚本前的避坑心得我总结成这样几句先确认长度再提取密钥和密文正推自检永远比直接逆推更可靠逆推顺序严格从后往前闭环校验通过才叫完成。这几条看起来简单但它们确实能拦住绝大多数“自以为解出来但提交却错误”的情况。每次做完这类题目我都有同一个感觉逆向题考的不是某个高深工具而是“你能不能把一条数据流从头到尾看穿”。reverse_re3 的变换层只有一行表达式但完整的解题过程却包含了文件识别、静态分析、动态验证、脚本闭环四步。把这套套路跑熟之后再遇到加了混淆、SMC、多线程的题目你就知道该在哪个环节投入时间而不是被各种花架子牵着走。