2.9. 如何解决第三方库调用加密脚本存在的问题

Pyarmor 提供了丰富的选项就是为了解决不同情况下遇到的问题,当使用一个复杂的第三方包调用加密脚本出现问题的时候,请首先花费一点时间看看 命令手册 ,去了解所有的选项和作用,有一些选项就是专门针对不同的应用情况设计的。 Pyarmor 开发组不会去告诉应该使用那个选项就可以解决你遇到的问题 ,而是你需要去学习和了解 Pyarmor,并找到适合自己需要的选项。

Python 在各个领域得到广泛的应用,有很多包我甚至从来都没有用过。对于我来说,不会去学习每一个包,然后确保其能够和 Pyarmor 兼容。通常的处理方式是用户报告相关的异常,根据异常的行号给出附近的源代码,我可以帮助分析哪些地方可能和 Pyarmor 发生冲突并给出相应的解决方案。

对于属于 Pyarmor 的问题,Pyarmor 开发组会尽快解决,但是有一些问题是 Pyarmor 自身无法解决的。

通常情况下,使用第三方包主要导致的问题来源有:

  • 使用 sys._getframe 去访问函数的局部变量,或者其他运行框架的信息,但是加密脚本的运行框架和普通脚本是不一样的

  • 使用 inspect 或者其他方式直接去访问函数的源代码(Byte code)等相关属性,它们所访问的属性正好就是 Pyarmor 所保护的

  • 使用 pickle 或者类似功能序列号加密函数,然后把加密函数传递给其他进程或者线程执行,但是加密函数是无法使用普通方法序列化的

这些问题都来源于第三方包使用到了加密脚本修改过的底层对象,更多的不同之处请参考 加密脚本和原来脚本的区别 。如果使用被修改的特性,就会出现不兼容的问题。特别是使用 BCC 模式 进行加密的脚本,改变的内容更多。

对于这些问题,有一些常用的解决方案,可以首先尝试下面的方式

  • 使用 RFT 模式 和选项 --obf-code 0

    RFT 模式 几乎不改变原来脚本的内部结构,所以一般不会导致不兼容性。使用 --obf-code 禁用代码对象的加密也是为了保持内部对象的结构不改变。这是一种推荐的解决方案:

    $ pyarmor gen --enable-rft --obf-code 0 /path/to/myapp
    

    首先使用最少的选项确保其能工作,然后在尝试更多的选项去增加安全性。例如:

    $ pyarmor gen --enable-rft --obf-code 0 --mix-str /path/to/myapp
    $ pyarmor gen --enable-rft --obf-code 0 --mix-str --assert-call /path/to/myapp
    
  • 忽略有问题的模块

    在一个复杂的应用中,如果只是个别模块存在问题,可以不加密这些模块。例如,如果只有模块 config.py 不能正常工作,使用模块私有配置的方式把这个模块忽略掉:

    $ pyarmor cfg -p myapp.config obf_code=0
    $ pyarmor gen [other options] /path/to/myapp
    

    也可以按照原来的方法加密,只是加密之后使用原来的脚本直接把加密脚本替换:

    $ pyarmor gen [other options] /path/to/myapp
    $ cp /path/to/myapp/config.py dist/myapp/config.py
    
  • 修改第三方库

    这是一个实例,如果使用别名 "myapi" 去调用,抛出 404 错误,但是不使用别名工作正常。

    @cherrypy.expose(alias='myapi')
       @cherrypy.tools.json_out()
       # pylint: disable=no-member
       @cherrypy.tools.authenticate()
       @cherrypy.tools.validateOptOut()
       @cherrypy.tools.validateHttpVerbs(allowedVerbs=['POST'])
       # pylint: enable=no-member
       def abc_xyz(self, arg1, arg2):
           """
           This is the doc string
           """
    

    导致问题出现的原因在于 cherrypy.expose 使用下面的语句

    parents = sys._getframe(1).f_locals
    

    因为 sys._getframe(1) 在加密脚本返回的不是期望的执行框架,把它修改成为下面的语句就可以在加密脚本正常使用:

    parents = sys._getframe(2).f_locals
    

    备注

    如果第三方包 cheerypy 也被其他程序使用,请为加密脚本创建一个私有的包

2.9.1. 常用的第三方库

这里列出了一些常用到的第三方库以及可能的解决方案,欢迎大家分享自己的使用经验,提交 Pull request 增加新的第三方库。

表-1. 常用第三方库列表

状态

备注

cherrypy

打补丁 1

问题原因是使用了 sys._getframe

pandas

打补丁 1

问题原因是使用了 sys._getframe

playwright

打补丁应该可以工作 2

尚未验证

nuitka

使用 restrict_module = 0 之后应该可以工作

尚未验证

说明

1(1,2)

通过打补丁的方式可以使用加密脚本

2

可以使用 RFT 模式 加密的脚本

3

只有使用 --obf-code 0 加密的脚本可以工作

4

任何方式都无法使用加密脚本

2.9.1.1. pandas

另外一个实例是 pandas

import pandas as pd

class Sample:
    def __init__(self):
        self.df = pd.DataFrame(
            data={'name': ['Alice', 'Bob', 'Dave'],
            'age': [11, 15, 8],
            'point': [0.9, 0.1, 0.4]}
        )

    def func(self, val: float = 0.5) -> None:
        print(self.df.query('point > @val'))

sampler = Sample()
sampler.func(0.3)

加密之后运行报错:

pandas.core.computation.ops.UndefinedVariableError: local variable 'val' is not defined

同样需要对其打补丁,把 pandas scope.py 里面的 sys._getframe(self.level) 修改成为 sys._getframe(self.level+1)sys._getframe(self.level+2) 或者 sys._getframe(self.level+3)

2.9.1.2. nuitka

把加密脚本作为普通脚本一样使用 Nuitka 进行处理,但是需要禁用约束模式:

$ pyarmor cfg restrict_module=0

如果不禁用约束模式,会导致校验错误 RuntimeError: unauthorized use of script

首先使用默认选项进行加密:

$ pyarmor gen foo.py

然后在尝试使用更多选项,但是约束相关的这些选项,例如 --private--restrict--assert-call--assert-import 可能无法使用。

2.9.1.3. streamlit

需要禁用下列选项,然后在进行加密:

$ pyarmor cfg restrict_module=0
$ pyarmor cfg clear_module_co=0

$ pyarmor gen foo.py

不禁用第一项可能会报错 RuntimeError: unauthorized use of script (1:1102)

不禁用第二项可能会报错 RuntimeError: the format of obfuscated script is incorrect (1:1082)

不过 Streamlit 依旧可能无法直接使用加密脚本,因为它是直接访问甚至修改 code object