背景
老大: python 源码要加密,不给看...
大灰狼先生:好嘞!
实现
实现加密网传可行的有2种方式,代码变量混淆,修改编译器操作码(此文采用的方式)
两步:
- 修改 python 编译器,使得 只有 该特定的编译器才能执行 对应的 pyc 文件
- 将基础环境打包成 docker 镜像,作为程序发布的基础镜像
1. 修改操作码方法介绍
修改 python 源码文件
相关的目标文件有三个
- Lib/opcode.py
- Include/opcode.h
- Python/opcode_targets.h
修改策略:打乱操作码
策略摘要:
- opcode.py 中
HAVE_ARGUMENT = 90
是分隔符,大于90 的操作码有参数,小于90的操作码没有参数 - 将所有操作码分别放入不同的list:
not_have_argument_code_list
和have_argument_code_list
,, 4 个回调操作码除外 - 将
not_have_argument_code_list
和have_argument_code_list
顺序随机打乱 - 遍历 opcode.py 中所有的操作码,依次取 两个 list的最后一个 操作码 作为当前 操作的新的操作码,并将其保存的新的操作码字典
replace_dict
- 将
replace_dict
根据操作码 由小到大排序,覆盖写入到Python/opcode_targets.h
文件
修改操作码执行方法 modify_opcodes.py 完整内容:
# -*- coding: utf-8 -*-
# @File : modify_opcodes.py
# @Date : 2019/11/28
# @Author : HiCooper
# @Desc : 修改操作码
# target Python-3.5.2
import argparse
import os
import random
import re
# 修改相关文件:
OPCODE_PY_PATH = "Lib/opcode.py"
OPCODE_H_PATH = "Include/opcode.h"
OPCODE_TARGETS_H_PATH = "Python/opcode_targets.h"
# 回调操作名集合:
CALL_OP = ['CALL_FUNCTION', 'CALL_FUNCTION_KW',
'CALL_FUNCTION_VAR', 'CALL_FUNCTION_VAR_KW']
# opcode_py 文件提取正则
regex_for_opcode_py = r'^(?P<key>[a-z_]+)+\(\'+(?P<name>[A-Z_]+)+\'+\,\s+(?P<code>\d+)(?P<extra>.*)'
# opcode_h 文件提取正则
regex_for_opcode_h = r'^#define\s+(?P<name>[A-Z_]+)\s+(?P<code>\d+)(?P<extra>.*)'
try:
from importlib.machinery import SourceFileLoader
except ImportError:
import imp
class ReplaceOpCode(object):
"""
1. opcode.py 中 `HAVE_ARGUMENT = 90` 是分隔符,大于90 的操作码有参数,小于90的操作码没有参数
2. 将所有操作码分别放入不同的list:`not_have_argument_code_list` 和 `have_argument_code_list`,, 4 个回调操作码除外
3. 将 `not_have_argument_code_list` 和 `have_argument_code_list` 顺序随机打乱
4. 遍历 opcode.py 中所有的操作码,一次区 两个 list的最后一个 操作码 作为当前 操作的新的操作码,并将其保存的新的操作码字典 `replace_dict`
5. 将 `replace_dict` 根据操作码 由小到大排序,写入到 `Python/opcode_targets.h` 文件
"""
def __init__(self, source_directory):
self.replace_dict = {}
self.not_have_argument_code_list = []
self.have_argument_code_list = []
self.set_list(source_directory)
def set_list(self, source_directory):
"""
1. 读取 opcode_py 的内容,保存操作码到 `not_have_argument_code_list` 和 `have_argument_code_list`(跳过 4 个回调操作码)
2. 随机打乱顺序
"""
f1 = open(os.path.join(source_directory, OPCODE_PY_PATH), 'r+')
infos = f1.readlines()
f1.seek(0, 0)
for line in infos:
rex = re.compile(regex_for_opcode_py).match(line)
if rex:
op_code = rex.group('code')
if rex.group('name') in CALL_OP:
continue
elif int(op_code) < 90:
self.not_have_argument_code_list.append(int(op_code))
else:
self.have_argument_code_list.append(int(op_code))
random.shuffle(self.not_have_argument_code_list)
random.shuffle(self.have_argument_code_list)
def replace_file(self, reg, file, is_update_opcode_h=False):
"""
读取 opcode.py 或 opcode.h 内容并进行行替换
"""
f1 = open(file, 'r+')
infos = f1.readlines()
f1.seek(0, 0)
for line in infos:
rex = re.compile(reg).match(line)
if rex:
code = self.get_new_op_code(rex, is_update_opcode_h)
line = line.replace(rex.group('code'), str(code))
f1.write(line)
f1.close()
def get_new_op_code(self, rex, is_update_opcode_h):
"""
获取新的操作码
"""
op_name = rex.group('name')
op_code = rex.group('code')
if is_update_opcode_h:
# 修改 opcode.h 文件时,从已完成字典读取
try:
new_op_code = self.replace_dict[op_name]
except:
new_op_code = op_code
return new_op_code
# 修改 opcode.py 文件时,设置 name 和 code 到 字典 replace_dict
if op_name in CALL_OP:
# 属于回调操作,默认原操作码
new_op_code = int(op_code)
else:
if int(op_code) < 90:
new_op_code = self.not_have_argument_code_list.pop()
else:
new_op_code = self.have_argument_code_list.pop()
self.replace_dict[op_name] = new_op_code
return new_op_code
def write_opcode_targets_contents(self, source_directory):
"""Write C code contents to the target file object.
"""
targets = ['_unknown_opcode'] * 256
for opname, op in sorted(self.replace_dict.items(), key=lambda nc: nc[1]):
targets[op] = "TARGET_%s" % opname
with open(os.path.join(source_directory, OPCODE_TARGETS_H_PATH), 'w') as f:
f.write("static void *opcode_targets[256] = {\n")
sep = ',%s' % os.linesep
f.write(sep.join([" &&%s" % s for s in targets]))
f.write("\n};\n")
def run(self, source_directory):
print('\n====== 开始修改操作码... ======\n')
self.replace_file(reg=regex_for_opcode_py, file=os.path.join(
source_directory, OPCODE_PY_PATH))
self.replace_file(reg=regex_for_opcode_h, file=os.path.join(
source_directory, OPCODE_H_PATH), is_update_opcode_h=True)
self.write_opcode_targets_contents(source_directory)
print('\n====== 修改完成! ======\n')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='modify python opcodes')
parser.add_argument('--src', dest='src', type=str,
help='Python source code path(support relative path)', required=True)
args = parser.parse_args()
src = os.path.abspath(args.src)
replaceOpCode = ReplaceOpCode(src)
replaceOpCode.run(src)
修改执行 python modify_opcodes.py --src=./Python-3.5.2
参考修改:https://blog.51cto.com/juispan/2065568
注意:不是所有的 python 版本都支持 修改操作码,这里测试ton过的 python版本是 Python-3.5.2
2. 构建python执行环境镜像
以centos7 为基础镜像,修改 python3.5.2 操作码并编译安装~~~~
准备文件:
- 上一步的 modify_opcode.py
- Python-3.5.2.tgz 源码压缩包,下载传送门
- 编译工程文件的 工具类 compile.py (后续构建项目镜像时会使用)
构建容器步骤摘要:
步骤1. 安装 编译 python 源码需要的基础包
步骤2. 修改操作码,编译安装,设置环境变量
步骤3. 添加软链
构建 镜像的 Dockfile 完整内容:
# 自定义 python3.5.2 环境
FROM centos:centos7
ENV LANG=en_US.UTF-8
# 工作目录
WORKDIR /python
# 添加脚本,py源码包
ADD Python-3.5.2.tgz compile.py modify_opcode.py /python/
# 安装基础包
RUN python -V && yum -y update && \
yum install -y yum-utils wget make device-mapper-persistent-data \
lvm2 net-tools vim-enhanced gcc zlib* openssl-devel readline sqlite-devel \
readline-devel libffi-devel libSM-devel libXrender libXext-devel && \
yum clean all
# 修改操作码,编译安装,设置环境变量,设置pip源,升级pip, 验证环境
RUN python modify_opcode.py --src=/python/Python-3.5.2/ && \
cd Python-3.5.2 && ./configure --prefix=/usr/local/python3 && \
make && make install && \
echo "export PATH=/usr/local/python3/bin:$PATH" >> /etc/profile.d/python3.sh && \
echo "export LANG=en_US.UTF-8" >> /etc/profile.d/python3.sh && \
source /etc/bashrc && \
mkdir ~/.pip && \
echo "[global]" >> ~/.pip/pip.conf && \
echo "index-url = https://pypi.tuna.tsinghua.edu.cn/simple" >> ~/.pip/pip.conf && \
pip3 install --upgrade pip --no-cache-dir && \
python3 -V && pip3 -V
# 添加软连接
RUN ln -s -b /usr/local/python3/bin/python3 /usr/bin/python3 && \
ln -s -b /usr/local/python3/bin/pip3 /usr/bin/pip3
# Add Tini
ENV TINI_VERSION v0.18.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
CMD ["/bin/bash"]
根据Dockerfile build镜像
docker build -t hicooper/py3.5.2:v1.0 .
验证镜像环境
假设上一步生成的 ImageId 为 23fdd4e27c63
docker run -it --name py3.5.2v1.0 23fdd4e27c63 /bin/bash
成功进入容器后,验证 python 环境就完事了
文件 compile.py 完整内容
# -*- coding: utf-8 -*-
import argparse
import os,sys,shutil
import compileall
def search(curpath, s):
L = os.listdir(curpath) #列出当前目录下所有文件
for subpath in L: #遍历当前目录所有文件
if os.path.isdir(os.path.join(curpath, subpath)): #若文件仍为目录,递归查找子目录
newpath = os.path.join(curpath, subpath)
search(newpath, s)
elif os.path.isfile(os.path.join(curpath, subpath)): #若为文件,判断是否包含搜索字串
if s in subpath and "__pycache__" in curpath:
#移动pyc文件到上级目录
parent_path = os.path.dirname(curpath) ##获得parent_path所在的目录即parent_path的父级目录
shutil.copy(os.path.join(curpath, subpath),parent_path)
#重命名
name=subpath.split(".")
re_name=(name[0]+"."+name[2])
os.rename(os.path.join(parent_path, subpath),os.path.join(parent_path, re_name))
if ".py" in subpath and ".pyc" not in subpath:
#删除py文件
print(os.path.join(curpath, subpath))
os.remove(os.path.join(curpath, subpath))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='modify python opcodes')
parser.add_argument('--src', dest='src', type=str,help='project source code', required=True)
args = parser.parse_args()
workingpath = args.src
compileall.compile_dir(workingpath)
search(workingpath, ".pyc")
print('\n ====== 编译完成! ======\n')
3. 构建项目镜像
以上一步构建的镜像作为基础环境
测试项目仅为一个 app.py 文件,文件内容
# -*- coding: utf-8 -*-
import jieba
text = '道理千万条,安全第一条,行车不规范,亲人两行泪'
print("原句:", text)
seg_list = jieba.cut(text)
print("分词: \n" + " / ".join(seg_list))
对应的 项目构建 Dockerfile 内容
FROM hicooper/py3.5.2:v1.0
ENV LANG=en_US.UTF-8
WORKDIR /app
ADD app.py /app
# install pageckage
RUN pip install jieba && \
python /python/compile.py --src=./
CMD ["python", "app.pyc"]
构建项目镜像docker build -t app .
运行项目docker run app
结果类似
原句: 道理千万条,安全第一条,行车不规范,亲人两行泪
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 1.309 seconds.
Prefix dict has been built succesfully.
分词:
道理 / 千万条 / , / 安全 / 第一条 / , / 行车 / 不 / 规范 / , / 亲人 / 两行 / 泪
结束
打包的项目镜像比较大,这里有:1.39GB, OMG!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。