吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 13736|回复: 88
收起左侧

[Python 原创] 手把手教你定制python解释器,保护你的python源代码

    [复制链接]
zldtb19931116 发表于 2021-9-17 16:06
本帖最后由 zldtb19931116 于 2021-9-17 16:47 编辑

由于python无需编译直接运行的特性,很容易导致源码泄露,自己的劳动成果被窃取。前段时间我所在的公司就出现这样的情况。

很多时候我们希望别人能使用我的代码,但不希望别人看到我的实现方式,这时候就需要一套机制来保护我的代码。

众所周知,.py代码可以编译位pyc,在一定程度上保护代码。但pyc其实是一堆字节码,通过对比字节码对应的操作很容易还原成.py。网上甚至有很多工具能直接还原。以python3.8.5为例:

首先下载python源码:https://www.python.org/ftp/python/3.8.5/Python-3.8.5.tgz

在源代码opcode.h里就定义了每个操作码对应的操作,直接将pyc里的操作码一个个翻译就行(有点类似将机器码翻译成汇编指令):

opcode.h

opcode.h


这种情况下想要保护我们的代码,就需要一种特别的方式,例如插入花指令,混淆代码。但这种方式对从事逆向工作的人来说,安全性并不高(我也经常逆向别人的代码,几乎看不到友好的源码,都是abcdefg甚至用一长串乱码作为方法名参数名。)。另一种比较安全的方式就是直接修改操作码,定制解释器。

按照这个思路,我们只需要将操作码随机呼唤,例如原来的0x01是POP_TOP,我们在opcode.h里将0x01换成NOP,同样的方式,我们随机的将更多opcode互换(注意小于90的只能和小于90的互换,因为90定义了操作是否有参数)。
image.png
注意修改Include/opcode.h以后,一下2个文件里的opcode也要同步修改,这一步可以写脚本完成,注意要跟opcode.h的替换一致:
Lib/opcode.py
Python/opcode_targets.h

然后编译我们自己的python解释器(按照官方文档,下载源代码按上面的修改后,直接一条龙configure,make,make install。install的时候可能会报错,这是因为编译过程会把一部分需要import的代码硬编码到一个数组里,我们修改了opcode会导致iimport失败,所以这儿我们还需要执行“make regen-importlib”)最后我们编写的py代码编译成pyc,那这个pyc在我们的环境能正常运行,别人拿到了无法运行也无法正常反编译。

例如,我们写个简单的代码:
[Python] 纯文本查看 复制代码
for i in range(10):
    print(i)


保存为test.py,运行:

运行test.py

运行test.py


然后我们用我们自己编译的python环境,将test.py编译成pyc
[Shell] 纯文本查看 复制代码
python3 -m py_compile test.py

使用以上命令后,会在当前路径下生成__pycache__目录,pyc文件就在这个目录里面,直接运行,效果如下:

运行pyc

运行pyc


可以看到正常运行,没有什么问题。这时候我们再把这个pyc拿到正常的python环境下运行,结果如下:

bad magic number

bad magic number


提示bad magic number in .pyc file。这就是因为我们替换了opcode,普通的python环境已经无法运行。
然后我们将这个pyc拿到网上的反编译工具,也会无法正常反编译。

做到这一步,其实还不够,因为别人直接在我们编译的python环境下还是可以反编译。因此,我们还需要进一步,将pyc代码加密。比如,我将pyc加密成pye格式。做这一步之前,我们简单看看pyc的结构。
image.png

有兴趣深究的可以慢慢研究各个字段的作用,这里我们只需要知道pyc前面有个PyObject_HEAD就行(因为我们加密opcode可以不需要前面这部分head), 前面这个head占16个字节,只起到表明文件类型、编译时间以及一些校验,对实际的代码运行影响不大,实际运行时这部分只是为了确认是不是一个合法的pyc,所以我们可以为了方便直接跳过,直接将后面的所有字节加密,我采用的是变形的aes加密。最后将加密后的字节存入一个新的文件,pye。
image.png

通常一个python工程,肯定不止一个py文件,一般会有一大堆,所以,加个递归循环,把目标文件夹下所有py编译成pyc,然后加密成pyc。整个过程保持文件名不变(这样确保可以你的import不会报错,正常运行)。
image.png

到这儿,py代码就被加密成pye了,不拿到我们的加密脚本和定制的python解释器,普通人肯定是没办法还原出源代码的。但是现在,我们的解释器也没办法运行pye,所以要继续修改python解释器运行逻辑,让解释器认识pye。为了简单,我们可以直接照抄运行pyc的逻辑,然后稍加修改,增加一个解密过程。

