4.4. Insight Into RFT Mode
For a simple script, pyarmor may reform the scripts automatically. In most of cases, it need extra work to make it work.
This chapter describes how RFT mode work, it’s helpful to solve RFT mode issues of complex package and scripts.
What does RFT mode change?
function
class
method
global variable
local variable
builtin name
import name
What does RFT mode not change?
argument in function definition
keyword argument name in call
all the strings defined in the module attribute
__all__
all the name starts with
__
It’s simple to decide whether or not transform a single name, but it’s difficult for each name in attribute chains. For example,
foo().stack[2].count = 3
(a+b).tostr().get()
So how to handle attribute stack
, count
, tostr
and get
? The problem is that it’s impossible to confirm function return type or expression result type. In some cases, it may be valid to return different types with different arguments.
There are 2 methods for RFT mode to handle name in the attribute chains which don’t know parent type.
rft-auto-exclude
This is default method.
The idea is search all attribute chains in the scripts and analysis each name in the chain. If not sure it’s safe to rename, add it to exclude table, and do not touch all the names in exclude table.
By default the file
.pyarmor/rft_exclude_table
is used to store exclude table.When pyarmor rft mode first run, exclude table is empty. It scans each script and append unknown names to exclude table. After all the scripts are obfuscated, it stores all the names in the exclude table to the file
.pyarmor/rft_exclude_table
.RFT mode doesn’t remove this file, only append new names to it repeatedly, please delete it manually when needed.
When second run rft mode, it loads exclude table from
.pyarmor/rft_exclude_table
. Comparing with the first time exclude table is empty, obviously the second time more names are kept, it may fix some name errors.It’s simple to use, but may leave more names not changed.
rft-auto-include
This method first search all the functions, classes and methods in the scripts, add them to include table, and transform all of them. If same name is used in attribute chains, but can’t make sure its type, leave attribute name as it is.
Note that in rft-auto-include mode, local variables will not be touched, but they’re renamed in next obfuscation process, unless you explicitly disable it by pyarmor cfg mix_localnames=0.
For a simple script, Pyarmor could transform the script automatically. But for a complex script, it may raise name binding error. For example:
$ python dist/foo.py AttributeError: module 'foo' has no attribute 'register_namespace'
In order to fix this problem, exclude the problem name, leave it as it is by this way:
$ pyarmor cfg rft_excludes + "register_namespace" $ pyarmor gen --enable-rft foo.py $ python dist/foo.py
Repeat these steps to exclude all problem names, until it works.
This method could transform more names, but need more efforts to make the scripts work.
4.4.1. Enable RFT Mode
Enable RFT mode in command line:
$ pyarmor gen --enable-rft foo.py
Enable it by pyarmor cfg:
$ pyarmor cfg enable_rft=1
$ pyarmor gen foo.py
Enable rft-auto-include method by disable rft_auto_exclude
:
$ pyarmor cfg rft_auto_exclude=0
Enable rft-auto-exclude method again:
$ pyarmor cfg rft_auto_exclude=1
4.4.2. Check transformed script
When trace rft mode is enabled, RFT mode will generate transformed script in the path .pyarmor/rft
with full package name:
$ pyarmor cfg trace_rft 1
$ pyarmor gen --enable-rft foo.py
$ ls .pyarmor/rft
Check the transformed script:
$ cat .pyarmor/rft/foo.py
Note
This feature only works for Python 3.9+
4.4.3. Trace rft log
When both of trace log and trace rft are enabled, RFT mode will log which names and attributes are transformed:
$ 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)
The first log means module sys
is transformed to pyarmor__1
The second log means wScan
is transformed to pyarmor__4
4.4.4. Exclude name rule
When RFT scripts complain of name not found error, just exclude this name. For example, if no found name mouse_keybd
, exclude this name by this command:
$ pyarmor cfg rft_excludes "mouse_keybd"
$ pyarmor gen --enable-rft foo.py
If no found name like pyarmor__22
, find the original name in the trace log:
$ grep pyarmor__22 pyarmor.trace.log
trace.rft foo:65 (self.height->self.pyarmor__22)
trace.rft foo:81 (self.height->self.pyarmor__22)
From search result, we know height
is the source of pyarmor__22
, let’s append it to exclude table:
$ pyarmor cfg rft_excludes + "height"
$ pyarmor gen --enable-rft foo.py
$ python dist/foo.py
Repeat these step until all the problem names are excluded.
4.4.5. Handle wild card form of import
The wild card form of import — from module import * — is a special case.
If this module is in the obfuscated package, RFT mode will parse the source and check the module’s namespace for a variable named __all__
If this module is outer package, RFT mode could not get the source. So RFT mode will import it and query module attribute __all__
. If this module could not be imported, it may raise ModuleNotFoundError
, please set PYTHONPATH
or any other way let Python could import this module.
If __all__
is not defined, the set of public names includes all names found in the module’s namespace which do not begin with an underscore character (‘_’).
4.4.6. Handle module attribute __all__
By default RFT mode doesn’t touch all the names in the module __all__
. If this name is defined as a Class, its methods and attributes are not changed.
It’s possible to ignore this attribute by this command:
$ pyarmor cfg rft_export__all__ 0
It will transform names in the __all__
, but it may not work if it’s imported by other scripts.
4.4.7. Manual ruler
This is only for rft-auto-include:
$ pyarmor cfg rft_auto_exclude=0
The rule is used to transform name in chain attributes
One line one rule, the rule format:
patterns actions
patterns = pattern1.pattern2.pattern3...
actions = X.X.X...
Each pattern is same as pattern in fnmatch
, each action X
is either char ?
or any word. ?
means transform the corresponding attribute automatically, any other word means not transform this word.
For example, a ruler:
self.task.x self.task.?
apply to this script
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)
First configure this ruler by command:
$ pyarmor cfg rft_rulers "self.task.x self.task.?"
Then check the result:
$ pyarmor gen --enable-rft foo.py
$ grep trace.rft pyarmor.trace.log
trace.rft foo:8 (self.task.x->self.task.pyarmor__2)
line 8 self.task.x
will be transformed to self.task.pyarmor__2
Let’s change action to self.?.?
, and check the result:
$ 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)
Do not change action to ?.?.?
, it doesn’t work, the first action can’t be ?
Let’s add new ruler to change self.task.y
, here need to use ^
to append new line to rulers:
$ 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)
Actually, both of rulers can combined to one:
$ 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)