CTF-逆向题-pyc思路


1. 什么是 pyc 文件

  • 简要介绍:
    • pyc 是 Python 源代码(.py)文件被编译后的字节码文件,通常位于 __pycache__ 文件夹中。
  • 在 CTF 中的常见考点:
    • 恶意字节码修改
    • 防止直接反编译的干扰
    • 反编译字节码
    • py代码逆向
    • 隐藏的调试信息

2. pyc 文件结构与原理

  • 2.1. 文件结构
    • 文件头部:包含 Python 版本、时间戳等信息。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      Python3.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 字节,取代时间戳和大小信息,用于更强的文件完整性校验)
    • 字节码内容:存储程序的操作指令。
  • 2.2. 常见的工具
    • uncompyle6:将 .pyc 文件还原为 .py 文件。
      • 安装方法 pip install uncompyle
    • pycdcpycdas:分别是反编译工具和动态分析工具。
    • marshal 模块:直接加载和读取 .pyc 文件。

3. pyc 逆向通用解题步骤

3.1. 检查文件版本

  • 使用die.exeexeinfo等图形化工具来分析文件类型和版本。

  • 使用 filebinwalk 命令分析文件头:

    1
    file example.pyc
  • 根据版本号选择合适的反编译工具。

    • 示例:
      • Python 3.7:uncompyle6
      • Python 3.9+:推荐 pycdc

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
      6
      import marshal

      with open('example.pyc', 'rb') as f:
      f.read(16) # 跳过头部
      code_obj = marshal.load(f)
      print(code_obj)
    • 手动解析字节码以发现关键逻辑。

  • 情况 2:动态加密

    • 结合 pycdas 动态分析:
      • 在执行时注入调试钩子或打印关键变量。

      • 示例代码:

        1
        2
        import dis
        dis.dis(code_obj)

3.4. 修改后重新运行

  • 若需要动态修改字节码逻辑,可以直接将字节码导出为 .py 文件后重新生成 .pyc 文件:

    1
    python -m py_compile modified.py

3.5.exe 解包pyc

  • 使用pyinstxtractor.pyexe变成结构体和一个文件
    使用方法:*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
2
3
4
5
MAGIC_NUMBER = (3413).to_bytes(2, 'little') + b'\r\n'  
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little')
HEX_MAGIC_NUMBER = hex(_RAW_MAGIC_NUMBER)
print(HEX_MAGIC_NUMBER)
print(HEX_MAGIC_NUMBER.upper()[2:])

这里的3413就是Python 3.8b4版本的Magic Number,执行一下,就得到了四字节的二进制码0x0A0D0D55。其他版本的对应二进制码,可以按照上面步骤计算得到。


4. 题目示例


5. 常见问题与解答

  • Q1:为什么 uncompyle6 提示版本不支持?
    • 检查文件是否属于较新版本 Python,可能需要切换工具(如 pycdc)。
  • 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
    107
    Known 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)