The Modes of Obfuscated Scripts¶
PyArmor could obfuscate the scripts in many modes in order to balance thesecurity and performance. In most of cases, the default mode works fine. But ifthe performace is to be bottle-block or in some special cases, maybe you needunderstand what the differents of these modes and obfuscate the scripts indifferent mode so that they could work as desired.
Super Mode¶
This featureSuper Mode is introduced from PyArmor 6.2.0. In this mode thestructure of PyCode_Type is changed, and byte code or word code is mapped, it’sthe highest security level in PyArmor. There is only one runtime file required,that is extensionpytransform
, and the form of obfuscated scripts is unique,no so calledBootstrap Code which may make some users confused. All theobfuscated scripts would be like this:
frompytransformimportpyarmorpyarmor(__name__,__file__,b'\x0a\x02...',1)
It’s recommended to enable this mode in suitable cases. Now only the latestPython versions are supported:
- Python 2.7
- Python 3.7
- Python 3.8
- Python 3.9
In order to enable it, set option--advanced2
toobfuscate:
pyarmorobfuscate--advanced2foo.py
More usage refer toUsing Super Mode
Note
It doesn’t work to mix super mode obfuscated scripts and non-super mode ones.
Super Plus Mode¶
This is an enhancement of super mode, it will convert some functions to binarycode. It’s introduced in PyArmor 7.0.1, and now only works for arch X86_64 andPython 3.7, 3.8, 3.9. From PyArmor 7.5.0, Python 3.10 with arch X86_64 works,and Python 3.7~3.10 for arch AARCH64 in Darwin and Linux works.
It requiresc
compiler. In Linux and Darwin,gcc
andclang
is OK. InWindows, onlyclang.exe
works. It could be configured by one of these ways:
- If there is any
clang.exe
, it’s OK if it could be run in any path. - Download and install Windows version ofLLVM
- Downloadhttps://pyarmor.dashingsoft.com/downloads/tools/clang-9.0.zip, it’sabout 26M bytes, there is only one file in it. Unzip it and save
clang.exe
to$HOME/.pyarmor/
.$HOME
is home path of current logon user, check theenvironment variableHOME
to get the real path.
Afterc
compiler works, enable super plus mode by--advanced5
:
pyarmorobfuscate--advanced5foo.py
Only partial functions in the module will be obfuscated by spp mode, all theothers are still obfuscated by super mode. The functions using any feature notsupported by spp mode will be ignored automatically, if something is wrong withthis module in super plus mode, insert one line at the beginning of the moduleto ignore the module manually:
# pyarmor options: no-spp-mode
Super plus mode will scan from the first line, ignore blank lines, parse theline starts with#
, and stop scanning for any other line. If it finds oneline begins withpyarmoroptions
, it will read the options after that. Italso works in the docstring to ignorefunction
orclass
, for example:
deffoo(a,b):'''pyarmor options: no-spp-mode'''pass
There are a few differences in the spp mode:
- Callingraise without argument not in the exception handler will raisedifferent exception.
>>>raiseRuntimeError: No active exception to reraise# In spp mode>>>raiseUnboundlocalError: local variable referenced before assignment
- Some exception messages may different from the plain script.
- Most of function attributes which starts with__ doesn’t exists,or the value is different from the original.
Unsupport features for spp mode:
unsupport_nodes=(ast.ExtSlice,ast.AsyncFunctionDef,ast.AsyncFor,ast.AsyncWith,ast.Await,ast.Yield,ast.YieldFrom,ast.GeneratorExp,ast.NamedExpr,ast.MatchValue,ast.MatchSingleton,ast.MatchSequence,ast.MatchMapping,ast.MatchClass,ast.MatchStar,ast.MatchAs,ast.MatchOr)
And unsupport functions:
- exec,
- eval
- super
- locals
- sys._getframe
- sys.exc_info
For example, the following functions are not obfuscated by super plusmode, because they use unsupported features or unsupported functions:
asyncdefnested():return42deffoo1():fornrange(10):yieldndeffoo2():frame=sys._getframe(2)print('parent frame is',frame)
Note
Super plus mode is not available in the trial version.
Advanced Mode¶
This featureAdvanced Mode is introduced from PyArmor 5.5.0. In this modethe structure of PyCode_Type is changed a little to improve the security. And ahook also is injected into Python interpreter so that the modified code objectscould run normally. Besides if some core Python C APIs are changed unexpectedly,the obfuscated scripts in advanced mode won’t work. Because this feature ishighly depended on the machine instruction set, it’s only available for x86/x64arch now. And pyarmor maybe makes mistake if Python interpreter is compiled byold gcc or some otherC compiles. It’s welcome to report the issue if Pythoninterpreter doesn’t work in advanced mode.
Take this into account, the advanced mode is disabled by default. In order toenable it, pass option--advanced
to commandobfuscate:
pyarmorobfuscate--advanced1foo.py
Upgrade Notes:
Before upgrading, please estimate Python interpreter in product environments tobe sure it works in advanced mode. Here is the guide
https://github.com/dashingsoft/pyarmor-core/tree/v5.3.0/tests/advanced_mode/README.md
It is recommended to upgrade in the next minor version.
Note
In trial version the module could not be obfuscated by advancedmode if there are more than about 30 functions in this module, (Itstill could be obfuscated by non-advanced mode).
Important
For Python3.9 advanced mode isn’t supported. It’s recommended to use supermode for any Python version which works with super mode.
VM Mode¶
VM mode is introduced since 6.3.3. VM mode is based on code virtualization, ituses a strong vm tool to protect the core algorithm of dynamic library. Thismode is an enhancement of advanced mode and super mode.
Enable vm mode with advanced mode by this way:
pyarmorobfuscate--advanced3foo.py
Enable vm mode with super mode by this way:
pyarmorobfuscate--advanced4foo.py
Though vm mode improves the security remarkably, but the size of dynamic libraryis increased, and the performance is reduced. The original size is about600K~800K, but in vm mode the size is about 4M. About the performances, refer toThe Performance of Obfuscated Scripts to test it.
Obfuscating Code Mode¶
In a python module file, generally there are many functions, eachfunction 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 indifferent ways depending on wrap mode.
- obf_code == 2
Almost same as obf_mode 1, but obfuscating bytecode by more complexalgorithm, and so slower than the former.
Wrap Mode¶
Note
For super mode, wrap mode is always enabled, it can’t be disabledin super mode.
- wrap_mode == 0
When wrap mode is off, the code object of each function will beobfuscated 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 0n+6 POP_TOPn+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 bytecodebetween offset 3 and n, and move the original bytecode at offset 0
- After function call, the last instruction is to jump tooffset 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 wrappedwithtry…finally block:
LOAD_GLOBALSN(__armor_enter__)N=lengthofco_constsCALL_FUNCTION0POP_TOPSETUP_FINALLYX(jumptowrapfooter)X=sizeoforiginalbytecodeHereit's obfuscated bytecode of original functionLOAD_GLOBALSN+1(__armor_exit__)CALL_FUNCTION0POP_TOPEND_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 Pythonscript. 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 toObfuscating Scripts With Different Modes
Restrict Mode¶
Each obfuscated script has its own restrict mode used to limit the usage of thisscript. When importing an obfuscated module and using any function or attribute,the restrict mode will be checked at first, raises protection exception if therestrict mode is violated.
There are 5 restrict mode, mode 2 and 3 are only for standalone scripts, mode 4is mainly for obfuscated packages, mode 5 for both.
- Mode 1
In this mode, the obfuscated scripts can’t be changed at all. For example,append oneprint statement at the end of the obfuscated scriptfoo.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, andthe main script must be obfuscated asEntry Script. It could be run byPython interpreter directly, or imported by other obfuscated scripts. When it’simported, it will check the caller and the main script, and make sure both ofthem are obfuscated.
For example,foo2.py is obfuscated by mode 2. It can be run like this:
pythonfoo2.py
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, it also protects module attributes. When visitingany module attribute or calling any module function, the caller will be checkedand raise protection exception if the caller is not obfuscated.
- Mode 4
It’s almost same as mode 3, the only difference is that it doesn’t check themain script is obfuscated or not 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 thispackage are obfuscated by restrict mode 4.
For example, there is packagemypkg:
mypkg/__init__.pyprivate_a.pyprivate_b.py
In the__init__.py
, define public functions and attributes which are used byplain scripts:
from.importprivate_aasmafrom.importprivate_basmbpublic_data='welcome'defproxy_hello():print('Call private hello')ma.hello()defpublic_hello():print('This is public hello')
In theprivate_a.py
, define private functions and attributes:
importsyspassword='xxxxxx'defhello():print('password is:%s'%password)
Then obfuscate__init__.py
by mode 1 and others by mode 4 in thedist:
dist/__init__.pyprivate_a.pyprivate_b.py
Now do some tests from Python interpreter:
importdistasmypkg# It worksmypkg.public_hello()mypkg.proxy_hello()print(mypkg.public_data)print(mypkg.ma)# It doesn't workmypkg.ma.hello()print(mypkg.ma.password)
- Mode 5 (New in v6.4.0)
Mode 5 is an enhancement of mode 4, it also protects the globals in theframe. When running any function in the mode 5, the outer plain script could getnothing from the globals of this function. It’s highest security, works for bothof standalone scripts and packages. But it will check each global variable inruntime, this may reduce the performance.
- Mode 100+ (New in v6.7.4)
This mode is an enhancement for mode 1-5 to enable an extra feature: moduleattribute__dict__
restriction. So mode 101 equals mode 1 plus thisfeature, and mode 102 equals mode 2 plus this feature, and so on.
If this feature is enabled, the module attribute__dict__
looks like anempty dictionary. And there are something changed for this module:
- All the objects in themodule.__dict__ will not be clean when cleanupingthis module
- After this module has been imported,module.__dict__ can’t be inserted ordeleted an item both implicitly and explicitly
- The function ofdirs,vars will also return empty
For example,
# This is foo6.py, obfuscated with mode 105# It's OKglobalvar_avar_a='This is global variable a'var_b='This is global variable b'delvar_adeffabico():globalvar_b# Wrong, remove item from module.__dict__ not in model leveldelvar_b# This is foo.py, obfuscated with mode 101importfoo6# The result is {}foo6.__dict__# The output is {}vars(foo6)# The output is []dirs(foo6)# OKfoo6.var_a='Changed by foo'# Wrong, add new item to __dict__foo6.__dict__['var_c']=1foo6.var_d=2
This feature only works for Python 3.7 and later, it takes no effect forprevious Python version.
Important
The protection of module attributes for mode 3 and 4 is introduced inv6.3.7. Before that, only function calling is protected.
Do not import any function or class from private module in the public__init__.py
, because only module attributes are protected:
# Right, import module onlyfrom.importprivate_aasma# Wrong, function `hello` is opened for plain scriptfrom.private_aimporthello
Note
Mode 2 and 3 could not be used to obfuscate the Python package, because themain script must be obfuscated either, otherwise it can’t not be imported.
Note
Restrict mode is applied to one single script, different scripts could beobfuscated by different restrict mode.
Note
If the scripts are obfuscated by--obf-code=0
, it will be taken as plainscript.
Let’s say there’re three scripts in a package
__init__.py
: [ restrict_mode : 1, obf-code 2]foo.py
: [restrict_mode : 4, obf-code 2]bar.py
: [restrict_mode : 1, obf-code 0]
Herebar.py
would appear as plain script at runtime due to obf-code=0.
Sofoo.py
cannot be imported insidebar.py
since it would appear likea plain script and hence cannot importfoo.py
. Butfoo.py
can beimported inside__init__.py
since it has obf-code=2 and hence would work.
From PyArmor 5.2, Restrict Mode 1 is default.
Obfuscating the scripts by other restrict mode:
pyarmorobfuscate--restrict=2foo.pypyarmorobfuscate--restrict=4foo.py# For projectpyarmorconfig--restrict=2pyarmorbuild-B
All the above restricts could be disabled by this way if required:
pyarmorobfuscate--restrict=0foo.py# For projectpyarmorconfig--restrict=0pyarmorbuild-B
If the obfuscates scripts uses the license generated bylicenses, inorder to disable all the restricts, pass option--disable-restrict-mode
tocommandlicenses. For example:
pyarmorlicenses--disable-restrict-moder001pyarmorobfuscate--with-license=licenses/r001/license.licfoo.py# For projectpyarmorconfig--with-license=licenses/r001/license.licpyarmorbuild-B
For more examples, refer toImproving The Security By Restrict Mode
From PyArmor 5.7.0, there is another implicit restrict for obfuscate scripts:theBootstrap Code must be in the obfuscated scripts and must bespecified as entry script. For example, there are 2 scriptsfoo.py andtest.py in the same folder, obfuscated by this command:
pyarmorobfuscatefoo.py
Inserting thebootstrap code into obfuscated scriptdist/test.py by manualdoesn’t work, because it’s not specified as entry script. It must be run thiscommand to insert theBootstrap Code:
pyarmorobfuscate--no-runtime--exacttest.py
If you need insert theBootstrap Code into plain script, first obfuscatean empty script like this:
echo"">pytransform_bootstrap.pypyarmorobfuscate--no-runtime--exactpytransform_bootstrap.py
Then importpytransform_bootstrap in the plain script.