Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Об одном методе охраны исходников Python-программы

Anna | 16.06.2014 | нет комментариев

Как всё начиналось

Некогда мне пришлось участвовать в разработке одного небольшого плана для научных расчётов, тот, что разрабатывался на языке программирования 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″.

Класс импортёра

#coding=utf-8

import os
import sys
import imp
import base64

EXT = '.b64'

#===============================================================================
class Base64Importer(object):
    """Служит для поиска и импорта python-модулей, кодированных в base64

    Класс реализует Import Protocol (PEP 302) для вероятности импортирования
    модулей, зашифрованных в base64 из указанного пакета.
    """

    #---------------------------------------------------------------------------
    def __init__(self, root_package_path):

        self.__modules_info = self.__collect_modules_info(root_package_path)

    #---------------------------------------------------------------------------
    def find_module(self, fullname, path=None):
        """Способ будет вызван при импорте модулей

        Если модуль с именем fullname является base64 и находится в заданной
        папке, данный способ вернёт экземпляр импортёра (finder), либо None, если
        модуль не является base64.
        """
        if fullname in self.__modules_info:
            return self
        return None

    #---------------------------------------------------------------------------
    def load_module(self, fullname):
        """Способ загружает base64 модуль

        Если модуль с именем fullname является base64, то способ попытается его
        загрузить. Возбуждает исключение ImportError в случае всякий ошибки.
        """
        if not fullname in self.__modules_info:
            raise ImportError(fullname)

        # Для потокобезопасности
        imp.acquire_lock()

        try:
            mod = sys.modules.setdefault(fullname, imp.new_module(fullname))

            mod.__file__ = "<{}>".format(self.__class__.__name__)
            mod.__loader__ = self

            if self.is_package(fullname):
                mod.__path__ = []
                mod.__package__ = fullname
            else:
                mod.__package__ = fullname.rpartition('.')[0]

            src = self.get_source(fullname)

            try:
                exec src in mod.__dict__
            except:
                del sys.modules[fullname]
                raise ImportError(fullname)

        finally:
            imp.release_lock()

        return mod

    #---------------------------------------------------------------------------
    def is_package(self, fullname):
        """Возвращает True если fullname является пакетом
        """
        return self.__modules_info[fullname]['ispackage']

    #---------------------------------------------------------------------------
    def get_source(self, fullname):
        """Возвращает начальный код модуля fullname в виде строки

        Способ декодирует начальные коды из base64
        """
        filename = self.__modules_info[fullname]['filename']

        try:
            with file(filename, 'r') as ifile:
                src = base64.decodestring(ifile.read())
        except IOError:
            src = ''

        return src

    #---------------------------------------------------------------------------
    def __collect_modules_info(self, root_package_path):
        """Собирает информацию о модулях из указанного пакета
        """
        modules = {}

        p = os.path.abspath(root_package_path)
        dir_name = os.path.dirname(p)   os.sep

        for root, _, files in os.walk(p):
            # Информация о нынешнем пакете
            filename = os.path.join(root, '__init__'   EXT)
            fullname = root.rpartition(dir_name)[2].replace(os.sep, '.')

            modules[fullname] = {
                'filename': filename,
                'ispackage': True
            }

            # Информация о модулях в нынешнем пакете
            for f in files:
                if not f.endswith(EXT):
                    continue

                filename = os.path.join(root, f)
                fullname = '.'.join([fullname, os.path.splitext(f)[0]])

                modules[fullname] = {
                    'filename': filename,
                    'ispackage': False
                }

        return modules

Как это работает? Первым делом при создании экземпляра класса собирается информация о модулях нашей библиотеки, которую мы «шифруем». После этого при загрузке определенного модуля, читается необходимый «зашифрованный» файл, «расшифровывается» и импортируется с поддержкой средств модуля imp теснее из «расшифрованной» текстовой строки. Как применять данный класс? Дюже легко. Дословно, одной строчкой включается вероятность импортировать «зашифрованные» исходники нашей библиотеки, а по сути ставится хук на импорт:

sys.meta_path.append(Base64Importer(root_pkg_path))

где root_pkg_path безусловный либо относительный путь к корневому пакету нашей библиотеки. При этом, убирая данную строчку, мы можем применять обыкновенные исходники, если они доступны. Всё происходит безусловно прозрачно, а все метаморфозы происходят в одном месте.

Вот и всё, с этого момента импорт модулей из нашей библиотеки осуществляется с перехватом и «расшифровкой». Наш хук будет дёргаться при любом вызове инструкции import, и если импортируется модули нашей библиотеки, хук будет их обрабатывать и загружать, остальные импорты будут обрабатываться стандартно. Что нам и требовалось для больше продвинутой обфускации. Представленный код импортёра и установки хука дозволено положить теснее в *.pyd файл и верить на то, что никто не будет его дизассемблировать в вере осознать, что мы здесь наворотили. В настоящем плане дозволено применять реальное шифрование, в том числе с применением аппаратного ключа, что должно повысить надёжность данного способа. Также метаморфоза имён файлов и конструкции пакетов может быть пригодным для большего запутывания.

Завершение

В качестве завершения хочу сказать, что я противник скрывать исходники, которые невозможно легко так взять и спрятать. В данном случае я не осмелюсь обговаривать этическую сторону вопроса и нужность/полезность сокрытия Python-исходников. Здесь я легко представил способ, как это дозволено сделать и получить какой-то итог. Безусловно, это не панацея. Python-код, подлинно, немыслимо спрятать всецело и от всех. Код модулей неизменно дозволено получить с поддержкой интроспекции встроенными вероятностями языка позже их загрузки, скажем, из переменной sys.modules. Но это теснее не так видимо, как если бы исходники были открыты первоначально.

Допустимо, что всё, что здесь написано и яйца выеденного не стоит — давным-давно вестимые истины, либо абсурд сумасшедшего. Но если вышеописанное кому-то может оказаться пригодным, я буду рад. Лично для меня данный навык был пригоден, правда бы потому, что дозволил отменнее разобраться в сильной и эластичной системе загрузки и импортирования модулей и Importer Protocol. О тех самых штуках, которые Почаще каждого не требуются разработчикам для написания программ на Python. Неизменно увлекательно узнать что-то новое.

Спасибо за внимание.

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB