Python pyc文件 bytecode的压缩, 加壳和脱壳解析

news/2024/5/19 22:50:46 标签: python, 字节码, 反编译, bytecode, PyInstaller

我们常常看到, 自己用PyInstaller等库打包的exe被别人反编译。而源代码在exe文件中是以字节码形式存储的。掌握了字节码的加密技巧, 就可以防止源代码的反编译

目录

1.字节码是什么

PyInstaller, py2exe等库会把编译生成的字节码打包进exe中。掌握字节码(bytecode)的知识, 对于PyInstaller打包exe的反编译是十分有用的。

在Python中, 字节码是一种独特的数据类型, 常存在于用python写成的函数中。先放示例:

python">>>> import dis
>>> def f(x):print('hello',x)

>>> type(f.__code__)
<class 'code'>
>>> f.__code__.co_code
b't\x00d\x01|\x00\x83\x02\x01\x00d\x00S\x00'
>>> dis.dis(f)
  1           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('hello')
              4 LOAD_FAST                0 (x)
              6 CALL_FUNCTION            2
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE
>>> 

上述示例中f.__code__就是bytecode对象, f.__code__.co_code就是字节码的二进制部分, 通过dis模块可以反编译、分析这些字节码
关于字节码基础知识, 参见作者的前一篇文章: Python pyc文件 bytecode 字节码解析与插入、编辑

2.包装字节码

python中, bytecode对象的属性是不可修改的。如:

python">>>> def f():pass
>>> f.__code__.co_code = b''
Traceback (most recent call last):
 ... ...
AttributeError: readonly attribute

为了使bytecode对象更易用, 我编写了Code类, 用于包装 (wrap)字节码对象, 使字节码对象变得更易操作。

python"># 读者可暂时跳过本代码, 进入下一节
import sys
try:
    from importlib._bootstrap_external import MAGIC_NUMBER
except ImportError:
    from importlib._bootstrap import MAGIC_NUMBER
from types import CodeType, FunctionType
from collections import OrderedDict
import marshal
import dis
import pickle

_py38=hasattr(compile('','','exec'), 'co_posonlyargcount')
class Code:
    """
# 用于doctest
>>> def f():print("Hello")

>>> c=Code.fromfunc(f)
>>> c.co_consts
(None, 'Hello')
>>> c.co_consts=(None, 'Hello World!')
>>> c.exec()
Hello World!
>>> 
>>> import os,pickle
>>> temp=os.getenv('temp')
>>> with open(os.path.join(temp,"temp.pkl"),'wb') as f:
...     pickle.dump(c,f)
... 
>>> 
>>> f=open(os.path.join(temp,"temp.pkl"),'rb')
>>> pickle.load(f).to_func()()
Hello World!
>>> 
>>> c.to_pycfile(os.path.join(temp,"temppyc.pyc"))
>>> sys.path.append(temp)
>>> import temppyc
Hello World!
>>> Code.from_pycfile(os.path.join(temp,"temppyc.pyc")).exec()
Hello World!
"""
# 关于CodeType: 
# 初始化参数
# code(argcount, kwonlyargcount, nlocals, stacksize, flags, codestring,
#    constants, names, varnames, filename, name, firstlineno,
#    lnotab[, freevars[, cellvars]])

    # 按顺序
    _default_args=OrderedDict(
         [('co_argcount',0),
          ('co_kwonlyargcount',0),
          ('co_nlocals',0),
          ('co_stacksize',1),
          # 如果是函数中的code, 则是OPTIMIZED, NEWLOCALS, NOFREE
          ('co_flags',64), # NOFREE
          ('co_code',b'd\x00S\x00'),#1   LOAD_CONST    0 (None)
                                    #2   RETURN_VALUE
          ('co_consts',(None,)),
          ('co_names',()),
          ('co_varnames',()),
          ('co_filename',''),
          ('co_name',''),
          ('co_firstlineno',1),
          ('co_lnotab',b''),
          ('co_freevars',()),
          ('co_cellvars',())
          ])
    # 与Python3.8及以上版本兼容
    if _py38:
        _default_args['co_posonlyargcount']=0
        _default_args.move_to_end('co_posonlyargcount', last=False)
        _default_args.move_to_end('co_argcount', last=False)

    _arg_types={key:type(value) for key,value in _default_args.items()}
    def __init__(self,code=None,auto_update=True):
        super().__setattr__('_args',self._default_args.copy())
        if code is not None:
            if isinstance(code,Code):
                self._args = code._args
                self._update_code()
            else:
                self._code=code
                for key in self._args.keys():
                    self._args[key]=getattr(code,key)
        else:
            self._update_code()
        self.auto_update=auto_update
    def __getattr__(self,name):
        _args=object.__getattribute__(self,'_args')
        if name in _args:
            return _args[name]
        else:
            # 调用super()耗时较大, 所以改用object
            return object.__getattribute__(self,name)
    def __setattr__(self,name,value):
        if name not in self._args:
            return object.__setattr__(self,name,value)
        if not isinstance(value,self._arg_types[name]):
            raise TypeError(name,value)
        self._args[name]=value
        if self.auto_update: self._update_code()
    def _update_code(self):
        self._code=CodeType(*self._args.values())
    def exec(self,globals_=None,locals_=None):
        if not self.auto_update: self._update_code()

        default={"__builtins__":__builtins__,"__doc__":None,
                  "__loader__":__loader__,"__name__":"__main__"}
        globals_ = globals_ or default
        if not locals_:locals_ = default.copy()
        return exec(self._code,globals_,locals_)
    def eval(self,globals_=None,locals_=None):
        if not self.auto_update: self._update_code()
        return eval(self._code,globals_,locals_)

    # for pickle
    def __getstate__(self):
        return self._args
    def __setstate__(self,state):
        super().__setattr__('_args',self._default_args.copy())
        self._args.update(state)
        if not _py38 and 'co_posonlyargcount' in state:
            del state['co_posonlyargcount']
        self._update_code()
    def __dir__(self):
        return object.__dir__(self) + list(self._args.keys())
    @classmethod
    def fromfunc(cls,function):
        c=function.__code__
        return cls(c)
    @classmethod
    def fromstring(cls,string,mode='exec',filename=''):
        return cls(compile(string,filename,mode))
    def to_code(self):
        return self._code
    def to_func(self,globals_=None,name=''):
        if globals_ is None:
            # 默认
            import builtins
            globals_=vars(builtins)
        return FunctionType(self._code,globals_,name)
    def pickle(self,filename):
        with open(filename,'wb') as f:
            pickle.dump(self,f)
    def show(self,*args,**kw):
        desc(self._code,*args,**kw)
    view=show
    def info(self):
        dis.show_code(self._code)
    def dis(self,*args,**kw):
        dis.dis(self._code,*args,**kw)

3.压缩字节码

压缩字节码的原理是构造一个新的bytecode, 也就是压缩壳, 然后把原先的bytecodemarshal.dumps()转为bytes类型, 然后压缩bytes, 再放入压缩壳中。类似EXE文件的加壳。
程序运行时, 先解压这个bytes数据, 再使用marshal.loads()重新转换为bytecode, 并执行。

python">import sys,marshal,zlib
try:
    from importlib._bootstrap_external import MAGIC_NUMBER
except ImportError:
    from importlib._bootstrap import MAGIC_NUMBER

def dump_to_pyc(pycfilename,code,pycheader=None):
    c=Code() # 构造一个压缩壳
# 反汇编的co_code
##2     0 LOAD_CONST               0 (455)
##      2 LOAD_CONST               1 (None)
##      4 IMPORT_NAME              0 (zlib)
##      6 STORE_NAME               0 (zlib)
##      8 LOAD_CONST               0 (455)
##     10 LOAD_CONST               1 (None)
##     12 IMPORT_NAME              1 (marshal)
##     14 STORE_NAME               1 (marshal)
##
##3    16 LOAD_NAME                2 (exec)
##     18 LOAD_NAME                1 (marshal)
##     20 LOAD_METHOD              3 (loads)
##     22 LOAD_NAME                0 (zlib)
##     24 LOAD_METHOD              4 (decompress)
##     26 LOAD_CONST               2 (数据)
##     28 CALL_METHOD              1
##     30 CALL_METHOD              1
##     32 CALL_FUNCTION            1
##     34 RETURN_VALUE
    c.co_code=b'''d\x00d\x01l\x00Z\x00d\x00d\x01l\x01Z\x01e\x02\
e\x01\xa0\x03e\x00\xa0\x04d\x02\xa1\x01\xa1\x01\x83\x01\x01\x00d\x01S\x00''' # 仅支持Python 3.7及以上, 因为不同版本Python使用的字节码有微小的差别
    c.co_names=('zlib', 'marshal', 'exec', 'loads', 'decompress')
    #也可换成bz2,lzma等其他压缩模块
    c.co_consts=(0, None,zlib.compress(marshal.dumps(code._code),
                                       zlib.Z_BEST_COMPRESSION))
    c.co_flags=64 # NOFREE
    c.co_stacksize=6
    with open(pycfilename,'wb') as f:
        # 写入 pyc 文件头
        if pycheader is None:
            # 自动生成 pyc 文件头
            if sys.winver >= '3.7':
                pycheader=MAGIC_NUMBER+b'\x00'*12
            else:
                pycheader=MAGIC_NUMBER+b'\x00'*8
        f.write(pycheader)
        # 写入bytecode
        marshal.dump(c._code,f)

