Безопасность в Дельфи




Программер против программера продолжение


Раздел "Анти крэковые мучения" Дмитрий Логинов ,
дата публикации 14 февраля 2001 г.

Первая часть статьи

Другой файл экспорта - для рядового утреннего включения. Возможно после этого ВКЛЮЧЕНИЯ, т.е. импорта справочников у вас будут не работать представления(view). Поэтому включение должно предусматривать их перекомпиляцию: alter view compile;

Также желательно после этого включить констрейнты, которые вы выключили. Как это сделать я думаю вы уже знаете. Еще один момент, хотя это важно делать каждый день, поэтому вы будете это делать при включении базы:
  • 1) это rebuild индексов
  • 2) это analyze для таблиц и индексов, что быстро работал стоимостной оптимизатор. можете воспользоваться стандартным PACKAGE и вызвать ANALYZE_SHEMA.

Тут-то и можно было бы остановиться, но из чувства паранойи я решил написать шифровку. Написана она на С++ с целью переноса на любую среду, где может быть Оракл. Выполняет две вещи:
  • 1) генерит ключ любого размера
  • 2) шифрует этим ключом любой файл

Сначала я решил просто предложить вам для скачивания прогу и перейти ко второй части АКМ, хотя я лично считаю интересными пару моментов реализации этой утилитки, весящей 70кил. Мои личные интересы не могут быть достаточными аргументами для того чтобы уделять чему-то пристальное внимание в АКМ. Но недавно я встретил несколько вопросов на эту тему в серьезных конфах вроде comp.lang.c++. Плюс вопросы читателей прошлых выпусков АКМ убедили меня в обратном. Т.е. в том чему надо уделить внимание.

Первый момент: у нас сейчас очень хитрая ситуация с шифрованием. А если быть точным, то требуется лицензия на использование шифрования в коммерческих задачах, по-моему. Короче оформился такой темный орган, как ФАПСИ. Говорят там сидят умные ребята, если их можно назвать ребятами. Но на местах, то бишь в провинции, встречаются уроды, которые не могут вскрыть даже вложенный ZIP архив, дважды запароленный. Задача, согласитесь, не из высшей математики. Она, эта задача, вполне по силам обывателю вроде меня. "Кадры решают все."

Двигаемся дальше, относительно ситуации с ФАПСИ. Некоторые из вас скажут - Но, парень, мы в работе постоянно используем PGP, запароленные архивы и прочее. И я вам отвечу, значит вы им пока не нужны. Не знаю, как сейчас ситуация в златоглавой со всякими пиратскими рынками, но в провинции все в ажуре. Рынки процветают. Дело даже не в пиратском офисе или компиляторе, на этих рынках прямо на прилавках лежит порнуха и прочие прелести интимной жизни трудящихся. Маленькие радости большой страны.

Итак, некоторые из вас понимают, что из-за шифрования вас могут взять за задницу, или просто это будет еще один повод потомить вас в застенках. Для этих людей я скажу: Есть очевидные вещи, которые трудно представить, как систему шифрования. Задайте эту задачку вашим юристам - это их работа. Задачка: Операция микропроцессора XOR. Это операция ЦП? Или это законы булевской алгебры? Или это система шифрования? Оставим эти споры для юристов, нас интересует надежность этой операции.

Действительно, трудно представить надежный алгоритм шифрования с использованием ИСКЛЮЧАЮЩЕГО-ИЛИ. Напомню вам о первой теореме криптографии(своими словами): Абсолютно криптостойкое шифрование должно использовать ключ длиной не меньшей длины данных. Не будем ставить сейчас эту задачу. Хотя мне уже рисуются несколько реализаций в виде:

рабочий винт и идентичный винт-ключ. Это так просто, что могло бы сработать.

Опустимся на землю. При чем здесь XOR и второй винт? Как вы заметили, в этой теореме не упоминается сложность шифрования, речь идет только о длине ключа и данных. Интересно! И так как винт, конечно, не подходит при наших условиях ограниченного финансирования и срочности реализации, то всплывает волшебное слово ZIP.

Теперь я вам объясню, почему, на первый взгляд, не подходит XOR. Во многих задачах криптоанализа, задаются условия близкие к следующему: Известен алгоритм, известна часть зашифренного текста, известен характер информации, имеется суперкомпьютер способный производить сложные вычисления очень быстро. Когда у вас есть зашифренный блок и часть открытой информации - то получить значение ключа не составляет труда.

Другое дело если этот ключ оооочень большой. Тогда сильно снижается вероятность получение большого куска открытого текста, чтобы сделать: KEY = ENCRYPT xor DECRYPT; или KEY = ENCRYPT operation DECRYPT; Это справедливо для любого симметричного ключа. Они поэтому и называются симметричными.

Предположим, что чем ближе длина ключа к общему объему всех шифруемых данных, тем выше криптостойкость системы. Предпосылка может оказаться ложной, но есть несколько частных доказательств ее истинности. Впрочем, для этого и создавался АКМ, чтобы можно было усмотреть недостатки ОБЩИМ всевидящим придирчивым ОКОМ.

Ближе к делу, почему бы нам не сгенерировать ключ длиной метров 80 для каждого файла и не наложить их друг на друга? Давайте рискнем, в крайнем случае это очень маленький риск, база как вы помните и так уже не функциональна(из-за удаления справочников). Поэтому продолжим эксперимент.

Добавлю еще вот что - сколько-то времени будет потрачено на преодоление вашей системы шифрования и только потом надо будет решать задачу НЕФУНКЦИОНАЛЬНОСТИ вскрытой базы. Такое маленькое издевательство. И тут мы подходим к принципиальному моменту, который касается и характеристик системы и ее реализации. Вопросы на эту тему я часто встречаю в конфах. Это ДАТЧИКИ ПСЕВДОСЛУЧАЙНЫХ ЧИСЕЛ(pseudo-random number generators). Итак, каково положение дел на этом фронте.
  • 1) В большинстве библиотек используются программные датчики случайных чисел, поэтому злобные математики называют их "по честному" - псевдослучайными(pseudo-random).
  • 2) Для защиты генерируемых данных используется стартовая процедурка. RANDOMIZE.
  • 3) Иногда для генерации используются аппаратно-программные датчики.

Давайте рассмотрим преимущества и недостатки каждого, с точки зрения взломщика.

Номер раз: Какую бы вы библиотеку не закачали, многочисленные rand и random, реализованные там имеют параметрическую зависимость. Т.е. Внутри прописана константа, с которой и начинается работа датчика. Например линейный датчик от фирмы Борланд: R(i+1) = ( a*R(i) + b ) mod c; // линейный конгруэнтный датчик // linear congruential generators Конгруэнтный - это значит, когда при вычислении(движении, преобразовании) число переходит в другое число. i := i + 1, конгруэтная последовательность действительных чисел.

А датчик программируется так // структурка отвечающая за R(i) typedef struct { unsigned lo; unsigned hi; } __seed_t; // здесь будем хранить промежуточное состояние счетчика static __seed_t Seed = { 1, 0 }; // плохой стиль, но да ладно #define MULTIPLIER 0x015a4e35L #define INCREMENT 1 // сама функция. деления по модулю нет (c=MaxInt) int rand() { Seed.lo = MULTIPLIER * Seed.lo + INCREMENT; return((int)(Seed.lo >> 16) & 0x7fff); }

Характеристики таких датчиков определяются значением констант a(MULTIPLIER) и b(INCREMENT). Здесь могу сказать просто "RTFM", Кнут, 2том, стр.29: "Эта последовательность называется линейной конгруэнтной последовательностью. Получение остатков по модулю m(в нашем случае с) отчасти напоминает предопределенность, когда шарик попадает в ячейку крутящегося колеса рулетки. Например, для m=10 и X(0)=a=c=7 получим последовательность: 7,6,9,0,7,6,9,0,... см формулу [ X(n+1) = ( a*X(n) + c ) mod m ] Как показывает этот пример, такая последовательность не может быть "случайной" при некоторых наборах чисел m, a, c и X(0)" Вот что говорят классики. Модуль рекомендуется выбирать m = 2^32(в степени 32), что мы и имеем. Это говорит кстати о том, что период этого датчика равен 2 гигам, как и максимальная длина ключа. Дальше идет повтор. Но даже чтобы добиться таких характеристик нужно, чтобы INCREMENT и МОДУЛЬ были взаимно просты. Есть еще два условия. И есть масса других интересных критериев, но нам они пока не интересны. Допустим, что с нашим датчиком все ОК. Что на самом деле не так, потому что эти датчики не удовлетворяют критерию монотонности. Было доказано, что генератор x = (69069*x + 113218009) mod 2^30 является лучшим линейным генератором, а он не выбирает младших разрядов целого. Поэтому Кнут рекомендует использовать другой датчик с более интересными характеристиками. Правда, сейчас повторюсь, нас интересует другое.

Номер ДВА: Защита генерируемой последовательности, как вы уже догадались реализована через задание стартового числа, в большинстве библиотек используется значение таймера. Зачем это надо? Ну все очень просто. Допустим вы имеет программу, что выводит на экран 5 случайных чисел. И сколько бы вы не пускали эту программу - она будет выводить одну и ту же пятерку чисел. Вот для устранения таких артефактов и нужен RANDOMIZE(srand).

Вот теперь давайте в комплексе рассмотрим эту проблему, затронутую в датчиках и в защите на старт. Допустим вы хакер и вам надо расшифровать данные. Уточним задачу: зашифрован EXE модуль, использовался сгенерированный на делфях ключ.

Ломается с полпинка. Объясняю как:

  • 1) Все исполняемые модули начинаются на MZP. Этого достаточно для начала.
  • 2) Любая система шифрования содержит шифрующий модуль(программку) на диске защищаемой машины. Значит можно определить какой датчик использовался
  • 3) Даже если модуль не доступен. Есть ряд широко распространенных датчиков, которые качаются с Инета или списываются с книги и используются незадачливыми программистами. Хороших датчиков мало и поверьте ФАПСИ они известны.
  • 4) Берем MZP, берем зашифрованный блок, берем алгоритм дешифрации(например XOR) и накладываем их друг на друга. Т.о. у нас есть один или несколько байт ключа.
  • 5) Выбираем генератор случайных чисел, который "использовал" программер. Подставляем известные значения ключа. Шифр взломан. И плевал я на вашу ПЕРВУЮ теорему криптографии.

Видите как сильно все зависит от реализации. Давайте вернемся в кресло программиста и попробуем помешать злобному хакеру. Самое простое решение - аппаратный датчик чисел. Это надежно, но не всегда доступно. Поэтому мы сначала пробежимся по программным решениям этой проблемки. Подход у меня будет простой - я сразу укажу на ошибочные рассуждизмы программистов, абсолютно не имеющих опыта, ни во взломе программ, ни в криптоанализе. Ну что же дело наживное, поехали:

- Мой код очень сложен. Я на паскале-то еле прочитал, а на асме его прочесть будет невозможно. Наш ответ: А кто говорил, что надо "анализировать" ассемблерный код? Помню в бытность свою системщиком, я очень интересовался вирусами. В этом было что-то от списывающего школьника. Так вот вирусы тогда только учились менять свою сигнатуру. И один из приемов - ВСТАВКА НЕЗНАЧАЩЕГО КОДА. Есть два способа обойти это.
  • а) агрессивный
  • б) пассивный
Оба основываются на том, что вы знаете, что ждать от этого участка программы. Вам нужно отследить определенные события. Это можно сделать в отладчике(агрессивно) или в IDA и HIEW(пассивно). Так это и происходит по сей день.
ТАК ШТЫ СЛОЖНОСТЬ КОДА - это последнее на что нужно рассчитывать. А лучше вообще не рассчитывать.

Я могу придумать очень простой алгоритм, который нельзя проанализировать и который не имеет недостатков. Наш ответ: Песня из той же консерватории, что и предыдущее попискивание.

Теперь перейдем на личности. Допустим программер допустил обе эти ошибки. Он думает - ага, если псевдослучайные числа можно вычислить по известному шифротексту, тогда надо сделать непредсказуемым алгоритм получения этих чисел. Т.е. почему бы не ввести ШУМ, в виде случайных пустых операций или просто неизвестных операндов.

// пример с пустыми операциями for(int i=0; i<key_size/sizeof(int); ++i) { GetLocalTime( &seconds ); for(int j=0; j<seconds.wMilliseconds; ++j) rand(); // дальнейшие итерации функции rand() unsigned r = (rand()<<16) ^ rand(); // XOR fwrite( &r, sizeof(int), 1, fkey ); } // пример с неизвестными операндами for(int i=0; i<key_size/sizeof(int); ++i) { unsigned rr; // лежит мусор из стека. НИ в коем случае не ининициализировать unsigned r = (rand()<<16) ^ rand() ^ rr; // XOR со слевым сдвигом ;) fwrite( &r, sizeof(int), 1, fkey ); }

Как вы догадываетесь это можно совместить и слегка усложнить. Если немного "посидеть" на спине, уставившись в потолок, дабы отдать дань "общепринятому мышлению", можно сообразить, что математически и алгоритмически вы ничего не усложнили. В первом случае вы будете часами ждать генерации даже 10Мегабайтного ключа. Плюс таймер, даже его тысячные доли секунды, не такой уж датчик случайных чисел. Я напишу такую же прогу и посмотрю СРЕДНЕЕ ЧИСЛО пустых вызовов rand(), а дальше перебором установлю ключ. Перебор не будет большим, т.к. приблизительное значение пустых rand() на каждое тело цикла мне известно. Относительно второго я даже говорить не буду, вы и так уже догадались как вскрывать такой "ШУМ".

Да, и еще! Совсем не подходит способ выделение большого куска из кучи в надежде, что он заполнен мусором. Под виндой у вас это не получиться. Под Юниксом не знаю.

Думаю доказательств непригодности вышеописанных ШУМОВ достаточно. Дальше посмотрим - использование других генераторов. Например, от Микрософт. Этот генератор ключа продайте конкурентам. Для тех кто не понял - надо перечитать то, что я говорил о датчиках. А лучше какую-нибудь ученую литературу. Нам пора закругляться.

Аппаратные датчики. Я ничего не слышал про хорошие отечественные генераторы. За рубежом давно используют либо радиационный фон или атмосферные шумы, у нас какие-то, на коленке спаянные, платы. Если вы собрались покупать такой датчик - проверьте его как следует. Если вы не знаете как это делать, то запаситесь терпением и литературой.

У аппаратно-программных генераторов, использующих аппаратуру ПК, единственный недостаток - привязанность к архитектуре ОС и компьютера. Недостаток реализации - использование чего-то периодического в системе. Например, счетчик тиков таймера, состояния портов и т.д. Я не рекомендую это использовать. Также не советую использовать ОЧЕНЬ БЫСТРЫЕ аппаратные счетчики, потому что возможно вы будете попадать на одно и тоже место. Температурные датчики также не желательно использовать. Текущие программные характеристики ОС(кол-во процессов, ниток и т.д.) тоже лучше не использовать. Может я что-то и забыл, но главнее знать, что нужно использовать.

Самым простым и универсальным аппаратным датчиком случайных чисел в компьютере является мышь либо клава. У них есть только два недостатка остальное все преимущества:
  • 1) зависимость от тупости пользователя
  • 2) медленность ;)
Т.е. если пользователь будет крутить мышь вокруг маленькой точки или жать одну и ту же кнопку на клавиатуре, то ключ будет ужасным. Или же если вы захотите сгенерить 90Мб-ый ключ то вам долго придется елозить мышкой по коврику или стучать по клаве.

Итак, ваша задача сейчас состоит, чтобы избавить эти способы от их недостатков.

Избавление от первого недостатка: - повысить образование пользователя, или написать инструкцию, или самому сгенерить ключ, или хранить предыдущий символ и не вводить новый если они не равны(хотя это глуповато). Избавление от второго недостатка: - массив введенных символов(например 100) + массив мусора, т.е. непроиницианализированый массив. Нужно просто составить комбинации этих массивов. И вы получите более или менее случайный числа. Трудоемко. Сложность реализации влечет большое количество дыр в надежности системе, т.е. потребуется большое время тестирования.
- опять же два массива. Один из них подстава. Другой параметр преобразования потока программного генератора случайных чисел. Давайте посмотрим: void generate( const int k_size, FILE* fkey ) { static const int sz(1024); unsigned char buf[sz]; int pos( sz*0.01 ); // один процент от объема fwrite( &buf[pos], 1, sz+600-pos, fkey ); // и т.д.

Проанализируем. SZ - это размер локально объявленного массива BUF. Как вы заметили, я его не инициализирую. Т.е. то что было в вершине стека - то и пишется в начало ключа. 600 - это уже используемая часть стека, если ее увеличить - запись в файл не сработает. Безусловно, 600 - это зависит от программы. POS - это позиция из массива BUF, с которой мы начинаем писать. Эта позиция может быть случайной. При каждом вызове программы в начало ключа будет попадать несусветная чушь плюс кусочек PSP(префикс программного сегмента). Хотя, конечно, какой это PSP? Это просто часть переменных среды и прочее окружение процесса.

Оптимистов остановлю сразу - не стоит массив делать длинным. Если вы сделаете его больше килобайта с небольшим хвостиком, то массив будет забиваться нулями и "стохастичность" массива сильно уменьшается.

Где же можно использовать столь маленький "случайный" блок? Помните я вам рассказывал, как можно легко вскрыть зашифрованный EXE-модуль? Т.е. в любом файле, который вы будете шифровать есть служебная повторяющаяся информация. Например, заголовок и хвост. Здорово было бы знать заранее, где такая информация. Но мы этим не располагаем, поэтому достаточно предусмотреть случай с заголовком. 1024 байта - это то что нужно народу, чтобы покрыть большинство заголовков. Есть, конечно, заголовки и побольше, поэтому рекомендую вам получить парочку таких блоков СЛУЧАЙНОЙ длины. Не совсем, случайной(0 или 10 зачем такие блоки), а колеблющиеся около килобайта. Импровизируйте.

Закончили с первым массивом. Для чего же второй? Это массив пустых итераций вашего генератора. Не бойтесь тормозов. Вводимые с клавы случайные символы можно "уменьшить", вычев из них код буквы "А". И, между прочим, есть очень быстрые генераторы, например Mersenne Twister генераторы, придуманные, по-моему, Makoto Matsumoto и Takuji Nishimura. Они в семь-восемь раз быстрее обычных линейных генераторов. Две очень быстрые реализации этих генераторов представлены в библиотеке BOOST++ - это mt11213b и mt19937. Но не советую использовать код, представленный самими авторами генераторов - он ужасен(медленный). Используйте оптимальный код, например, из библиотеки BOOST++

Потребуется еще один генератор - генератор случайных перестановок. Чтобы перемешивать массив символов, в смысле массив пустых итераций. Можете взять любой у Кнута - RTFM параграф 3.4.2 "Случайные выборки и перемешивания", чтобы по Инету не шляться. Сами напишете свой код и сами оптимизируйте - нет ничего лучше. Для ленивых привожу самый простой:

// шаблончик мешалки для массивов типа T template void random_mix( T* const array_mix, int N ) { // аналог repeat...until (на один переход меньше) do swap( array_mix[ random(N) ], array_mix[ N-- ] ); while( N>=0 ); }

Качество этого скремблера полностью зависит от статистических характеристик функции random.

Перед подведением итогов хочется добавить. Конечно, вам лучше написать "консольку", которая цепляет СТАРТОВЫЙ массив случайных чисел, полученный либо случайным давлением по клаве, либо через PGP либо через ИНЕТ(какой-нить атмосферный шум). Так будет гибче и оптимальней.

Правда, у ВСЕЙ выше описанной системы генерации БОЛЬШИХ случайных ключей есть один главный недостаток. Это использование одного и того же алгоритма генерации псевдослучайных чисел. Советую избавиться и от этого если у вас будет время. Существуют два способа:

прежде чем читать: ВЫ ИМЕЕТЕ ПОЛНОЕ ПРАВО И УВЕРЕННОСТЬ ОСТАНОВИТЬСЯ НА УЖЕ ПОЛУЧЕННОЙ СИСТЕМЕ.
Ее характеристики вполне удовлетворительны.

  • 1) Установить библиотечку, где есть много генераторов псевдослучайных чисел. Собрать этих генераторов штучек 100-200 и запихать указатели на их функции в массивчик. "Случайно" его перемешать (параметрами НАДО использовать аппаратно полученные случайные числа - клава, мышь или PGP), потом все это можно смело пускать в обиход, т.е. стохастически использовать эти генераторы. НО ПОМНИТЕ - ОНИ МОГУТ ГЕНЕРИТЬ ОДНО И ТОЖЕ ЧИСЛО. Ведь каждый из них не знает состояние другого. Обойдите это.
  • 2) Этот способ для маньяков, т.е. тех у кого навалом времени. Написать генератор генераторов псевдослучайных чисел. Т.к. я уверен, что это никто делать не будет - как это делать я описывать не буду.

В завершении хочу сказать о паре важных моментов использования системы:
  • а) Шифр для полученного ключа выбирайте в соответствии с вашими юридическими правами.
  • б) Программу шифровальщик держите на том же переносном носителе, где вы держите сгенерированный ключ и копии удаленных справочников.
  • в) Не храните переносной носитель с ключом в дисководе и вообще на территории вашей фирмы. Не делайте этого даже в течение рабочего дня. Пусть лучше ключ привозят вечером на операцию закрытия базы. Дело в том, что при серьезных маски-шоу у вас отберут ВСЕ вещи и возможно обыщут каждого, кто был в офисе на момент облавы.









Начало  Назад  Вперед


Книжный магазин