โš” Protect your python code ๐Ÿ‡ฌ๐Ÿ‡ง#

โ„น Introduction#

Python scripts can be protected in order to avoid stealing of intellectual property, algorithms or anything else you want to hide from the final user. As using an interpreter (usually CPython), your code is translated from human readable language (python) in python bytecodes (stored in __pycache__) ; with some modifications of CPython source code, you could extract all the code (even if itโ€™s obfuscated) without difficulties.

How to dump code object to disk ?

Compile Python from source. Modify the _PyEval_EvalFrameDefault function such that it dumps the code object to disk.

In this article, I propose two ways of protecting your python scripts. An easy one using a python tool named PyArmor which relies on an external unknown not open-source library and a solider one using a tool which I appreciate whose name is Enigma Protector (it protects executable on windows ๐Ÿ’ ).

๐Ÿ›ก Using PyArmor#

Protection level : โญโญ
Protection level on Windows : โญโญโญ
Difficulty to implement : โญ

โœ… Pros

โŒ Cons

Multiple platforms

Using external C compiled Library (not open-source)

Full python implementation (open-source)

Virtual Machine only for Windows

52 $ by project

Runtime obfuscation of python script only

Hardware lock

More weak protection than Enigma Protector

Seamless Replacement

No trial (need a double distribution)

Easy packing with PyInstaller

Need to have a licence file for each final user

Can protect the complete program

PyArmor Core need an update for each new plateform/python version

๐Ÿ”ง Prerequisites#

After buying a capsule on the website, register the program.

pyarmor register pyarmor-regfile-1.zip
pyarmor register
> INFO     PyArmor Version 7.4.1
> INFO     Python 3.10.4
> PyArmor Version 7.4.1
> Registration Code: pyarmor-vax-000****
> This code is authorized to "Rรฉmi MEVAERE (Personal) <****@*****.fr>"

The program we want to protect is composed of two scripts :

File : main.py

from fibonacci import fibonacci

print("Welcome to the test program")
nbr = input("Please enter an integer : ")
if None != (fib_list := fibonacci(nbr)):
    print("Sequence of Fibonacci : ")
    print(fib_list)
input("Press key to stop")

File : fibonacci.py

def fibonacci(n):
    try:
        nbr = int(n)
        if nbr <= 0:
            raise ValueError

        FibArray = [0, 1]

        for i in range(nbr - 2):
            FibArray.append(FibArray[-1] + FibArray[-2])

        return FibArray

    except:
        print("Incorrect input")
        return None

๐Ÿ“œ Obfuscating a script#

First we want to protect the script fibonacci.py. Itโ€™s recommended to use --advanced parameter to improve the protection but it doesnโ€™t change anything for this demonstration. To protect seriously your script/program you need to use this parameter, see the manual.

pyarmor obfuscate --exact fibonacci.py  --output protected_script

Image 1

The new directory contains the protected script fibonnaci.py and the library _pytransform.dll

We donโ€™t know what contains _pytransform.dll, itโ€™s an executable from PyArmor. Not open-source.

Image 1

Exported functions from _pytransform.dll

File : protected_script\fibonacci.py is now obfuscated and unreadable.

from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'\x50\x59\x41\x52\x4d\x4f\x52\x00\x00\x03\x0a\x00\x6f\x40\x5f\x0c\x21\x4f\xb4\xa5\x99\xa3\x7f\x6d\xe2\xb8\xa7\xf5\x32\x1c\x5a\xf7\xb2\x6d\xbd\xa5\x72\x57\x6c\xd2\x94\x4a\x38\x8d\xce\xd2\xac\x3f\x29\x2b\x26\x8f\x99\xde\xc2\xff\xb3\xa3\xa1\xbb\xcc\x1c\x83\x03\xc4\x5b\x80\x63\xb8\xef\x8a\x9d\x7f\x03\x8e\x79\x96\x14\x87\x22\xb7\x7f\x6d\x43\xf\xb1\x1c\x8b\xda\x7\xcc\x8f\x89\xbf\xa5\x74\x0a\x1e\x61\x78\x46\x37\xc1\x99\x35\xec\xe0\xfc\x33\xa9\xb\x90\x89\x36\xc7\x25\x3x37\xd8', 2)

The program main.py works as expected :

