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

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


  1. MaryRabinovich
    29.08.2022 23:43
    +3

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

    И всё таки: а для чего тут так? Цель в том, чтобы кого-то запутать? А что за проект? В каком проекте может быть так вот остро важна особенная безопасность... кода? Это код какой-то хитрющий как ноу-хау? Т.е., вот это вот всё для защиты авторских прав на код?

    Вроде бы, в основном в защите нуждаются не код, а данные.


    1. SerjV
      30.08.2022 00:28
      +3

      Вроде бы, в основном в защите нуждаются не код, а данные.

      Полагаю, что пытаются решить задачу "как на python (PHP, можно продолжить) написать проприетарное-проприетарное ПО". Для пыхи некоторые решения есть, но вот видимо захотели для питона...


      1. Dmitry89 Автор
        30.08.2022 10:17
        +1

        В числе прочих есть задача сделать так, чтобы ПО работало, а внести в него изменения самостоятельно было весьма затруднительно. MaryRabinovich делает акцент на том, что важны данные, а не код, и это отчасти верно, код позволяет работать с данными, и, например, данные из 1С для ИП Иванова ценнее, чем сама 1С, однако для 1С как вендора важнее обеспечить невозможность запатчить 1С до состояния "работает и жрать не просит". Так что, если статью применять к реальным задачам - то только к такми.


        1. SerjV
          30.08.2022 15:49
          +2

          однако для 1С как вендора важнее обеспечить невозможность запатчить 1С до состояния "работает и жрать не просит".

          В этом и есть смысл проприетарного ПО, в общем-то...


    1. ubahhukob
      30.08.2022 16:32

      Или вин рар юзать ещё и сэкономит место


  1. lenyaplay
    30.08.2022 05:28

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