if len(sys.argv) == 1:
    print('Usage: %s [filename]' % sys.argv[0])

for file in sys.argv[1:]:
    data=open(file,'rb').read()
    if data[16]==0xe3: #标识pyc文件头的结束, marshal数据的开始
        old_header=data[:16];data=data[16:]
    else:
        old_header=data[:12];data=data[12:]
    co = Code(marshal.loads(data))
    dump_to_pyc(file,co,pycheader=old_header)
    print('Processed:',file)

4.加壳字节码(方法一):修改co_code

加壳字节码与压缩不同, 加壳字节码会阻止字节码uncompyle6之类的反编译反编译
这种方法在每个bytecodeco_code末尾加上多余的S\x00
co_consts里依然有bytecode, 而这些bytecode又有co_consts, 所以需要递归处理。

python"># pyc文件压缩、保护工具
import sys,marshal
from inspect import iscode
try:
    from importlib._bootstrap_external import MAGIC_NUMBER
except ImportError:
    from importlib._bootstrap import MAGIC_NUMBER

def process_code(co):
    # 在`co_code`末尾加上多余的`S\x00'。
    co.co_lnotab = b''
    co.co_code += b'S\x00'
    co.co_filename = ''
    #co.co_name = ''
    co_consts = co.co_consts
    # 递归处理
    for i in range(len(co_consts)):
        obj = co_consts[i]
        if iscode(obj):
            data=process_code(Code(obj))
            co_consts = co_consts[:i] + (data._code,) + co_consts[i+1:]
    co.co_consts = co_consts
    return co

def dump_to_pyc(pycfilename,code,pycheader=None):
    # 制作pyc文件
    with open(pycfilename,'wb') as f:
        # 写入 pyc 文件头
        if pycheader is None:
            # 自动生成 pyc 文件头
            if sys.winver >= '3.7':
                pycheader=MAGIC_NUMBER+b'\x00'*12
            else:
                pycheader=MAGIC_NUMBER+b'\x00'*8
        f.write(pycheader)
        # 写入bytecode
        marshal.dump(code._code,f)

for file in sys.argv[1:]:
    data=open(file,'rb').read()
    if data[16]==0xe3:
        old_header=data[:16];data=data[16:]
    else:old_header=data[:12];data=data[12:]
    co = Code(marshal.loads(data))

    process_code(co)
    dump_to_pyc(file,co,pycheader=old_header)
    print('Processed:',file)

尝试反编译加壳后的pyc文件, 意外发现:

python"># --- This code section failed: ---

 L.   2         0  LOAD_CONST               0
                2  LOAD_CONST               None
                4  IMPORT_NAME              sys
                6  STORE_NAME               sys
                8  LOAD_CONST               0
               10  LOAD_CONST               None
               12  IMPORT_NAME              marshal
               14  STORE_NAME               marshal
               ... ... 
              294  LOAD_CONST               None
              296  RETURN_VALUE
              298  RETURN_VALUE
               -1  RETURN_LAST