C:\Users\remi\AppData\Local\pypoetry\Cache\virtualenvs\protect-python-knxkg4Ko-py3.10\Scripts\python.exe C:/Users/remi/Documents/PyCharmProjects/protect-python/protected_script/main.py
Welcome to the test program
Please enter an integer : 55
Sequence of Fibonacci : 
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049, 12586269025, 20365011074, 32951280099, 53316291173, 86267571272]
Press key to stop

๐Ÿ’พ Obfuscating a program#

We want to protect the program and not only the script.

pyarmor obfuscate main.py  --output protected_program

The two scripts are now obfuscated in protected_program directory.

๐Ÿ” Hardware lock (protect)#

Our program is not protected, it can be run by anyone. In order to protect-it we need to obfuscate it without the licence file, which will be sent to the final user.

pyarmor obfuscate main.py  --with-license outer --output register_program

Running the program gives us :

C:\Users\remi\AppData\Local\pypoetry\Cache\virtualenvs\protect-python-knxkg4Ko-py3.10\Scripts\python.exe C:/Users/remi/Documents/PyCharmProjects/protect-python/register_program/main.py
Read file license.lic failed, No such file or directory

We want to generate some licence files for decryption. Here are the options :

-e, --expired YYYY-MM-DD

Expired date for this license

-d, --bind-disk SN

Bind license to serial number of harddisk

-4, --bind-ipv4 IPV4

Bind license to ipv4 addr

-m, --bind-mac MACADDR

Bind license to mac addr

-x, --bind-data DATA

Pass extra data to license, used to extend license type

We are changing our program main.py to main_pyarmor.py to extract some data bind when generating the licence.

from fibonacci import fibonacci
from pytransform import get_license_info
info = get_license_info()

print("Welcome to the test program")
print("Data from licence : ",info['DATA'])
nbr = input("Please enter an integer : ")
if None != (fib_list := fibonacci(nbr)):
    print("Sequence of Fibonacci : ")
    print(fib_list)
input("Press key to stop")
pyarmor obfuscate main_pyarmor.py  --with-license outer --output register_program

In order to bind to specific material we need to get hardware info with hdinfo command.

pyarmor hdinfo
> Hardware informations got by PyArmor:
> Serial number of first harddisk: "FV994730S6LLF07AY"
> Default Mac address: "f8:ff:c2:27:00:7f"
> Ip address: "192.168.121.100"

Create a licence and copy it to the same folder of the script :

pyarmor licenses --expired 2019-10-10 --bind-data "MY_PROGRAM_V2" lic001

Image 1

The licence file is generated in the lic001 folder

Running the program failed logically cause the licence is expired :

C:\Users\remi\AppData\Local\pypoetry\Cache\virtualenvs\protect-python-knxkg4Ko-py3.10\Scripts\python.exe C:/Users/remi/Documents/PyCharmProjects/protect-python/register_program/main_pyarmor.py
> License is expired

Generation of a new licence :

pyarmor licenses --expired 2025-10-10 --bind-disk "FV994730S6LLF07AY" --bind-data "MY_PROGRAM_V2" lic002

Everything works as excepted :

C:\Users\remi\AppData\Local\pypoetry\Cache\virtualenvs\protect-python-knxkg4Ko-py3.10\Scripts\python.exe C:/Users/remi/Documents/PyCharmProjects/protect-python/register_program/main_pyarmor.py
> Welcome to the test program
> Data from licence :  MY_PROGRAM_V2
> Please enter an integer : 6
> Sequence of Fibonacci : 
> [0, 1, 1, 2, 3, 5]
> Press key to stop

๐Ÿ“ฆ Packaging with PyInstaller#

Sometimes itโ€™s useful to pack the program in a standalone executable, in order to do that PyArmor provides some facilities.

pyarmor pack main_pyarmor.py --with-license outer

A dist folder is created and the protected executable is inside. Copy the licence.lic file and run the program, thatโ€™s all ! If you want to customize the executable please read the manual.

๐Ÿ”ฎ Using EnigmaProtector#

Protection level on Windows : โญโญโญโญโญ
Difficulty to implement : โญโญโญ

โœ… Pros

โŒ Cons

Very good protection

Only Windows

Trial control

Using external program (not open-source)

Registration manager

