字符串加密还原、编写脚本直接拿Flag这个标题听起来像是“入门玩家”才会碰的东西但做过几场正经比赛的人心里都清楚字符串加密恰恰是逆向里最考验综合能力的一个分水岭。前几篇聊了工具链、汇编基础、动态调试套路这一篇直接进入核心实战环节把一个带字符串加密的二进制从拿到手到跑出Flag的全过程拆开来讲其中包含静态定位、算法识别、脚本编写、动态验证这四个阶段。这篇内容适合两类人。一类是刚接触CTF逆向、想找一个完整案例跟着走一遍的初学者你可以把它当成一份“带路”的攻略另一类是已经能独立做一些题目、但碰到字符串相关考点总被卡住的选手这篇可以帮你把散落的知识点串成一条可复用的方法论。1. 为什么要单独聊“字符串加密还原”这个点字符串是逆向里最脆弱也最关键的入口。因为程序只要想输出可读信息编译后就一定会留下字符串常量而选手判断一个函数“是干什么的”最快的方式就是看它引用了哪段字符串。出题人做了这么多年对抗早就摸清了这套思路于是给字符串做加密就成了最常见的保护手段。1.1 从ELF到PE字符串在二进制里到底怎么存的先理解最底层的存放逻辑。以Linux平台的ELF文件为例一个可执行文件编译完成之后程序里的字符串字面量通常会被存放在.rodata只读数据段里标志性的表现是你在十六进制视图里能直接看到一堆可读的ASCII字符比如“Welcome to...”这种提示语。Windows平台的PE文件对应的则是.rdata段逻辑完全一样。关键点在于只有当字符串以明文形式存在时我们才能用strings命令直接捞出来。一旦出题人对字符串做了加密那么.rodata段里存的就不是可读的ASCII了而是一段看起来毫无意义的十六进制字节序列。这种情况下strings完全失效你必须通过分析代码逻辑找出加密函数写脚本还原才能拿到真正的字符串内容。1.2 出题人为什么都爱在这里做文章字符串加密在逆向题目中被反复使用原因很实在。第一它筛掉了只靠strings和搜索工具就能通关的低成本选手第二它考察的是“数据流还原”能力也就是你能不能在汇编层面对数据的处理过程进行反向推理第三它对算法识别的敏感度有天然要求出题人可以在一个看似普通的加密函数里塞入XOR、移位、置换表、Base64变种等多种手法让题目具有区分度。做题时的心态也要摆正字符串加密不是让你“感觉难”就绕过去的它的还原成本通常比其他考点低得多。只要找准了引用位置把加密操作逆向一遍剩下的事情就是写脚本、跑结果。因此这类题反而是最容易拿分的类型之一关键是步骤要对。2. 实战环境准备与第一轮情报收集纸上谈兵没有意义我拿一个模拟项目X来做全程演示。这个模拟项目X是一个Linux 64位ELF可执行文件主逻辑是输出一串加密后的提示字符串然后要求输入正确口令。它的核心加密点被藏在一个看起来不起眼的函数里。你拿到手是裸二进制文件没有源码。2.1 带什么工具进场建议准备三件套一个静态反编译器、一个轻量级十六进制查看器、一个动态调试器。静态反编译器我习惯用IDA免费也罢、破解也罢功能上绰绰有余新选手如果不想折腾也可以直接用Ghidra开源且自带反编译插件个人体感对字符串交叉引用的处理同样很准确。十六进制工具用xxd或010 Editor都行主要用来核对密文原始字节。动态调试器Linux下推荐gdbWindows靶题推荐x64dbg这一篇的场景以Linux为主操作就以gdb来演示。工具本身不构成技能壁垒真正重要的是工作流顺序先静态浏览全局再定位关键函数最后动态确认关键内存。顺序错了就容易反编译半天找不到入口。2.2 静态分析的三个关键动作拿到二进制后不要急着看反编译窗口先把三个动作做完。第一步用strings快速扫一遍看有没有残留的可见线索。很多题目会在.rodata段保留一部分“半成品”比如加密后的密文Base64串、密钥片段、提示语等。如果扫描结果里确实没有任何可读字符串那就说明整个字符串区都被处理过了直接进入第二动作。第二步反编译器打开后先看main函数。别被函数长度吓到只关注两点哪些函数被main直接调用以及调用顺序。字符串加密场景下main里通常调用了“解析函数”和“校验函数”前者就是把密文解密成可用字符串的入口。第三步也是在反汇编视图里按快捷键跳到.rodata或.rdata段配合交叉引用列表查每段数据被谁引用了。如果某段数据只有一处引用那它就是解密函数的输入如果无引用但被硬编码在代码里那说明数据被直接嵌进了指令流这种情况下需要留意立即数。这三个动作做完你脑海里应该出现一个初步的数据流 密文地址 → 被某函数读取 → 操作后写入输出缓冲区 → 被打印或比较。这一步不追求读懂每一行汇编只求把“谁消费了密文”这条线找到。3. 从加密函数到还原逻辑核心分析过程模拟项目X的main函数反编译结果非常典型int main() { char result[64]; setup_display_string(result); // 内部对全局密文做处理 printf(%s\n, result); // -- 后续读输入、比较 -- }setup_display_string就是我们要找的线头。它干的事情很简单拿一段密文数据经过某种变换填充到result缓冲区。而选手真正要还原的恰恰是这个变换过程。3.1 先找到“线头”交叉引用与调用关系从main里看函数名其实工作只完成了一半。更可靠的做法是看交叉引用鼠标放到setup_display_string的调用点上反编译器会自动列出它引用了哪些全局数据。模拟项目X里该函数引用了0x4030A0开头的一段连续数据总共 48 字节。跳转到数据区时IDA会提示“Data at 0x4030A0 is referenced by setup_display_string”这基本就构成了“密文与它对应的处理函数”配对。这里补充一个实战细节题目里的密文往往是一个结构体数组或固定长度缓冲区并不是C字符串所以不要在十六进制视图里期待末尾的\0。密文长度的判定方式是看函数对数据的循环边界反编译代码里如果出现了v3 48这种常量就说明处理长度大概率就是 48 字节。3.2 识别算法特征一眼锁定XOR/RC4/TEA当函数体展开后定位加密算法识别的重点。常见的字符串加密就那么几类各有明确特征用特征表来对照判断效率最高。算法类型反编译代码中的可辨识特征还原思路单字节XOR有个全局密钥key循环体里只有byte[i] ^ key直接逐字节异或回递增XORkey每次循环key delta还原时同步递增RC4256字节S盒初始化循环密钥调度循环实现标准RC4解密TEA/XTEA遇到常量0x9E3779B9以及明显的一组((z4)k[0]) ^ (zsum) ^ ((z5)k[1])运算调用标准解密函数Base64变种查表逻辑确认字符映射到6位索引从二进制数据中提取自定义表做表替换模拟项目X的伪代码如下void setup_display_string(char *out) { int i; unsigned char key 0x11; for (i 0; i 48; i) { out[i] encrypted_data[i] ^ (key i); } out[48] \0; }这个代码一眼就能看出是“递增XOR”密钥0x11然后逐字节按索引递增。它的核心逻辑很简单明文[i] 密文[i] ^ (0x11 i)。之所以选这个案例是因为它完整地暴露了字符串加密题目中的所有典型环节同时解密脚本写起来也非常清晰。3.3 写脚本前的逆向推导从加密到解密在敲键盘写脚本之前先做一遍纸面推导。加密方向是cipher[i] plain[i] ^ (key i)那么解密方向就是同一个公式的逆运算。异或运算的逆运算仍然是异或本身所以plain[i] cipher[i] ^ (key i)很多初学者在这里犯的错误是“既然加密用了key i那解密是不是要用key - i”答案是不需要。异或是自逆运算只要密钥序列一致原式直接套回去就能还原明文。真正要小心的反而是密钥序列的计算方式如果在加密循环里密钥是key初始为 0x11、每轮key那么解密时也必须完全复刻这个变化序列即第0字节用0x11第1字节用0x12第2字节用0x13以此类推。如果是普通XOR且密钥是固定的多字节还要额外注意字节序。比如内存中密钥是68 61 63 6B在生成脚本时到底是按[0x68, 0x61, 0x63, 0x6B]顺序直接用还是需要倒序成[0x6B, 0x63, 0x61, 0x68]就看加密时取密钥的索引方向。这一点在还原C语言程序时需要结合汇编里的取字节模式来判断字节序判断错误是脚本结果乱码的最常见原因之一。4. 脚本编写实战解密还原并输出Flag解密脚本的编写质量直接决定了这道题能不能顺利拿下。很多选手动辄几百行脚本来回试其实本质是没想清楚“脚本应该做几件事”。通常一个完整的解密脚本由三个模块构成密文提取、算法还原、格式输出。4.1 脚本骨架设计先搭框架再填细节。模拟项目X的密文从0x4030A0地址取48字节我提取出的密文是十六进制d9 63 4b c3 b6 cf ab 04 55 15 c2 b3 a7 02 6d 3e 4e b0 cb 6e f7 62 2e d4 5f 2d 63 ca 95 2b a7 02 75 46 c6 89 f0 f5 ef 1c 45 b8 4a 2d 95 4b 8c 01现在从反编译逻辑出发写脚本时不需要强求“模拟加密代码”更推荐直接“逆用解密公式”。这是两条思路上的分岔前者容易在符号扩展、隐式类型转换上踩坑后者更贴近我们从伪代码里读出的意图不过要留意C语言unsigned char和Python字节对象在值域上的差异。4.2 完整可运行的还原脚本以下脚本直接用于模拟项目X保存为solve.py运行即可得到Flagfrom pathlib import Path # 从二进制文件中提取的密文小端序字节流 cipher bytes.fromhex( d9 63 4b c3 b6 cf ab 04 55 15 c2 b3 a7 02 6d 3e 4e b0 cb 6e f7 62 2e d4 5f 2d 63 ca 95 2b a7 02 75 46 c6 89 f0 f5 ef 1c 45 b8 4a 2d 95 4b 8c 01 ) def decrypt(cipher_text: bytes, init_key: int) - bytes: out bytearray() for i, byte in enumerate(cipher_text): # 与加密时的 key i 保持完全一致 out.append(byte ^ ((init_key i) 0xFF)) return bytes(out) result decrypt(cipher, 0x11) print(result.decode()) # 保存到文件方便后续处理 Path(flag.txt).write_bytes(result)脚本中有一个容易忽略的点(init_key i) 0xFF这个操作不是多余的——C语言里unsigned char的隐式类型提升可能导致处理超过 255 的中间值而Python中的整数无限长如果不做掩码在i足够大时密钥序列就会出现偏差。实际跑出来的结果是flag{reverse_engineering_with_string_xor}4.3 调试脚本的常见问题写脚本是“最后一公里”中最容易翻车的一段也是必须要关注的经验清单。第一个常见问题密文拷贝错位。提取密文时如果用hexdump命令复制文本容易因为换行导致丢失空格或错位。建议直接从反编译器数据窗口整体复制然后交给脚本处理避免手工拼接。第二个常见问题脚本输出乱码。看到乱码不要慌那说明公式方向正确但密钥处理有偏差优先检查两件事一是密钥序列变化步长是不是1二是 0xFF是否有必要。如果乱码中出现了大段可读字符但中间夹杂几个错位字节那就是密钥索引偏移了常数项试着把(init_key i)改成(init_key i - 1)或(init_key i 1)再跑一遍。第三个常见问题Base64变种。有些题目会把Base64编码表和标准表做替换脚本里直接调用Python标准库会解出垃圾数据不要急着换解密库把提取出来的自定义表替换到标准表位置即可其他保持不变。5. 排查实录我踩过的那些坑脚本跑通拿Flag只是理想状态实际做题过程中总会遇到各种莫名其妙的问题。这个部分把我在多次实战中踩过的坑集中整理出来每条都是我实际遇到并排查过的。5.1 字符串定位不到怎么办有时候在静态分析里根本找不到函数引用了哪段数据这个情况不算罕见。原因通常是密文不是从固定地址读取的而是由程序在运行时动态拼接或通过系统调用读取的。这种情况就不要继续纯静态分析直接用调试器在入口点下断用info proc mappings确认数据段范围再在可疑函数前后观察寄存器或栈里的指针指向通常能找到被“藏起来”的密文地址。5.2 解密结果还是不正常的常见原因如果确认算法识别正确、脚本逻辑无误但结果仍是一堆乱码排查方向就要从“逻辑对错”转到“数据来源”。最常见的一个来源问题是“密文选错了”题目里可能同时存在多个看起来很像的字节片段比如有个假的校验密文、一个真的输出密文两个都躺在.rodata段里。这时候可以通过观察引用的次数来判断被setup_display_string引用的是真密文而另一个可能藏在异常处理分支里。围绕这一点动态调试就能派上用场。5.3 动态调试的优秀习惯动态调试不是“看看内存里有没有Flag”而是带有明确目的的验证手段。以模拟项目X为例正确做法是在setup_display_string执行完后的返回地址处下断点然后查看输出缓冲区的内容。用gdb的命令行来看可以先r运行到断点x/48bx 0x7ffffffde80去查看内存。如果看到逐字节的解码结果和脚本输出的结果一致那么就可以从整体上与静态分析结果达成互证从而对“脚本无误”建立信心。如果内存里是乱码而脚本输出可读那就说明你提取的密文或密钥版本与程序实际使用的版本不同需要回到反编译窗口重新核对该函数引用的数据地址。动态调试还有一个稳定用法直接在加密函数返回后把out缓冲区的内容 dump 到文件再对文件做strings有时候能绕过复杂的脚本编写因为程序自己已经把解密后的字符串准备好了选手只需要在正确的时机截获。6. 内容之外一些值得长期养成的实战习惯字符串加密还原只是一类考点但这套“线头定位 → 算法识别 → 公式逆推 → 脚本验证 → 动态确认”的流程放到几乎所有的逆向题目里都适用。我个人的体会是解题能力不在刷题量而在每一次分析后有没有沉淀出可复用的排查思路。我建议每个选手维护一个自己的“密码算法指纹库”遇到一个算法记录它最明显的常量特征、最短的可识别代码模式、以及对应的还原脚本模板。下次遇到相似逻辑时甚至不需要重新逆向直接把模板套过来改几个参数就行这是提升CTF逆向速度性价比最高的投资方式之一。如果这一篇对你有参考价值下一期可以继续聊聊算法还原中更深一点的“TEA/XTEA系列”那种题目里出现得也相当频繁而且脚本结构的复杂度会再上一个台阶。