通过阅读python3.8.5源码,我们能发现,主入口在Program/python.c里面
image.png

这儿调用了Py_BytesMain函数,我们一路跟,跳转到Modules/main.c里的pymain_run_python函数。
image.png
这里面的几个if分别表示了在命令行运行,作为模块运行和文件运行的入口,我们直接到pymain_run_file里看,一顿操作后,终于在Python/pythonrun.c里的PyRun_SimpleFileExFlags函数403行找到了pyc的运行逻辑:
image.png
maybe_pyc_file判断如果是pyc文件,执行下面的run_pyc_file来运行pyc文件。所以我们直接照着maybe_pyc_file,在下面新加一个maybe_pyc_file,再在run_pyc_file下面加一个run_pye_file,函数的参数和返回值类型保持不变。我们先来看看这个函数做了什么:
image.png
maybe_pyc_file比较简单,先判断文件后缀名是否是’.pyc',然后校验magic_number,也就是pyc前面几个字节(开头说的PyObject_HEAD)。 至于run_pyc_file函数就不用看了,先直接复制就行,我们只需要知道他是在运行pyc代码,当然有兴趣的也可以进去看看,我一般喜欢简单粗暴。
我们在maybe_pye_file里直接校验下文件后缀名是否为'.pye'就行,这儿我没有加校验,后续可能会加。。。
image.png
然后把run_pyc_file里的代码直接照抄到run_pye_file里,这里调用了Python/marshal.c里的PyMarshal_ReadLastObjectFromFile函数,从文件里读取python 的object。
image.png

那我们也照着在marshal.c里加一个PyMarshal_ReadLastObjectFromEncryptedFile函数,还是参数和返回值类型保持不变,
image.png
在原来的PyMarshal_ReadLastObjectFromFile代码基础上加上解密过程就行,解密算法我这儿为了效率用的aes,跟上面的加密过程对应,这样就照着pyc运行过程完成了pye的运行。最后我们在if (maybe_pyc_file(fp, filename, ext, closeit))后面加上else if(maybe_pye_file(fp, filename, ext_pe, closeit)),然后运行run_pye_file就完成了.
image.png

到这儿我们编译python源码,configure、make、make install一条龙以后,试着用上面的脚本把teste.py加密成pye文件,然后运行,代码编译没出错的话,就会看到如下结果。如果编译出错,别忘了我们新加的几个函数都要声明(java和python用惯了有段时间没写c,我一开始就忘了)。
微信截图_20210917150103.png

到现在,就能正常把py代码加密成pye,然后正常运行了,如果我们修改过的python解释器代码不泄露,应该还是很难被别人还原出源码的。但是到现在还差一步,我们单个文件能运行了,但是如果是python工程,有多个py文件要相互import,到这一步就会抛出异常说无法找到模块,这是因为python解释器现在在import的时候还是不能正常import pye,所以我们还要继续改import的逻辑,让python能import我们加密后的pye。

这部分代码是用python来实现的,所以修改起来相对比较容易,代码位置:/Lib/importlib/_bootstrap_external.py,在290行有个BYTECODE_SUFFIXES列表,这个就是import支持的格式,我们直接在这个列表里加上一个 ".pye"
image.png image.png

最后在SourcelessFileLoader(FileLoader, _LoaderBasics)这个类的get_code方法里加上一段对pye文件的处理就行,当后缀名为pye,照着下面的代码,中间加个aes_dec将pye文件解密,解密算法还是跟之前处理pyc的一样即可。
image.png

到这儿,基本就全部完成了,我们再次configure,make ,make install一次,写2个python代码,中间相互import调用方法的,加密成pye,然后运行,没报错,完美!为了测试,我在网上随便找了个Django项目,进行测试:
首先加密django项目
image.png
然后pip3 install -r requirements.txt安装需要的依赖。makemiigrations,migrate。
最后运行,原本运行命令应该是:
[Shell] 纯文本查看 复制代码
python3 manage.py runserver 0.0.0.0:8000

现在因为我们把所有代码都加密成了pye,所以应该是:
[Shell] 纯文本查看 复制代码
python3 manage.pye runserver 0.0.0.0:8000

除了后缀名,其他所有参数保持不变.

最终,正常运行:
微信截图_20210917160912.png
image.png

