python

Там сложно, ты не разберешься

  • вторник, 30 августа 2022 г. в 00:39:01
https://habr.com/ru/company/otus/blog/682872/
  • Блог компании OTUS
  • Python


В своей первой статье на Хабре я описывал опыт реверсинга и модификации проекта, доставшегося по наследству. Конечно, в отношении проекта на 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, хоть их и кот наплакал, но все же существуют. 

Делятся они приблизительно на следующие группы:

  1. Код на Python, обернутый вместе с интерпретатором другим компилируемым языком, возможно преобразованный в .pyd

  2. .pyc-файлы, при создании которых использовался модифицированный интерпретатор, и стандартным интерпретатором они не выполняются

  3. Единственные найденные мной примеры crackmes на чистом Python. Демонстрируют, что код можно запутать. “Распутывание” сводится к расстановке переносов, удалении неиспользуемых символов, которые валидны синтаксически, однако не несут никакой смысловой нагрузки, и, возможно, недолгой отладке с модификациями на лету.

Crackmes, как мерило того, насколько можно усложнить задачу “распутывания” кода, и ответа на вопрос “что же хотел сказать автор”, в случае с Python показывает, что голым Python-кодом в общем-то ежа не напугаешь (лишь бы не рассмешить).

Modus operandi

Зная основную теорию, можно выдвинуть ряд гипотез о точках внедрения защитных механизмов:

  1. .py 

Если говорить о преобразовании кода на Python в другой код на Python, который делает ту же работу, однако менее читабелен, либо совсем нечитабелен, то на ум приходит слово “обфускация”. 

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

Единственным методом защиты на данном этапе может быть рекомендация писать настолько некачественный код, чтобы его модификация вошла в число опасных БДСМ-практик.

  1. .pyc \ .pyo

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

Структура .pyc-файла достаточно проста:

  1. Первые 2 байта это magic_number, указывающий на версию интерпретатора.

  2. 2 фиксированных байта 0x0D 0x0A

  3. 4 байта дата и время последней модификации

  4. Далее следует байт-код

Для решения задачи невозможности декомпиляции байт-кода так или иначе придется модифицировать используемую реализацию Python, по-сути создавая кастомизированную версию виртуальной машины, поддерживающую привидение байт-кода к выполнимому виду.

Запутать байт-код можно, применяя следующие техники:

  1. Шифрование. При компиляции дополнительно преобразовать весь байт-код в невыполнимый без стадии расшифровки.

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

Недостатками подобных методов являются:

  1. Необходимость поддержки при переходе на новые версии Python

  2. В случае с шифрованием - хранение ключа для расшифровки на машине с запускаемым кодом, либо имплементации протокола обмена ключами.

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

  1. .pyd \ .so

Самый эффективный, с точки зрения возможностей защиты метод - компиляция в исполняемый код (не байт-код виртуальной машины Python, а машинный код), и дальнейшее “запутывание”.

Сама сборка .py-файла в библиотеку производится с помощью cython, и достаточно нетривиальна. В результате мы имеем код, который не имеет вообще ничего общего с изначальным исходным кодом, и декомпилирован быть не может (только дизассемблирован и исследован, в результате чего можно делать выводы об алгоритмах и особенностях кода). 

В заключение хочу порекомендовать курсы по Python от моих друзей из OTUS. Подробнее о курсах по ссылкам ниже: