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 the Runtime Package, save it 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'

Note

The runtime package pytransform in the output path dist also could be move to any other Python path, only if it could be imported.

From v5.7.2, the Runtime Package also could be generate by command runtime separately:

pyarmor runtime

Obfuscating Package No Conflict With Others

Note

New in v5.8.7

Suppose there are 2 packages obfuscated by different developers, could they be imported in the same Python interpreter?

If both of them are obfuscated by trial version of pyarmor, no problem, the answer is yes. But if anyone is obfuscated by registerred version, the answer is no.

Since v5.8.7, the scripts could be obfuscated with option --enable-suffix to generate the Runtime Package with an unique suffix, other than fixed name pytransform. For example:

pyarmor obfuscate --enable-suffix foo.py

The output would be like this:

dist/
    foo.py
    pytransform_vax_000001/
        __init__.py
        ...

The suffix _vax_000001 is based on the registration code of PyArmor.

For project, set enable-suffix by command config:

pyarmor config --enable-suffix 0
pyarmor build -B

Or disable it by this way:

pyarmor config --enable-suffix 1
pyarmor build -B

Distributing Obfuscated Scripts To Other Platform

First list all the avaliable platform names by command download:

pyarmor download
pyarmor download --help-platform

Display the detials with option --list:

pyarmor download --list
pyarmor download --list windows
pyarmor download --list windows.x86_64

Then specify platform name when obfuscating the scripts:

pyarmor obfuscate --platform linux.armv7 foo.py

# For project
pyarmor build --platform linux.armv7

Obfuscating scripts with different features

There may be many available dynamic libraries for one same platform. Each one has different features. For example, both of windows.x86_64.0 and windows.x86_64.7 work in the platform windwos.x86_64. The last number stands for the features:

  • 0: No anti-debug, JIT, advanced mode features, high speed
  • 7: Include anti-debug, JIT, advanced mode features, high security

It’s possible to obfuscate the scripts with special feature. For example:

pyarmor obfuscate --platform linux.x86_64.7 foo.py

Note that the dynamic library with different features aren’t compatible. For example, try to obfuscate the scripts with --platform linux.x86_64.0 in the Windows, the obfuscated scripts don’t work in the target machine:

pyarmor obfuscate --platform linux.x86_64.0 foo.py

Because the full features dynamic library is used in the Windows by default. To fix this problem, set the enviornment variable PYARMOR_PLATFORM to same feature platform as target machine. For example:

PYARMOR_PLATFORM=windows.x86_64.0 pyarmor obfuscate --platform linux.x86_64.0 foo.py

Running Obfuscated Scripts In Multiple Platforms

From v5.7.5, the platform names are standardized, all the available platform names list here Standard Platform Names. And the obfuscated scripts could be run in multiple platforms.

In order to support multiple platforms, all the dynamic libraries for these platforms need to be copied to Runtime Package. For example, obfuscating a script could run in Windows/Linux/MacOS:

pyarmor obfuscate --platform windows.x86_64 \
                  --platform linux.x86_64 \
                  --platform darwin.x86_64 \
                  foo.py

The Runtime Package also could be generated by command runtime once, then obfuscate the scripts without runtime files. For examples:

pyarmor runtime --platform windows.x86_64,linux.x86_64,darwin.x86_64
pyarmor obfuscate --no-runtime --recursive \
                  --platform windows.x86_64,linux.x86_64,darwin.x86_64 \
                  foo.py

Because the obfuscated scripts will check the dynamic library, the platforms must be specified even if there is option --no-runtime. But if the option --no-cross-protection is specified, the obfuscated scripts will not check the dynamic library, so no platform is required. For example:

pyarmor obfuscate --no-runtime --recursive --no-cross-protection foo.py

Note

If the feature number is specified in one of platform, for example, one is windows.x86_64.0, then all the other platforms must be same feature.

Note

If the obfuscated scripts don’t work in other platforms, try to update all the downloaded files:

pyarmor download --update

If it still doesn’t work, try to remove the cahced platform files in the path $HOME/.pyarmor

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

It’s convenient to create a shell script /usr/local/bin/pyarmor3, the content is:

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

And

chmod +x /usr/local/bin/pyarmor3

then use pyarmor3 as before.

In the Windows, create a bat file pyarmor3.bat, the content would be like this:

C:\Python36\python C:\Python27\Lib\site-packages\pyarmor\pyarmor.py %*

Run bootstrap code in plain scripts

Before v5.7.0 the Bootstrap Code could be inserted into plain scripts directly, but now, for the sake of security, the Bootstrap Code must be in the obfuscated scripts. It need another way to run the Bootstrap Code in plain scripts.

First create one bootstrap package pytransform_bootstrap by command runtime:

pyarmor runtime -i

Next move bootstrap package to the path of plain script:

mv dist/pytransform_bootstrap /path/to/script

It also could be copied to python system library, for examples:

mv dist/pytransform_bootstrap /usr/lib/python3.5/ (For Linux)
mv dist/pytransform_bootstrap C:/Python35/Lib/ (For Windows)

Then edit the plain script, insert one line:

import pytransform_bootstrap

Now any other obfuscated modules could be imported after this line.

Note

Before v5.8.1, create this bootstrap package by this way:

echo "" > __init__.py
pyarmor obfuscate -O dist/pytransform_bootstrap --exact __init__.py

Run unittest of obfuscated scripts

In most of obfuscated scripts there are no Bootstrap Code. So the unittest scripts may not work with the obfuscated scripts.

Suppose the test script is /path/to/tests/test_foo.py, first patch this test script, refer to run bootstrap code in plain scripts

After that it works with the obfuscated modules:

cd /path/to/tests
python test_foo.py

The other way is patch system package unittest directly. Make sure the bootstrap package pytransform_bootstrap is copied in the Python system library, refer to run bootstrap code in plain scripts

Then edit /path/to/unittest/__init__.py, insert one line:

import pytransform_bootstrap

Now all the unittest scripts could work with the obfuscated scripts. It’s useful if there are many unittest scripts.

Let Python Interpreter Recognize Obfuscated Scripts Automatically

In a few cases, if Python Interpreter could recognize obfuscated scripts automatically, it will make everything simple:

  • Almost all the obfuscated scripts will be run as main script
  • In the obfuscated scripts call multiprocessing to create new process
  • Or call Popen, os.exec etc. to run any other obfuscated scripts

Here are the base steps:

  1. First create one bootstrap package pytransform_bootstrap:

    pyarmor runtime -i
    

    Before v5.8.1, it need be created by obfuscating an empty package:

    echo "" > __init__.py
    pyarmor obfuscate -O dist/pytransform_bootstrap --exact __init__.py
    
  2. Then create virtual python environment to run the obfuscated scripts, move the bootstrap package to virtual python library. For example:

    # For windows
    mv dist/pytransform_bootstrap venv/Lib/
    
    # For linux
    mv dist/pytransform_bootstrap venv/lib/python3.5/
    
  1. Edit venv/lib/site.py or venv/lib/pythonX.Y/site.py, import pytransform_bootstrap before the main line:

    import pytransform_bootstrap
    
    if __name__ == '__main__':
        ...
    

It also could be inserted into the end of function main, or anywhere they could be executed as module site is imported.

After that in the virtual environment python could run the obfuscated scripts directly, because the module site is automatically imported during Python initialization.

Refer to https://docs.python.org/3/library/site.html

Note

The command pyarmor doesn’t work in this virtual environment, it’s only used to run the obfuscated scripts.

Note

Before v5.7.0, you need create the bootstrap package by the Runtime Files manually.

Obfuscating Python Scripts In Different Modes

Advanced Mode is introduced from PyArmor 5.5.0, it’s disabled by default. Specify option --advanced to enable it:

pyarmor obfuscate --advanced 1 foo.py

# For project
cd /path/to/project
pyarmor config --advanced 1
pyarmor build -B

From PyArmor 5.2, the default Restrict Mode is 1. It could be changed by the option --restrict:

pyarmor obfuscate --restrict=2 foo.py
pyarmor obfuscate --restrict=3 foo.py

# For project
cd /path/to/project
pyarmor config --restrict 4
pyarmor build -B

All the restricts could be disabled by this way if required:

pyarmor obfuscate --restrict=0 foo.py

# For project
pyarmor config --restrict=0
pyarmor build -B