说明我们的这套加密没有问题。目前这套东西已经在我们的多个项目里使用(除了上面的opcode重新替换了下以外),未发现不兼容或者其他的异常。

正式使用时,只需要将我们改造的python解释器和加密过的代码一起放到生产服务器运行即可。

成品暂时就不发了,等过段时间代码优化下再丢到github,有兴趣可以自己动手做起来。

免费评分

参与人数 16威望 +2 吾爱币 +116 热心值 +15 收起 理由
levin222 + 1 + 1 我很赞同!
zhaoqingdz + 1 + 1 谢谢@Thanks!
JUNGLE_CN + 2 + 1 谢谢@Thanks!
内存空间不足 + 1 + 1 我很赞同!
三滑稽甲苯 + 2 + 1 我很赞同!
marione + 1 + 1 谢谢@Thanks!
duyi324 + 1 用心讨论,共获提升!
15136562259 + 1 + 1 太厉害了!!!
pinhai + 1 用心讨论,共获提升!
xuesha + 1 + 1 鼓励转贴优秀软件安全工具和文档!
Tortrix + 1 + 1 谢谢@Thanks!
人比黄瓜瘦儿 + 1 + 1 虽然我看不懂,但是我痛恶白嫖党!
笙若 + 1 + 1 谢谢@Thanks!
苏紫方璇 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
阳光肥肥 + 1 + 1 用心讨论,共获提升!
461735945 + 1 + 1 谢谢@Thanks!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

twilight2017 发表于 2022-10-8 09:14

非常感谢您的回复,那我在这里详细描述一下我遇到的问题
我的设计目的:交换BINARY_ADD和BINARY_SUBTRACT的opcode,定制私有python解释器
1.交换Include/opcode.h中BINARY_ADD和BINARY_SUBTRACT的字节码定义

2.修改Lib/opcode.py
https://note.youdao.com/yws/public/resource/190637575cda02a565aa8e2be9ec6950/xmlnote/7B5F15C6B0A74155BD6D78F5DC55F788/4782
3.修改Python/opcode_targets.h

随后执行.configure, 随后执行make 在这一步报错:
https://note.youdao.com/yws/public/resource/190637575cda02a565aa8e2be9ec6950/xmlnote/DEA6841561BC4CE184BC7A207199CBA1/4787
请教您,这是什么原因呢
levin222 发表于 2023-5-17 00:11
再回来看这个方案

解决 import .pye 文件的这个问题:
最后在SourcelessFileLoader(FileLoader, _LoaderBasics)这个类的get_code方法里加上一段对pye文件的处理就行,当后缀名为pye,照着下面的代码,中间加个aes_dec将pye文件解密,解密算法还是跟之前处理pyc的一样即可。

这个解密算法写在 python\Lib\importlib\_bootstrap_external.py 这个文件里,那是不是,只要任何人看到这里,就是同样运行  _aes_dec()这方法,解密出 pyc文件呢?其他都ok,这里很懵逼啊..是不是我理解哪儿出问题??
yhx5773489 发表于 2021-9-17 16:20
Light紫星 发表于 2021-9-17 16:21
直接用Cython编译成pyd多好
Prozacs 发表于 2021-9-17 16:34
Light紫星 发表于 2021-9-17 16:21
直接用Cython编译成pyd多好

pyd就不可逆了啊
 楼主| zldtb19931116 发表于 2021-9-17 16:37
Prozacs 发表于 2021-9-17 16:34
pyd就不可逆了啊

我们已经试过编译成pyd和linux下编译成so,都有现成的办法直接逆出来,不然也不费这功夫了
 楼主| zldtb19931116 发表于 2021-9-17 16:39
Light紫星 发表于 2021-9-17 16:21
直接用Cython编译成pyd多好

我们已经试过编译成pyd和linux下编译成so,都有现成的办法逆出来,只是不像py那么清晰
RoyPenn 发表于 2021-9-17 16:49
感谢分享,好东西,又学到了
QingYi. 发表于 2021-9-17 17:04
谁都不放心谁,攻与防 都要进步
阳光肥肥 发表于 2021-9-17 17:10
魔改python解释器来增大逆向难度, 不过有试过nuitka吗?将py打包成exe,似乎不是很好逆的样子
 楼主| zldtb19931116 发表于 2021-9-17 17:26
阳光肥肥 发表于 2021-9-17 17:10
魔改python解释器来增大逆向难度, 不过有试过nuitka吗?将py打包成exe,似乎不是很好逆的样子

除了用自己的一套opcode,否则都能调试出来
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 22:43

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表