资讯动态

CTF逆向工程实战:从UPX脱壳到TEA算法逆向与Z3求解

发布时间:2026/8/8 15:13:49 来源:尧图企业网站定制
1. 项目概述与核心思路拆解最近在复盘MoeCTF2025的逆向工程题目第一周的题目设计得相当有梯度从基础的UPX脱壳到经典的TEA算法逆向再到各种花指令和混淆非常适合新手入门和巩固基础。这篇文章我就以第一周的几道典型题目为例带你走一遍完整的逆向分析流程从拿到一个加壳的二进制文件开始到最终解密出Flag。我会重点分享在实战中遇到的那些“坑”以及我是如何一步步踩过去并找到解决方案的。无论你是刚接触CTF逆向的新手还是想系统梳理一下基础技能的老手相信这篇实战解析都能给你带来一些启发。逆向工程的核心在我看来就是“理解程序意图”和“还原设计逻辑”的过程。题目作者会设置各种障碍比如加壳、混淆、反调试、复杂的加密算法等我们的目标就是穿透这些迷雾找到程序验证Flag的核心逻辑。第一周的题目很好地体现了这一点它没有上来就扔给你一个巨复杂的VM虚拟机或者OLLVM一种代码混淆器而是从最基础的“壳”开始让你先学会如何“打开”一个程序。2. 逆向工程基础认识“壳”与UPX脱壳实战2.1 壳是什么为什么需要脱壳在开始实战之前我们得先搞清楚一个概念什么是“壳”你可以把它想象成程序的“外包装”。软件开发者在发布程序时出于保护代码逻辑、防止被轻易逆向分析、或者压缩程序体积等目的会给原始的可执行文件我们称之为“原程序”或“本体”套上一层额外的代码。这层代码会在程序运行时最先执行它的任务可能是解密被压缩或加密的原程序代码、进行反调试检测然后再将控制权交还给原程序。对于逆向分析者来说这个“壳”就像一堵墙挡住了我们直接查看程序内部逻辑的视线。我们用IDA Pro、Ghidra这类静态分析工具打开一个加壳的程序通常只能看到壳的代码而看不到真正的main函数、算法逻辑等关键信息。因此“脱壳”就成了逆向分析的第一步即去除这层外包装恢复出原始的可执行文件。2.2 UPX壳的特征与手动脱壳在第一周的题目中我们遇到的是UPX壳。UPX是一款开源、免费、跨平台的可执行文件压缩工具它非常流行以至于很多CTF逆向入门题都喜欢用它。UPX壳的特点是它通常只进行压缩不涉及复杂的加密或反调试因此脱壳相对简单。如何识别UPX壳最直接的方法是使用查壳工具比如Exeinfo PE或Detect It Easy。用这些工具打开目标程序如果显示“UPX”字样那基本就确认了。此外用十六进制编辑器查看文件开头UPX压缩的文件通常有明确的UPX!等字符串标识。标准脱壳方法upx -d对于标准的UPX壳脱壳非常简单使用UPX官方工具的命令行即可upx -d packed_program.exe -o unpacked_program.exe这里的-d参数代表解压缩decompresspacked_program.exe是你的加壳文件-o后面指定输出文件名。实战踩坑UPX特征修改然而CTF题目不会这么简单。在第一周的upx_revenge这道题中直接使用upx -d会失败。这是因为出题人修改了UPX的头部特征信息。UPX工具在脱壳时会检查文件头部的特定魔数Magic Number和字段如果这些信息被篡改upx -d就无法识别这是一个有效的UPX压缩文件从而拒绝脱壳。注意这是一个非常经典的“反脱壳”技巧。UPX头本质上是给脱壳工具看的“说明书”并不影响程序运行。修改它就像把产品说明书撕掉几页让自动工具看不懂但产品本身还能用。手动修复UPX特征进行脱壳遇到这种情况我们需要手动修复UPX头。步骤如下定位UPX头用十六进制编辑器如010 Editor, HxD打开程序。搜索字符串UPX!通常能在文件靠前的位置找到。分析修改对比一个正常UPX文件的头部和题目文件的头部。出题人可能修改了版本号、压缩方式标识等字段。常见的修改是把UPX!改成UPX?或者其他字符。修复关键字段我们需要将其改回标准的UPX标识。通常只需要将文件开头附近的UPX?或其他变体改回UPX!即可。有时也需要关注紧随其后的版本号字段如0确保其值在正常范围内通常是0。保存并再次脱壳保存修改后的文件再次尝试upx -d命令。如果修复正确此时应该能成功脱壳。修复成功后我们就可以用IDA Pro等工具打开脱壳后的程序开始真正的静态分析了。这个过程锻炼的是你对文件格式的初步理解和动手修改二进制数据的能力。3. 静态分析入门IDA Pro基础操作与字符串检索成功脱壳后我们得到了程序的“本体”。接下来就要请出逆向工程师的瑞士军刀——IDA Pro或免费的Ghidra、Cutter等进行静态分析。静态分析指在不运行程序的情况下通过反汇编、反编译来理解代码逻辑。3.1 定位程序入口与主函数用IDA Pro打开脱壳后的程序它会自动进行分析。分析完成后我们首先需要找到程序的入口点Entry Point和主要的用户代码逻辑如main函数。入口点IDA通常会自动跳转到程序的入口函数如start或mainCRTStartup。这些是编译器生成的启动代码负责初始化环境最后调用我们写的main函数。寻找main函数在复杂的启动代码中直接找main可能比较困难。一个更高效的方法是利用字符串引用。3.2 利用字符串快速定位关键代码程序在运行中总会输出一些提示信息比如“Please input flag:”、“Correct!”、“Wrong!”。这些字符串是定位关键代码的绝佳路标。在IDA中按下快捷键Shift F12可以打开“字符串窗口”Strings Window。这里列出了程序中所有的字符串常量。搜索关键字符串在字符串窗口中寻找与程序功能相关的字符串。例如在ez3或A simple program这类题目中我们可能会看到“Correct!”、“Wrong!”、“moectf{”等。交叉引用Xref双击找到的字符串IDA会跳转到该字符串在数据段通常是.rdata或.data节的位置。然后将光标放在该字符串的变量名上按下X键会弹出一个“交叉引用列表”Cross-References。这里列出了所有在代码中引用了该字符串的位置。跳转到引用处通常输出“Correct!”或进行字符串比较的代码就在主逻辑附近。双击交叉引用列表中的地址IDA会带你跳转到引用该字符串的代码处。这里很可能就是main函数或者核心的校验函数。通过这种方法我们可以绕过复杂的启动代码直接“空降”到程序最核心的逻辑部分极大地提升了分析效率。4. 算法逆向核心TEA加密算法分析与解密脚本编写在成功定位到核心校验函数后我们就进入了逆向工程最核心也最有挑战性的部分理解并逆向程序的算法逻辑。第一周的A cup of teA和Two cups of tea都是经典的TEA算法逆向题。4.1 TEA算法原理速览TEATiny Encryption Algorithm是一种简洁高效的分组加密算法密钥128位4个32位整数明文分组64位2个32位整数。它的特点是代码极其简短但安全性在相当长一段时间内被认为足够强因此经常出现在CTF中。其加密的核心伪代码如下C语言风格void tea_encrypt(uint32_t* v, uint32_t* k) { uint32_t v0 v[0], v1 v[1]; uint32_t sum 0; uint32_t delta 0x9E3779B9; // 一个黄金分割率相关的常数 for (int i 0; i 32; i) { // 典型轮数为32 sum delta; v0 ((v1 4) k[0]) ^ (v1 sum) ^ ((v1 5) k[1]); v1 ((v0 4) k[2]) ^ (v0 sum) ^ ((v0 5) k[3]); } v[0] v0; v[1] v1; }解密则是加密的逆过程需要从最终的sum值delta * 32开始反向迭代void tea_decrypt(uint32_t* v, uint32_t* k) { uint32_t v0 v[0], v1 v[1]; uint32_t sum 0x9E3779B9 * 32; // 加密结束时的sum值 uint32_t delta 0x9E3779B9; for (int i 0; i 32; i) { v1 - ((v0 4) k[2]) ^ (v0 sum) ^ ((v0 5) k[3]); v0 - ((v1 4) k[0]) ^ (v1 sum) ^ ((v1 5) k[1]); sum - delta; } v[0] v0; v[1] v1; }关键点解密时先操作v1再操作v0顺序与加密相反。sum从最大值开始递减。4.2 题目实战识别与逆向自定义TEA在A cup of teA这道题中IDA反编译出的加密函数结构与标准TEA高度相似但存在一些“魔改”Delta常数不同题目中的delta是1131796而非标准的0x9E3779B9。运算细节可能微调需要仔细对照反编译代码确认加法、异或、移位的顺序和组合是否与标准TEA完全一致。分析步骤定位加密函数通过字符串引用或函数调用关系找到名为sub_xxxxxxx的加密函数。提取关键参数密钥Key在调用加密函数附近通常能看到一个数组的初始化里面是4个32位整数这就是TEA的128位密钥。密文Ciphertext同样在加密函数调用后会有一个数组与我们的输入加密后的结果进行比较。这个用于比较的数组就是目标密文。Delta值与轮数在加密函数的循环里找到累加的常数delta和循环次数通常是32。编写解密脚本根据识别出的加密逻辑包括任何魔改编写对应的解密脚本。Python的ctypes库或直接使用 0xFFFFFFFF进行32位无符号整数运算是关键。以A cup of teA为例的解密脚本框架def tea_decrypt(v0, v1, key): delta 1131796 # 题目自定义的delta total (delta * 32) 0xFFFFFFFF # 加密结束时的sum for i in range(32): # 注意解密顺序与加密相反先v1后v0 v1 (v1 - ((key[3] (v0 5)) ^ (total v0) ^ (key[2] (v0 4)))) 0xFFFFFFFF v0 (v0 - ((key[1] (v1 5)) ^ (total v1) ^ (key[0] (v1 4)))) 0xFFFFFFFF total (total - delta) 0xFFFFFFFF return v0, v1 # 从IDA中提取的密钥和密文 key [289739801, 427884820, 1363251608, 269567252] cipher [2026214571, 578894681, 1193947460, -229306230, 73202484, 961145356, -881456792, 358205817, -554069347, 119347883] # 将密文中的负数转换为无符号32位整数 cipher [c 0xFFFFFFFF for c in cipher] plain_ints [] for i in range(0, len(cipher), 2): dec_v0, dec_v1 tea_decrypt(cipher[i], cipher[i1], key) plain_ints.extend([dec_v0, dec_v1]) # 将解密出的整数按小端序转换为字节 flag_bytes b for num in plain_ints: flag_bytes num.to_bytes(4, little) # 小端序转换 # 去除可能的填充零得到flag flag flag_bytes.rstrip(b\x00).decode(ascii) print(fFlag: {flag})实操心得在写解密脚本时最常遇到的坑是整数溢出处理和字节序问题。在内存中多字节整数有大小端之分。程序在将输入的字符串字节数组拷贝到整数数组进行加密时会遵循系统的字节序x86/x64是小端序。因此解密后我们需要将整数按小端序还原回字节。使用Python的int.to_bytes(4, little)可以完美地处理这个问题。5. 进阶技巧与问题排查动态调试、花指令与Z3求解5.1 动态调试让程序自己“说话”静态分析有时会遇到极其复杂或混淆严重的代码这时动态调试Dynamic Debugging就派上用场了。我们使用调试器如x64dbg, OllyDbg, GDB运行程序可以实时查看内存、寄存器、栈的变化单步跟踪代码执行流程。在guess题目中的应用guess题目的逻辑是生成一个随机数让你猜10次。无论对错程序都会用RC4解密一段数据只有猜对了才会将解密结果作为Flag输出。静态分析时我们发现反编译失败IDA提示positive sp value这是栈指针不平衡导致的反编译错误。解决方案动态调试获取随机数下断点我们不需要完全修复反编译。直接在rand函数调用之后或者比较输入与随机数的指令处下断点。运行并输入运行程序在命令行随意输入一个数字。查看寄存器/内存当程序断下时在调试器中查看存储随机数的寄存器如EAX/RAX或内存地址的值。这个值就是本轮游戏的“答案”。修改流程或直接获取Flag知道了答案我们可以选择Patch程序将判断跳转指令如JNZ改为JMP或NOP强制程序执行输出Flag的分支。让程序自然执行重新运行程序输入正确的随机数程序就会自己打印出Flag。动态调试让我们能够绕过复杂的静态分析直接获取到运行时的关键信息是解决反调试、代码混淆的利器。5.2 花指令处理清理分析障碍花指令Junk Code是故意插入的、干扰反汇编器或反编译器正常工作的无用指令。在flows这道题中IDA无法正常反编译solve函数就是因为其中包含了无效的跳转指令。识别与修复花指令识别在IDA的汇编视图IDA View中你会看到一些奇怪的跳转模式例如jz $1跳转到下一条指令紧接着是一条无效指令如db 0xE8然后才是真正的代码。反汇编器可能会把数据误解析为代码。修复NOP掉最直接的方法是将这些干扰性的跳转指令如jz,jnz和紧随其后的无效字节全部用NOP指令0x90替换。在IDA中你可以按F2编辑指令或者使用Edit - Patch program - Assemble。重新定义函数清理完花指令后IDA可能仍然认为这块区域不是函数。你需要按U取消定义将错误识别的代码取消然后按P创建函数让IDA重新分析这块区域通常就能成功反编译了。处理花指令需要耐心和对汇编指令集的熟悉核心思路就是帮助反汇编器区分开真正的指令和干扰数据。5.3 约束求解当逆向变成“解方程”有些题目逆向到最后你会发现输入Flag需要满足一系列复杂的算术或逻辑约束条件。手动求解几乎不可能这时就需要引入约束求解器比如Z3。在ez3题目中的应用题目名ez3就是提示。程序的核心校验逻辑是一个循环将输入的每个字符经过一系列乘、加、异或、取模运算后与一个已知的数组进行比较。这本质上是一个方程组F(flag[0], flag[1], ..., flag[n]) target_array。使用Z3求解的步骤定义变量将Flag的每个字节定义为Z3的位向量BitVec变量。添加约束将IDA中逆向出来的运算过程用Z3的语法重写一遍作为约束条件添加到求解器Solver中。添加额外约束通常我们还会知道Flag的格式比如以moectf{开头以}结尾字符是可打印ASCII等。这些信息作为额外的约束能极大缩小解空间快速找到唯一解。求解并输出调用求解器的check()和model()方法获取解。from z3 import * solver Solver() flag [BitVec(ff{i}, 8) for i in range(34)] # 假设flag长度34 # 1. 添加算法约束 (根据逆向代码编写) for i in range(34): # 这里填写具体的运算约束例如 # b_i 47806 * (flag[i] i) ... # solver.add(b_i % 51966 target[i]) pass # 2. 添加格式约束 for i, c in enumerate(bmoectf{): solver.add(flag[i] c) solver.add(flag[-1] ord(})) for f in flag: solver.add(And(f 0x20, f 0x7e)) # 可打印字符 if solver.check() sat: m solver.model() result bytes([m.eval(f).as_long() for f in flag]) print(result.decode())Z3将我们从繁琐的数学计算中解放出来让我们能更专注于逆向出正确的约束关系。6. 其他题型实战技巧与总结6.1 迷宫类题目 (maze)这类题目通常会将Flag的验证过程隐藏在一个走迷宫的游戏里。地图数据往往以二维数组的形式硬编码在程序中。技巧首先在IDA的字符串窗口或数据段中寻找看起来像地图的数据大量重复的字符如#,., ,S,E等。找到后将其提取出来用文本编辑器整理成矩阵。求解可以手动走也可以写脚本BFS/DFS自动求解最短路径。路径序列如SSDDWWAA可能就是Flag的一部分或全部。6.2 Android逆向 (ezandroid,ezandroid.pro)Android应用的逆向主要针对.apk文件。工具使用jadx-gui或JEB进行Java层反编译查看MainActivity等逻辑。Native层如果遇到native方法说明核心逻辑在.so动态库中。需要将lib/arch/xxx.so文件提取出来用IDA Pro进行逆向分析JNI_OnLoad和Java_com_xxx_MainActivity_check这类函数。ezandroid.pro这道题就考察了从Java层定位到Native层.so文件并逆向其中SM4国密算法加密逻辑的能力。6.3 游戏修改类 (2048_master_re)这类题目伪装成一个游戏如2048要求达到特定分数或条件来触发Flag。思路不要真的去玩目标是找到存储游戏状态如分数、棋盘数据的变量在内存或文件中的位置。方法静态分析在IDA中搜索与分数显示、胜利判断相关的字符串。动态调试/修改用Cheat Engine等工具附加进程搜索分数值找到内存地址然后直接修改为目标值。文件修改如题目所示游戏数据可能保存在外部文件如layout.dat中。用十六进制编辑器找到对应分数存储的偏移量并修改。6.4 常见问题排查速查表问题现象可能原因解决方案upx -d脱壳失败UPX文件头被修改用十六进制编辑器修复UPX!等特征字段IDA F5反编译失败1. 花指令干扰2. 栈指针不平衡3. 函数识别错误1. 清理花指令NOP掉2. 尝试调整栈指针或使用Edit - Functions - Stack pointer3. 按U取消定义再按P重新定义函数解密脚本输出乱码1. 字节序错误2. 算法逆向有误如加减、轮序3. 未处理负数有符号转无符号1. 检查to_bytes用的是‘little’还是‘big’2. 仔细对照IDA伪代码确认解密是加密的严格逆过程3. 使用c 0xFFFFFFFF进行无符号转换动态调试时程序崩溃1. 反调试检测2. 断点位置不当破坏了关键数据1. 尝试隐藏调试器插件或命令行参数2. 在函数入口、API调用后等安全位置下断Z3求解无解或解太多1. 约束条件写错2. 约束不足解空间大1. 逐行核对逆向代码与Z3约束2. 增加已知条件约束如Flag格式、字符范围逆向工程是一门需要耐心、细心和大量实践的技术。MoeCTF2025第一周的题目就像一份精心设计的“新手村”地图涵盖了从脱壳、静态分析、动态调试到算法逆向、约束求解等多个基础技能点。我的建议是不要只满足于写出最终的解题脚本更要理解每一步操作背后的原理为什么要这样脱壳这个字符串引用为什么能带我到主函数TEA解密脚本的每一步运算和加密是如何对应的只有把“为什么”搞清楚了你的技能树才算真正扎下了根。遇到问题时多结合静态分析和动态调试让程序在运行时给你答案。最后善用工具IDA, x64dbg, Python, Z3和社区资源但更要锻炼自己独立分析和解决问题的能力。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价