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.

Advanced 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 foo.py

In next minor version, this mode may be enabled by default.

Upgrade Notes:

Before upgrading, please estimate Python interpreter in product environments to be 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 if there are more than about 30 functions in one module, this module could not be obfuscated by advanced mdoe (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.

Wrap Mode

  • 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

  1. First op is JUMP_ABSOLUTE, it will jump to offset n
  2. 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
  3. 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

  1. __armor_enter__ will restore the obfuscated bytecode
  2. Execute the real function code
  3. 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_algorithm( original_code  );
sprintf( buffer, "__pyarmor__(__name__, __file__, b'%s', 1)", obfuscated_code );
  • obf_mod == 0

In this mode, 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)

They’re eanbled by default. And they only could be changed in the project, refer to Obfuscating Scripts With Different Modes

Restrict Mode

From PyArmor 5.2, Restrict Mode is default setting. In restrict 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'...')

And obfuscated script must be imported from obfuscated script. No any other statement can be inserted into obfuscated scripts.

For examples, it works:

$ cat a.py
from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'...')

$ python a.py

It doesn’t work, because there is an extra code “print”:

$ cat b.py
from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'...')
print(__name__)

$ python b.py

Restrict mode could be disabled by this way if required:

pyarmor obfuscate --restrict=0 foo.py