The modes of Obfuscating Code Mode, Wrap Mode, Obfuscating module Mode could not be changed in command obfucate. They only could be changed by command config when Using Project. For example:

pyarmor init --src=src --entry=main.py .
pyarmor config --obf-mod=1 --obf-code=1 --wrap-mode=0
pyarmor build -B

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 ntplib import NTPClient
from time import mktime, strptime
import sys

def get_license_data():
    from ctypes import py_object, PYFUNCTYPE
    from pytransform import _pytransform
    prototype = PYFUNCTYPE(py_object)
    dlfunc = prototype(('get_registration_code', _pytransform))
    rcode = dlfunc().decode()
    index = rcode.find(';', rcode.find('*CODE:'))
    return rcode[index+1:]

def check_ntp_time():
    NTP_SERVER = 'europe.pool.ntp.org'
    EXPIRED_DATE = get_license_data()
    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_ntp_time()

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():
    # PyArmor Plugin: check_ntp_time()
    check_ntp_time()

So the plugin takes effect.

If the plugin file isn’t in the current path, or $HOME/.pyarmor/plugins, 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 --bind-data 20190501 MYPRODUCT-0001
cp licenses/MYPRODUCT-0001/license.lic dist/

Note

It’s better to insert the content of ntplib.py into the plugin so that NTPClient needn’t be imported out of obfuscated scripts.

Important

The output function name in the plugin must be same as plugin name, otherwise the plugin will not take effects.

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
    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 -x " --exclude copy_license.py" \
            -e " --onefile --icon logo.ico --runtime-hook copy_license.py" foo.py
    

Option --without-license tells pack 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 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 code-001
    cp license/code-001/license.lic dist/
    
    dist/foo.exe
    

Bundle obfuscated scripts with customized spec file

If there is a customized spec file works, for example:

pyinstaller myscript.spec

It could be used to pack obfuscated scripts by little changed:

  • Add module pytransform to hiddenimports
  • Add extra path DISTPATH/obf/temp to pathex and hookspath

After changed, it may be like this:

