Simple script protection in Python

Stack: Python 3.11.7, subprocess, ntplib, getpass for system, time, password,

PyArmor 8+ for obfuscation.

Scenario: An application has been developed that gives an advantage over competitors or contains confidential data. The application will need to be installed by several employees (you can increase the number by slightly changing the approach), and you do not plan to transfer part of the logic to the server.

Other scenarios are also possible; I have described the most likely case, in my opinion, when this kind of protection may be needed.

In this article, I will tell you several ways that will prevent you from launching an application where it shouldn’t, and will most likely discourage you from meddling with your code again.

So, one fine day, an ungrateful user leaves for a new company, takes your executable with him, receives bonuses and rejoices. Meanwhile, a local IT specialist launches Hydra (Ghidra) or any other reverse engineering tool. And here is the result of your work for yearmonth, a week, day, ended up with a competitor.

To avoid such a development, we can: 1) bind the script to the hard drive, MAC address, date, device name, motherboard UUID, BIOS/TPM version, IP address, token/certificate, network connection, USB key, etc. further emphasize what is necessary 2) obfuscate (disguise) the script itself.

It is not necessary to bind the script to all parameters at once, just select a couple of them. For example, if the user will work with equipment provided by the employer, you can link the script to the unique characteristics of the device, and if you intend to use uncontrolled equipment, then use passwords or time restrictions.

When I used all possible dependencies

When I used all possible dependencies

It's especially effective if users don't know which dependencies you used, making it harder to replace them on a new device.

Let's move on to libraries.

1. subprocess

Used to obtain the serial number of the hard drive, ensuring that the program is linked to a specific device.

To get the serial number of a hard drive on Windows, on the command line:

wmic diskdrive get serialnumber

Checking the hard drive serial number:

def get_disk_serial_number():
    try:
        output = subprocess.check_output('wmic diskdrive get serialnumber', shell=True)
        serial = output.decode().split("\n")[1].strip()
        return serial
    except Exception as e:
        return None

def check_disk_serial(expected_serial):
    serial_number = get_disk_serial_number()
    return serial_number == expected_serial

2.ntplib

Used to obtain accurate time from an NTP server, which prevents manipulation of the system time.

We can limit the validity period of our script, for example, until December 31, 2024, and then even if the employee takes the hard drive with him, the program will cease to operate after some time.

Function to get time from NTP server:

def get_time_from_ntp():
    try:
        ntp_client = ntplib.NTPClient()
        response = ntp_client.request('pool.ntp.org')  # Используем публичный NTP-сервер
        ntp_time = datetime.fromtimestamp(response.tx_time, tz=timezone.utc)  # Преобразуем время в формат UTC
        return ntp_time
    except Exception as e:
        print(f"Ошибка при обращении к NTP-серверу: {e}")
        return None

def check_expiration_date(expiration_date_str):
    expiration_date = datetime.strptime(expiration_date_str, "%Y-%m-%d").date()  # Преобразуем в date
    
    # Получаем текущую дату с NTP-сервера
    current_date = get_time_from_ntp()
    
    if current_date is None:
        print("Ошибка: не удалось получить текущую дату с NTP-сервера.")
        return False  # Если не удалось получить дату, возвращаем ошибку и не продолжаем выполнение

    if current_date.date() <= expiration_date:  # Преобразуем current_date в date для сравнения
        return True
    else:
        return False

1-2. Checking all conditions

def check_conditions():
    expiration_date_str = "2024-12-31"  # Дата истечения
    expected_serial = "ABC123"  # Серийный номер жесткого диска
    
    # Проверка срока действия
    if not check_expiration_date(expiration_date_str):
        print("Ошибка: срок действия лицензии истек или не удалось получить текущую дату.")
        return False

    # Проверка серийного номера диска
    if not check_disk_serial(expected_serial):
        print("Ошибка: неверный серийный номер диска.")
        return False

    print("Все проверки пройдены успешно.")
    return True

This way, the script will only run on the specified devices until the expiration date; you can do the same with other parameters.

3. getpass (optional)

Used to enter a password securely without displaying it on the screen.

In principle, passwords do not have to be entered taking into account the two previous conditions, but if you want to increase security, then why not.

Add a function to get the current month:

