Крупные производители тиражного программного обеспечения отказались от защиты своих продуктов, сделав ставку на их массовое распространение. Пусть лицензионные копии приобретает лишь несколько процентов пользователей, но, если число этих копий измеряется миллионами, даже считанные проценты выливаются в солидную сумму, с лихвой окупающую разработку. Однако для небольших коллективов и индивидуальных программистов такая тактика неприемлема. Им необходимо предотвратить "пиратское" копирование своего продуктаПредоставление исходных текстов часто является обязательным условием заказчика и закрепляется в контракте. Той же цели добиваются сторонники популярного движения открытых исходных текстов, ратующие за их свободное распространение. Кроме того, исходные тексты могут банальным образом украсть. Поэтому, стоит внимательно отнестись к организации технологической защиты интеллектуальной собственности. ПРОТИВОДЕЙСТВИЕ ИЗУЧЕНИЮ ИСХОДНЫХ ТЕКСТОВВообще говоря, открытость исходных текстов - понятие расплывчатое.
Автор статьи [1] справедливо задается вопросом: "Является ли бинарная программа в 1 Kбайт более открытой, чем миллион строк исходников без адекватной инфраструктуры и документации?" В самом деле, мало иметь исходный текст - в нем еще предстоит разобраться. Достаточно удалить все или, по крайней мере, большую часть комментариев, дать переменным и функциям бессмысленные, ничего не говорящие имена, как в программе не разберется и сам автор. А наличие и полнота комментариев к исходному тексту в контракте, как правило, не оговаривается. Получается, что контракт может быть формально выполнен, а предоставленный заказчику исходный текст практически бесполезен. "Практически" не означает "полностью": такой простой прием не позволит обеспечить абсолютную защиту. Воспрепятствовать анализу можно отделением, абстрагированием алгоритма от языка реализации. Например, реализовать критичные для раскрытия компоненты на машине Тьюринга [2], а ее поддержку обеспечить на целевом языке. Уровней абстракции может быть несколько - чем их больше, тем труднее осуществлять анализ. Помимо машины Тьюринга для этой цели подходят стрелка Пирса, сети Петри и т. д. Такой подход дает превосходный результат, но требует глубоких математических знаний и значительных накладных расходов - программировать на машине Тьюринга намного сложнее, чем на ассемблере. (Отдельный вопрос - эффективность полученной программы. - Прим. ред. журнала "Открытые системы") Для многих проектов это неприемлемо, поэтому приходится использовать другие приемы. Динамическое ветвлениеПрограмму, состоящую из нескольких сотен тысяч строк, невозможно рассматривать как простую совокупность команд. На таком уровне детализации "за деревьями леса не видно". Сначала необходимо проанализировать взаимосвязь отдельных функций друг с другом, выделить интересующие фрагменты и лишь затем изучать реализацию самих функций. Чем выше степень дробления программы, тем труднее анализ. Элементарные функции, состоящие из десятка строк, сами по себе дают мало информации. Для формирования целостной картины необходимо рассмотреть вызывающий их код, поднимаясь по иерархии вызовов до тех пор, пока не удастся реконструировать весь алгоритм целиком или, по крайней мере, охватить его ключевой фрагмент. Чтобы построить дерево вызовов, нужно уметь отслеживать перекрестные ссылки в обоих направлениях: определять, какой функции передается управление данным вызовом, и, наоборот, находить все вызовы, передающие управление данной функции. Этому легко воспрепятствовать, воспользовавшись динамическим ветвлением - т. е. вычислением адреса перехода непосредственно перед передачей управления. Если функция возвращает не результат своей работы, а указатель на следующую выполняемую функцию (результат работы можно возвратить через аргументы, переданные по ссылке), то статический анализ не позволит определить порядок выполнения программы и построить дерево вызовов станет невозможно. Попутно исчезнет масса дублируемого кода, в частности, станет ненужной проверка результата завершения на корректность - в случае возникновения ошибки функция сама передаст управление нужной ветви программы. Здесь стоит сделать одну оговорку. Язык Си не позволяет объявить функцию, возвращающую указатель на функции - это объявление рекурсивное. Приходится объявлять функцию, возвращающую бестиповой указатель void *. Аналогично - если функция в ходе своей работы вызывает какие-то другие функции, лучше делать это не напрямую, а передавать указатели на вызываемые функции как аргументы. Такой подход не только препятствует анализу, но еще и увеличивает гибкость программы, облегчая повторное использование старого кода в новых проектах - при должной культуре программирования каждая функция представляет "вещь в себе" и не привязана ко всем остальным. Контекстная зависимостьРассмотрим теперь другой прием, когда алгоритм работы большинства функций зависит от флага - глобальной переменной в пределах одного модуля. Если флаги изменяются в зависимости от результата, возвращаемого функцией, автоматически возникает контекстная зависимость (не слишком сильная, но это лучше, чем ничего). Работа одной функции становится зависимой от других, вызванных до нее. Анализ одной, отдельно взятой функции, становится многовариантным. Чтобы определить, что конкретно она делает, необходимо знать значение флагов, а значит - иметь представление о том, какие функции выполнялись до этого и какие именно данные они обрабатывали.
|