Там сложно, ты не разберешься
- вторник, 30 августа 2022 г. в 00:39:01
В своей первой статье на Хабре я описывал опыт реверсинга и модификации проекта, доставшегося по наследству. Конечно, в отношении проекта на Python "реверсинг" - это гипербола, однако с чем-то ранее неизвестным столкнуться все же получилось. Если вкратце - вместо классических исходников использовались модули, загружаемые из .pyc, а не классических .py файлов. Философия "защитников" базируется на принципе "Там сложно, никто не разберется".
Ход событий же показал, что во-первых не так уж и сложно (передача параметров в хранимую процедуру PgSQL, и получение результата, возврат его пользователю - далеко не шедевр обфускации, скорее тут будет более применим принцип "Там несложно, любой разберется, но не захочет"), а во-вторых - кто-нибудь да поймет и найдет способ изменить поведение в нужном ключе.
Есть ли все-таки методы защиты исходников на python, и какие (относительно вменяемые) методы можно применять для решения этого вопроса?
Заходят как-то в бар .pyo, .pyc, и .pyd…
…а бармен им говорит "как дела в байт-коде, пацаны?"
Перед тем, как разобраться в методах защиты исходных кодов, вспомним как устроено выполнение скриптов в Python.
Реализация Python (в классическом случае это CPython), представляет собой компилятор и виртуальную машину. Скрипт, написанный на Python, преобразуется компилятором в байт-код, коий, в свою очередь, выполняется виртуальной машиной.
Байт-код же состоит из кодов операций (опкодов) виртуальной машины, и сопутствующих опкоду аргументов.
.py-файлы содержат исходный код, и выполняются виртуальной машиной, проходя этап преобразования в байт-код.
Однако, можно каждый раз не преобразовывать .py-файлы в байт-код, да и зачем, если они, например, не меняются? Для хранения и выполнения байт-кода служат .pyc-файлы (их можно, например, наблюдать в директориях __pycache__).
.pyc-файлы содержат готовый байт-код, непосредственно выполняемый виртуальной машиной.
.pyo-файлы также как и .pyc-файлы содержат готовый байт-код, и, по-сути ничем, кроме предварительной оптимизации кода (вырезание assert`ов и docstring`ов) не отличаются.
Кроме хранения байт-кода для виртуальной машины возможен сценарий, при котором исходный код на Python преобразуется в исходный код на языке C, и затем собирается в .pyd-файлы (для Windows) или .so для Linux.
А вот в наше время…
Вы когда-нибудь задумывались, почему на Python не делают crackmes? Вроде бы можно взять скрипт, преобразовать его в байт-код и…с той же легкостью преобразовать обратно в исходный код, каким-нибудь uncompyle. Да и отладка Python-скрипта - дело не сказать что очень сложное. Однако crackmes на python, хоть их и кот наплакал, но все же существуют.
Делятся они приблизительно на следующие группы:
Код на Python, обернутый вместе с интерпретатором другим компилируемым языком, возможно преобразованный в .pyd
.pyc-файлы, при создании которых использовался модифицированный интерпретатор, и стандартным интерпретатором они не выполняются
Единственные найденные мной примеры crackmes на чистом Python. Демонстрируют, что код можно запутать. “Распутывание” сводится к расстановке переносов, удалении неиспользуемых символов, которые валидны синтаксически, однако не несут никакой смысловой нагрузки, и, возможно, недолгой отладке с модификациями на лету.
Crackmes, как мерило того, насколько можно усложнить задачу “распутывания” кода, и ответа на вопрос “что же хотел сказать автор”, в случае с Python показывает, что голым Python-кодом в общем-то ежа не напугаешь (лишь бы не рассмешить).
Modus operandi
Зная основную теорию, можно выдвинуть ряд гипотез о точках внедрения защитных механизмов:
.py
Если говорить о преобразовании кода на Python в другой код на Python, который делает ту же работу, однако менее читабелен, либо совсем нечитабелен, то на ум приходит слово “обфускация”.
Однако обфускация, как процесс изменения исходного кода, может в том числе приводить к изменениям в работе алгоритмов, например в быстродействии, что не есть хорошо.
Единственным методом защиты на данном этапе может быть рекомендация писать настолько некачественный код, чтобы его модификация вошла в число опасных БДСМ-практик.
.pyc \ .pyo
Байт-код можно декомпилировать обратно в читаемый исходный код, что собственно и является главной опасностью. Следовательно, направление действий в данном случае - затруднение анализа и декомпиляции байт-кода.
Структура .pyc-файла достаточно проста:
Первые 2 байта это magic_number, указывающий на версию интерпретатора.
2 фиксированных байта 0x0D 0x0A
4 байта дата и время последней модификации
Далее следует байт-код
Для решения задачи невозможности декомпиляции байт-кода так или иначе придется модифицировать используемую реализацию Python, по-сути создавая кастомизированную версию виртуальной машины, поддерживающую привидение байт-кода к выполнимому виду.
Запутать байт-код можно, применяя следующие техники:
Шифрование. При компиляции дополнительно преобразовать весь байт-код в невыполнимый без стадии расшифровки.
Обфускация опкодов. Суть метода заключается в дополнительном преобразовании непосредственно опкодов таким образом, чтобы "сдвинуть" опкод относительно его оригинального местоположения в таблице, превратив его в другой опкод, что, в свою очередь, сделает невозможным выполнение такого байт-кода немодифицированной версией виртуальной машины.
Недостатками подобных методов являются:
Необходимость поддержки при переходе на новые версии Python
В случае с шифрованием - хранение ключа для расшифровки на машине с запускаемым кодом, либо имплементации протокола обмена ключами.
В случае с обфускацией опкодов - возможность анализа сдвигов (при применении простых алгоритмов вроде шифра Цезаря), либо необходимость применения более сложных алгоритмов, с целью усложнения статического анализа.
.pyd \ .so
Самый эффективный, с точки зрения возможностей защиты метод - компиляция в исполняемый код (не байт-код виртуальной машины Python, а машинный код), и дальнейшее “запутывание”.
Сама сборка .py-файла в библиотеку производится с помощью cython, и достаточно нетривиальна. В результате мы имеем код, который не имеет вообще ничего общего с изначальным исходным кодом, и декомпилирован быть не может (только дизассемблирован и исследован, в результате чего можно делать выводы об алгоритмах и особенностях кода).
В заключение хочу порекомендовать курсы по Python от моих друзей из OTUS. Подробнее о курсах по ссылкам ниже: