Python代码保护 | pyc 混淆从入门到工具实现

之前接触到 Python 逆向相关的一些 CTF 题目(最近一次是某符的 game),有的给出 Python 的伪指令,还有的直接给了一个被替换过指令的 pyc 文件,于是学习了一下Python 的字节码。学习过程中发现替换字节码指令这个操作其实是 Python 源码保护的一种方式,于是想到有没有不去修改 Python 解释器的方法去保护源码(增加对抗的成本)。

查阅资料发现 Python 源码有几种保护的方式:

1.生成 pyc 文件:这感觉完全不能算保护,uncompyle6 一键反编译,支持 Python 1.0 到 3.8 全部版本(恐怖)

2.py 源码混淆:一般针对 py 源码混淆就是往代码里插入一些没有意义跳转分支,修改变量名和函数名等这些操作,但是这种虽然阅读起来很难理解,但是混淆效果并不好。

3.打包成可执行的二进制文件

4.自定义 opcode 的 Python 解释器

学习了 Python 字节码之后,就想从 pyc 文件入手,去做一些混淆,因为虽然 uncompyle6 使得 pyc 文件反编译变得很简单,但是简单的无效指令就可能使这类工具失效。

查阅资料过程中发现其实针对 Python2 的字节码有较多的分析,但是针对 Python3 字节码分析就几乎没有了;虽说原理的都是一样的,但是指令和格式上 Python3 都有了一定的变化,并且其实Python3 不同版本之间的变化也是较大的,所以下面先对 pyc 格式进行简单的版本对比分析,然后再谈谈混淆的思路。

生成 pyc 文件
pyc 文件其实包含的是 Python 虚拟机可执行的的 byte-code。

Python 自带的 py_compile 模块可以直接把源码编译成 pyc 文件,我们平时的的模块导入(import)也是会将导入的模块对应的文件编译成 pyc 的。

pyc 文件的格式

magic number + 源代码文件信息 + PyCodeObject

  • 4个字节的 magic number
  • 12个字节的源代码文件信息(不同版本的 Python 包含的长度和信息都不一样,后面说)
  • 序列化之后的 PyCodeObject

magic number

像大多数的文件格式一样,pyc 文件开头也有一个 magic number,不过不一样的是 pyc 文件的 magic number 并不固定,而是不同版本的 Python 生成的 pyc 文件的 magic number 都不相同。这里可以看到看不同版本的 Python 的 magic number 是多少。前两个字节以小端的形式写入,然后加上 \r\n 形成了四个字节的 pyc 文件的magic number

如 Python2.7 的 magic number 为 MAGIC_NUMBER = (62211).to_bytes(2, ‘little’) + b’\r\n’

我们可以看到的前四个字节的16进制形式为 03f3 0d0a
在这里插入图片描述
python 2.7生成的 pyc 文件前32个字节

源代码文件信息

源代码文件信息在 Python 不同的版本之后差别较大

  • 在Python2的时候,这部分只有4个字节,为源代码文件的修改时间的 Unix timestamp(精确到秒)以小端法写入,如上图 (1586087865).to_bytes(4, ‘little’).hex() -> b9c7 895e。
  • 在 Python 3.5 之前的版本已经找不到了(后面就都从 Python 3.5 开始讨论了)
  • Python 3.5 和 3.6 相对于 Python 2,源代码文件信息这部分,在时间后面增加了4个字节的源代码文件的大小,单位字节,以小端法写入。如源码文件大小为87个字节,那么文件信息部分就写入 5700 0000。加上前面的修改时间,就是 b9c7 895e 5700 0000
    在这里插入图片描述
    python 3.6生成的 pyc 文件前32个字节
  • 从 Python3.7 开始支持 hash-based pyc 文件

Changed in version 3.7: Added hash-based .pyc files. Previously, Python only supported timestamp-based invalidation of bytecode caches.

也是就说,Python 不仅支持校验 timestrap 来判断文件是否修改过了,也支持校验 hash 值。Python 为了支持 hash 校验又使源代码文件信息这部分增加了4个字节,变为一共12个字节。
在这里插入图片描述
python 3.7生成的 pyc 文件前32个字节

但是这个 hash 校验默认是不启用的(可以通过调用 py_compile 模块的 compile 函数时传入参数invalidation_mode=PycInvalidationMode.CHECKED_HASH 启用)。不启用时前4个字节为0000 0000,后8个字节为3.6和3.7版本一样的源码文件的修改时间和大小;当启用时前4个字节变为0100 0000或者0300 0000,后8个字节为源码文件的 hash 值。

PyCodeObject

其实这是一个定义在 Python 源码 Include/code.h 中的结构体,结构体中的数据通过 Python 的 marshal 模块序列化之后存到了 pyc文件当中。(不同版本之间 PyCodeObject 的内容是不一样的,但是这就导致了不同版本之间的 Python 产生的 pyc 文件其实并不完全通用,以下举例均使用 python 3.7)

marshal 模块中实现了一些基本的 Python 对象(也就是 PyObject )的序列化,一个 PyObject 序列化时首先会写入一个字节表示这是一个什么类型的 PyObject,不同类型的 PyObject 对应的类型如下,PyCodeObject 对应的就是 TYPE_CODE,写入第一个字节就是63。

// Python/marshal.c
// ......
#define TYPE_NULL               '0'
#define TYPE_NONE               'N'
#define TYPE_FALSE              'F'
#define TYPE_TRUE               'T'
#define TYPE_STOPITER           'S'
#define TYPE_ELLIPSIS           '.'
#define TYPE_INT                'i'
/* TYPE_INT64 is not generated anymore.
   Supported for backward compatibility only. */
#define TYPE_INT64              'I'
#define TYPE_FLOAT              'f'
#define TYPE_BINARY_FLOAT       'g'
#define TYPE_COMPLEX            'x'
#define TYPE_BINARY_COMPLEX     'y'
#define TYPE_LONG               'l'
#define TYPE_STRING             's'
#define TYPE_INTERNED           't'
#define TYPE_REF                'r'
#define TYPE_TUPLE              '('
#define TYPE_LIST               '['
#define TYPE_DICT               '{'
#define TYPE_CODE               'c'
#define TYPE_UNICODE            'u'
#define TYPE_UNKNOWN            '?'
#define TYPE_SET                '<'
#define TYPE_FROZENSET          '>'
#define FLAG_REF                '\x80' /* with a type, add obj to index */

// 以下都是Python3.5之后支持的
#define TYPE_ASCII              'a'
#define TYPE_ASCII_INTERNED     'A'
#define TYPE_SMALL_TUPLE        ')'
#define TYPE_SHORT_ASCII        'z'
#define TYPE_SHORT_ASCII_INTERNED 'Z'
// ......

在这里插入图片描述
python 3.7生成的 pyc 文件前32个字节

但是我们发现我们第17个字节也就是 PyCodeObject 的第一个字节却是 0xe3,这是因为 PyObject 对象第一个字节还可以有一个 flag(# define FLAG_REF ‘\x80’),即第一个字节为0x63 | 0x80 -> 0xe3。( FLAG_REF 表示将这个对象加入引用列表,当下次再出现这个对象的实现就可以不用再序列化一遍这个对象,直接使用 TYPE_REF 取这个对象就可以了;算是 Python 序列化的一种优化吧。Python2 实现不同。)

/* Bytecode object */
typedef struct {
   
  PyObject_HEAD
    int co_argcount;            /
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值