4.4. 深入了解 RFT 模式
Pyarmor 的 RFT 模式能够对模块中的函数,类,方法,属性,变量等进行重命名,相当于把源代码重新写了一下,所以这种加密方式不可逆的。
启用 RFT 模式之后,模块中的所有定义的名称都会发生变化,外部脚本将无法使用原来的名称进行导入。如果需要输出某些名称被外部脚本使用,需要在模块中定义 __all__
,这里面列出的名称会被保留。
RFT 模式会修改下列名称
函数
类
方法
全局变量
局部变量
内置名称
导入到名称
RFT 模式不会修改的名称
参数名称
调用函数的时候使用的关键字名称
使用模块属性
__all__
输出的所有名称所有以
__
开头的名称
使用 RFT 模式的时候,必须把所有使用的脚本和包在同一条命令进行加密。Pyarmor 会分析脚本之间的调用关系,对导入的名称也进行正确的重命名。例如:
src/
foo.py
joker/
__init__.py
card.py
使用 RFT 模式加密脚本 foo.py 以及包 joker 的命令如下:
pyarmor gen --enable-rft foo.py src/joker/
Pyarmor 使用内置的自动规则和人工配置的规则来分析脚本,并进行重命名。虽然 RFT 模式的自动规则可以处理大多数的情况,但是对于复杂的脚本和包,肯定会存在一些名称没有被正确处理。因为 Python 语言自身的特点,Pyarmor 并不认为可以找到算法自动处理所有的情况,所以对特殊情况,必须要进行人工配置规则。RFT 模式的目标是不断更新和完善自动规则,对于不能自动处理的部分,能够自动生成相关的参考配置,用户通过这些参考配置生成人工规则。
对于复杂的脚本,不要期望 Pyarmor 会自动进行处理所有情况。例如,
foo().stack[2].count = 3
(a+b).tostr().get()
是否对属性 stack
, count
, tostr
和 get
进行重命名呢?在有些情况下,使用静态的语法分析根本无法判断属性所在的变量类型,因为有些表达式的类型是动态执行的时候确定的,甚至同一个表达式会在不同的情况下返回不同的类型。
为了处理这种情况,RFT 模式提供了两种方法:
rft-auto-exclude
自动排除未知属性,这是默认方法。
这种方法是搜索所有的脚本中所有的属性链,如果属性所在的类型名称无法确定,那么把这个属性名称增加到排除属性表,所有被排除的属性在其他任何脚本中都不会被重命名。
排除属性表存放的文件名称是
.pyarmor/rft_exclude_table
第一次运行 RFT 模式加密脚本的时候,这个表是空的。RFT 模式依次扫描各个脚本,发现无法处理的属性,就把它添加到这个表中。当所有的脚本都加密完成之后,这个表里面的名称就是所有无法处理的属性名称。
这时候再次使用 RFT 模式使用相同的参数加密脚本,就可以保证所有的未知属性没有命名,基本不会出现名称绑定的错误。
RFT 模式不会自动删除这个表
.pyarmor/rft_exclude_table
,而是不断增加新的未知属性到里面。如果有需要的话,可以把这个表删除掉,然后重新开始一个全新的重命名过程。这种方法使用比较简单,但是可能会排除很多名称,查看排除属性表可以知道那些属性名称被保留下来了。
rft-auto-include
自动命名所有的类和方法。
这种方法是首先扫描全部脚本,把脚本中函数,类和方法全部都进行重命名,确保这些名称能被重命名。
对于那些属性链中无法处理的属性名称,保留下来不进行处理。
用户需要运行加密脚本,如果出现名称错误,那么把这些名称人工进行排除,或者使用人工规则进行排除。
不断重复上述步骤,直到没有名称错误出现。
这种方法能够重命名大部分的名称,但是需要更多额外的工作。
需要注意的是这种方式局部变量不会被重命名,但是它们会在后面的加密过程中被重命名,除非你使用命令 pyarmor cfg mix_localnames=0 禁止重命名局部变量。
4.4.1. 启用 RFT 模式
使用下面的命令启用 RFT mode:
$ pyarmor gen --enable-rft foo.py
也可以使用 pyarmor cfg 通过配置文件启用:
$ pyarmor cfg enable_rft=1
$ pyarmor gen foo.py
启用 rft-auto-include 方法通过禁用 rft_auto_exclude
:
$ pyarmor cfg rft_auto_exclude=0
重新启用 rft-auto-exclude 方法:
$ pyarmor cfg rft_auto_exclude=1
4.4.2. 查看重命名之后的完整脚本
使用 RFT 模式修改后的脚本究竟能否满足安全需要?可能需要先看一下。
在 Python 3.9+ 启用 RFT 跟踪模式,可以输出相应的脚本:
$ pyarmor cfg trace_rft 1
$ pyarmor gen --enable-rft foo.py
$ ls .pyarmor/rft
foo.py
转换后对应的脚本是 .pyarmor/rft/foo.py
:
$ cat .pyarmor/rft/foo.py
转换后的脚本表示的只是 RFT 模式所进行的修改,并不完全等价于加密后的脚本,它还会按照加密选项的设置进行下面的处理。例如 docstring 现在还保留着,但是如果使用了下面的配置项:
$ pyarmor cfg optimize 2
加密脚本最终会把所有 DocString 删除。
备注
Python 3.8 以及之前的版本不支持该功能。
4.4.3. 查看重命名的名称
如果需要跟踪那些名称被重命名,可以同时启用日志跟踪和RFT 跟踪选项,这时候会生成跟踪日志 pyarmor.trace.log
,里面有所有的重命名日志:
$ pyarmor cfg enable_trace=1 trace_rft=1
$ pyarmor gen --enable-rft foo.py
$ grep trace.rft pyarmor.trace.log
trace.rft foo:1 (import sys as pyarmor__1)
trace.rft foo:12 (self.wScan->self.pyarmor__4)
第一个日志记录了 sys
被重命名为 pyarmor__1
第二条日志记录了属性 wScan
被重命名为 pyarmor__4
4.4.4. 人工排除属性名称
如果加密后的脚本出现名称错误,最简单的解决方式就是把这个名称人工排除,也就是说,在所有的脚本中保留这个名称(属性)而不进行重命名。下面的命令把名称 mouse_keybd
保留下来:
$ pyarmor cfg rft_excludes + "mouse_keybd"
$ pyarmor gen --enable-rft foo.py
如果错误显示的名称是像 pyarmor__22
这样的格式,那么首先通过跟踪日志反向查找出原来的名称:
$ grep pyarmor__22 pyarmor.trace.log
trace.rft foo:65 (self.height->self.pyarmor__22)
trace.rft foo:81 (self.height->self.pyarmor__22)
例如,上例中 height
是 pyarmor__22
原来的名称,那么使用下面的命令保留这个名称:
$ pyarmor cfg rft_excludes + "height"
$ pyarmor gen --enable-rft foo.py
$ python dist/foo.py
如果需要把这些人工排除的名称清除,使用下面的命令:
$ pyarmor cfg rft_excludes ""
4.4.5. 处理模块属性 __all__
模块属性 __all__
中定义的名称会被保留下来,可以输出名称供外部脚本使用。例如,下面的函数 foo
会被保留下来不进行重命名
__all__ = ['foo']
def foo(msg):
print(msg)
def _private_foo(msg):
print(msg)
如果输出的是一个类,那么类中的方法和属性也会被保留下来。
有时候可能需要重命名所有的类和方法,可以使用下面的选项让 RFT 模式忽略 __all__
中定义的名称:
$ pyarmor cfg rft_export__all__ 0
4.4.6. 处理特殊的导入语句
导入全部名称的语句 — from module import *
— 会进行特殊的处理。
如果是从一个加密模块里面导入所有名称,那么 RFT 模式会直接解析源代码并直到属性 __all__
的定义。
如果是从一个外部模块中导入所有名称,那么 RFT 模式会尝试直接导入这个模块,并且尝试得到模式属性 __all__
。如果导入失败,会导致 RFT 模式报错退出。在这种情况下,需要设置环境变量 PYTHONPATH
或者其他任何方式让 Python 可以导入这个模块。
如果模块属性 __all__
没有定义,那么模块的所有属性中不是以 _
开头的名称都会被导入进来。
4.4.7. 自定义重命名规则
自定义规则可用于重命名无法自动识别的类型的属性名称,自定义规则仅适用于 rft-auto-include:
$ pyarmor cfg rft_auto_exclude=0
定义全局重命名规则:
pyarmor cfg rft_rulers ^ "self.task.x self.task.?"
定义模块私有重命名规则:
pyarmor cfg -p joker.card rft_rulers "self.task.x self.task.?"
重命名规则格式:
属性链模式 对应属性重命名方式
属性链模式是以 .
分开的 fnmatch
的模式字符串,然后使用此模式和属性链进行匹配
满足匹配的属性链按照后面的重命名方式进行处理
?
: 总是进行重命名其他值: 保留原来的名称
人工规则只用来修改属性名称,而忽略第一个名称
例如,下面的规则:
self.task.x self.task.?
应用于下面的脚本
1class Sdipmk:
2
3 def __init__(self):
4 self.width = 100
5 self.height = 200
6
7 def move(self, x, y, absolute=False):
8 self.task.x = int(abs(x*65536/self.width)) if absolute else int(x)
9 self.task.y = int(abs(y*65536/self.height)) if absolute else int(y)
10 return Mouse(MS_MOVE, x, y)
使用下面的命令配置重命名规则:
$ pyarmor cfg rft_rulers "self.task.x self.task.?"
然后检查结果:
$ pyarmor gen --enable-rft foo.py
$ grep trace.rft pyarmor.trace.log
trace.rft foo:8 (self.task.x->self.task.pyarmor__2)
第 8 行的 self.task.x
被重命名为 self.task.pyarmor__2
让我们修改一下重命名规则,然后在看看结果:
$ pyarmor cfg rft_rulers "self.task.x self.?.?"
$ grep trace.rft pyarmor.trace.log
trace.rft foo:8 (self.task.x->self.pyarmor__1.pyarmor__2)
接下来增加一条新规则重命名 self.task.y
,注意使用 ^
来增加规则:
$ pyarmor cfg rft_rulers ^"self.task.y self.?.?"
$ grep trace.rft pyarmor.trace.log
trace.rft foo:8 (self.task.x->self.pyarmor__1.pyarmor__2)
trace.rft foo:9 (self.task.y->self.pyarmor__1.pyarmor__3)
这两条规则可以合并成为一条,这里使用 =
进行配置,会自动删除原来的所有规则:
$ pyarmor cfg rft_rulers = "self.task.* self.?.?"
$ grep trace.rft pyarmor.trace.log
trace.rft foo:8 (self.task.x->self.pyarmor__1.pyarmor__2)
trace.rft foo:9 (self.task.y->self.pyarmor__1.pyarmor__3)