1

背景

老大: python 源码要加密,不给看...
大灰狼先生:好嘞!

实现

实现加密网传可行的有2种方式,代码变量混淆修改编译器操作码(此文采用的方式)

两步

  1. 修改 python 编译器,使得 只有 该特定的编译器才能执行 对应的 pyc 文件
  2. 将基础环境打包成 docker 镜像,作为程序发布的基础镜像

1. 修改操作码方法介绍

修改 python 源码文件

相关的目标文件有三个

  • Lib/opcode.py
  • Include/opcode.h
  • Python/opcode_targets.h

修改策略:打乱操作码
策略摘要:

  1. opcode.py 中 HAVE_ARGUMENT = 90 是分隔符,大于90 的操作码有参数,小于90的操作码没有参数
  2. 将所有操作码分别放入不同的list:not_have_argument_code_listhave_argument_code_list,, 4 个回调操作码除外
  3. not_have_argument_code_listhave_argument_code_list 顺序随机打乱
  4. 遍历 opcode.py 中所有的操作码,依次取 两个 list的最后一个 操作码 作为当前 操作的新的操作码,并将其保存的新的操作码字典 replace_dict
  5. 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 操作码并编译安装~~~~

准备文件:

  1. 上一步的 modify_opcode.py
  2. Python-3.5.2.tgz 源码压缩包,下载传送门
  3. 编译工程文件的 工具类 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!!!


大灰狼先生
6 声望0 粉丝