Стек: 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 не исключение, помимо этого существуют десятки других способов получить ваш код или воспроизвести его, изучив функционал (что символизируют врата без стен на превью). Но для этого уже придется потратить время, деньги, нервы и все в таком духе, и это уже совсем другая история.

Комментарии (21)


  1. outlingo
    18.10.2024 14:43

    Не ставя под вопрос решение о защите кода (и обфускации как механизма защиты) все же замечу, что если

    Разработано приложение, которое ... содержит конфиденциальные данные

    Тогда у вас проблемы.

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


    1. Den_BL Автор
      18.10.2024 14:43

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


  1. Zolg
    18.10.2024 14:43

    местный айтишник запускает гидру

    Если местный айтишник умеет в гидру, то и набрать в любимом поисковике "unpack pyarmor" осилит


    1. Den_BL Автор
      18.10.2024 14:43

      Я сомневаюсь, что "unpack pyarmor" будет достаточно, что сломать последнюю версию pyarmor. То что сломать можно да, но это нетривиальная задача и в большинстве компаний (особенно в малом и среднем бизнесе) маловероятно, что такой сотрудник найдется.


      1. Zolg
        18.10.2024 14:43

        В большинстве компаний и с сотрудниками умеющими в гидру (кроме как запустить) тоже не густо


  1. baldr
    18.10.2024 14:43

    Честно говоря, все эти способы в интерпретируемом скрипте совершенно не помешают взять и отключить их.

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

    Обфускация - в принципе, хороший способ. Может очень сильно усложнить жизнь. Ещё можно попробовать построить интерпретатор питона из исходников, но, скажем, поменять в нём опкоды. Пока до этого догадаются "исследователи" - может пройти неделя.

    Но самое надёжное - перевести в сишный код и скомпилировать.


    1. 0xC0CAC01A
      18.10.2024 14:43

      Но самое надёжное - перевести в сишный код и скомпилировать.

      Это тоже детский сад, но - старшая группа ))


    1. Den_BL Автор
      18.10.2024 14:43

      Спасибо за комментарий. В статье я хотел рассказать о доступных инструментах именно на питоне. Перевести в сишный код хорошая идея, но единственное я не знаю как себя буду вести библиотеки вроде Селениум, при такой операции.


  1. 0xC0CAC01A
    18.10.2024 14:43

    Ну и сколько часов продержится такая защита, если 30 лет назад в экзкешниках, в машинном коде умудрялись все эти проверки находить и NOPами забивать?


    1. Den_BL Автор
      18.10.2024 14:43

      Против реверс-инженера недолго продержится. Но если руководство поставит эту задачу, кому то еще, думаю они через неделю махнут рукой. А как противостоять реверс-инженеру надо писать статью "Сложная защита скрипта без выноса логики на сервер")


      1. Lodinn
        18.10.2024 14:43

        Не надо. "Как противостоять реверс-инженеру?" - примерно никак. Только если не обфусцируете код, вычисляющий 2*2, в десять слоёв. Иначе говоря - пока экономический эффект от слома меньше, чем стоимость собственной разработки. Наглядный пример - нашумевший взлом криптокошелька (Joe Grand / RoboForm). С достаточной мотивацией и прямым доступом к девайсу/софту ломается практически всё.

        То, что происходит на чужом компе, контролировать в принципе нельзя. К моменту, когда конфиденциальная информация утекла туда, you've been pwned. Поэтому не стоит тратить ресурсы на фантазии, надо переосмыслять задачу.


    1. baldr
      18.10.2024 14:43

      если 30 лет назад в экзкешниках, в машинном коде умудрялисьесли 30 лет назад в экзкешниках, в машинном коде умудрялись

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


  1. drdead
    18.10.2024 14:43

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

    как работать будем в случае поломки физического ЖД, так и в случае если это клауд-сервер и ребята-девопсы запилили рестор из снэпшота после неудачного апдейта винды?


    1. Den_BL Автор
      18.10.2024 14:43

      Запаковать заново экзешник, изменить зависимости при необходимости


      1. Sly_tom_cat
        18.10.2024 14:43

        Кто мешает это сделать вору?


  1. allter
    18.10.2024 14:43

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


  1. vp7
    18.10.2024 14:43

    Извините, но статья уровня ученика 5-7 классов обычной школы.

    Дальше по вашим пунктам:

    1. NTP. Он доступен не везде, но допустим, доступен. Подменить NTP сервер - задачка уровня школьника.

    2. Запуск внешних приложений через subprocess - меняется тем же школьником, в вашем случае даже путь не прописан, поэтому поможет игра в переменную path, иначе помог бы любой вариант jail/chroot.

    3. Обфускация. Самое сильное из вашего списка и реально может потребовать 1-5 дней человека, знакомого с python и механизмами обфускации (если не найдётся готового решения). Но у вас реально такой важный продукт, что конкурент не захочет потратить это время на его вскрытие? Если да,... но не пофигу ли?

    Единственный реально работающий способ - перенос бизнес-логики на находящееся у вас оборудование.


  1. vagon333
    18.10.2024 14:43

    Использую Nuitka для перевода Python кода в бинарник.
    Пробовал дебажить бинарник.
    Логику проследить по бинарнику довольно сложно.


  1. GarfieldX
    18.10.2024 14:43

    Хотелось бы услышать пример что за такое приложение упоминается. Для чего оно может понадобится и какие задачи решать. К чему вопросы? Для разраба с 20+ лет стажем на всяком разном статейка похожа на заметки студента. Такая же фигня в голове была в то время и тоже всякие "защиты* мастерил. Только время было другое и десктопные приложения были. Сейчас же уже все давно реализуется через веб. Потому очень интересно какое такое приложение может быть нынче у работника что может иметь интерес доя конкурента.


  1. Keeper10
    18.10.2024 14:43

    Оттранслируйте файлы модулей в *.pyc и распространяйте их. От скрипт-киддисов будет достаточно. От профессиональных реверсеров не спасёт ничего.


    1. baldr
      18.10.2024 14:43

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