Защита Python приложений: шифрование и обфускация кода

Раздел: Python -> Безопасность

Основные способы защиты Python кода от прямого чтения

Иногда требуется ограничить доступ к исходному коду Python при распространении приложения. Наиболее эффективным считается обфускация с помощью специализированных утилит, таких как PyArmor. Однако существуют и другие подходы, каждый из которых подходит для разных сценариев. Ниже представлены варианты с примерами и пояснениями.

Как зашифровать код Python с максимальной защитой от декомпиляции?

PyArmor - это инструмент, который преобразует байткод и обфусцирует его, делая восстановление исходного кода крайне сложным. Установка и использование:

pip install pyarmor
pyarmor obfuscate script.py

зашифровать код python (зашифровать код python)

После выполнения создается папка dist с защищенными файлами. Для запуска используется pyarmor run dist/script.py или упакованный вариант.

Проблема: PyArmor требует лицензии для коммерческого использования. Бесплатно можно защищать только некоммерческие проекты. Также защищенные файлы могут быть обнаружены антивирусами как подозрительные.

Как быстро скрыть код с помощью base64 и exec, не заботясь о надежности?

Простое кодирование в base64 не является шифрованием, но может отпугнуть случайных пользователей:

import base64

code = '''
def secret():
    return "Секретные данные"
'''
encoded = base64.b64encode(code.encode())
print(encoded)
# Декодирование и выполнение
exec(base64.b64decode(encoded))

Ошибки: Если исходный код содержит синтаксические ошибки, они проявятся при exec. Кроме того, любой может декодировать base64 мгновенно. Такой метод подходит только для очень низкого уровня защиты.

Как использовать сжатие и сериализацию байткода через marshal?

С помощью модуля marshal можно сериализовать байткод, затем сжать и закодировать:

import marshal, zlib, base64

code = compile('print("Hello")', '<string>', 'exec')
compressed = zlib.compress(marshal.dumps(code))
encoded = base64.b64encode(compressed)
with open('secret.py', 'w') as f:
    f.write('import marshal, zlib, base64\n')
    f.write(f'exec(marshal.loads(zlib.decompress(base64.b64decode({encoded!r}))))')

Такой подход скрывает исходный текст, но байткод все еще может быть дизассемблирован (например, через dis).

Проблема: Версии Python могут быть несовместимы с marshalled байткодом. При переходе на новую версию придется пересоздавать защищенный файл. Также код выполняется каждый раз через exec, что медленнее.

Как применить симметричное шифрование (Fernet) для защиты кода на Python?

Используется библиотека cryptography. Сначала генерируется ключ, затем код шифруется и помещается в загрузчик:

from cryptography.fernet import Fernet

# Генерация ключа (однократно)
key = Fernet.generate_key()
cipher = Fernet(key)

code = b'print("Защищенный вывод")'
encrypted_code = cipher.encrypt(code)

# Загрузчик (должен знать ключ)
loader = f'''
from cryptography.fernet import Fernet
cipher = Fernet({key!r})
exec(cipher.decrypt({encrypted_code!r}))
'''
with open('enc_loader.py', 'w') as f:
    f.write(loader)

Ошибки: Ключ должен храниться отдельно (например, в переменной окружения или встроенном ресурсе). Если злоумышленник получит доступ к ключу, безопасность теряется. Также шифрование не защищает от отладки или дампа памяти во время выполнения.

Расширенные примеры и сценарии использования

1. Полный процесс с PyArmor и упаковкой в один исполняемый файл

PyArmor можно комбинировать с PyInstaller для создания exe-файла:

Пример
# Установка
pip install pyarmor pyinstaller

# Обфускация модуля
pyarmor obfuscate --output obf_dist my_project/main.py

# Упаковка обфусцированных файлов
pyinstaller --onefile --add-data "obf_dist;." obf_dist/main.py
Результат: создается один .exe файл, внутри которого защищенный код. При запуске PyArmor восстанавливает байткод в памяти.

2. Собственная система шифрования с ключом и проверкой лицензии

Пример, где код расшифровывается только при наличии действительной лицензии (файл ключа):

Пример
# encrypt_license.py
from cryptography.fernet import Fernet
import json

def generate_license(plaintext_code):
    key = Fernet.generate_key()
    cipher = Fernet(key)
    encrypted = cipher.encrypt(plaintext_code.encode())
    # Сохраняем ключ в лицензионный файл
    with open('license.bin', 'wb') as f:
        f.write(key)
    with open('protected_code.bin', 'wb') as f:
        f.write(encrypted)
    return encrypted, key

if __name__ == '__main__':
    code = """
def business_logic(data):
    return sum(data) / len(data)
"""
    generate_license(code)

# loader.py (поставляется пользователю)
from cryptography.fernet import Fernet
import sys

if not os.path.exists('license.bin'):
    sys.exit('Лицензия не найдена')
with open('license.bin', 'rb') as f:
    key = f.read()
with open('protected_code.bin', 'rb') as f:
    enc = f.read()

cipher = Fernet(key)
exec(cipher.decrypt(enc).decode())

3. Использование obfuscator из модуля python-obfuscator (pyminifier)

Альтернативный инструмент - pyminifier, который обфусцирует имена переменных:

Пример
pip install pyminifier
pyminifier --obfuscate --nonlatin myfile.py -o obfuscated.py
Результат: файл obfuscated.py, содержащий код с замененными идентификаторами на нечитаемые символы (например, кириллицу или юникод). Однако декомпиляция по-прежнему возможна.

4. Комбинирование marshal + zlib + base64 с динамическим именем функции

Создание загрузчика, который хранит зашифрованный код внутри себя и запускает его после распаковки:

Пример
import marshal, zlib, base64

def create_loader(original_code):
    # Компилируем и сериализуем
    compiled = compile(original_code, '<secret>', 'exec')
    data = zlib.compress(marshal.dumps(compiled))
    b64 = base64.b64encode(data).decode()
    
    loader_template = f'''
import marshal, zlib, base64
_data = {b64!r}
_code = marshal.loads(zlib.decompress(base64.b64decode(_data)))
exec(_code)
'''
    with open('loader_gen.py', 'w') as f:
        f.write(loader_template)
    return loader_template

# Использование
orig = '''
def factorial(n):
    return 1 if n <= 1 else n * factorial(n-1)
print(factorial(5))
'''
create_loader(orig)
Результат: создается файл loader_gen.py, который при запуске выводит 120. Код исходный не виден, но восстановим через marshal.loads.

5. Шифрование с помощью AES (PyCryptodome) и выполнение в памяти

Более сложный вариант с использованием PyCryptodome:

Пример
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
import base64

password = b"сильный ключ"
salt = b"уникальная соль"
key = PBKDF2(password, salt, dkLen=32, count=100000)

plaintext = b"print('AES защищенный код')"
cipher = AES.new(key, AES.MODE_EAX)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)

# Сохраняем в файл загрузчик
loader = f'''
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
import base64

password = b"сильный ключ"
salt = b"уникальная соль"
key = PBKDF2(password, salt, dkLen=32, count=100000)

nonce = {base64.b64encode(cipher.nonce).decode()!r}
tag = {base64.b64encode(tag).decode()!r}
ct = {base64.b64encode(ciphertext).decode()!r}

cipher = AES.new(key, AES.MODE_EAX, nonce=base64.b64decode(nonce))
data = cipher.decrypt_and_verify(base64.b64decode(ct), base64.b64decode(tag))
exec(data.decode())
'''
with open('aes_loader.py', 'w') as f:
    f.write(loader)
Результат: при запуске aes_loader.py выводится строка. Без знания пароля расшифровать нельзя. Но пароль хранится в теле загрузчика, что делает этот метод уязвимым для статического анализа.

Зашифровать код Python - comments

En
зашифровать код python (python)