PyArmor: как запутать код, чтобы защитить программное обеспечение
Все еще не шифруете свой скрипт? Тогда самое время изучить обфускацию. Сегодня познакомлю с полезной библиотекой PyArmor, расскажу о двух методах работы модуля и на собственном примере покажу, как запутать код от нежелательного просмотра третьими лицами.
В повседневной работе существуют ситуации, когда по очевидным причинам необходимо предоставить скрипты заказчику, но, пересылая собственные разработки, в полном объеме можно потерять контроль над ними, включая авторские права на реализованные коды.
В таких случаях целесообразно защитить собственные коды, зашифровав их (защитить/сохранить/добавить условия для управления зависимостями внутри кода), точно также, как если бы стояла задача предоставления кода для пользования клиенту в течение какого-либо определенного периода времени.
Разработчики скриптов знают, что код на Python поддерживает анализ байт-кода, позволяющий ускорять работу интерпретатора и сам код на Python очень сложно защитить от нежелательного просмотра третьими лицами. Даже новички в разработке скриптов на Python могут заполучить исходный скрипт .py из файла .exe.
Для этого случая на Python существует очень полезная библиотека pyarmor с помощью которой можно воспользоваться всеми вышеизложенными функциями защиты скрипта от нежелательного взлома и метод, который позволяет защитить код называется обфускация.
Библиотека PyArmor имеет несколько вариантов работы – через консоль, а также с использованием localhost GUI – графического пользовательского интерфейса.
Установка и использование библиотеки pyarmor
В консоли необходимо выполнить следующую команду, чтобы установить модуль:
Для того чтобы начать работу с графическим интерфейсом библиотеки необходимо сперва его установить, выполнив следующую команду:
Библиотеки успешно установлены, теперь можно приступить непосредственно к самому шифрованию скрипта.
В первую очередь необходимо создать отдельную папку, в которой будет храниться скрипт с кодом «my_script.py».
В качестве примера скрипта возьму самый простейший скрипт с математическими функциями и с определением функции вывода (функция вывода довольно проста и не нуждается в каких-либо пояснениях):
Теперь зашифрую этот код, выполнив в консоли несколько команд.
Для начала необходимо изменить путь до директории в которой лежит файл через:
Затем необходимо выполнить команду обфускации:
Команда выполнена успешно – obfuscate 1 scripts Ok.
Теперь в исходной папке появилась новая директория dist, в которой содержится папка pytransform и зашифрованный скрипт my_script.py, при открытии которого, на первый взгляд, совершенно невозможно понять какой код там содержится.
Теперь можно проверить функцию вывода зашифрованного скрипта my_script.py – выполним его с помощью консоли python и увидим, что действительно зашифрованный скрипт работает и выводит верный результат подсчета математической функции.
Давайте теперь посмотрим, как можно защитить файл и разрешить работы с ним со встроенной виртуальной датой – все это необходимо для того, чтобы действительно защитить и продать свой код заказчику с добавлением реальной лицензии.
Если после запуска скрипта программа выдает сообщение об ошибке «Дата действия лицензии истекла», то это означает, что встроенная дата (после которой программа не запустится) уже прошла.
Если же имеется несколько взаимосвязанных файлов, которые необходимо зашифровать, то можно использовать команду recursive вместо restrict.
Реализация шифрования может быть выполнена в разных операционных системах, например, Windows, Ubuntu.
GUI библиотеки
Что касается работы с графическим пользовательским интерфейсом библиотеки pyarmor, необходимо выполнить команду в консоли:
Выполнение команды откроет локальный хост графического пользовательского интерфейса localhost.
Нажимаю раздел «Obfuscаte Script Wizаrd», выбираю исходный путь — path до скрипта и указываю название скрипт, нажимаю «Далее».
Нажимая «obfuscate» появляется аналогичная папка dist с зашифрованным в ней скриптом.
Подводя итог, можно сказать, что библиотека pyarmor с высокой скоростью выполнения шифрует скрипт, тем самым защищая его, теперь можно не беспокоиться о сохранности разработанного кода и с 100% вероятностью сохранения конфиденциальности программного обеспечения передавать его третьим лицам для использования.
Encryption for Protecting Python Source Code
Python is a great programming language, it has so many uses, but one thing that it doesn’t do well is help protect your hard work from others. Python source code is plain-text, which means that anyone with access to your files can see what you wrote. Not great when you’ve just written the latest advancement in artificial intelligence (AI) or the best machine learning (ML) algorithm on the planet.
Why can’t I just distribute Bytecode?
Python has a great feature where it first compiles your source code into bytecode; this is a low-level platform-independent representation of your source code. Back in the days when computers were slow, this was helpful, but when trying to distribute secure code this is a problem. Most solutions for securing Python code involve the distribution of .pyc files. Now, this isn’t all that bad as it does take some effort to reverse engineer a .pyc file. However, that still leaves the possibility for reverse engineering of the file to take place.
Bytecode also limits the version of Python your userbase requires to run your code. If your end-users upgrade their Python version then your code may stop working altogether due to the use of pickle; Python’s object serialisation library. This is where SOURCEdefender can help. It is a commercial offering that has been written from the ground up to help protect Python code and to overcome some of the issues you face when changing Python versions such as bytecode magic numbers.
AES 256-bit Encryption
Under the hood, SOURCEdefender scrambles your plain-text source code with AES-256 encryption. AES is a symmetric algorithm that uses the same key for both encryption and decryption (the security of an AES system increases exponentially with key length).
Installation
The sourcedefender package is available from PyPi and can be installed in the usual way:
Об одном способе защиты исходников Python-программы
Однажды мне пришлось участвовать в разработке одного небольшого проекта для научных расчётов, который разрабатывался на языке программирования Python. Изначально Python был выбран как удобный и гибкий язык для экспериментов, визуализации, быстрого прототипирования и разработки алгоритмов, но в дальнейшем стал основным языком разработки проекта. Надо заметить, что проект был хоть и не большим, но довольно насыщенным технически. Для обеспечения требуемой функциональности, в проекте широко применялись алгоритмы теории графов, математическая оптимизация, линейная алгебра и статистика. Также использовались декораторы, метаклассы и инструменты интроспекции. В процессе разработки пришлось использовать сторонние математические пакеты и библиотеки, например, такие как numpy и scipy, а также многие другие.
Со временем стало ясно, что переписывать проект на компилируемом языке слишком затратно по времени и ресурсам. Скорость работы и потребление памяти не являлись критичными показателями в данном случае и были вполне приемлемыми и достаточными. Поэтому было принято решение оставить всё как есть, и продолжить разработку и поддержку проекта на языке Python. К тому же, документация по большей части уже была написана с использованием Sphinx.
Проект являлся библиотекой, функции которой использовались в одном из модулей расширения в крупном программном комплексе. Программный комплекс был написан на C++, являлся коммерческим продуктом, имел защиту с аппаратным ключом и поставлялся клиентам без предоставления исходных кодов.
Здесь сразу обозначилась новая проблема: как защитить исходные коды нашей Python-библиотеки? Может быть, в ином случае никто бы не стал этим заниматься, я бы уж точно, но в библиотеке были реализованы некоторые ноу-хау, и руководители проекта не хотели, чтобы данные наработки попали к конкурентам. Так как я был одним из исполнителей, мне пришлось озаботиться данной проблемой. Далее я постараюсь рассказать об основной идее, что из этого вышло, и как нам удалось скрыть Python-исходники от лишних глаз.
Что предлагают люди
Как известно, наверное, большинству разработчиков, Python — язык интерпретируемый, динамический с богатыми возможностями интроспекции. Бинарные файлы модулей *.pyc и *.pyo (байт-код) легко декомпилируются, поэтому распространять их в чистом виде нельзя (если уж мы решили не показывать исходники по-настоящему).
Как, я думаю, любой на моём месте, сначала я решил поискать, а что вообще делают люди в таких случаях? Первые же поисковые запросы показали, что люди не знают, что делать и спрашивают об этом на stackoverflow и в других местах, например, вот вопрос на stackoverflow. Поискав, я пришёл к выводу, что везде предлагают несколько спорных способов:
- Забить и не париться, всё равно, кому надо — расковыряет;
- Переписать на компилируемом языке;
- Сделать обфускацию исходников, например с помощью раз и два;
- Транслировать все Python-модули в модули расширения (*.pyd) с помощью Cython или Nuitka (как сделал warsoul — автор данной статьи);
- Заменить опкоды в исходниках Python-интерпретатора и распространять свою сборку, как предлагалhodik.
По многим причинам я отбросил все эти способы как неподходящие. Например, обфускация Python-кода. Ну какая может быть обфускация, когда синтаксис языка построен на отступах, а сам язык пронизан «хитрой интроспекцией»? Транслировать все Python-модули в бинарные модули расширения тоже не представлялось возможным, т. к. проект, напомню, был достаточно сложным технически с использованием множества сторонних пакетов, да и сам состоял из большого числа модулей в многоуровневой иерархии пакетов, которые было утомительно перегонять в *.pyd, а потом ловить баги, вылезающие на ровном месте. Возиться с заменой опкодов не хотелось, так как пришлось бы распространять и поддерживать собственную сборку интерпретатора Python, да ещё и компилировать им Python-модули всех используемых сторонних библиотек.
В какой-то момент мне показалось, что эта идея с защитой Python-исходников бесполезная, надо всё это бросить и убедить руководство, что ничего не выйдет и заняться чем-нибудь полезным. Отдаём *.pyc файлы и ладно, кто там будет разбираться? Убедить не получилось, переписывать библиотеку на C++ никому не хотелось, а проект нужно было сдавать. В итоге всё же кое-что получилось сделать. Об этом, если всё ещё интересно, можно прочитать далее.
Что сделали мы
Что может лучше всего защитить какую-либо информацию на цифровом носителе от посторонних? Я думаю, что это шифрование. Вооружившись этой фундаментальной идеей, я решил, что исходники надо шифровать, а иначе и быть не должно. Для стороннего наблюдателя, который начал проявлять излишний интерес, всё это должно выглядеть как куча непонятных файлов с непонятным содержимым. Вполне себе обфускация, но более продвинутая чем заменять имена переменных и вставлять пустые строчки.
Ход моих мыслей был следующим:
- Шифруем каким-либо способом все исходники нашей Python-библиотеки, можно их даже перемешать и изменить имена файлов модулей и пакетов;
- Пишем обвязку для того, чтобы Python-интерпретатор умел загружать и импортировать модули из зашифрованных текстовых файлов (расшифровка, восстановление структуры пакетов и имён файлов, импорт и т. д.);
- «Прячем» всё это в бинарный модуль расширения (*.pyd), чтобы никто не догадался.
Основная идея, думаю, ясна — это более продвинутая обфускация. Как это сделать? Погуглив, я пришёл к выводу, что сделать это вполне реально и даже достаточно просто. С шифрованием исходников всё понятно, зашифровать и/или обфусцировать файлы можно множеством способов, главное, чтобы там была «каша» и «ничего не понятно», а также всё это должно возвращаться к первоначальному виду неизвестным способом (в случае обфускации). Для приведённого здесь примера я буду использовать Python-модуль base64 для «шифрования». В некритичных случаях можно применять замечательный пакет obfuscate.
Python the Importer Protocol
Как же нам реализовать возможность импортировать модули из зашифрованных файлов? К счастью, в Python реализована система хуков при импорте, которая работает на основе Importer Protocol (PEP 302). Значит эту возможность и будем использовать. Для перехвата импортов используется словарь sys.meta_path , в котором должны храниться объекты finder/loader , реализующие Importer Protocol. Опрос этого словаря всегда происходит до того момента, как будут проверены пути в sys.path .
Для минимальной реализации протокола импорта нужно реализовать два метода: find_module и load_module . Метод find_module отвечает за поиск конкретного модуля/пакета (ведь нам нужно перехватывать импорт только своих модулей, а остальные отдавать на откуп стандартному механизму), а метод load_module , соответственно, загружает конкретный модуль только если он был «найден» в методе find_module .
Итак, вроде бы добрались до сути. Можно привести простой пример. Минимальный пример класса, реализующего Importer Protocol, подходящего для наших целей. Он будет заниматься импортом модулей «зашифрованных» base64 из обычной структуры пакетов (в данном случае для простоты мы просто «зашифровали содержимое» файлов, но никак не меняли их имена и структуру пакетов). Считаем, что расширения файлов для наших модулей будут гордо называться ".b64".
Как это работает? Первым делом при создании экземпляра класса собирается информация о модулях нашей библиотеки, которую мы «шифруем». Затем при загрузке конкретного модуля, читается нужный «зашифрованный» файл, «расшифровывается» и импортируется с помощью средств модуля imp уже из «расшифрованной» текстовой строки. Как использовать данный класс? Очень легко. Буквально, одной строчкой включается возможность импортировать «зашифрованные» исходники нашей библиотеки, а по сути ставится хук на импорт:
где root_pkg_path абсолютный или относительный путь к корневому пакету нашей библиотеки. При этом, убирая данную строчку, мы можем использовать обычные исходники, если они доступны. Всё происходит абсолютно прозрачно, а все изменения происходят в одном месте.
Вот и всё, с этого момента импорт модулей из нашей библиотеки осуществляется с перехватом и «расшифровкой». Наш хук будет дёргаться при любом вызове инструкции import, и если импортируется модули нашей библиотеки, хук будет их обрабатывать и загружать, остальные импорты будут обрабатываться стандартно. Что нам и требовалось для более продвинутой обфускации. Представленный код импортёра и установки хука можно положить уже в *.pyd файл и надеяться на то, что никто не будет его дизассемблировать в надежде понять, что мы тут наворотили. В реальном проекте можно использовать настоящее шифрование, в том числе с использованием аппаратного ключа, что должно повысить надёжность данного метода. Также изменение имён файлов и структуры пакетов может быть полезным для большего запутывания.
Заключение
В качестве заключения хочу сказать, что я противник скрывать исходники, которые нельзя просто так взять и скрыть. В данном случае я не осмелюсь обсуждать этическую сторону вопроса и нужность/полезность сокрытия Python-исходников. Тут я просто представил метод, как это можно сделать и получить какой-то результат. Естественно, это не панацея. Python-код, действительно, невозможно скрыть полностью и от всех. Код модулей всегда можно получить с помощью интроспекции встроенными возможностями языка после их загрузки, например, из переменной sys.modules . Но это уже не так очевидно, как если бы исходники были открыты изначально.
Возможно, что всё, что тут написано и яйца выеденного не стоит — давно известные истины, либо бред сумасшедшего. Но если вышеописанное кому-то может оказаться полезным, я буду рад. Лично для меня данный опыт был полезен, хотя бы потому, что позволил лучше разобраться в мощной и гибкой системе загрузки и импортирования модулей и Importer Protocol. О тех самых штуках, которые чаще всего не требуются разработчикам для написания программ на Python. Всегда интересно узнать что-то новое.
Спасибо за внимание.
UPD 14.08.2013:
По просьбе tangro сделал минимальный проект, в котором демонстрируется описанный способ, но без настоящего шифрования (применены только некие алгоритмы обратимого преобразования).
Скачать zip-архив можно по ссылке. Нужен Python 2.7 x86 под Windows. Запускать нужно скрипт «test_main.py».
UPD 2:
И более интересный пример, в котором производятся некоторые вычисления. Здесь все импорты и вызовы функций из зашифрованных модулей скрыты в бинарном модуле. Скачать архив можно по ссылке.
How to Protect Python Source Code
Are there ways to protect Python code? Yes, if you are willing to do some extra work.
Python is one of the most popular languages for machine learning and web development. While its popularity comes from its easy syntax and many supporting libraries, the fact that it is an interpreted language poses two problems. First, performance is an issue. The added step of compilation and not being able to access the memory directly put constraints on the code execution. Second, the source code is visible in clear text so if security is a concern for you, Python is not a good language of choice. In this article, I describe two compile options to protect your Python code.
Option 1: Compile using CPython
One possible option is to compile a python file (*.py) into its equivalent compiled file (*.pyc). If you have used Python in any fashion, CPython does this automatically for you. CPython first compiles a *.py file to a *.pyc file. The *.pyc file, in turn, is interpreted and executed by CPython VM.
To compile python files manually, run:
This will generate corresponding *.pyc files under the __pycache__ subfolder. To run against all files:
After the compilation, you can run *.pyc files instead of *.py files
While the above solution of compiling python files is easy to implement, it is also easy to reverse-engineer the compiled bytecode files to get the source code. All you need to do is to “decompile” the bytecode (*.pyc).
Option 2: Wrap with Cython
A better option is to wrap it using Cython which is a C extension of Python. Cython is a different language from Python but its syntax is similar to Python with additional static type declarations like C. The intended purpose of Cython is to leverage the performance gain that C provides, but you can also use it to compile your Python code to C files. C compiler turns python files into SO (Shared Object) files which can be imported as Python libraries like any other python modules.
First, install Cython in your virtual environment:
- Write Cython code in hello.pyx file:
Cython is a superset of Python, so practically speaking, you can just rename *.py to *.pyx file and it works. This, however, is not recommended if your purpose is to speed up the code.
2. Create a Python script (setup.py) that compiles hello.pyx to C code through the setuptools extension of Cython:
(For more details about what the setup module does, please refer here)
3. Now execute the setup.py with the optional parameters to :
This will create a C file hello.c and an SO code. In my Windows environment, the file name Cython produced for the SO file is “hello.cp37-win_amd64.pyd” which is basically a DLL.*
One caveat is that you need to have a C++ compiler on your computer. If not, you will encounter the following error in Windows:
To fix the issue in Windows, the best way is to install Visual Studio (Community version) with the “Desktop development with C++ option.”
Once installed, you should be able to find vcvarsall.bat under “%ProgramFiles%\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat”
In the case of Mac, the command should just work.
4. Now you can use the SO file like any other Python libraries in a Python code:
Diagrammatically, here is the whole process:
All you have to ship is now just a wrapper (executor.py in the example case) and SO files which are compiled C binaries that are impossible to decompile.
Conclusion
Though CPython and Cython compilers are not meant for source code protection, I have shown how we can use them to obfuscate python code. However, this is not bullet-proof since any software can be reverse-engineered if given enough time and effort. The technological solutions are only meant to discourage or make it difficult to hack. To receive legal protection, it is necessary to sign a license agreement and/or add a license key if needed.