Простая защита скрипта на Python

от автора

Стек: Python 3.11.7, subprocess, ntplib, getpass для системы, времени, пароля,

PyArmor 8+ для обфускации.

Сценарий: Разработано приложение, которое дает преимущество перед конкурентами, или содержит конфиденциальные данные. Приложение предстоит установить нескольким сотрудникам (можно увеличить количество немного изменив подход), при этом вы не планируете переносить часть логики на сервер.

Возможны и другие сценарии, я описал наиболее на мой взгляд вероятный случай, когда такого рода защита может понадобиться.

В данной статье я расскажу несколько способов, которые не дадут запустить приложение, где не следует, и скорее всего отобьют желание лишний раз лезть в ваш код.

Итак, в один прекрасный день неблагодарный пользователь уходит в новую компанию, прихватывает с собой ваш экзешник, получает бонусы и радуется. Тем временем местный айтишник запускает гидру (Ghidra) или любой другой инструмент для реверс-инжиниринга. И вот результат вашей работы за год, месяц, неделю, день, оказался у конкурента.

Чтобы избежать такого развития события мы можем: 1) привязать скрипт к жесткому диску, MAC-адресу, дате, имени устройства, UUID материнской платы, версии BIOS/TPM, IP-адресу, токену/сертификату, сетевому соединению, USB-ключу и так далее нужное подчеркнуть 2) обфусцировать (замаскировать) сам скрипт.

Необязательно привязывать скрипт сразу ко всем параметрам, достаточно выбрать пару из них. Например, если пользователь будет работать с оборудованием, предоставленным работодателем, можно привязать скрипт к уникальным характеристикам устройства, а если предполагается использование неподконтрольной техники, то использовать пароли или временные ограничения.

Когда использовал все возможные зависимости

Когда использовал все возможные зависимости

Особенно эффективно если пользователи не знают, какие именно зависимости вы использовали, таким образом будет сложнее их подменить на новом устройстве.

Перейдем к библиотекам.

1. subprocess

Используется для получения серийного номера жесткого диска, обеспечивая привязку программы к конкретному устройству.

Для получения серийного номера жесткого диска на Windows, в командной строке:

wmic diskdrive get serialnumber

Проверка серийного номера жесткого диска:

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

Используется для получения точного времени с NTP-сервера, что предотвращает манипуляции с системным временем.

Мы можем ограничить срок действий нашего скрипта, например до 31.12.2024, и тогда даже если сотрудник заберет с собой жесткий диск, программа прекратит свое действие через какое-то время.

Функция для получения времени с NTP-сервера:

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. Проверка всех условий

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 

Таким образом скрипт будет запускаться только на указанных устройствах до даты истечения, аналогично можно сделать и с другими параметрами.

3. getpass (опционально)

Используется для безопасного ввода пароля без его отображения на экране.

В принципе пароли не обязательно вводить с учетом двух предыдущих условия, но если хочется повысить безопасность, то почему бы и нет.

Добавляем функцию для получения текущего месяца:

def get_current_month():     current_time = get_time_from_ntp()  # Получаем текущее время с NTP-сервера     if current_time is None:         return None     return current_time.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)

Дальше идет пример с использованием 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 

В первоначальном коде лучше указывать реальные причины отказа в доступе, когда убедитесь, что все работает, рекомендую заменить все ошибки на “В доступе отказано”, тогда сложнее будет определить почему именно скрипт не работает.

PyArmor 8+

Подкоманды в PyArmor 8+ изменились по сравнению с прошлыми версиями, я не встретил на русском языке описания работы новых подкоманд и отчасти поэтому решил написать эту статью.

Благо, документация написана хорошо и не заняла много времени.

В PyArmor8+ фактически 3 подкоманды:

 1. reg/man— используется для регистрации новой лицензии или обновления существующей лицензии PyArmor.
2. gen (generate, g)— генерирует обфусцированные скрипты и необходимые runtime файлы.

3. cfg — показывает и настраивает параметры среды PyArmor.

В платной версии PyArmor 8+ есть собственные команды для привязки скрипта к устройству и установлению срока действии лицензии.

pyarmor gen -O dist4 -e 30 script.py— скрипт с датой истечения срока действия 30 дней.

pyarmor gen -O dist5 -b "00:16:3e:35:19:3d HXS2000CN2A" script.py — компьютер сможет запустить скрипт, только если адрес Ethernet и жёсткий диск совпадают.

Пошаговая обфускация (доступна и в бесплатной версии, в платной функционал расширен):

  1. Обфусцируем исходный скрипт:

    pyarmor gen -O dist script.py

    -O dist – создание директории

  2. Обфускация некоторых модулей может вызвать ошибки при запуске, так как они ожидаются в неизменённом виде, тогда:

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

    --exclude tkinter – исключает модуль tkinter из процесса обфускации.

  3. Упаковываем обфусцированный скрипт с 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 — гарантирует, что сборка будет выполнена с нуля.

--onefile — создаёт один исполняемый файл без дополнительных папок

--icon=icon.ico – иконка

--add-data "any.csv;." — добавляет файлы необходимые для работы скрипта

Важный момент при сборке pyinstaller сам определяет какие библиотеки используются в вашем коде и добавляет их в экзешник, но когда вы упаковываете обфусцированный скрипт pyinstaller часто не может определить какие библиотеки вы использовали поэтому:

--hidden-import pandas — добавляет библиотеки необходимые для работы скрипта

Если —hidden-import недостаточно и pyinstaller неправильно осуществляет сборку, можно посмотреть каких библиотек не хватает (будет видно в командной строке при запуске) и использовать:

--collect-all tkinter — гарантирует, что все файлы и зависимости (скрипты, модули и ресурсы) для tkinter будут собраны и добавлены в итоговый исполняемый файл.

Резюмируя, пункты 1-3 статьи направлены на защиту от несанкционированного использования приложения пользователем, а PyArmor предотвращает попытки реверс-инжиниринга и декомпиляции.

P.S. Любую защиту можно сломать и PyArmor не исключение, помимо этого существуют десятки других способов получить ваш код или воспроизвести его, изучив функционал (что символизируют врата без стен на превью). Но для этого уже придется потратить время, деньги, нервы и все в таком духе, и это уже совсем другая история.


ссылка на оригинал статьи https://habr.com/ru/articles/851760/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *