1.5. Customization and Extension

Pyarmor provides the following ways to extend:

1.5.1. Changing runtime package name

New in version 8.2: [1]

By default the runtime package name is pyarmor_runtime_xxxxxx

This name is variable with any valid package name. For example, set it to my_runtime:

pyarmor cfg package_name_format "my_runtime"
[1]Pyarmor trial version could not change runtime package name

1.5.2. Appending assert functions and modules

New in version 8.2.

Pyarmor 8.2 introduces configuration item auto_mode to protect more functions and modules. The default value is and, --assert-call and --assert-import only protect modules and functions which Pyarmor make sure they’re obfuscated.

If set its value to or, then all the names in the configuration item includes are also protected. For example, appending function fookoo to assert list:

$ pyarmor cfg ast.call:auto_mode "or"
$ pyarmor cfg ast.call:includes "foo koo"

$ pyarmor gen --assert-call foo.py

For example, also protect hidden imported module joker.card:

$ pyarmor cfg ast.import:auto_mode "or"
$ pyarmor cfg ast.import:includes "joker.card"

$ pyarmor gen --assert-import joker/

1.5.3. Using plugin to fix loading issue in darwin

New in version 8.2.

In darwin, if Python is not installed in the standard path, the obfuscated scripts may not work because extension module pyarmor_runtime in the runtime package could not be loaded.

Let’s check the dependencies of pyarmor_runtime.so:

$ otool -L dist/pyarmor_runtime_000000/pyarmor_runtime.so

dist/pyarmor_runtime_000000/pyarmor_runtime.so:

    pyarmor_runtime.so (compatibility version 0.0.0, current version 1.0.0)
    ...
    @rpath/lib/libpython3.9.dylib (compatibility version 3.9.0, current version 3.9.0)
    ...

Suppose target device has no @rpath/lib/libpython3.9.dylib, but @rpath/lib/libpython3.9.so, in this case pyarmor_runtime.so could not be loaded.

We can create a plugin script .pyarmor/myplugin.py to fix this problem

__all__ = ['CondaPlugin']

class CondaPlugin:

    def _fixup(self, target):
        from subprocess import check_call
        check_call('install_name_tool -change @rpath/lib/libpython3.9.dylib @rpath/lib/libpython3.9.so %s' % target)
        check_call('codesign -f -s - %s' % target)

    @staticmethod
    def post_runtime(ctx, source, target, platform):
        if platform.startswith('darwin.'):
            print('using install_name_tool to fix %s' % target)
            self._fixup(target)

Enable this plugin and generate the obfuscated script again:

$ pyarmor cfg plugins + "myplugin"
$ pyarmor gen foo.py

See also

Plugins

1.5.4. Using hook to bind script to docker id

New in version 8.2.

Suppose we need bind script app.py to 2 dockers which id are docker-a1 and docker-b2

First create hook script .pyarmor/hooks/app.py

def _pyarmor_check_docker():
    cid = None
    with open("/proc/self/cgroup") as f:
        for line in f:
            if line.split(':', 2)[1] == 'name=systemd':
                cid = line.strip().split('/')[-1]
                break

    docker_ids = __pyarmor__(0, None, b'keyinfo', 1).decode('utf-8')
    if cid is None or cid not in docker_ids.split(','):
        raise RuntimeError('license is not for this machine')

_pyarmor_check_docker()

Then generate the obfuscated script, store docker ids to runtime key as private data at the same time:

$ pyarmor gen --bind-data "docker-a1,docker-b2" app.py

Run the obfuscated script to check it, please add print statements in the hook script to debug it.

See also

Hooks __pyarmor__()

1.5.5. Using hook to check network time by other service

New in version 8.2.

If NTP is not available in the target device and the obfuscated scripts has expired date, it may raise RuntimeError: Resource temporarily unavailable.

In this case, using hook script to verify expired data by other time service.

First create hook script in the .pyarmor/hooks/foo.py:

def _pyarmor_check_worldtime(host, path):
    from http.client import HTTPSConnection
    expired = __pyarmor__(1, None, b'keyinfo', 1)
    conn = HTTPSConnection(host)
    conn.request("GET", path)
    res = conn.getresponse()
    if res.code == 200:
        data = res.read()
        s = data.find(b'"unixtime":')
        n = data.find(b',', s)
        current = int(data[s+11:n])
        if current > expire:
            raise RuntimeError('license is expired')
     else:
         raise RuntimeError('got network time failed')
_pyarmor_check_worldtime('worldtimeapi.org', '/api/timezone/Europe/Paris')

Then generate script with local expired date:

$ pyarmor gen -e .30 foo.py

Thus the obfuscated script could verify network time by itself.

See also

Hooks __pyarmor__()

1.5.6. Protecting extension module pyarmor_runtime

New in version 8.2.

This example shows how to check the file content of an extension module to make sure it’s not changed by others.

First create a hook script .pyarmor/hooks/foo.py:

1
2
3
4
5
6
7
def check_pyarmor_runtime(value):
    from pyarmor_runtime_000000 import pyarmor_runtime
    with open(pyarmor_runtime.__file__, 'rb') as f:
        if sum(bytearray(f.read())) != value:
            raise RuntimeError('unexpected %s' % filename)

check_pyarmor_runtime(EXCEPTED_VALUE)

Line 7 EXCEPTED_VALUE need to be replaced with real value, but it doesn’t work to get the sum value of pyarmor_runtime.so after building, because each build the sum value is different. We need use a post-runtime plugin to get the expected value and update the hook script automatically

# Plugin script: .pyarmor/myplugin.py

__all__ = ['CondaPlugin', 'RuntimePlugin']

class RuntimePlugin:

    @staticmethod
    def post_runtime(ctx, source, target, platform):
        with open(target, 'rb') as f:
            value = sum(bytearray(f.read()))
        with open('.pyarmor/hooks/foo.py', 'r') as f:
            source = f.read()
        source = source.replace('EXPECTED_VALUE', str(value))
        with open('.pyarmor/hooks/foo.py', 'r') as f:
            f.write(source)

class CondaPlugin:
    ...

Then enable this plugin:

$ pyarmor cfg plugins + "myplugin"

Finally generate the obfuscated script, and verify it:

$ pyarmor gen foo.py
$ python dist/foo.py

This example is only guide how to do, it’s not safe enough to use it directly. There is always a way to bypass open source check points, please write your private check code. There are many other methods to prevent binary file from hacking, please learn and search these methods by yourself.

See also

Hooks

1.5.7. Comments within outer key

New in version 8.2.

The outer key ignores all the printable text at the header, so it’s possible to insert some readable text in the outer key as comments.

Post-key plugin is designed to do this. The following example plugin will print all the key information in the console, and write expired date to outer key file:

# Plugin script: .pyarmor/myplugin.py

from datetime import datetime

__all__ = ['CommentPlugin']

class CommentPlugin:

    @staticmethod
    def post_key(ctx, keyfile, **keyinfo):
        expired = None
        for name, value in keyinfo.items():
            print(name, value)
            if name == 'expired':
               expired = datetime.fromtimestamp(value).isoformat()

        if expired:
            print('patching runtime key')
            comment = '# expired date: %s\n' % expired
            with open(keyfile, 'rb') as f:
                keydata = f.read()
            with open(keyfile, 'wb') as f:
                f.write(comment.encode())
                f.write(keydata)

Enable this plugin and generate an outer key:

$ pyarmor cfg plugins + "myplugin"
$ pyarmor gen key -e 2023-05-06

Check comment:

$ head -n 1 dist/pyarmor.rkey

See also

Plugins