的都是通过 w_byte 写到 pyc 文件中。了解完函数的整体结构之后,我们再看一下具体细节,看看它在写入对象的时候到底写入了哪些内容?
static void w_complex_object(PyObject *v, char flag, WFILE *p) { //...... else if (PyList_CheckExact(v)) { W_TYPE(TYPE_LIST, p); n = PyList_GET_SIZE(v); W_SIZE(n, p); for (i = 0; i < n; i++) { w_object(PyList_GET_ITEM(v, i), p); } } else if (PyDict_CheckExact(v)) { Py_ssize_t pos; PyObject *key, *value; W_TYPE(TYPE_DICT, p); /* This one is NULL object terminated! */ pos = 0; while (PyDict_Next(v, &pos, &key, &value)) { w_object(key, p); w_object(value, p); } w_object((PyObject *)NULL, p); } //...... }
以列表和字典为例,它们在写入的时候实际上写的是内部的元素,其它对象也是类似的。
def foo(): lst = [1, 2, 3] # 把列表内的元素写进去了 print( foo.__code__.co_consts ) # (None, 1, 2, 3)
但问题来了,如果只是写入元素的话,那么Python在加载的时候怎么知道它是一个列表呢?所以在写入的时候不能光写数据,类型信息也要写进去。我们再看一下上面列表和字典的写入逻辑,里面都调用了W_TYPE,它负责将类型信息写进去。
因此无论对于哪种对象,在写入具体数据之前,都会先调用W_TYPE将类型信息写进去。如果没有类型信息,那么当Python加载pyc文件的时候,只会得到一坨字节流,而无法解析字节流中隐藏的结构和蕴含的信息。
所以在往 pyc 文件里写入数据之前,必须先写入一个标识,诸如TYPE_LIST, TYPE_TUPLE, TYPE_DICT等等,这些标识正是对应的类型信息。
如果解释器在 pyc 文件中发现了这样的标识,则预示着上一个对象结束,新的对象开始,并且也知道新对象是什么样的对象,从而也知道该执行什么样的构建动作。当然,这些标识也是可以看到的,在底层已经定义好了。
到了这里可以看到,Python 对 PyCodeObject 对象的导出实际上是不复杂的。因为不管什么对象,最后都为归结为两种简单的形式,一种是数值写入,一种是字符串写入。
上面都是对数值的写入,比较简单,仅仅需要按照字节依次写入 pyc 即可。然而在写入字符串的时候,Python 设计了一种比较复杂的机制,有兴趣可以自己阅读源码,这里不再介绍。
字节码混淆
最后再来说一下字节码混淆,我们知道 pyc 是可以反编译的,而且目前也有现成的工具。但这些工具它会将每一个指令都解析出来,所以字节码混淆的方式就是往里面插入一些恶意指令(比如加载超出范围的数据),让反编译工具在解析的时候报错,从而失去作用。
但插入的恶意指令还不能影响解释器执行,因此还要插入一些跳转指令,从而让解释器跳过恶意指令。
混淆之后多了两条指令,其中偏移量为 8 的指令,参数为 255,但执行的时候会发生越界,因此反编译的时候毫无疑问会报错。而解释器在执行的时候却没有问题,因为在执行到偏移量为 6 的指令时出现了一个绝对跳转,直接跳到偏移量为 10 的指令了。
因此对于解释器执行来说,混淆前后是没有区别的。但对于反编译工具而言就会无法正常工作,因为它会把每一个指令都解析一遍。
根据这个思路,我们可以插入很多很多的恶意指令,然后再用跳转指令来跳过这些不合法指令。当然混淆的手段并不止这些,我们还可以添加一下虚假的分支,然后在执行时跳转到真实的分支当中。
而这一切的目的,都是为了防止别人根据 pyc 文件反推出源代码。不过这种做法属于治标不治本,如果真的想要保护源代码的话,可以使用 Cython 将其编译成 pyd ,这是最推荐的做法。
本文地址:百科问答频道 https://www.neebe.cn/wenda/903398_3.html,易企推百科一个免费的知识分享平台,本站部分文章来网络分享,本着互联网分享的精神,如有涉及到您的权益,请联系我们删除,谢谢!