CTF逆向入门:从pyc文件到flag的Python字节码逆推实战

发布时间:2026/10/13 15:14:36
CTF逆向入门:从pyc文件到flag的Python字节码逆推实战 第一次看到 BUUCTF 里的 [GWCTF 2019] pyre我以为是某个冷门语言框架的名字。点进去才发现它给的是一个.pyc文件考的是 Python 逆向。pyre这个题名放在 CTF 语境下基本可以理解为 Python Reverse 的缩写也就是把一个 Python 程序编译后的字节码文件拿到手反编译、读逻辑、逆算法最后把 flag 还原出来。这类题对刚接触逆向的新手非常友好不用折腾汇编也不用面对 C 反编译出来的一堆花指令和虚函数表。你只需要一点 Python 基础、两个趁手的工具再加上一条“从结果倒着推”的思路就能走完从.pyc到 flag 的完整链路。本文不会只丢给你一个答案而是把每一步为什么这么做、遇到报错怎么处理、位运算怎么恢复都拆开讲一遍。1. 先搞清楚这道题在考什么1.1 从 pyre 这个命名说起很多新手拿到题目第一时间会去搜“pyre 是什么”。有人会告诉你 pyre 是火炬、柴堆的意思但在 CTF 逆向题里它就是 Python Reverse 的合写。出题人的逻辑很直接写一个 Python 脚本把 flag 做一系列变换然后编译成.pyc文件不给你源码让你从字节码里逆出输入。这道题的核心考点有三个pyc文件结构识别。使用反编译工具把字节码还原成近似源码。对源码里的变换逻辑做逆运算得到原始输入。所以它本质上不是“破解程序运行”而是“恢复被隐藏的逻辑”。你不需要理解 CPU 指令也不需要调调试器只要能把 Python 的编译产物看穿就能得手。1.2 文件识别这不是普通二进制拿到题目压缩包解压后通常是一个pyre.pyc或者main.pyc。第一步不是直接拖进 IDA而是先看文件类型。file pyre.pyc xxd pyre.pyc | head -n 5正常情况下file会提示这是python 3.6 bytecode之类的结果。xxd则可以看到前几个字节的魔数magic number。Python 解释器在加载.pyc文件时会先检查这个魔数如果与当前解释器不匹配就会拒绝加载。所以拿到一个pyc先确定它是哪个 Python 版本编译的非常关键。我习惯的做法是用当前环境里的 Python 查一下可用的魔数映射import importlib.util for magic, version in importlib.util.MAGIC_NUMBERS: if version (3, 6) and version (3, 9): print(hex(magic), version)跑完之后再去对比xxd看到的头基本就能锁定版本。这道题从反编译信息来看常见版本是 Python 3.6/3.7 左右对应工具选择也简单后面会细说。1.3 工具准备反编译器与 Python 环境做pyc逆向工具链不需要太豪华三样东西就够用工具作用备注uncompyle6反编译pyc为 Python 源码老牌工具适合 Python 2.4 到 3.8decompyle3uncompyle6 的改进分支对 Python 3.7 兼容更好pycdcC 写的反编译器支持新版 Python输出偶尔有瑕疵disPython 自带的字节码反汇编模块反编译工具失败时的兜底方案安装 uncompyle6 很简单pip install uncompyle6pycdc 则需要自己拉源码编译过程略麻烦但遇到高版本pyc时很值得。我的建议是环境里同时留 uncompyle6 和 pycdc一个不行换另一个。不要只装一个就硬刚很多时间其实浪费在工具兼容性上。2. 反编译 pyc让字节码“开口说话”2.1 第一条命令file 和 uncompyle6先确认文件类型file pyre.pyc如果确认是 Python 3.6 的字节码直接跑uncompyle6 pyre.pyc pyre.py cat pyre.py顺利的话你会看到类似这样的信息# uncompyle6 version 3.7.4 # Python bytecode 3.6 # Embedded file name: pyre.py print(Welcome to Re World!) print(Your input is your flag,but I don\t know what is your flag:) l len(input(input:)) a [] for i in range(l): ...注意开头几行uncompyle6 version、Python bytecode、Embedded file name是工具自动加的注释不是源码的一部分。真正要看的是下面的print、input、循环和列表操作。这时候很多人会犯一个错误直接照着反编译结果里的变量名去猜逻辑然后闷头写逆脚本。我建议先别急先把反编译代码里的“入口”和“出口”找出来。2.2 从反编译结果中锁定核心代码一个典型的pyc逆向题反编译后的源码通常有三块干扰信息比如大段print或者看似复杂但实际不影响 flag 的变量。输入处理input(input:)这是你输入的字符串也就是 flag。核心比较一组列表或字节串最后用来和变换结果比较。所以我会先搜索几个关键词input for if 在编辑器里打开反编译结果后重点看循环体内部做了什么。常见的变换套路是for i in range(l): a.append(ord(flag[i]) ^ i) for i in range(l): a[i] (a[i] i) 0xff把输入字符串变成 ASCII 码再用索引i做异或、加减、取反这类操作。这类操作在 CTF 里出现频率极高因为逆向过程简单但又比直接明文比较有“逆向感”。2.3 反编译失败时的兜底方案uncompyle6 偶尔会报Decompilation failed。原因通常是 pyc 版本稍新、文件头被改动、或者存在某些控制流导致反编译器无法重建 AST。不要慌先用 pycdc 再试一次pycdc pyre.pyc pyre_pycdc.py如果还是失败用 Python 自带的dis模块直接看字节码import marshal, dis with open(pyre.pyc, rb) as f: data f.read()[16:] # 跳过 pyc 头部 code marshal.loads(data) dis.dis(code)这个方案不需要任何第三方反编译器直接读入 code object 并打印字节码指令。虽然看着不像源码那么直观但至少能看到函数调用顺序、比较对象、常量和全局变量名。很多情况下从字节码里反而能发现更真实的操作顺序。注意跳过头部时不要永远固定写 16 字节。Python 3.6 之后一般是 16 字节但不同版本略有差异。用xxd先确认再处理。3. 正向算法拆解搞清楚它怎么加密3.1 一类常见的 Python 加密写法这类题目出题人最喜欢的写法是先用ord()把字符变整数然后每个字符独立做几轮运算最后与目标数组比较。运算无非是异或、加减、取反、移位。它们看起来各不相同但逆向手段是统一的从最后一步开始一步一步反着做。为了说明思路我给一个典型的正向逻辑模型flag flag{pyre_reverse} s [ord(c) for c in flag] enc [] # 第一轮异或索引 for i in range(len(s)): enc.append((s[i] ^ i) 0xff) # 第二轮加上索引 for i in range(len(s)): enc[i] (enc[i] i) 0xff # 第三轮按字节取反 for i in range(len(s)): enc[i] (~enc[i]) 0xff # 此时 enc 就是 target target [153, 145, 154, 152, 124, 133, 122, 135, 138, 160, 125, 134, 121, 146, 117, 116, 122, 130]这个模型里用到了三种常见操作异或、加法、按位取反。真实题目可能只有其中一两步或者把顺序换一下但逆向思路完全一样。3.2 用一个可复现的例子讲透变换顺序我们先拿第一个字符f来手动演算一遍这样你才能理解脚本里每个 0xff是干嘛的。f的 ASCII 码是 102索引i 0。第一轮异或102 ^ 0 102第二轮加法102 0 102第三轮取反~102 0xff 153所以target[0]是 153。再算第二个字符lASCII 108索引i 1。108 ^ 1 109 109 1 110 ~110 0xff 145所以target[1]是 145。看到规律没有每一个字符只和它自己的索引有关和前后字符无关。这种变换串行关系很弱逆向时即使不严格按照从后往前也能恢复但我还是强烈建议你按“从最后一步倒序恢复”的习惯写因为真实题目很可能会让后一轮的某个位置依赖前一轮的前一个位置那时候顺序错了会全盘崩溃。3.3 手工算一遍比直接跑脚本更踏实很多新手拿到题目后恨不得立刻跑脚本秒出 flag求快没问题但至少要对其中一两个字符做手工推导。原因很简单你只有理解了变换过程才能在逆推时知道为什么先取反、再减索引、最后异或。如果只是抄网上的逆推模板一旦题目步骤顺序变化你就卡住了。我在做这类题时会在纸上先写正向过程f(102) - ^0 : 102 - 0 : 102 - ~ : 153然后从 153 反推153 - ~ : 102 - -0 : 102 - ^0 : f这就像穿衣服出门先穿袜子再穿鞋回家第一步是脱鞋然后才是脱袜子。加密过程也是一层一层的操作逆过程必须从最外层开始剥。4. 逆向脚本实战从 enc 到 flag4.1 逆推的铁律后做的先逆看到正向代码里有三轮操作逆向脚本就必须倒着写。上面例子中正向顺序是异或 - 加法 - 取反所以逆向顺序是取反 - 减法 - 异或这里最容易翻车的是把取反放在最后才处理。如果你先减索引、再异或还没取反得到的一定是一堆乱码。原因是取反操作改变了整个字节的空间分布必须最先恢复否则后面的减法和异或都作用在错误的数据上。4.2 完整逆向脚本与运行结果把上面的target数组拿来完整逆推脚本如下enc [ 153, 145, 154, 152, 124, 133, 122, 135, 138, 160, 125, 134, 121, 146, 117, 116, 122, 130 ] # 逆第三轮取反的逆运算仍是取反按字节 for i in range(len(enc) - 1, -1, -1): enc[i] (~enc[i]) 0xff # 逆第二轮加法变减法 for i in range(len(enc) - 1, -1, -1): enc[i] (enc[i] - i) 0xff # 逆第一轮异或的逆运算是自己 for i in range(len(enc) - 1, -1, -1): enc[i] enc[i] ^ i print(.join(map(chr, enc)))运行结果flag{pyre_reverse}这个结果和我们一开始构造的 flag 完全一致说明逆推逻辑正确。实际操作时你只需要把enc替换成题目反编译后给出的目标数组即可。4.3 位运算、负数、数组对齐的细节写逆向脚本时有三个细节一定要上心。第一个细节是 0xff。Python 的整数是无限精度的但加密函数通常只处理一个字节的数据。比如~102在 Python 里得到的是-103直接参与后续运算会和 C 语言里的行为不一致。加上 0xff后它会截断为低 8 位得到 0 到 255 之间的值。如果你在逆向脚本里漏了 0xff最终解出来一定有问题。第二个细节是负数。有时候反编译出来的target数组里会有负数例如-100。这通常是因为数据在 C 层面被当作 signed char 处理了或者反编译器输出方式导致。处理方式很简单x x 0xff转成无符号字节后再做逆推。不要直接拿-100去做加法减法那样会绕远路。第三个细节是数组长度对齐。如果正向代码里有for i in range(l)和for i in range(len(enc))两个循环长度可能不一致。写脚本前先print(len(enc))确认和target长度一致。否则某个索引越界后结果看起来正常实际却悄悄丢了字符。5. 避坑与排错速查5.1 反编译工具出问题怎么办这是实操中最常遇到的问题单独拎出来说。uncompyle6 报错、输出乱码、或者直接输出一段invalid大多是因为 pyc 字节码版本和工具支持的版本不匹配。你可以按下面的顺序排查现象可能原因解决方案uncompyle6 提示 decompilation failedpyc 版本太新或文件头损坏先 file 确认版本换 pycdc 或 dis 手动读pycdc 输出 Python 代码但语法错误pycdc 对复杂控制流还原能力有限手动调整缩进和括号必要时看 dis 字节码反编译结果里出现大量无效符号文件可能在传输时被改动对比原始哈希或检查 pyc 头部魔数marshal.loads 报错跳过的头部长度不对用 xxd 查看确认头部实际大小我在实测中最常用的兜底顺序是uncompyle6 - pycdc - dis不要一上来就开 dis那个效率太低但也不要死磕 uncompyle6换工具比硬分析快得多。5.2 算法逆推最容易翻车的三个点第一取反没按字节处理。~x在 Python 里是“补码取反”对一个字节来说先取反再按位截断和直接255 - x效果一样。但如果你用的是~x后不加 0xff逆推时就会出现负数参与运算结果完全错乱。第二加法减法没有及时取模。加密时可能对结果做了 0xff逆推时就要同步做 0xff。比如(enc[i] - i) 0xff这个“截断”不是在最后统一做而是在每一步加减后都做。很多新手只在最后加了一个 0xff中间某一步可能溢出答案就不对。第三两个独立循环的顺序搞反。如果题目里先对每个字符做第一轮变换再对每个字符做第二轮变换那么逆向时必须先逆第二轮再逆第一轮。就算两轮操作对每个字符独立顺序也一样重要。养成“从后往前”的习惯可以避免这类问题。5.3 给新手的 pyc 逆向固定套路做多了之后我总结了一个固定流程每次拿到pyre类题目都这么干file判断文件类型xxd看 pyc 头部。根据 Python 版本选反编译工具尝试还原源码。在源码里找input、循环、比较数组定位核心变换。把正向变换分成步骤确认每一步操作。从目标数组开始按反向顺序写逆脚本。每一步逆推后都打印中间值和手算预期值核对。这套流程对大部分 Python 字节码题都成立。尤其是第 6 点很多人会忽略。其实逆向脚本不怕慢就怕错。每逆一步就打印一次能让你在最短时间内定位是哪一行出错。我在实际刷题中发现最省时间的做法是准备一个专属的“逆推工具箱”里面放好 uncompyle6、pycdc、以及几个常用的逆推代码片段。遇到这种题直接套用流程把精力留给那些需要脑力的逻辑分析而不是浪费在环境配置和工具报错上。这也算是我从pyre这类题目里收获到的最实用经验。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询