The Modes of Obfuscated Scripts¶
PyArmor could obfuscate the scripts in many modes in order to balance the security and performance. In most of cases, the default mode works fine. But if the performace is to be bottle-block or in some special cases, maybe you need understand what the differents of these modes and obfuscate the scripts in different mode so that they could work as desired.
This feature Advanced Mode is introduced from PyArmor 5.5.0.In this mode the structure of PyCode_Type is changed a little to improve the security. And a hook also is injected into Python interpreter so that the modified code objects could run normally. Besides if some core Python C APIs are changed unexpectedly, the obfuscated scripts in advanced mode won’t work. Because this feature is highly depended on the machine instruction set, it’s only available for x86/x64 arch now. And pyarmor maybe makes mistake if Python interpreter is compiled by old gcc or some other C compiles. It’s welcome to report the issue if Python interpreter doesn’t work in advanced mode.
Take this into account, the advanced mode is disabled by default. In order to
enable it, pass option
--advanced to command obfuscate:
pyarmor obfuscate --advanced 1 foo.py
In next minor version, this mode may be enabled by default.
Before upgrading, please estimate Python interpreter in product environments to be sure it works in advanced mode. Here is the guide
It is recommended to upgrade in the next minor version.
In trial version the module could not be obfuscated by advanced mdoe if there are more than about 30 functions in this module, (It still could be obfuscated by non-advanced mode).
Obfuscating Code Mode¶
In a python module file, generally there are many functions, each function has its code object.
- obf_code == 0
The code object of each function will keep it as it is.
- obf_code == 1 (Default)
In this case, the code object of each function will be obfuscated in different ways depending on wrap mode.
- obf_code == 2
Almost same as obf_mode 1, but obfuscating bytecode by more complex algorithm, and so slower than the former.
- wrap_mode == 0
When wrap mode is off, the code object of each function will be obfuscated as this form:
0 JUMP_ABSOLUTE n = 3 + len(bytecode) 3 ... ... Here it's obfuscated bytecode of original function ... n LOAD_GLOBAL ? (__armor__) n+3 CALL_FUNCTION 0 n+6 POP_TOP n+7 JUMP_ABSOLUTE 0
When this code object is called first time
- First op is JUMP_ABSOLUTE, it will jump to offset n
- At offset n, the instruction is to call PyCFunction __armor__. This function will restore those obfuscated bytecode between offset 3 and n, and move the original bytecode at offset 0
- After function call, the last instruction is to jump to offset 0. The really bytecode now is executed.
After the first call, this function is same as the original one.
- wrap_mode == 1 (Default)
When wrap mode is on, the code object of each function will be wrapped with try…finally block:
LOAD_GLOBALS N (__armor_enter__) N = length of co_consts CALL_FUNCTION 0 POP_TOP SETUP_FINALLY X (jump to wrap footer) X = size of original byte code Here it's obfuscated bytecode of original function LOAD_GLOBALS N + 1 (__armor_exit__) CALL_FUNCTION 0 POP_TOP END_FINALLY
When this code object is called each time
- __armor_enter__ will restore the obfuscated bytecode
- Execute the real function code
- In the final block, __armor_exit__ will obfuscate bytecode again.
Obfuscating module Mode¶
- obf_mod == 1 (Default)
The final obfuscated scripts would like this:
__pyarmor__(__name__, __file__, b'\x02\x0a...', 1)
The third parameter is serialized code object of the Python script. It’s generated by this way:
PyObject *co = Py_CompileString( source, filename, Py_file_input ); obfuscate_each_function_in_module( co, obf_mode ); char *original_code = marshal.dumps( co ); char *obfuscated_code = obfuscate_whole_module( original_code ); sprintf( buffer, "__pyarmor__(__name__, __file__, b'%s', 1)", obfuscated_code );
- obf_mod == 0
In this mode, the last statement would be like this to keep the serialized module as it is:
sprintf( buffer, "__pyarmor__(__name__, __file__, b'%s', 0)", original_code );
And the final obfuscated scripts would be:
__pyarmor__(__name__, __file__, b'\x02\x0a...', 0)
All of these modes only could be changed in the project for now, refer to Obfuscating Scripts With Different Modes
From PyArmor 5.7.0, the Bootstrap Code must be in the obfuscated scripts and must be specified as entry script. For example, there are 2 scripts foo.py and test.py in the same folder, obfuscated by this command:
pyarmor obfuscate foo.py
Inserting the bootstrap code into obfuscated script dist/test.py by manual doesn’t work, because it’s not specified as entry script. It must be run this command to insert the Bootstrap Code:
pyarmor obfuscate --no-runtime --exact test.py
If you need insert the Bootstrap Code into plain script, first obfuscate an empty script like this:
echo "" > pytransform_bootstrap.py pyarmor obfuscate --no-runtime --exact pytransform_bootstrap.py
Then import pytransform_bootstrap in the plain script.
From PyArmor 5.5.6, there are 4 restrice modes:
- Mode 1
In this mode, obfuscated scripts must be one of the following formats:
__pyarmor__(__name__, __file__, b'...') Or from pytransform import pyarmor_runtime pyarmor_runtime() __pyarmor__(__name__, __file__, b'...') Or from pytransform import pyarmor_runtime pyarmor_runtime('...') __pyarmor__(__name__, __file__, b'...')
No any other statement can be inserted into obfuscated scripts.
For examples, the obfuscate scirpt b.py doesn’t work, because there is an extra line “print”:
$ cat b.py from pytransform import pyarmor_runtime pyarmor_runtime() __pyarmor__(__name__, __file__, b'...') print(__name__) $ python b.py
- Mode 2
In this mode, except that the obfuscated scripts can’t be changed, there are 2 restricts:
- The entry script must be obfuscated
- The obfuscated scripts could not be imported out of the obfuscated script
For example, this command will raise error if the foo.py is obfuscated by restrict mode 2:
$ python -c'import foo'
- Mode 3
In this mode, there is another restrict base on Mode 2:
- All the functions in the obfuscated script cound not be called out of the obfuscated scripts.
- Mode 4
It’s similar with Mode 3, but there is a exception:
- The entry script could be plain script
It’s mainly used for obfuscating Python package. The __init__.py is obfuscated by restrict mode 1, all the other scripts are obfuscated by restrict mode 4.
For example, it’s the content of mypkg/__init__.py
# mypkg/ # __init__.py is obfuscated by restrict mode 1 # foo.py is obfuscated by restrict mode 4 # The "foo.hello" could not be called by plain script directly from .foo import hello # The "open_hello" could be called by plain scirpt def open_hello(msg): print('This is public hello: %s' % msg) # The "proxy_hello" could be called by plain scirpt def proxy_hello(msg): print('This is proxy hello: %s' % msg) # The "foo.hello" could be called by obfuscated "__init__.py" hello(msg)
Mode 2 and 3 could not be used to obfuscate the Python package, because it will be imported from other plain scripts.
Restrict mode is applied to one single script, different scripts could be obfuscated by different restrict mode.
From PyArmor 5.2, Restrict Mode 1 is default.
Obfuscating the scripts by other restrict mode:
pyarmor obfuscate --restrict=2 foo.py pyarmor obfuscate --restrict=4 foo.py # For project pyarmor config --restrict=2 pyarmor build -B
All the above restricts could be disabled by this way if required:
pyarmor obfuscate --restrict=0 foo.py # For project pyarmor config --restrict=0 pyarmor build -B
For more examples, refer to Improving The Security By Restrict Mode