CTF Python逆向实战:从ez_python题解析字节码反编译与混淆对抗

发布时间:2026/10/20 18:38:44
CTF Python逆向实战:从ez_python题解析字节码反编译与混淆对抗 1. 从一道CTF题看Python逆向的典型套路ez_python这个标题在CTF圈子里几乎自带一种信号——出题人想让你觉得简单但实际动手之后往往会发现坑都埋在你看不见的地方。第一届长城杯网络安全大赛的这道题名字虽然叫ez但它考察的东西一点都不ezPython字节码的阅读能力、反编译工具的选择与局限、以及面对混淆代码时的耐心和直觉。我接触过不少刚入门CTF的朋友他们遇到Python逆向题的第一反应是找工具反编译一下不就行了。这个思路本身没错但问题在于工具能帮你还原出源码却不一定能帮你理解源码。尤其是当出题人在源码里埋了多层嵌套、变量名混淆、编码转换、甚至动态执行的时候你拿到的那份反编译结果可能比汇编还难读。这篇文章面向的是有一定Python基础、正在打CTF或者准备打CTF的读者。不管你是刚接触逆向的新手还是已经做过几道Python题但总觉得不得要领的老手我都会从这道题出发把Python逆向的完整思路链路拆开来讲——从拿到文件的第一步该看什么到工具怎么选到代码怎么读到最终flag怎么拿。更重要的是我会告诉你每一步为什么要这样做而不是只丢给你一个操作清单。2. 拿到文件后的第一分钟判断你面对的是什么2.1 文件类型识别与初步侦察很多人拿到题目附件之后第一反应是直接双击运行或者丢进IDE。这个习惯在CTF里非常危险。正确的做法是先做静态侦察搞清楚你面对的是什么类型的文件。在Linux环境下file命令是你的第一把武器file ez_python如果输出里出现Python script、Python bytecode或者ELF之类的字样你就能快速判断方向。Python逆向题常见的文件形态有三种纯.py源码文件这种最简单但出题人往往会做代码混淆或者用exec动态执行。.pyc字节码文件这是Python编译后的字节码需要反编译工具还原。打包后的可执行文件用PyInstaller、cx_Freeze等工具打包成二进制需要先解包再反编译。对于ez_python这道题根据比赛常见的出题风格大概率是.pyc文件或者打包后的ELF。如果是.pyc你需要确认它的Python版本——这一点极其关键因为不同版本的字节码指令集不同用错版本的反编译工具会直接报错或者还原出错误的代码。python3 -c import sys; print(sys.version)或者更直接的方式查看.pyc文件头部的magic numberxxd ez_python.pyc | head -1.pyc文件的前4个字节是magic number它对应着具体的Python版本。比如Python 3.8的magic number是0x550D0D0APython 3.9是0x610D0D0A。你可以通过查阅Python官方文档中的magic number对照表来确认版本。注意如果你拿到的.pyc文件magic number被人为修改过反编译工具会拒绝处理。这时候你需要手动把magic number改回正确的值或者直接用对应版本的Python去加载它。2.2 为什么版本匹配这么重要我见过太多人在这上面栽跟头。有人用Python 3.10的uncompyle6去反编译一个Python 3.6的.pyc文件结果工具直接崩溃或者输出一堆乱码。这不是工具的问题是字节码本身就不兼容。Python的字节码指令集在不同版本之间会有增删改。比如Python 3.8引入了ROT_FOUR和ROT_N指令Python 3.9改变了WITH_EXCEPT_START的行为Python 3.10又对MATCH语句相关的指令做了大量调整。如果你的反编译工具不支持目标版本它就无法正确解析这些指令还原出来的代码自然就是错的。所以我的建议是先确认版本再选工具。确认版本的方法有三种看文件头magic number最准确看题目附件中是否有版本提示比如文件名里带36、38之类的数字用python -m dis直接反汇编观察指令集特征第三种方法最直接因为dis模块是Python标准库自带的你用什么版本的Python去运行它它就能反汇编对应版本的字节码。如果你不确定版本可以多试几个版本的Python看哪个能正常加载。3. 反编译工具的选择没有银弹3.1 主流工具对比与适用场景Python反编译工具的选择是一个老生常谈的话题但我发现很多教程只是罗列工具名称却不告诉你什么时候该用哪个。这里我根据自己的实际使用经验给出一份对比工具名称支持版本优点缺点uncompyle6Python 2.7 - 3.8还原度高社区活跃不支持3.9decompyle3Python 3.7 - 3.8uncompyle6的改进版版本覆盖窄pycdcPython 2.7 - 3.11支持版本最广还原质量参差不齐unpyc37-3.10Python 3.7 - 3.10针对新版本优化项目维护不稳定对于ez_python这道题如果它是Python 3.8及以下版本uncompyle6是首选。如果是3.9及以上pycdc是更稳妥的选择。安装uncompyle6很简单pip install uncompyle6使用方式uncompyle6 -o . ez_python.pyc这会把反编译结果输出到当前目录下生成一个ez_python.py文件。pycdc需要从源码编译git clone https://github.com/zrax/pycdc.git cd pycdc cmake . make然后使用./pycdc ez_python.pyc ez_python.py3.2 当反编译工具失效时怎么办这是我想重点讲的部分。很多CTF题目的设计目的就是让你的自动化工具失效。出题人可能会修改magic number让工具拒绝识别在字节码中插入无效指令导致反编译崩溃使用自定义的代码对象结构把关键逻辑放在动态执行的字符串里遇到这些情况你需要回到最底层的方法用dis模块手动反汇编。import dis import marshal with open(ez_python.pyc, rb) as f: f.read(16) # 跳过文件头不同版本长度不同 code marshal.load(f) dis.dis(code)这会输出字节码的汇编形式。虽然看起来不如反编译后的源码直观但它是最可靠的方法——只要Python能加载这个文件dis就能反汇编它。读字节码汇编需要一些练习但核心逻辑并不复杂。Python的字节码是基于栈的每条指令从栈上取操作数、执行操作、把结果压回栈。你只需要关注几个关键指令LOAD_CONST加载常量LOAD_FAST加载局部变量STORE_FAST存储局部变量CALL_FUNCTION调用函数COMPARE_OP比较操作JUMP_IF_FALSE/JUMP_IF_TRUE条件跳转把这些指令串起来你就能还原出程序的逻辑流。4. 代码阅读的核心技巧从混淆中找线索4.1 变量名混淆的应对策略反编译出来的代码第一眼看上去可能长这样def a1(b2, c3): d4 for e5 in range(len(b2)): f6 ord(b2[e5]) ^ c3 d4 chr(f6) return d4变量名全是无意义的字母数字组合这是最常见的混淆手段。面对这种情况我的做法是先看函数调用关系再看数据流。具体来说找到程序的入口点通常是if __name__ __main__:或者直接执行的代码看入口点调用了哪些函数追踪输入数据在这些函数之间是怎么传递的根据数据的变化反推每个函数的功能比如上面的a1函数虽然变量名看不懂但逻辑很清晰它遍历输入字符串对每个字符做异或操作然后拼接成新字符串。这就是一个典型的异或加密函数。你可以手动把变量名改成有意义的名字def xor_encrypt(input_str, key): result for i in range(len(input_str)): xor_val ord(input_str[i]) ^ key result chr(xor_val) return result改完名字之后代码的可读性会大幅提升。4.2 编码转换与多层嵌套的拆解CTF题目中另一个常见的套路是多层编码。比如先把flag做Base64编码再做异或再做反转最后和某个字符串比较。反编译出来的代码可能把这些操作分散在多个函数里你需要一层一层剥开。我的建议是从最终的比较语句倒推。找到类似这样的代码if result xYz123...: print(Correct!)然后追踪result是怎么来的。它可能是某个函数的返回值那个函数又调用了另一个函数……一直追到最开始的输入。在这个过程中你可能会遇到一些Python内置函数的调用比如base64.b64encode/base64.b64decodebinascii.hexlify/binascii.unhexlifyzlib.compress/zlib.decompresscodecs.encode/codecs.decode这些函数的特征很明显看到它们你就知道这一层用的是什么编码。把每一层的操作反过来做一遍就能还原出原始输入。4.3 动态执行代码的识别与处理最让人头疼的情况是出题人用了exec或eval动态执行代码。反编译出来的代码可能长这样exec(base64.b64decode(aW1wb3J0IG9z...))这时候静态分析就到此为止了你需要动态执行来获取真正的代码。但直接执行陌生代码有风险我的做法是先把exec替换成print看看它要执行的是什么如果是多层嵌套就一层一层剥确认安全后再实际执行# 把 exec(...) 改成 print(...) print(base64.b64decode(aW1wb3J0IG9z...).decode())这样你就能看到被隐藏的代码然后再决定下一步怎么做。注意在实际比赛中动态执行代码可能包含反调试逻辑或者环境检测。如果你在本地执行结果和预期不符可能是代码检测到了你的运行环境。这时候可以尝试用沙箱或者虚拟机来运行。5. 从ez_python看CTF逆向的通用思维模型5.1 逆向不是读代码而是猜意图很多人把逆向当成把二进制翻译成源码的过程这个理解太狭隘了。真正的逆向是理解出题人的意图。出题人为什么要这样写他为什么用这个算法而不是那个他为什么在这里加一层编码当你开始思考这些问题的时候你就不再是被动地读代码而是主动地推测代码的行为。以ez_python为例如果我在反编译结果中看到一个函数对输入做了逐字符异或然后和固定字符串比较我会立刻想到异或的密钥是什么密钥是固定的还是动态生成的如果密钥是固定的那直接异或回去就行了如果密钥是动态生成的我需要找到密钥的生成逻辑。这种猜意图的能力比会使用多少工具重要得多。5.2 验证假设写脚本比手算快当你推测出加密逻辑之后不要急着手动计算。写一个Python脚本来自动化验证你的假设def decrypt(ciphertext, key): return .join(chr(ord(c) ^ key) for c in ciphertext) cipher ... # 从反编译代码中提取的密文 for key in range(256): result decrypt(cipher, key) if flag in result.lower() or ctf in result.lower(): print(fKey{key}: {result})这种暴力枚举的方法在CTF中非常实用尤其是当你对密钥不确定的时候。5.3 常见Python逆向题型总结根据我的经验Python逆向题大致可以分为以下几类题型特征解题思路简单异或代码中有^操作暴力枚举密钥或直接异或回去多层编码嵌套调用base64、hex等逐层逆向解码字节码混淆变量名无意义、控制流平坦化手动重命名、还原控制流动态执行使用exec/eval替换为print获取真实代码自定义VM模拟指令集执行理解VM指令含义写反汇编器打包二进制PyInstaller等打包先解包提取.pyc再反编译ez_python这道题大概率属于前两类或者第三类。不管属于哪一类核心思路都是一样的静态分析找线索动态验证确认结果。6. 实操中的坑与经验6.1 反编译结果不完整怎么办有时候uncompyle6会报错说某个指令无法解析然后输出不完整的代码。这时候不要慌你可以尝试用--verify参数让工具做更严格的校验换一个工具试试pycdc往往能处理uncompyle6处理不了的情况用dis手动反汇编出问题的部分我遇到过一次uncompyle6在处理一个包含try/except/finally嵌套的代码对象时崩溃了但pycdc成功还原了。所以手头常备两三个反编译工具是很有必要的。6.2 字符串被加密了怎么找有些题目会把关键字符串加密存储运行时才解密。这时候你在反编译结果里搜flag、correct之类的关键词是搜不到的。应对方法是找解密函数。通常解密函数会在程序早期被调用解密后的字符串会存储在某个变量里。你可以在反编译代码中搜索decode、decrypt、unmarshal等关键词关注程序开头的初始化代码动态调试在解密函数执行后打印变量值6.3 如何提高反编译代码的可读性反编译出来的代码往往缺少注释、变量名混乱、格式糟糕。我的做法是用IDE的格式化功能先整理一遍手动重命名关键变量和函数添加注释说明每一步的作用把复杂的表达式拆成多行这个过程看起来费时但实际上能帮你理清思路。很多时候当你把代码整理清楚之后解题思路就自然浮现了。6.4 不要忽视Python版本的影响我再强调一次Python版本对逆向的影响非常大。同一个算法用Python 3.7和Python 3.11编译出来的字节码可能完全不同。如果你在本地用Python 3.11反编译一个Python 3.7的.pyc文件即使工具不报错还原出来的代码也可能有细微的错误。最稳妥的做法是用与目标文件相同版本的Python来运行反编译工具。你可以用pyenv或者conda来管理多个Python版本。# 使用pyenv安装指定版本 pyenv install 3.8.10 pyenv local 3.8.10 # 然后在这个版本下安装uncompyle6 pip install uncompyle6 uncompyle6 ez_python.pyc7. 从解题到出题逆向思维的进阶当你做过足够多的Python逆向题之后你会开始理解出题人的思路。这时候你可以尝试自己出题这会极大地提升你的逆向能力。出题的时候你需要考虑用什么版本的Python编译用什么方式混淆代码在哪里埋陷阱如何确保题目可解但又不至于太简单比如你可以尝试# 简单的异或加密 flag flag{test} key 0x42 encrypted .join(chr(ord(c) ^ key) for c in flag) print(encrypted)然后把这个加密逻辑嵌入到一个更大的程序中加上一些无关的函数调用和变量混淆。这样一道简单的逆向题就出来了。当然实际比赛中的题目会比这复杂得多。ez_python虽然名字叫ez但它考察的知识点可能包括字节码阅读、反编译工具使用、编码识别、逻辑推理等多个方面。能把这道题做出来说明你已经具备了Python逆向的基本功。我在实际做题过程中最大的体会是耐心比技巧更重要。很多时候你离答案只差多读一遍代码、多试一个密钥、多换一个工具。那些看起来复杂的混淆拆开之后往往就是几个简单的操作叠加。保持冷静一步一步来flag就在那里。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询