CTF-逆向题-pyc思路
CTF-逆向题-pyc思路
Txy1. 什么是 pyc
文件
- 简要介绍:
pyc
是 Python 源代码(.py
)文件被编译后的字节码文件,通常位于__pycache__
文件夹中。
- 在 CTF 中的常见考点:
- 恶意字节码修改
- 防止直接反编译的干扰
- 反编译字节码
- py代码逆向
- 隐藏的调试信息
2. pyc 文件结构与原理
- 2.1. 文件结构
- 文件头部:包含 Python 版本、时间戳等信息。
1
2
3
4
5
6
7
8
9
10
11Python3.3 以下版本文件头仅包含:
Magic Number(4 字节,小端序)
时间戳(4 字节,记录文件编译时间,POSIX 时间戳)
Python3.3 到 Python3.7(不包含 3.7)文件头包含:
Magic Number(4 字节,小端序)
时间戳(8 字节,扩展为 64 位精度)
字节码大小(4 字节,用于校验文件完整性)
Python3.7+,引入了字节码缓存目录的全新文件格式。后者对文件反编译没有影响,全部填充0即可
Magic Number(4 字节,小端序)
flags(4 字节,从 Python 3.7 开始引入,默认值为 0。)
文件哈希值(8 字节,取代时间戳和大小信息,用于更强的文件完整性校验) - 字节码内容:存储程序的操作指令。
- 文件头部:包含 Python 版本、时间戳等信息。
- 2.2. 常见的工具
uncompyle6
:将.pyc
文件还原为.py
文件。- 安装方法
pip install uncompyle
- 安装方法
pycdc
和pycdas
:分别是反编译工具和动态分析工具。marshal
模块:直接加载和读取.pyc
文件。
3. pyc 逆向通用解题步骤
3.1. 检查文件版本
使用
die.exe
或exeinfo
等图形化工具来分析文件类型和版本。使用
file
或binwalk
命令分析文件头:1
file example.pyc
根据版本号选择合适的反编译工具。
- 示例:
- Python 3.7:
uncompyle6
- Python 3.9+:推荐
pycdc
- Python 3.7:
- 示例:
3.2. 直接反编译
方法 1:使用
uncompyle6
1
uncompyle6 -o output_dir example.pyc
- 输出的
.py
文件可能已经可以直接使用。
- 输出的
方法 2:使用
pycdc
1
pycdc example.pyc > output.py
- 注意检查输出的逻辑是否完整,有时需要补充变量名或注释。
3.3. 无法直接反编译的情况
情况 1:魔改字节码
使用
marshal
解析原始数据:1
2
3
4
5
6import marshal
with open('example.pyc', 'rb') as f:
f.read(16) # 跳过头部
code_obj = marshal.load(f)
print(code_obj)手动解析字节码以发现关键逻辑。
情况 2:动态加密
- 结合
pycdas
动态分析:在执行时注入调试钩子或打印关键变量。
示例代码:
1
2import dis
dis.dis(code_obj)
- 结合
3.4. 修改后重新运行
若需要动态修改字节码逻辑,可以直接将字节码导出为
.py
文件后重新生成.pyc
文件:1
python -m py_compile modified.py
3.5.exe
解包为pyc
- 使用
pyinstxtractor.py
把exe
变成结构体和一个文件
使用方法:*python .\pyinstxtractor.py .\attachment.exe
- 重点:再把时间属性和版本的魔术字放回去保存
- python文件打包成exe文件的过程中,会抹去pyc文件前面的部分信息,所以在反编译之前需要检查并添加上这部分信息,这部分信息可以通过struct文件获取。
- 使用010editor中打开struct文件后,把struct文件前几个字节插入
pyc源码文件
开头。(具体要插入几个字节还是要看解包后的文件,根据python版本。)
- pyc 反编译
3.6.修复pyc
Magic Number 已知列表,在文末附上
接下来就算找到了Magic Number
的版本对照表。但是,我们知道的Magic Number
是四字节二进制数据。
转换代码:
1 | MAGIC_NUMBER = (3413).to_bytes(2, 'little') + b'\r\n' |
这里的3413
就是Python 3.8b4
版本的Magic Number
,执行一下,就得到了四字节的二进制码0x0A0D0D55
。其他版本的对应二进制码,可以按照上面步骤计算得到。
4. 题目示例
5. 常见问题与解答
- Q1:为什么
uncompyle6
提示版本不支持?- 检查文件是否属于较新版本 Python,可能需要切换工具(如
pycdc
)。
- 检查文件是否属于较新版本 Python,可能需要切换工具(如
- Q2:如何定位反编译后的逻辑错误?
- 通过插入打印调试语句,逐步分析代码的运行轨迹。
- Q3:遇到pycdc和uncompyle6都无法反编译的情况怎么办
直接摆烂,可以尝试使用pycdas来转成字节码,根据字节码自行反编译,实在看不懂字节码,大不了直接丢给AI呗。
6. 总结与建议
- 熟悉
.pyc
文件的基本结构有助于快速解题。 - 工具选择是关键:尽量掌握多种工具的用法。
- 遇到干扰时,不妨尝试动态分析和字节码级别解析。
附录
Magic Number 对照表
- 如果需要更详细的版本历史,可参考 Python 源码中的
Lib/importlib/_bootstrap_external.py
文件。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107Known values:
# Python 1.5: 20121
# Python 1.5.1: 20121
# Python 1.5.2: 20121
# Python 1.6: 50428
# Python 2.0: 50823
# Python 2.0.1: 50823
# Python 2.1: 60202
# Python 2.1.1: 60202
# Python 2.1.2: 60202
# Python 2.2: 60717
# Python 2.3a0: 62011
# Python 2.3a0: 62021
# Python 2.3a0: 62011 (!)
# Python 2.4a0: 62041
# Python 2.4a3: 62051
# Python 2.4b1: 62061
# Python 2.5a0: 62071
# Python 2.5a0: 62081 (ast-branch)
# Python 2.5a0: 62091 (with)
# Python 2.5a0: 62092 (changed WITH_CLEANUP opcode)
# Python 2.5b3: 62101 (fix wrong code: for x, in ...)
# Python 2.5b3: 62111 (fix wrong code: x += yield)
# Python 2.5c1: 62121 (fix wrong lnotab with for loops and
# storing constants that should have been removed)
# Python 2.5c2: 62131 (fix wrong code: for x, in ... in listcomp/genexp)
# Python 2.6a0: 62151 (peephole optimizations and STORE_MAP opcode)
# Python 2.6a1: 62161 (WITH_CLEANUP optimization)
# Python 2.7a0: 62171 (optimize list comprehensions/change LIST_APPEND)
# Python 2.7a0: 62181 (optimize conditional branches:
# introduce POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE)
# Python 2.7a0 62191 (introduce SETUP_WITH)
# Python 2.7a0 62201 (introduce BUILD_SET)
# Python 2.7a0 62211 (introduce MAP_ADD and SET_ADD)
# Python 3000: 3000
# 3010 (removed UNARY_CONVERT)
# 3020 (added BUILD_SET)
# 3030 (added keyword-only parameters)
# 3040 (added signature annotations)
# 3050 (print becomes a function)
# 3060 (PEP 3115 metaclass syntax)
# 3061 (string literals become unicode)
# 3071 (PEP 3109 raise changes)
# 3081 (PEP 3137 make __file__ and __name__ unicode)
# 3091 (kill str8 interning)
# 3101 (merge from 2.6a0, see 62151)
# 3103 (__file__ points to source file)
# Python 3.0a4: 3111 (WITH_CLEANUP optimization).
# Python 3.0b1: 3131 (lexical exception stacking, including POP_EXCEPT
#3021)
# Python 3.1a1: 3141 (optimize list, set and dict comprehensions:
# change LIST_APPEND and SET_ADD, add MAP_ADD #2183)
# Python 3.1a1: 3151 (optimize conditional branches:
# introduce POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE
#4715)
# Python 3.2a1: 3160 (add SETUP_WITH #6101)
# tag: cpython-32
# Python 3.2a2: 3170 (add DUP_TOP_TWO, remove DUP_TOPX and ROT_FOUR #9225)
# tag: cpython-32
# Python 3.2a3 3180 (add DELETE_DEREF #4617)
# Python 3.3a1 3190 (__class__ super closure changed)
# Python 3.3a1 3200 (PEP 3155 __qualname__ added #13448)
# Python 3.3a1 3210 (added size modulo 2**32 to the pyc header #13645)
# Python 3.3a2 3220 (changed PEP 380 implementation #14230)
# Python 3.3a4 3230 (revert changes to implicit __class__ closure #14857)
# Python 3.4a1 3250 (evaluate positional default arguments before
# keyword-only defaults #16967)
# Python 3.4a1 3260 (add LOAD_CLASSDEREF; allow locals of class to override
# free vars #17853)
# Python 3.4a1 3270 (various tweaks to the __class__ closure #12370)
# Python 3.4a1 3280 (remove implicit class argument)
# Python 3.4a4 3290 (changes to __qualname__ computation #19301)
# Python 3.4a4 3300 (more changes to __qualname__ computation #19301)
# Python 3.4rc2 3310 (alter __qualname__ computation #20625)
# Python 3.5a1 3320 (PEP 465: Matrix multiplication operator #21176)
# Python 3.5b1 3330 (PEP 448: Additional Unpacking Generalizations #2292)
# Python 3.5b2 3340 (fix dictionary display evaluation order #11205)
# Python 3.5b3 3350 (add GET_YIELD_FROM_ITER opcode #24400)
# Python 3.5.2 3351 (fix BUILD_MAP_UNPACK_WITH_CALL opcode #27286)
# Python 3.6a0 3360 (add FORMAT_VALUE opcode #25483)
# Python 3.6a1 3361 (lineno delta of code.co_lnotab becomes signed #26107)
# Python 3.6a2 3370 (16 bit wordcode #26647)
# Python 3.6a2 3371 (add BUILD_CONST_KEY_MAP opcode #27140)
# Python 3.6a2 3372 (MAKE_FUNCTION simplification, remove MAKE_CLOSURE
# #27095)
# Python 3.6b1 3373 (add BUILD_STRING opcode #27078)
# Python 3.6b1 3375 (add SETUP_ANNOTATIONS and STORE_ANNOTATION opcodes
# #27985)
# Python 3.6b1 3376 (simplify CALL_FUNCTIONs & BUILD_MAP_UNPACK_WITH_CALL
#27213)
# Python 3.6b1 3377 (set __class__ cell from type.__new__ #23722)
# Python 3.6b2 3378 (add BUILD_TUPLE_UNPACK_WITH_CALL #28257)
# Python 3.6rc1 3379 (more thorough __class__ validation #23722)
# Python 3.7a1 3390 (add LOAD_METHOD and CALL_METHOD opcodes #26110)
# Python 3.7a2 3391 (update GET_AITER #31709)
# Python 3.7a4 3392 (PEP 552: Deterministic pycs #31650)
# Python 3.7b1 3393 (remove STORE_ANNOTATION opcode #32550)
# Python 3.7b5 3394 (restored docstring as the first stmt in the body;
# this might affected the first line number #32911)
# Python 3.8a1 3400 (move frame block handling to compiler #17611)
# Python 3.8a1 3401 (add END_ASYNC_FOR #33041)
# Python 3.8a1 3410 (PEP570 Python Positional-Only Parameters #36540)
# Python 3.8b2 3411 (Reverse evaluation order of key: value in dict
# comprehensions #35224)
# Python 3.8b2 3412 (Swap the position of positional args and positional
# only args in ast.arguments #37593)
# Python 3.8b4 3413 (Fix "break" and "continue" in "finally" #37830)
评论