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 Super Mode is introduced from PyArmor 6.2.0. In this mode the
structure of PyCode_Type is changed, and byte code or word code is mapped, it’s
the highest security level in PyArmor. There is only one runtime file required,
that is extension
pytransform, and the form of obfuscated scripts is unique,
no so called Bootstrap Code which may make some users confused. All the
obfuscated scripts would be like this:
from pytransform import pyarmor pyarmor(__name__, __file__, b'\x0a\x02...', 1)
It’s recommended to enable this mode in suitable cases. Now only the latest Python versions are supported:
- Python 2.7
- Python 3.7
- Python 3.8
It may support Python 3.5, 3.6 later, but Python 3.0~3.4 is out of plan.
In order to enable it, set option
--advanced 2 to obfuscate:
pyarmor obfuscate --advanced 2 foo.py
More usage refer to Using Super Mode
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
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).
VM mode is introduced since 6.3.3. VM mode is based on code virtualization, it uses a strong vm tool to protect the core algorithm of dynamic library. This mode is an enhancement of advanced mode and super mode.
Enable vm mode with advanced mode by this way:
pyarmor obfuscate --advanced 3 foo.py
Enable sumper mode with advanced mdoe by this way:
pyarmor obfuscate --advanced 4 foo.py
Though vm mode improves the security remarkably, but the size of dynamic library is increased, and the performance is reduced. The original size is about 600K~800K, but in vm mode the size is about 4M. About the performances, refer to The Performance of Obfuscated Scripts to test it.
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
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 == 2 (Default)
Use different cipher algorithm, more security and faster, new since v6.3.0
- 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
Each obfuscated script has its own restrict mode used to limit the usage of this script. When importing an obfuscated module or start to run any function in the obfuscated module, it will first check the restrict mode of this script. If the restrict mode is violated, protection exception will be raised.
There are 4 restrict mode, mode 2 and 3 are mainly for standalone scripts, and mode 4 is for obfuscated packages.
- Mode 1
In this mode, the obfuscated scripts can’t be changed at all. For example, append one print statement at the end of the obfuscated script foo.py:
__pyarmor__(__name__, __file__, b'...', 1) print('This is obfuscated module')
This script will raise restrict exception when it’s imported.
- Mode 2
In this mode, the obfuscated scripts can’t be imported from plain script. But it could be run by Python interpreter directly, or imported by other obfuscated scripts.
For example, foo2.py is obfuscated by mode 2. It can be run like this:
But try to import it from any plain script. For example:
python -c'import foo2'
It will raise protection exception.
- Mode 3
It’s an enhancement of Mode 2. When call any function in the obfuscated module with mode 3, it will check the caller. If the caller is not obfuscated, protection exception is raised.
For example, foo3.py, mod3.py are obfuscated in mode 3. The former is main script, it will call some functions in the latter. If try to call any function in the mod3 from plain script test.py, for example:
import mod3 mod3.hello()
Now run command python test.py, the first statement import will check the caller test.py, if test.py is not obfuscated, it will raise exception.
Note that in case mod3 has been imported before by anyway, the restrict mode will be ignored.
In the second line, when mod3.hello starts, it will check the caller test.py again. If test.py is not obfuscated, it will raise exception.
- Mode 4
It’s almost same as mode 3, when any function in the module is called, it will check the caller, make sure the caller is obfuscated. But it doesn’t check the caller when it’s imported.
It’s mainly used to obfuscate the Python package. The common way is that the __init__.py is obfuscated by restrict mode 1, all the other modules in this package are obfuscated by restrict mode 4.
Since v6.3.7, there is a big improvement. The plain script couldn’t visit module attributes by anyway if this module is obfucated by mode 2, 3 or 4.
Mode 2 and 3 could not be used to obfuscate the Python package.
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
From PyArmor 5.7.0, there is another implicit restrict for obfuscate scripts: 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.