a = Analysis(['myscript.py'],
             pathex=[os.path.join(DISTPATH, 'obf', 'temp'), ...],
             binaries=[],
             datas=[],
             hiddenimports=['pytransform', ...],
             hookspath=[os.path.join(DISTPATH, 'obf', 'temp'), ...],

Now run command pack by this way:

pyarmor pack -s myscript.spec myscript.py

That’s all.

Note

This featuer is introduced since v5.8.0

Before v5.8.2, the extra path is DISTPATH/obf, not DISTPATH/obf/temp

Improving The Security By Restrict Mode

By default the scripts are obfuscated by restrict mode 1, that is, the obfuscated scripts can’t be changed. In order to improve the security, obfuscating the scripts by restrict mode 2 so that the obfuscated scripts can’t be imported out of the obfuscated scripts. For example:

pyarmor obfuscate --restrict 2 foo.py

Or obfuscating the scripts by restrict mode 3 for more security. It will even check each function call to be sure all the functions are called in the obfuscated scripts. For example:

pyarmor obfuscate --restrict 3 foo.py

However restrict mode 2 and 3 aren’t applied to Python package. There is another solutiion for Python package to improve the security:

  • The .py files which are used by outer scripts are obfuscated by restrice mode 1
  • All the other .py files which are used only in the package are obfuscated by restrict mode 4

For example:

cd /path/to/mypkg
pyarmor obfuscate --exact __init__.py exported_func.py
pyarmor obfuscate --restrict 4 --recursive \
        --exclude __init__.py --exclude exported_func.py .

More information about restrict mode, refer to Restrict Mode

Checking Imported Function Is Obfuscated

Sometimes it need to make sure the imported functions from other module are obfuscated. For example, there are 2 scripts main.py and foo.py:

$ cat main.py

import foo

def start_server():
    foo.connect('root', 'root password')

$ cat foo.py

def connect(username, password):
    mysql.dbconnect(username, password)

In the obfuscated main.py, it need to be sure foo.connect is obfuscated. Otherwise the end users may replace the obfuscated foo.py with this plain code:

def connect(username, password):
    print('password is %s', password)

One solution is to check imported functions by decorator assert_armored in the main.py. For example:

import foo

def assert_armored(*names):
    def wrapper(func):
        def _execute(*args, **kwargs):
            for s in names:
                # For Python2
                # if not (s.func_code.co_flags & 0x20000000):
                # For Python3
                if not (s.__code__.co_flags & 0x20000000):
                    raise RuntimeError('Access violate')
                # Also check a piece of byte code for special function
                if s.__name__ == 'connect':
                    if s.__code__.co_code[10:12] != b'\x90\xA2':
                        raise RuntimeError('Access violate')
            return func(*args, **kwargs)
        return _execute
    return wrapper

@ assert_armored(foo.connect, foo.connect2)
def start_server():
    foo.connect('root', 'root password')
    foo.connect2('user', 'user password')

Plugin Implementation

First write a plugin script asser_armored.py:

def assert_armored(*names):
    def wrapper(func):
        def _execute(*args, **kwargs):
            for s in names:
                # For Python2
                # if not (s.func_code.co_flags & 0x20000000):
                # For Python3
                if not (s.__code__.co_flags & 0x20000000):
                    raise RuntimeError('Access violate')
                # Also check a piece of byte code for special function
                if s.__name__ == 'connect':
                    if s.__code__.co_code[10:12] != b'\x90\xA2':
                        raise RuntimeError('Access violate')
            return func(*args, **kwargs)
        return _execute
    return wrapper

Then edit main.py , insert plugin markers. For examples:

import foo

# {PyArmor Plugins}

# PyArmor Plugin:  @assert_armored(foo.connect, foo.connect2)
def start_server():
    foo.connect('root', 'root password')
    ...

So the original script could be run normally when it’s not obfuscated. Only when it’s distributed, obfuscating the script with this plugin:

pyarmor obfuscate --plugin assert_armored main.py

Note

After v5.7.2, if you prefer, the marker could be this form:

# @pyarmor_assert_armored(foo.connect, foo.connect2)

Call pyarmor From Python Script

It’s also possible to call PyArmor methods inside Python script not by os.exec or subprocess.Popen etc. For example

from pyarmor.pyarmor import main as call_pyarmor
call_pyarmor(['obfuscate', '--recursive', '--output', 'dist', 'foo.py'])

In order to suppress all normal output of pyarmor, call it with --silent

from pyarmor.pyarmor import main as call_pyarmor
call_pyarmor(['--silent', 'obfuscate', '--recursive', '--output', 'dist', 'foo.py'])

From v5.7.3, when pyarmor called by this way and something is wrong, it will raise exception other than call sys.exit.

Check license periodly when the obfuscated script is running

Generally only at the startup of the obfuscated scripts the license is checked. Since v5.9.3, it also could check the license per hour. Just generate a new license with --enable-period-mode and overwrite the default one. For example:

pyarmor obfuscate foo.py
pyarmor licenses --enable-period-mode code-001
cp licenses/code-001/license.lic ./dist

Work with Nuitka

Because the obfuscated scripts could be taken as normal scripts with an extra runtime package pytransform, they also could be translated to C program by Nuitka. When obfuscating the scprits, the option --restrict 0 and --no-cross-protection should be set, otherwise the final C program could not work. For example, first obfustate the scripts:

pyarmor obfuscate --restrict 0 --no-cross-protection foo.py

Then translate the obfuscated one as normal python scripts by Nuitka:

cd ./dist
python -m nuitka --include-package pytransform foo.py
./foo.bin

There is one problem is that the imported modules (packages) in the obfuscated scripts could not be seen by Nuitka. To fix this problem, first generate the corresponding .pyi with original script, then copy it within the obfuscated one. For example:

# Generating "mymodule.pyi"
python -m nuitka --module mymodule.py

pyarmor obfuscate --restrict 0 --no-bootstrap mymodule.py
cp mymodule.pyi dist/

cd dist/
python -m nuitka --module mymodule.py

But it may not take advantage of Nuitka features by this way, because most of byte codes aren’t translated to c code indeed.

Note

So long as the C program generated by Nuitka is linked against libpython to execute, pyarmor could work with Nuitka. But in the future, just as said in the Nuitka official website:

It will do this - where possible - without accessing libpython but in C
with its native data types.

In this case, pyarmor maybe not work with Nuitka.