def get_current_month():
    current_time = get_time_from_ntp()  # Получаем текущее время с NTP-сервера
    if current_time is None:
        return None
    return current_time.month  # Извлекаем номер текущего месяца

Function to check password based on month:

def check_password():
    month_number = get_current_month()  # Получаем текущий месяц
    if month_number is None:
        print("Ошибка: не удалось получить текущий месяц.")
        return False

    # Пароли для каждого месяца
    passwords_by_month = {
        1: "jan_pass_43kX",
        2: "feb_pass_wQ84",
        3: "mar_pass_29LZ",
        4: "apr_pass_fG12",
        5: "may_pass_98Xy",
        6: "jun_pass_Ue47",
        7: "jul_pass_kP93",
        8: "aug_pass_qB21",
        9: "sep_pass_Tm56",
        10: "oct_pass_Lz77",  
        11: "nov_pass_Ew32",
        12: "dec_pass_Vj89"
    }

    # Получаем правильный пароль для текущего месяца
    correct_password = passwords_by_month.get(month_number)

The following is an example using tkinter:

    # Создаем главное окно
    root = tk.Tk()
    root.withdraw()  # Скрыть основное окно

    # Открываем диалоговое окно для ввода пароля
    entered_password = simpledialog.askstring("Пароль", f"Введите пароль для месяца {month_number}:", show='*')

    if entered_password == correct_password:
        print("Доступ разрешен.")
        return True
    else:
        print("Неверный пароль. Доступ запрещен.")
        return False

In the initial code it is better to indicate the real reasons for the denial of access, when you are sure that everything is working, I recommend replacing all errors with “Access denied”, then it will be more difficult to determine exactly why the script does not work.

PyArmor 8+

Subcommands in PyArmor 8+ have changed compared to previous versions; I have not found a description of the work of the new subcommands in Russian, and that is partly why I decided to write this article.

Good, documentation Well written and didn't take long.

There are actually 3 subcommands in PyArmor8+:

1. reg/man– used to register a new license or update an existing PyArmor license.
2. gen (generate, g)– generates obfuscated scripts and necessary runtime files.

3. cfg – Shows and configures PyArmor environment parameters.

The paid version of PyArmor 8+ has its own commands for linking a script to a device and setting the license expiration date.

pyarmor gen -O dist4 -e 30 script.py– script with an expiration date of 30 days.

pyarmor gen -O dist5 -b "00:16:3e:35:19:3d HXS2000CN2A" script.py – the computer will be able to run the script only if the Ethernet address and the hard drive match.

Step-by-step obfuscation (available in the free version, the paid version has expanded functionality):

  1. Let's obfuscate the source script:

    pyarmor gen -O dist script.py

    -O dist – directory creation

  2. Obfuscation of some modules can cause errors at startup, since they are expected in an unmodified form, then:

    pyarmor gen -O dist --exclude tkinter script.py

    --exclude tkinter – excludes the module tkinter from the obfuscation process.

  3. We package the obfuscated script with PyInstaller:

pyinstaller –clean –onefile –icon=icon.ico –add-data “any.csv;.” –hidden-import pandas –hidden-import numpy –hidden-import tkinter –collect-all tkinter script.py

--clean – guarantees that the assembly will be done from scratch.

--onefile – creates one executable file without additional folders

--icon=icon.ico – icon

--add-data "any.csv;." – adds files necessary for the script to work

Important point when building, pyinstaller itself determines which libraries are used in your code and adds them to the executable, but when you package an obfuscated script, pyinstaller often cannot determine which libraries you used, so:

--hidden-import pandas – adds libraries necessary for the script to work

If –hidden-import is not enough and pyinstaller does not build correctly, you can see which libraries are missing (will be visible on the command line when launched) and use:

--collect-all tkinter – ensures that all files and dependencies (scripts, modules and resources) for tkinter will be compiled and added to the final executable file.

To summarize, paragraphs 1-3 of the article are aimed at protecting against unauthorized use of the application by the user, and PyArmor prevents attempts at reverse engineering and decompilation.

PS Any protection can be broken and PyArmor is no exception, besides this there are dozens of other ways to get your code or reproduce it by studying the functionality (which is symbolized by the gate without walls in the preview). But for this you will have to spend time, money, nerves and everything like that, and this is a completely different story.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *