Пролог…
Привет, Habr! Компания, в которой я работаю, занимается системами умного дома и видеонаблюдения. Услуга работает по форме SaaS. Но недавно было решено продавать и локальное решение. За основу «коробки» было решено взять облако, немного его переделать и вот готов новый продукт…
С переездом с облачных «рельс» возник ряд вопросов. Один из них в том, что весь проект написан на Python‑ язык интерпретируемый на лету и не защищенный от копирования от слова совсем. Второй момент, что в проекте используется Django. Фреймворк со своими зависимостями, которые еще аукнулись нам в процессе поиска решений. То с чем мы столкнулись и к чему пришли далее в статье.
Глава 1. Поиск решения
Как защитить код от изменения и копирования? Как сказал классик: «С этим вопросом я пошел в Интернет». И вот что удалось найти:
Решение 1 - смирись
Многие на форумах на вопрос как защищаетесь отвечают- никак. Аргументируют это тем, что кому надо и так взломают, а у python другая философия. Будь открыт миру и все такое.
Все так, но нашему проекту много лет и не хотелось бы терять наработки. Один нечестный клиент и весь труд насмарку.
Решение 2 - перепиши
Второй по популярности ответ- переписать на компилируемый язык программирования. Кроме Python у нас используется Go. Как вариант рассматривали переписать только важную часть на него, но проект настолько монструозный, что переписать его будет долго и дорого. Бизнес не оценил находку…
Решение 3 - отдавай только .pyc файлы
Как и большинство интерпретируемых языков программирования, Python перед выполнением кода переводит его формат байт-кода(такие файлы имеют формат .pyc и лежат в директории __pycache__). Уже этот байт код исполняется интерпретатором. Идея подхода- вручную “скомпилировать” весь код в байт-код. Удалить исходники и запускать pyc файлы.
Плюсы такого метода:
Код становится не читаемым
Минусы:
Такой код восстанавливается обратно в идентичном виде. Вплоть до названий переменных.
Пример исполнения.
Дан скрипт
def foo():
"""Тестовая функция"""
print("Hello, Habr!")
if __name__ == "__main__":
foo()
“Компилируем” его
python -m compileall main.py
на выходе получаем
Тут уже подозрительно что мы видим в неизменном виде комментарии и часть функции.
Восстановим код.
Возьмем библиотеку uncompyle6.
uncompyle6 -o . pycache/main.cpython-38.pyc
Получаем все в исходном виде вплоть до комментариев.
Внимание!!! Библиотека работает с кодом максимум 3.8 версии.
Итог данного метода. Если вам нужно быстро и хоть как-то защитить код и при этом рисками можно пренебречь- то этот метод для вас. Но нас этот метод не устроил на тот момент.
Решение 4 - Обфускация кода
Обфуска́ция (от лат. obfuscare — затенять, затемнять; и англ. obfuscate — делать не очевидным, запутанным, сбивать с толку) или запутывание кода — приведение исходного кода или исполняемого кода программы к виду, сохраняющему её функциональность, но затрудняющему анализ, понимание алгоритмов работы и модификацию при декомпиляции. [1]
Суть подхода, что мы делаем код не читаемым, меняя понятные названия переменных на кракозябры.
Один из самых популярных проектов для обфускации pyarmor
Программа интересная. На простых скриптах все хорошо, но большой проект не удастся обфусцировать обычной версией - стоит ограничение в 32768 байт. Нужна платная. Проект иностранный, а ситуация сейчас… Эх, ну в общем сами понимаете.
Решение 5. Найти готовое решение по защите.
Тут все просто, в свете последних событий все производители таких решений, что мы нашли ушли из России. Если есть отечественные варианты буду признателен в комментариях.
Глава 2. Свой импортер и загрузчик
Перебрав предыдущие варианты мы поняли, что нам нужно:
Шифровать код, а не обфусцировать/компилировать и т.д его.
Метод должен быть простым и минимально затрагивать старый код
Должно быть достаточно сложной чтобы дилетанты вовсе не могли сломать, а опытных если не остановить, то хотя бы затруднить взлом.
Должен быть прост в поддержке, ибо нанимать/выделять сеньоров для сопровождения сего никто не даст.
После этого я наткнулся на PEP 302 и статью на хабре(какая ирония).
Если вкратце, pep 302 добавил новый набор перехватчиков импорта, которые предлагают лучшую настройку механизма импорта Python. В существующую схему можно внедрить хук нового стиля, что позволяет более точно контролировать, как модули находятся и как они загружаются.
Для его реализации нужно реализовать свой класс-импортер или класс-загрузчик(зависит от задачи), добавить в sys.path_hooks и все. Должно заработать.
Ура! - воскликнул я. Вот и решение.
Собрал небольшой пет-проект и вуаля- все работает.
Глава 3. Не все так просто как казалось…
После этого началось внедрение в основной проект. Таким образом нужно было защитить три подпроекта.
Первый заработал без сучка и без задоринки. Если не считать некоторые проблемы с тестами проект работал.
Настало время второго проекта…
Во втором проекте в одной из зависимостей использовался модуль inspect.
Hidden text
Модуль inspect предоставляет несколько полезных функций, помогающих получить информацию о живых объектах, таких как модули, классы, методы, функции, трассировки, объекты фреймов и объекты кода.(Дословный перевод:https://docs.python.org/3/library/inspect.html)
Этот модуль к моему удивлению активно был связан с ядром самого языка, написанном на C, при этом не подчинялся стандартному механизму импорта. После недельной битвы с модулем, понял, что придется идти в Си. НО! Вспомнив 4 пункт с ограничениями остановился. Сишников в команде нет, а подкладывать такую свинью коллегам не хочется.
Эпилог
Увы и ах! Но после двух недель изысканий мы остановились на первом варианте, а как альтернативу вариант с “компилированием” в pyc-файлы. Юристы пошли дописывать дополнительные пункты в договор, а мы продумывать что в перспективе переписать на GO.
Какой урок я вынес? Простыми способами Python не защитить, теперь я могу сказать это испытав на своей шкуре. Если у читателя есть навыки и желания, можно окунуться в дебри СИшного кода, но это уже другая история…
Ссылки:
Комментарии (16)
MountainGoat
00.00.0000 00:00Просто берём Nuitka, который делает файлы, работающие так же как .pyc но гораздо труднее читаемые. Потому что там стандартный загрузчик и скомпилированный код. Сломать простую защиту в таком коде не проще и не сложнее, чем в скомпилированном С++ - то есть элементарно. А вот понять всю логику, конечно, будет уже напряжнее. В первую очередь, как и с Denuvo - из -за отсутствия хороших инструментов для реверса такой системы.
Из сложностей - код придётся разметить, и с Django мало кто пробовал его использовать.
P.S. А ещё существует Cython.
vagon333
00.00.0000 00:00Аналогичная задача — защита кода.
Просто берём Nuitka ...
Смотрел в сторону Nuitka. Интересное решение.
У вас получилось использовать в коммерческих продуктах?MountainGoat
00.00.0000 00:00Мои коммерческие продукты защищены в первую очередь Законом Неуловимого Джо. А Nuitka это в основном чтобы не подключать весь вычислительный стек чтобы один раз файл декодировать.
vagon333
00.00.0000 00:00Красиво ответили.
Нужно будет показать ваш ответ знакомому адвокату. Пусть поучится.
evlisouski
00.00.0000 00:00-2Почему бы не использовать зашифрованную ВМ в качестве локального сервера? Вроде как даже вполне рабочий костыль
TyVik
00.00.0000 00:00Так python же - компилируемый язык программирования!
Шучу, но в каждой шутке есть доля правды. Посмотрите код pydantic. Там, вроде, после сборки получаются so файлы. Правда, в Django много метамагии, может и не сработать. Но хоть критичную бизнес часть можно попробовать пересобрать.
moscow_intelligent
00.00.0000 00:00Как уже упомянули, nuitka это прекрасное решение вашей проблемы. А если вместе с ней использовать pyarmor на максимальном уровне обфускации, то вообще нечитабельно будет. Такое просто лень реверсить будет.
shamanis
00.00.0000 00:00+1На заре этого проекта я еще игрался с Numba, но не осилил. Привет бывшим коллегам ;)
Brogahnl
Выносить критичные части в Сython?
iuabtw
В своё время так и сделал с защитой - компилируешь обычный питон скрипт в .o и всё. Из проблем только - сборку настроить.
UsmanovTim Автор
Думали переписать на компилируемый язык, но и Go и Cython - это время. А в нашем случае выносить очень много.
gdasar
А разве на Cython нужно переписывать? Вроде как достаточно просто скомпилировать питоновский скрипт и уже он будет "защищён", плюс станет работать чуточку быстрее.
MountainGoat
Если в Cython просто в лоб засунуть Питоновский код, то на выходе получится питоновский же байткод, завёрнутый в бинарник. Чтобы Cython что-то компилировал, нужно код размечать.