199 $ + 69 $ / year update by dev.

Licensing system

Need Cython and MSVC to compile

Wide range of check-up

Protect sensitive parts only

Hardware Lock ++

Message designer

Protected strings

VirtualBox for file/registry hiding

Virtual Machine (RISC)

Independant of new python version

๐Ÿ”ง Prerequisites#

  • Microsoft windows (enigma protector works natively on windows, ๐Ÿท wine is suitable for running EXE on ๐Ÿง Linux )

  • Cython

  • MSVC build-tools

  • The enigma protector software (199$), the trial version will raise some alerts from antivirus, cause some crackers & hackers use it to protect/hide their malwares.

โš™ Installing and using Cython#

About Cython

Cython is a programming language that aims to be a superset of the Python programming language, designed to give C-like performance with code that is written mostly in Python with optional additional C-inspired syntax. Cython is a compiled language that is typically used to generate CPython extension modules.

Cython could generate with your Python code a c++ file which could be compiled in a dynamic link library (DLL) which could be directly called by your python code.

๐Ÿ”ฐ Install Cython#

pip install cython

๐Ÿ”„ Convert our first program#

The first file is the python file script you want to convert in C. Please note the extension .pyx. File : fib.pyx

# cython: language_level=3
def fib(n):
    """Print the Fibonacci series up to n."""
    a, b = 0, 1
    while b < n:
        print(b, end=' ')
        a, b = b, a + b
    print()

Now we need to create the setup.py, which is a python Makefile (for more information see Source Files and Compilation File : setup.py

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("fib.pyx"),
)

Run the following command :

python setup.py build_ext --inplace

It converts it in c and compile with msvc in fib.cp38-win_amd64.pyd

Image 1

DLL Export Viewer tells us this is a valid DLL

Calling the DLL created is really easy from python. Python treats it like a module :

import fib

fib.fib(50000000)  # will give the expected result

๐Ÿ›ก Protecting your app#

The goal is to protect your python app. In order to do this, you will need :

  • some of your most important code in a .pyx file which will be converted in c++, this code will be protected

  • call the API of enigma protector to introduce the protection (RISC virtual machine etc.)

  • packed the DLL produced by cython with enigma protector

  • Use it !

Important

C compiled file are faster than python cause they doesnโ€™t deal with python object structure. C files are also faster when they deals with loops, and cause they doesnโ€™t deal with the GIL, cython gives you the opportunity to use all your CPU cores.

Avertissement

Be carreful, GIL exists to avoid some complex problem with memory access to shared variables and handle correctly garbage collector.

โš’ Prepare the compilation#

Here we are working with a c++ file. It doesnโ€™t change a lot except in setup.py. File : setup.py

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

setup(
    name='Test app',
    ext_modules=[
        Extension('test_it',
                  sources=['script_test.pyx'],
                  extra_link_args=['/MAP'],
                  libraries=["enigma_ide64"],
                  language="c++")
    ],
    cmdclass={'build_ext': build_ext}
)

๐Ÿ‘“ Watch the API#

Enigma Protector API

A Marker is a set of bytes placed into the source code and helping Enigma Protector find the code inside markers for processing. A marker consists of two parts: begin marker and end marker.

// Markers API
void __declspec(dllimport) __stdcall EP_Marker(char* Name);

EP_RegHardware function serves for retrieving unique user PC information. The function does not have parameters. If the function succeeds, the return value is a pointer to the null terminated ANSI string. If the function fails, the return value is 0.

// Registration API
LPCWSTR __declspec(dllimport) __stdcall EP_RegHardwareIDW();

I will not copy all the manual in this article, if you want more infos about the API please read the manual.

โž• Add enigma headers files in working dir#

In order to compile, you need to move two files from the sdk/VC path of Enigma Protector. You will need to use MSVC. File 1 : enigma_ide.h File 2 : enigma_ide64.lib

Image 1

Copy two files in the project folder

In enigma_ide.h, just insert the following line.

#include <windows.h>

โš” Protecting a script#

File : script_test.pyx

# distutils: language = c++
# cython: language_level=3

# import functions from enigma_ide64.lib
cdef extern from "enigma_ide.h":
    void EP_Marker(char* Name)
    char* EP_RegHardwareID()


# Declare a trivial function
def sum_it(number1, number2):
    return number1 + number2

# Call and print the EP_RegHardwareID
ansi_str_hid = str(EP_RegHardwareID(), 'cp1252')
print('ANSI :', ansi_str_hid)


# Protect this with RISC virtualization
EP_Marker("vm_risc_begin")
a = 4
b = 7
c = a + b
print('Virtualized :', c)
EP_Marker("vm_risc_end")

# Classic python code
print("Give me the sum :", sum_it(1, 2))
input("End, press key")

Convert and build with msvc

python setup.py build_ext --inplace

Image 1

Use-it like a normal module

import test_it

Image 1

Output

๐ŸŽ‡ Using Widestring Char (unicode) in Cython - wchar_t*#

Sometimes itโ€™s necessary to use more than ANSI characters, in order to use WideChar itโ€™s necessary to adapt the code.

# distutils: language = c++
# cython: language_level=3

# Imports and declaration to work fromWideChar
from cpython.ref cimport PyObject
from libc.stddef cimport wchar_t

cdef extern from "Python.h":
    PyObject * PyUnicode_FromWideChar(wchar_t *w, Py_ssize_t size)

# import functions from enigma_ide64.lib
cdef extern from "enigma_ide.h":
    void EP_Marker(char* Name)
    char* EP_RegHardwareID()
    wchar_t * EP_RegHardwareIDW()

# Declare a trivial function

def sum_it(number1, number2):
    return number1 + number2

# Call and print the EP_RegHardwareID
ansi_str_hid = str(EP_RegHardwareID(), 'cp1252')
print('ANSI :', ansi_str_hid)

cdef PyObject * pystr = PyUnicode_FromWideChar(EP_RegHardwareIDW(), -1)
wide_str_hid = str(<object> pystr)
print('WideChar :', wide_str_hid)


# Protect this with RISC virtualization
EP_Marker("vm_risc_begin")
a = 4
b = 7
c = a + b
print('Virtualized :', c)
EP_Marker("vm_risc_end")

# Classic python code
print("Give me the sum :", sum_it(1, 2))
input("End, press key")

python setup.py build_ext --inplace
๐Ÿ›ก๏ธ Protect with Enigma โš”๏ธ
import test_it
> ANSI : 02EF34-F57F02
> WideChar : 02EF34-F57F02
> Virtualized : 11
> Give me the sum : 3
> End, press key

๐Ÿ” Registration feature#

In order to test registration features we need to add to our program some code which will only be executed if the user is registered :

# Crypt this stuff which will only be decrypted with registration
EP_Marker("reg_crypt_begin1")
print("This is an encrypted section")
nbr = input("Enter a number : ")
print(nbr + " + 5 = ", str(int(nbr) + 5))
EP_Marker("reg_crypt_end1")

First you need to generate a key, please note that in this article I will use WideChar (Unicode in configuration). Itโ€™s more complex than ANSI, if you understand with unicode, it will be straightforward for ANSI.

Image 1

Generate a new key for testing

Registration dialog#

If you want to use the registration dialog just use the dedicated API

cdef extern from "enigma_ide.h":
    void EP_RegShowDialog()

if not registered:
    EP_RegShowDialog()

After registering the software it will decrypt the first section ahead.

Manual registration#

I prefer handle myself the registration

cdef extern from "Python.h":
    wchar_t * PyUnicode_AsWideCharString(object, Py_ssize_t *)

# Declare function in enigma_ide64.lib
cdef extern from "enigma_ide.h":
    bool EP_RegCheckAndSaveKeyW(wchar_t * Name, wchar_t * Key);

As an example I will try with two keys, one legit and one false.


reg_name = u"Test-WrongUSER"
reg_key = u"CKML369-XGSH5DW-RVG2ANU-W4FG4K4-J2RQYHM-32SD3LD-XJPKSYB-S5RPPPE-SEURZXQ"
reg_name_2 = "Rรฉmi MEVAERE"
reg_key_2 = "CKML369-XGSH5DW-RVG2ANU-W4FG4K4-J2RQYHM-32SD3LD-XJPKSYB-S5RPPPE-SEURZXQ"
cdef Py_ssize_t length
cdef wchar_t *reg_name_wchar = PyUnicode_AsWideCharString(reg_name, &length)
cdef wchar_t *reg_key_wchar = PyUnicode_AsWideCharString(reg_key, &length)
cdef wchar_t *reg_name_wchar_2 = PyUnicode_AsWideCharString(reg_name_2, &length)
cdef wchar_t *reg_key_wchar_2 = PyUnicode_AsWideCharString(reg_key_2, &length)

print("Registration to : ", reg_name)
if EP_RegCheckAndSaveKeyW(reg_name_wchar, reg_key_wchar):
    print("Registration OK, please restart APP")
    quit()
else:
    print("Registration ERROR")

print("Registration to : ", reg_name_2)
if EP_RegCheckAndSaveKeyW(reg_name_wchar_2, reg_key_wchar_2):
    print("Registration OK, please restart APP")
    quit()
else:
    print("Registration ERROR")

Test registration#

You can use the encrypted section (EP_Marker) to know if the registration succeed. But I prefer using this function in the API :

cdef extern from "enigma_ide.h":
    int EP_RegKeyStatus();

registered = False
if EP_RegKeyStatus() == 1:
    registered = True

if registered:
    print("You are registered")
else:
    print("You are NOT registered")

๐Ÿ—ƒ๏ธ File virtualization#

Itโ€™s possible to hide some files inside the executable. You will access to these files directly from your script.

Image 1

Add a file virtual_file.txt with some text

You could read the file with python easily :

with open('virtual_file.txt') as f:
    lines = f.readlines()

print("------- virtual_file.txt -------")
print(lines)
print("--------------------------------")

๐Ÿงต Protected strings#

You can add some sensible protected strings in your code. We will see in this section how to retrieve them :

Image 1

Add some strings in Enigma Protector

API : Extract AnsiString or binary file#

No problem in this case, cause enigma.h expose a function accepting char* string.

from libcpp cimport bool
from libc.stdlib cimport malloc, free

cdef extern from "enigma_ide.h":
    int EP_ProtectedStringByID(int ID, char * Buffer, int Len);

Extract the string :

buf_size = EP_ProtectedStringByID(2, b'', 0)
print("Size of the string #2: ", buf_size)

cdef char * buf_string_2 = <char *> malloc((buf_size + 1) * sizeof(char))

if EP_ProtectedStringByID(2, buf_string_2, buf_size) != 0:
    print(str(buf_string_2, 'cp1252'))
else:
    print("Error can't extract #2")

free(buf_string_2)

Extract the binary data :


buf_size = EP_ProtectedStringByID(3, b'', 0)
print("Size of the string #3: ", buf_size)

cdef char * buf_string_3 = <char *> malloc((buf_size + 1) * sizeof(char))

if EP_ProtectedStringByID(3, buf_string_3, buf_size) != 0:
    print(buf_string_3)
else:
    print("Error can't extract #3")
    
free(buf_string_3)

API : Extract WideString#

There is a problem with the API provided by Enigma, first you need to change the signature of EP_ProtectedStringByID in enigma.h

// Protection API
//int __declspec(dllimport) __stdcall EP_ProtectedStringByID( int ID, const char* Str, int Len);
int __declspec(dllimport) __stdcall EP_ProtectedStringByID( int ID, const wchar_t* Str, int Len);
int __declspec(dllimport) __stdcall EP_ProtectedStringByKey( const char* Key, const char* Str, int Len);

empty = ''
cdef wchar_t *empty_wchar = PyUnicode_AsWideCharString(empty, &length)

buf_size = EP_ProtectedStringByID(1, empty_wchar, 0) 
print("Size of the string #1: ", buf_size)

cdef wchar_t * buf_string = <wchar_t *> malloc((buf_size // sizeof(wchar_t) + 1) * sizeof(wchar_t))
cdef PyObject * pystr_2

if EP_ProtectedStringByID(1, buf_string, buf_size) != 0:
    pystr_2 = PyUnicode_FromWideChar(buf_string, -1)
    wide_string = str(<object> pystr_2)
    print('WideString :', wide_string)
else:
    print("Error can't extract #1")

๐Ÿ’๐Ÿปโ€โ™‚๏ธConclusion#

In this article, we have seen how to protect our python program or some sensitive parts that we donโ€™t want to share. Each time, we have to trust an external program (not open-source). The performance will be degraded.