Parse error at or near `None' instruction at offset -1

说明加壳字节码, 的确能阻止字节码uncompyle6反编译反编译

5.加壳字节码(方法二):混淆变量名

还有一种更加彻底的方法, 也就是将字节码中的变量名改成其他名称, 甚至不符合Python语法的变量名都可以。
这样可以使反编译后的代码难以理解。如果不符合Python语法, 甚至根本无法反编译
co_varnames属性包含了该code使用的本地变量的名称。这里将其中的变量名全部修改为"0", “1”, “2”, …。
基于方法一的代码, 将process_code函数改成下面这样:

python">def process_code(co):
    co.co_lnotab = b''
    co.co_code += b'S\x00'
    co.co_filename = ''
    co_consts = co.co_consts
    co.co_varnames=tuple(str(i) for i in range(len(co.co_varnames))) # 这一步修改、混淆变量名
    # 递归处理
    for i in range(len(co_consts)):
        obj = co_consts[i]
        if iscode(obj):
            data=process_code(Code(obj))
            co_consts = co_consts[:i] + (data._code,) + co_consts[i+1:]
    co.co_consts = co_consts
    return co

6.解压缩, 脱壳字节码

解压缩, 脱壳字节码, 也就是解压原先压缩壳中的bytes数据, 再使用marshal.loads()重新转换为bytecode, 并写入pyc文件。

python">import sys,marshal,traceback
try:
    from importlib._bootstrap_external import MAGIC_NUMBER
except ImportError:
    from importlib._bootstrap import MAGIC_NUMBER

def dump_to_pyc(pycfilename,data,pycheader=None):
    # --snip-- 见前文

for file in sys.argv[1:]:
    try:
        with open(file,'rb') as f:
            d=f.read()
            if d[16]==227:  # 寻找数据开始的'\xe3'标志
                old_header=d[:16];d=d[16:]
            else:
                old_header=d[:12];d=d[12:]
            c=marshal.loads(d)

            modname=c.co_names[0] if len(c.co_names)>=1 else ''
            if modname in ('bz2','lzma','zlib'):
                mod=__import__(modname)
                data=mod.decompress(c.co_consts[2]) # 解压数据
                marshal.loads(data) # 测试解压后数据完整性
                dump_to_pyc(file,data,old_header)
                print('Processed:',file)

            else:
                raise TypeError('不是压缩的pyc文件: '+file)
    except Exception:
        traceback.print_exc()

总结

前面介绍了Python字节码的压缩, 加壳和脱壳, 主要途径是修改字节码的指令, 以及修改、混淆变量名
Python 字节码这一特性有广泛的用途, 例如pyc文件加密、结构优化, 防止反编译pyc文件等, 可以用来做PyInstaller等库打包的exe的源码保护。


http://www.niftyadmin.cn/n/1735324.html

相关文章

小程序笔记

什么是小程序? 小程序是一种不需要下载、安装即可使用的应用&#xff0c;它实现了应用触手可及的梦想&#xff0c;用户扫一扫或者搜一下就能打开应用&#xff0c;也 实现了用完即走的理念&#xff0c;用户不用安装太多应用&#xff0c;应用随处可用&#xff0c;但又无须安装卸…

Python pyd文件的制作和编译,以及程序源代码的保护

在Python程序开发后, 有可能想要保护程序的源代码, 避免被uncompyle6等库反编译。 目录pyd文件是什么安装Visual Studio C编译器方法1: 从py文件生成pyd文件 (常用)方法2: 编写C/C代码, 编译成pyd文件pyd文件是什么 pyd文件类似于DLL, 一般用C/C语言编译而成, 可用作模块导入P…

永久关闭IE 浏览器停止支持提示的方法 (针对360安全卫士或Windows 10)

最近, IE11浏览器已被微软官方停止支持, 用户打开原有的IE浏览器会看见各种提示, 给用户的使用带来了不便。本文介绍在Windows系统中关闭IE 浏览器停止支持提示的方法。 目录针对 Windows 10针对360安全卫士作者探索过程结语针对 Windows 10 打开IE浏览器右上角的设置按钮, 找…

Linux项目自动化构建工具——make和makefile

make和makefile 一.基本使用二.make并不是每一次都会进行编译三.原理四.特殊符号 一.基本使用 首先创建一个mycode.c文件&#xff0c;接着使用vim写几行代码。 接着创建一个makefile文件&#xff08;这里的m大写小写均可但需要在当前目录下&#xff09;&#xff0c;并使用vim进…

Python 调用Windows文件搜索功能 设计搜索框(可解决Windows 11/10 搜索框不能打字)

笔者最近购买了一台Windows 11 电脑, 安装了Windows更新后, 发现资源管理器的文件搜索框不能打字。是不是系统内置的搜索功能损坏了? 其实没有, 只是系统界面出了问题。真正的搜索功能仍然可以通过Python用search-ms链接调用。 目录调用系统搜索功能设计tkinter界面及程序调用…

Python os模块 设计文件夹自动备份、同步工具

背景 我们经常使用U盘来储存和备份文件。但是备份一个文件夹到U盘的时候, 如果文件夹之前已经放到U盘, 那么怎么办? 多数读者会选择替换U盘中原有的文件。但是: 首先, 这种方式速度慢。如果文件夹中有几十上百个文件, 全部复制到U盘, 还不如只复制最近修改的几个文件。 其次,…

Python sha256+zlib库 实现文件加密算法

数字化时代下&#xff0c;数据安全对各大公司及个人的重要性不言而喻。作为Python语言使用者, 如何进行数据的加密和解密呢&#xff1f;本文带领大家来了解一下。 目录sha256算法标准库知识加密与解密数据主程序sha256算法 sha256算法是目前应用非常广泛的数据加密算法, 可以简…

Python pyglet 自制3D引擎入门(二) -- 绘制立体心形,动画和相机控制

Python作为目前较广泛的编程语言, 用于制作3D游戏可谓得心应手。本文讲解使用Python pyglet库自制简易3D引擎的方法技巧。 上篇&#xff1a;Python pyglet 自制3D引擎入门(一) – 绘制几何体、创建3D场景 目录导入pyglet及初始化相机控制3D图形绘制用计时器实现动画效果主程序…