Advanced Topics

Obfuscating Many Packages

There are 3 packages: pkg1, pkg2, pkg2. All of them will be obfuscated, and use shared runtime files.

First change to work path, create 3 projects:

mkdir build
cd build

pyarmor init --src /path/to/pkg1 --entry __init__.py pkg1
pyarmor init --src /path/to/pkg2 --entry __init__.py pkg2
pyarmor init --src /path/to/pkg3 --entry __init__.py pkg3

Then make runtime files, save them in the path dist:

pyarmor build --output dist --only-runtime pkg1

Next obfuscate 3 packages, save them in the dist:

pyarmor build --output dist --no-runtime pkg1
pyarmor build --output dist --no-runtime pkg2
pyarmor build --output dist --no-runtime pkg3

Check all the output and test these obfuscated packages:

ls dist/

cd dist
python -c 'import pkg1
import pkg2
import pkg3'

Distributing Obfuscated Scripts To Other Platform

First list and download dynalic library of target platform by command download:

pyarmor download --list
pyarmor download linux_x86_64

Then specify platform name as obfuscating the scripts:

pyarmor obfuscate --platform linux_x86_64 foo.py

For project:

pyarmor build --platform linux_x86_64

Obfuscating Scripts By Other Version Of Python

If there are multiple Python versions installed in the machine, the command pyarmor uses default Python. In case the scripts need to be obfuscated by other Python, run pyarmor by this Python explicitly.

For example, first find pyarmor.py:

find /usr/local/lib -name pyarmor.py

Generally it should be in the /usr/local/lib/python2.7/dist-packages/pyarmor in most of linux.

Then run pyarmor as the following way:

/usr/bin/python3.6 /usr/local/lib/python2.7/dist-packages/pyarmor/pyarmor.py

Obfuscating Python Scripts In Different Modes

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

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

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

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)

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

Using Plugin to Extend License Type

PyArmor could extend license type for obfuscated scripts by plugin. For example, check internet time other than local time.

First create plugin check_ntp_time.py:

# Uncomment the next 2 lines for debug as the script isn't obfuscated,
# otherwise runtime module "pytransform" isn't available in development
# from pytransform import pyarmor_init
# pyarmor_init()

from pytransform import get_license_code
from ntplib import NTPClient
from time import mktime, strptime
import sys

NTP_SERVER = 'europe.pool.ntp.org'
EXPIRED_DATE = get_license_code()[4:]

def check_expired():
    c = NTPClient()
    response = c.request(NTP_SERVER, version=3)
    if response.tx_time > mktime(strptime(EXPIRED_DATE, '%Y%m%d')):
        sys.exit(1)

Then insert 2 comments in the entry script foo.py:

...

# {PyArmor Plugins}

...

def main():
    # PyArmor Plugin: check_expired()

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    main()

Now obfuscate entry script:

pyarmor obfuscate --plugin check_ntp_time foo.py

By this way, the content of check_ntp_time.py will be insert after the first comment:

# {PyArmor Plugins}

... the conent of check_ntp_time.py

At the same time, the prefix of second comment will be stripped:

def main():
    check_expired()

So the plugin takes effect.

If the plugin file isn’t in the current path, use absolute path instead:

pyarmor obfuscate --plugin /usr/share/pyarmor/check_ntp_time foo.py

Or set environment variable PYARMOR_PLUGIN. For example:

export PYARMOR_PLUGIN=/usr/share/pyarmor/plugins
pyarmor obfuscate --plugin check_ntp_time foo.py

Finally generate one license file for this obfuscated script:

pyarmor licenses NTP:20190501

Bundle Obfuscated Scripts To One Executable File

Run the following command to pack the script foo.py to one executable file dist/foo.exe. Here foo.py isn’t obfuscated, it will be obfuscated before packing:

pyarmor pack -e " --onefile" foo.py
dist/foo.exe

If you don’t want to bundle the license.lic of the obfuscated scripts into the executable file, but put it outside of the executable file. For example:

dist/foo.exe
dist/license.lic

So that we could generate different licenses for different users later easily. Here are basic steps:

  1. First create runtime-hook script copy_licese.py:

    import sys
    from os.path import join, dirname
    with open(join(dirname(sys.executable), 'license.lic'), 'rb') as fs:
        with open(join(sys._MEIPASS, 'license.lic'), 'wb') as fd:
            fd.write(fs.read())
    
  2. Then pack the scirpt with extra options:

    pyarmor pack --clean --without-license \
            -e " --onefile --icon logo.ico --runtime-hook copy_license.py" foo.py
    

Option –without-license tells pyamor not to bundle the license.lic of obfuscated scripts to the final executable file. By option –runtime-hook of PyInstaller, the specified script copy_licesen.py will be executed before any obfuscated scripts are imported. It will copy outer license.lic to right path.

Try to run dist/foo.exe, it should report license error.

  1. Finally run pyarmor licenses to generate new license for the obfuscated scripts, and copy new license.lic and dist/foo.exe to end users:

    pyarmor licenses -e 2020-01-01 tom
    cp license/tom/license.lic dist/
    
    dist/foo.exe