Техника и философия хакерских атак
Продолжение...
Теперь используемый алфавит может состоять из любого множества символов. Отметим, что предложенная реализация не самая эффективная. Есть множество других интересных алгоритмов. На каком-нибудь одном из них мы останавливаться не будем, т.к. это было бы слишком утомительно.
Теперь можно атаковать любую одностороннюю функцию, перебирая все допустимые аргументы и занося их в таблицу, после заполнения которой можно найти все аргументы А, для которых справедливо f:a = key, где key - известный результат функции. Если выбранная функция инъективна и существует не более одного аргумента для заданного кey, то в построении таблицы нет никакой необходимости. Первый совпадающий аргумент и будет искомым.
Еще раз замечу, что хеш-функция и односторонняя функция - не одно и то же. Так, например, произведение двух простых чисел - это хороший пример качественной односторонней функции, не обладающей свойствами и не являющейся хеш-преобразованием.
Возможна ли эффективная атака на RSA и подобные системы? Над проблемой разложения чисел на простые сомножители безуспешно бились многие знаменитые математики. Но до сих пор не доказано, что она неразрешима. Быть может, среди хакеров найдется математический гений, который решит эту проблему? Но пока ничего кроме перебора предложить не удается. Единственным выходом является не полный, а самоорганизующийся табличный поиск. Возможно создание специализированного чипа, занимающегося таким перебором. Умножение очень простая операция, поэтому она легко реализуется аппаратно, и такой чип может очень дешево стоить.
Системы, построенные на односторонних функциях, при условии отсутствия ошибок реализации взлому, как правило, не поддаются. Впрочем, программы без ошибок сегодня редкость. Как уже отмечалось, использование контрольной суммы помогает найти пароль. Крайне редко в системах массового применения используют односторонние хеш-функции. Множество полученных паролей много меньше всего допустимого множества, кроме того, среди них можно поискать осмысленные последовательности, которые с большой вероятностью и являются искомым паролем.
Как ни парадоксально, но использование хороших хеш-функций уменьшает число возможных паролей, облегчая работу взломщика. Использование плохих - дает информацию об исходном пароле, по которой его можно частично восстановить. Поэтому никакие хеш-функции нельзя использовать для "свертки" с пароля. Например, RAR не проверяет пароль на валидность, а сверяет CRC расшифрованных данных. Это не дает никакой информации об исходном пароле, но работает медленно. С другой стороны, у всех быстрых алгоритмов шифрования (хвалебные сообщения о которых периодически появляются в разных источниках) есть существенный недостаток - быстрый перебор увеличивает шансы лобовой атаки.
Рассмотрим еще одну область применения хеш-функций. Пусть у нас есть некоторый пароль p, который "зашит" в электронном ключе или введен пользователем. Сравнивать пароль типа if(UserPassword!=MyPassword) abort(); нельзя т.к. это очень просто ломается. Можно расшифровать паролем некоторый фрагмент кода или использовать его как константу. Если для шифрования выбрана криптостойкая схема, пароль можно найти только перебором всех возможных. Контроль правильности ввода можно обеспечить с помощью контрольных сумм. Эта схема очень надежна, но в большинстве приложений реализована с ошибками. Как уже догадался читатель, контрольная сумма снимается с пароля, а не с расшифрованных данных. Но это далеко не самая глупая ошибка! В целях безопасности электронный ключ не должен сообщать пароль в "прямом" виде. С него снимается хеш-сумма, которая и передается для расшифровки. Таким образом, мы имеем два значения разных хеш функций. Первое - для контроля правильности пароля, второе - для расшифровки. Мы получаем систему двух уравнений, решением которого будет пересечение двух множеств реверсированных хеш-преобразований. Это сокращает множество перебираемых паролей вплоть до единственного. Такой подход применим в случае идеальных хеш-функций. Другим решением будет попытка вычислить вторую контрольную сумму из первой.
Распространенной ошибкой является и использование "длинных" crc, сравнимых с длиной пароля. Читатель может сам подсчитать сколько возможных восьмисимвольных паролей имеют одно и то же значение 32-битной контролькой суммы. Однако использование более коротких хеш-сумм увеличивает вероятность того, что неверный пароль будет воспринят как правильный.
Популярная система Novell NetWare 3.x-4.x использовала уязвимый алгоритм. Суть его сводилась к тому, что с пароля снималась хеш-сумма, с которой затем сравнивался вводимый пароль. Эта функция была обратима. Таким образом можно получить МНОЖЕСТВО паролей, которые все воспринимались системой как верные.
Сегодня не существует алгоритмов, однозначно определяющих, является произвольная функция односторонней или нет. Вынесение такого заключения всегда сопряжено со сложным комплексным анализом поиска от противного. Если алгоритм обращения не будет найден коллективом математиков, то предполагается, что он не будет найден и взломщиком.
В действительности же лишь редкие фирмы могут позволить себе анализ подобного уровня. Обычно ограничиваются беглым осмотром. К чему это приводит, мы уже имеем возможность наблюдать.
Из этого факта вытекает невозможность существования универсального алгоритма для реверсирования хеш функций. Даже если предположить, что такой существовал бы, то все хеш-функции в первую очередь проверялись бы на устойчивость перед последним.
Однако в качестве примера реверсируем популярные хеш-функции. Существует довольно универсальный пошаговый алгоритм. Суть его в следующем: для каждого допустимого символа мы выполняем хеш-обратную операцию. Полученный результат рекурсивно обращаем до получения последовательности заданной длины.
Ниже приведен упрощенный алгоритм для получения всех допустимых паролей для заданной хеш-суммы посимвольного подсчета суммы всех элементов (см. функцию GetHashe в примере 'GetMe01').
ReHashe(unsigned char hashe) { for (unsigned char a='A';a<'a';a++) { if (!hashe-=a) return; ReHashe(hashe); } }
Данный пример не рекомендуется запускать. Легко доказать, что для любого hashe существует бесконечное множество таких ключей, что f(key) == hashe. Реально разрядность паролей ограничена емкостью стека. При исчерпании последнего возникнет исключительная ситуация и выполнение программы будет аварийно завершено.
Более того, легко доказать, что данный пример может "провалиться" в бесконечный рекурсивный спуск. Пусть hash - нечетное число, а A - четное. Тогда (hash-a) никогда не будет равно нулю.
Кроме этого, нужно ввести ограничение на глубину рекурсии, определяемую максимально допустимой длиной ожидаемого пароля.
Не менее очевидный недостаток предложенного алгоритма состоит в том, что выдаваемые им пароли не отсортированы по длине. Нужно, чтобы используемый алгоритм начинал с коротких паролей, переходя к более длинным.
Выше мы доказывали, что хеш-функция может быть реверсирована только на конечном перечисленном множестве Z. Для этого на каждом шаге мы должны проверять, является ли полученная последовательность подмножеством хотябы одного элемента Z, и если нет, то исключать данную ветвь двоичного дерева.
Упорядочить должным образом обращаемые ключи можно отображением на заранее отсортированное множество, хотя это потребует накладных расходов на память и предварительную сортировку элементов.
В ряде случаев использование универсальных алгоритмов оказывается неприемлемо и тогда для каждой функции разрабатывается собственный уникальный алгоритм.
Попробуем количественно выразить ожидаемое число паролей для другой популярной функции hashe(A)= a1 xor a2 xor a3...xor an. Для начала докажем, что для любого a, равного x1 xor x2, существуют только две пары x1 и x2, при условии что X [0,1]. Как известно из булевской алгебры операция xor может быть представлена следующей логической матрицей.
xor ¦ 0 1 ----+------------ 0 ¦ 0 1 ¦ 1 ¦ 1 0 ¦
Мы видим, что для каждого значения функции существуют ровно две пары x1, x2, что и требовалось доказать. Поэтому ожидаемое число паролей будет 2^n, где n - разрядность хеш-суммы. Разрядность пароля при этом не выше разрядности хеш-суммы. Т.е. мы провели простое побитовое обращение функции xor. Для восьмибитной хеш-суммы это число равно 0x100. Т.е. значение хеша нам абсолютно ничего не дает, т.к. x1 и x2 могут быть любыми! Однако, x1x2 это 2^16 вариантов, т.е. знание хеш суммы все же позволяет сократить число перебираемых паролей в 0x100 раз. Неплохо; но даже этот результат можно улучшить. Множество допустимых символов пароля, как правило, много меньше 0x100. Пусть ожидаемый пароль состоит только из символов 'A'..'Z'. Тогда для него существует не более 2^5 возможных пар x1 и x2!
Последним мы рассмотрим реверс мультипликационного алгоритма. Пусть A = CX mod N, тогда X = k*N/C+A, где k !=0. Но в пределах [0,N) мультипликационная функция инъективна! Для доказательства этого вспомним, что любое число можно представить как x*n+y единственным образом.
Мультиплексный алгоритм относится к числу наиболее популярных. Это лишний раз подчеркивает, что разработчики систем защиты наплевательски относятся к своим продуктам и не обладают даже начальными знаниями в области криптоанализа.
Все приведенные хеш-функции не являются односторонними и значительно уменьшают число возможных паролей. Совсем иначе дело обстоит с односторонними функциями, использующимися в криптостойких системах. Казалось бы, если обращение их дело безнадежное, то на этом можно ставить точку и даже не пытаться заниматься заведомо неразрешимой проблемой. Отчасти это верно. Действительно, необратимость большинства функций сомнений не вызывает, но другие, отличные от обращения, пути часто остаются неучтенными и не закрытыми, лишний раз подтверждая слабость большинства реализаций. Часто односторонние функции вычисляются очень быстро. Таким образом, выгоднее сначала последовательно перебрать все возможные пароли, дающие заданный хеш, а затем атаковать шифр заданным множеством паролей. Все "сильные" криптостойкие алгоритмы, как правило, очень медленны. Именно это и препятствует атаке. Так, например. на P-120 скорость перебора UNIX - MD5 не превышает 200 паролей\сек. Тогда как многие хеш-функции до 300.000 и выше! Однонаправленность сама по себе не спасает функцию. Да, реверс не возможен, но прямая атака (т.е. последовательный перебор всех аргументов до совпадения со значением функции) эффективнее прямой атаки на шифр. Впрочем, не стоит обольщаться. Весьма вероятно, что несмотря на грубые ошибки реализации криптосистемы взлом будет лежать за допустимой гранью. Скажем, мы найдем пароль не за биллион, а за десять лет. Так, вероятно, и рассуждают конструкторы защит. А ведь в эти рассуждения вкралась ошибка! На чем держится криптосистема? На малой скорости перебора паролей и большом количестве подходящих под хеш ключей. При условии использования бессмысленного пароля хакеру осталось бы только развести руками. Однако пользователи склонны выбирать осмысленные пароли. Если такой встретится в множестве подходящих под хеш паролей, то в дальнейшей атаке на систему, возможно, уже не будет нужды! Время, затраченное на атаку, в таком случае определяется только скоростью выполнения хеш-преобразования! Предложенный механизм очень близок к словарной атаке и основан на "слабых" паролях. Поиск "осмысленного" пароля представляет выборку всех паролей, включающих в себя по крайней мере трехсимвольный фрагмент словарного слова и отсортированных по убыванию длины подстроки.
Существует по крайней мере еще одно уязвимое место, введенное в некоторые криптосистемы под давлением правительства. Это так называемые "мастер-пароли". Предполагается, что они должны быть известны исключительно спецслужбам и не могут быть найдены иначе как методом перебора. Удивительно, но встречаются словарные мастер-пароли. Так, например, AWARD_SW. Забавно: когда даже пользователи начинают осознавать слабость словарных паролей и администраторы используют нечто вроде t%7Nh6i!!AMPER!!SrF, самое мощное оружие (ведь оно дает доступ ко ВСЕМ паролям) так уязвимо для атаки. Однако криптографы предпочитают использовать вместо мастер-паролей односторонние функции с секретом. Это означает, что в действительности эти функции совсем не односторонние и существует простое обратное решение. Но вот только найти его не более просто, чем атаковать пароль.
Существует несколько наивных подходов к этой проблеме, и некоторые секреты действительно легко вскрыть. Но не все так просто. Качественные алгоритмы для этого уже давно существуют. Ниже будет приведен один из них.
В заключении мы рассмотрим области применения хеш-функций, не относящиеся к криптографии и защите программ. Знание их существенно облегчает дизассемблирование и анализ изучаемых программ. Например, множество команд хешевого интерпретатора нельзя получить никаким образом, отличным от обращения хеш-функции. Аналогично обстоит дело и с хеш-поиском.
Простейшие системы шифрования
" - Высказана мысль или нет, она существует и имеет свою власть,- сказал Туек. - Ты можешь обнаружить однажды, что грань между жизнью и смертью у Свободных слишком тонка."
Ф. Херберт. "Дюна".
Данная глава является кратким обзором проблемы для неподготовленного читателя. Остальные могут ее смело пропустить.
Неоправданно популярный способ:
if (!IsValidUser()) { Message("Invalid user! Aborting..."); Abort; }
транслируется компилятором приблизительно в следующий код:
CALL IsValidUser OR AX,AX JZ continue ^^^^^^^^^^^^^ PUSH offset str_invalid_user CALL Message CALL Abort continue: ; нормальное продолжение исполнения программы ...........
и может быть легко взломан хакером изменением всего одного байта.Поменяв выделенную строку на JZ continue на JMP continue, злоумышленник получает работоспособную копию программы. Независимо от алгоритма функции IsValidUser - будь то проверка ключевого диска или ввод серийного номера - совершается безусловный переход на ветку нормального продолжения исполнения программы. На языке Си исправленная программа будет выглядеть так:
IsValidUser(); if (!true) { Message("Invalid user! Aborting..."); Abort; }
Т.е. ветка {...} никогда не получит управления! На самом деле все не так просто, поскольку в исполняемом файле нужно еще найти эту инструкцию перехода. К тому же разработчики защиты это всячески пытаются затруднить. Запутывают алгоритм, используют самомодифицирующийся код, применяют недокументированные вызовы операционной системы... Однако такие препятствия хакера не смущают. Технологии противодействия заметно обгоняют эволюцию систем защиты. А с появлением дизассемблера IDA, отладчика Soft-Ice и распаковщика cup386 копание в чужом коде не только превратилось в удовольствие, но и стало доступно широкому кругу лиц. Десять лет назад всего выше перечисленного богатства еще не было, процессор работал только в реальном режиме, и не было никакой возможности уберечь отладчик от разрушающего воздействия со стороны изучаемой программы. Хакеры "старого поколения" работали в основном карандашом, листком бумаги и головой. Зачастую код приходилось дизассемблировать в уме, а недокументированные вызовы изучали, погружаясь в недра операционной системы. Это не только требовало высокого профессионализма, но и огромного количества свободного времени, которое было нечем занять.
Россия в этом преуспела. Кадровые специалисты, скучавшие на работе, восприняли защиты как увлекательную головоломку. Так или иначе, с той поры мы настолько привыкли к бесплатному поломанному программному обеспечению, что до сих пор поддаемся соблазну купить пиратский диск, а не платить вдесятеро большую сумму фирме-производителю.
Отсюда и возникло твердое убеждение: как бы разработчик ни защищал свой продукт, все равно сломают. На самом же деле существуют алгоритмы, делающие взлом по меньшей мере неэффективным. Основанные на криптографии, они обеспечивают не только надежную, но и математически обоснованную степень защиты.
Допустим, пароль, введенный пользователем, расшифровывает рабочий код программы. Мне могут возразить, что это не спасает от банальной кражи пароля. Что помешает одному пользователю использовать пароль другого? Но если пароль взят из неочевидных источников, например электронных ключей, взлом становиться трудным делом.
Кроме того, даже использование в качестве пароля ключевого диска или файла требует по крайней мере одной легальной копии для взлома. Пока такая копия попадет к руки хакера и будет нелегально распространена, разработчик успевает продать достаточное число экземпляров продукта. Кроме того, каждый уважающий себя кодокопатель считает долгом чести самостоятельно написать генератор ключей, а не использовать уже существующий. Это позволит какое-то время поддержать объем продаж.
Впрочем, организационные проблемы выходят за рамки данной книги, и мы на этом закончим их рассмотрение. Гораздо интереснее рассмотреть популярные криптосистемы и возможные атаки на них.
Большинство защит сегодня используют шифровку своего кода в целях затруднения анализа и модификации кода. Ключ, используемый в расшифровщике, хранится непосредственно в последней, поэтому теоретическая криптостойкость подобной системы равна нулю. Впрочем, это не важно, т.к. преследуются совсем другие цели. Кроме IDA, ни один известный мне дизассемблер не может работать с шифрованным кодом. Отладчик не сможет функционировать, если декодер использует необходимые ему ресурсы. Наконец, непосредственная модификация кода становиться невозможна. При этом сам алгоритм шифра и его криптостойкость не играют никакой роли! Действительно, если пароль, используемый декодером, известен, то использовать криптостойкие алгоритмы бессмысленно!
Поэтому наиболее популярными являются криптостемы на основе логической операции xor. Одним из ее свойств является зеркальность. Повторное шифрование результата восстановит исходный текст. Шифровщик и дешифровщик устроены одинаково, что упрощает и сокращает код. Докажем, что
a xor b xor a = b.
Для этого просто перечислим все возможные значения a и b в следующей табличке:
a\b ¦ 0 ¦ 1 -----+---------------------+--------------------- 0 ¦ 0 xor 0 xor 0 == 0 ¦ 0 xor 1 xor 0 == 1 ¦ ¦ 1 ¦ 1 xor 0 xor 1 == 0 ¦ 1 xor 1 xor 1 == 1 ¦
Заметим, что xor битовая операция. Аргументы a и b могут иметь только два значения: 0,1. Однако никто не запрещает проводить ту же операцию для последовательности битов. Команда процессора XOR word, const на самом деле представляет собой не word xor const, а последовательность операций над каждой парой битов двух переменных.
Теперь обратим внимание, что a xor 0 == a; a xor 1 == !a. Т.е. значащими в маске шифрования являются только единичные биты. Поэтому рекомендуется выбирать такую маску, в которой единичные и нулевые биты равномерно перемешаны. К примеру, 00001111b (0xF) будет плохой маской, т.к. оставляет неизменными четыре старшие бита в каждом символе шифротекста, что позволяет (как будет показано ниже) успешно атаковать шифр. Маска 01010011 полностью уничтожает вероятностное распределение исходных символов в шифротексте, поэтому считается хорошей.
Возможна шифровка двух видов - статическая и динамическая. В первом случае дешифровщик работает только один раз, после чего исходный текст может быть полностью восстановлен. Иными словами, наступает момент, когда не остается ни одного зашифрованного фрагмента. Такой подход имеет очень простую реализацию, но крайне неэффективен. Хакер может снять дамп с памяти в момент окончания работы расшифровщика и записать его на диск. После чего полученный файл можно будет изучать штатными средствами. Это невозможно выполнить для динамической расшифровки, когда ни в какой момент код не будет расшифрован полностью. При вызове процедуры он расшифровывается, а при выходе зашифровывается опять. Технически можно написать декодер, который расшифрует весь код в автономном режиме, но это довольно сложно и требует тщательного анализа защиты.
Покажем это на примере самошифрующийся программы. Традиционно такие программы выполняются на ассемблере, но можно реализовать их на Си, Паскале и подобных языках, даже не используя ассемблерных вставок, а работая с памятью через указатели.
Рассмотрим простейший пример (file://CD:/SRC/Crypt00.asm).
LEA SI,beginCrypt ; Расшифровываем с этого адреса Repeat: ; <-----------------------------¬ XOR Byte ptr [SI],077h ; Расшифровать очередной байт ¦ INC SI ; Переместить указатель ¦ CMP SI,offset endCrypt ; ?Конец достигнут ¦ JNA Repeat ; --{SI<=offset endCrypt}-------
Чтобы полученная программа оказалась работоспособна, необходимо вручную зашифровать фрагмент [offset beginCrypt,offset endCrypt] по xor 0x77. Для этого можно воспользоваться утилитой HIEW, скриптом IDA или написать процедуру, которая это сделает автоматически.
********** рисунок p1 ******** рисунок p2 ******
Теперь сравним два дампа: до и после шифровки. Шифровка исказила исходный дамп до неузнаваемости, исчезла текстовая строка "Hello,World!". Этот прием может использоваться злоумышленником для сокрытия текстовых фрагментов в вирусах, троянских программах и т.д.
Шифровка затруднила и изучение программы. Вот что выдаст дизассемблер в нашем случае.
1AEF:0100 BE0D01 MOV SI,010D 1AEF:0103 803477 XOR BYTE PTR [SI],77 1AEF:0106 46 INC SI 1AEF:0107 81FE2401 CMP SI,0124 1AEF:010B 76F6 JBE 0103 1AEF:010D C3 RET ; < отсюда все зашифровано 1AEF:010E 7ECD JLE 00DD 1AEF:0110 62 DB 62 1AEF:0111 76BA JBE 00CD 1AEF:0113 56 PUSH SI 1AEF:0114 B43F MOV AH,3F 1AEF:0116 121B ADC BL,[BP+DI] 1AEF:0118 1B18 SBB BX,[BX+SI] 1AEF:011A 5B POP BX 1AEF:011B 57 PUSH DI 1AEF:011C 2018 AND [BX+SI],BL 1AEF:011E 051356 ADD AX,5613 1AEF:0121 7A7D JPE 01A0 1AEF:0123 53 PUSH BX
Как разобраться в этой дикой мешанине кода и данных? Что делать и как с этим жить?
Тут на помощь приходит уникальный дизассемблер IDA, поддерживающая встроенный Си-подобный язык. Следующий скрипт (file://CD:/SRC/crypt00. idc) выполнит все автоматически. Чтобы его запустить на выполнение, нужно дать команду : idax -Scrypt00.idc crypt00.com
Рассмотрим, как он работает:
for (a=0x10D;a<0x124;a++) // Цикл дешифровки { c=Byte(MK_FP(0x1000,a)); // Взять байт c = c ^ 0x77; // Расшифровать PatchByte(MK_FP(0x1000,a),c); // Записать результат }
Фактически мы копируем алгоритм расшифровщика, с точностью до реализации. Приведенный код расшифровывает загруженный образ файла, который потом IDA будет в состоянии дизассемблировать. Вот за эту возможность она горячо любима всеми хакерами. В самом деле, не нужно выходить из уютной и привычной среды дизассемблера в агрессивную среду отладчика. Дожидаться окончания расшифровки и записывать дамп на диск (а ведь не всякий отладчик обеспечивает такую возможность). Загружать полученный образ в дизассемблер и, если что-то не ладится, повторять все вновь.
SOURCER, являясь автоматическим пакетным дизассемблером, для этой цели не годится. Однако мы все же можем заставить его работать, если подадим на вход уже расшифрованный файл. Для этого воспользуемся не менее любимой утилитой хакеров HIEW, которая аналогичным образом обеспечивает пошаговую интерпретацию встроенного ассемблер-подобного языка.
Для начала нам потребуется узнать начало и конец зашифрованного фрагмента. Переключим HIEW в режим дизассемблера и обратим внимание на следующие строки:
00000000: BE0D01 mov si,0010D ; <-- начало зашиф. фрагмента 00000003: 803477 xor b,[si],077 00000006: 46 inc si 00000007: 81FE2401 cmp si,00124 ; <-- конец зашиф. фрагмента 0000000B: 76F6 jbe 000000003
Поскольку в com файле базовое смещение 0x100, то истинным адресом начала будет 0x10D - 0x100 = 0xD. Переведем курсор на полученный адрес, введем следующий скрипт XOR AX,77 и начнем расшифровку. Код на глазах "оживет" и, словно феникс из пепла, одна за одной возникнут "родные" инструкции. Теперь файл можно сохранить на диск и обработать любым дизассемблером вплоть до debug.com! Вот только работать он больше не будет. Наша ошибка в том, что мы не удалили дешифровщик, который "не знает", что код уже расшифрован и портит его. Достаточно заменить маску шифрования 0х77 на 0х0, чтобы устранить это упущение.
Однако неограниченные возможности IDA позволяют проделать ту же самую операцию и на ее встроенном языке! Для этого нам необходимо воспользоваться файловым вводом\выводом.
while (1) { c=fgetc(hIn); if (c==-1) break; c=c ^ 0x77; if (fputc(hOut,c),hOut)==-1) break; }
Полученный дамп можно загрузить в любой дизассемблер (в IDA, например) и продолжить его изучение. Конечно, тот же самый скипт не хуже будет обрабатываться и обычным компилятором Си (и даже быстрее выполняться), но это потребует компилятора (которого может и не оказаться под рукой), к тому IDA в этом отношении просто удобнее. Дело в том, что зачастую анализ алгоритма декодера - далеко не тривиальная задача, и он заметно облегчается, когда в IDA перед глазами возникает его дизассемблированный код. Можно даже не вникать в его смысл, а просто покомандно переписать на Си. В этом отношении IDA не имеет равных.
В некоторых случаях ресурсов и быстродействия встроенного языка явно не хватает, и тогда приходится прибегать к помощи MS VC или оптимизированного ассемблера. Но в таких случаях уместнее воспользоваться другими методами.
Выше я назвал использование отладчика "агрессивным" методом. В каком-то смысле это метод "грубой силы". Никакого вникания в алгоритм декодера не требуется. Достаточно лишь точно определить момент завершения расшифровки и скинуть дамп в файл. Это очень популярный прием, однако необходимо помнить, что жизнь "человеку с отладчиком" испортить очень легко. Возможны самые разнообразные эффекты - от блокирования трассировки до "глухого" завешивания системы. Выход из этого положения только один: использовать хороший отладчик.
Данный пример не содержит антиотладочных приемов, поэтому для его изучения подойдет любой отладчик. Воспользуемся замечательной утилитой trsutil, входящий в комплект антивируса AVP 2.2 PRO. Это очень компактный, но многофункциональный отладчик, который подходит для решения широкого спектра задач. Подробное обсуждение его возможностей впереди, а пока ознакомимся лишь с некоторыми из них.
**************** рисунок p3 *************************
Пошагово трассируя программу, можно наблюдать за ходом расшифровки. "В живом" виде понять алгоритм декодера несравненно легче, чем в дизассемблере. Дождавшись окончания цикла, записываем дамп. Вся операция отняла меньше минуты, тогда как использование IDA отнимет по меньшей мере десяток минут.
Однако над полученным дампом требуется еще поработать. Если не "убить" дескриптор, как уже было сказано выше, то дескриптор "убьет" код. Использование специализированных средств может ускорить и облегчить эту операцию. Обычно эту задачу возлагают на автоматические распаковщики, которые хорошо с ней справляются. Часто, но не всегда. Один из мощнейших распаковщиков Cup386 имеет ручной режим распаковки. По сути это полноценный интегрированный отладчик необычайной мощности. На данном этапе нас это еще не интересует, поэтому выберем простейшую пошаговую трассировку, для чего запустим cup с ключами /1 crypt00.com /d. Как и в первом случае, дожидаемся выхода из цикла (для этого можно подогнать курсор к строке 0х10D и нажать F4). В этот момент весь код расшифрован и может быть немедленно исполнен. Если бы существовал способ "сфотографировать" это состояние и записать, чтобы в любой момент можнобыло его восстановить, не начиная выполнения программы с самого начала... И cup как раз обеспечивает такой снимок! Он позволяет сохранять значение любых регистров и точку входа и сохранять их в exe файле. Для его создания необходимо выбрать пункт меню "Create executable file" и задать длину сохраняемого фрагмента. В нашем примере она равна 0xD. Никакие регистры нам сохранять не нужно, поэтому выбираем "Just create, no preserving" и нажимаем заветную кнопочку "ОК". Мы получим готовый работоспособный файл, который можно запускать, загружать в отладчик и дизассемблер в рекордно короткие сроки - на все действия вполне достаточно пяти-десяти секунд! И это при том самом впечатляющем результате, который мы получили!
Увы, cup не всесилен, и для динамической шифровки уже невозможно получить полностью расшифрованный файл, т.к. в любой момент будет расшифрована только часть его.
Рассмотрим простой пример динамической шифровки на основе все той же операции xor (file://CD:/SRC/Crypt02.asm). Давайте перед выводом каждой процедуры ее сначала расшифровывать, а при выходе - вновь зашифровывать. Таким образом в любой момент будет открыта только небольшая часть кода, а остальная - зашифрована.
EXECUTE PROC NEAR CALL CRYPT ; Расшифровываем процедуру PUSH BP ; Сохраняем BP для вложенных вызовов CALL Word ptr [BP+2] ; Запускаем расшифрованную проц. POP BP ; Восстанавливаем BP CALL CRYPT ; Зашифровываем проц. обратно RETN ; --\ ENDP
Чтобы данный пример мог работать, необходимо каким-то образом сообщить процедуре CRYPT начало и длину шифруемого фрагмента. Один из вариантов - перед каждой процедурой расположить блок данных, содержащий все необходимые сведения.
CPB macro Begin,End,XorMask ; CPM (CRYPT PREDEF BLOCK) DW offset &End - offset &Begin DW offset &Begin DB XorMask endm
Определим для удобства метод run следующим образом:
RUN macro ProcA MOV BP,offset &ProcA-5 CALL EXECUTE endm
Тогда вызов подпрограммы будет выглядеть как run proc1. Рассмотрим дизассемблированный листинг этого вызова:
start proc near mov bp, 234h call sub_0_11A retn start endp
Процедура sub_0_11A и есть execute, - чтобы в этом убедиться, достаточно бегло взглянуть на нижеследующий фрагмент:
sub_0_11A proc near call sub_0_107 push bp call word ptr [bp+2] pop bp call sub_0_107 retn sub_0_11A endp
Интересующая нас процедура находится по адресу [BP+2] == 0x234 + 0x2 = 0x236; [0x236] == 0x239. Однако дизассемблер не может восстановить код процедуры, т.к. он еще зашифрован.
seg000:0236 dw 239h seg000:0239 db 0C9h ; г seg000:023A db 21h ; ! seg000:023B db 75h ; u seg000:023C db 0DAh ; - seg000:023D db 7Ch ; | seg000:023E db 0B7h ; ¬ seg000:023F db 3 ;
И невозможно никаким образом снять дамп более чем с одной процедуры одновременно. Вообще-то данный пример при вызове вложенных процедур не зашифровывает родительские (для упрощения понимания), но это нам мало чем помогает.
Дизассемблирование программы становится невозможным. Как с этим бороться? Можно воспользоваться отладчиком, но это не будет полноценной заменой дизассемблированному листингу. Можно поочередно записывать дампы всех процедур по мере их выполнения, но это очень утомительная и бесконечно долгая процедура. И что потом делать с этой кучей дампов?
До недавнего времени дизассемблирование динамических защит было сложным и неэффективным занятием, на которое отваживались далеко не все. С появлением IDA все изменилось.
Запустим несложный скрипт на выполнение (file://CD:/SRC/crypt02.idc).
static try() { auto a,p,l,c,mask; p = ScreenEA(); mask = Byte(p+4); l = Word (p); for (a=0;a<l;a++) { c = Byte(p+a+5); c = c ^ mask; PatchByte(p+a+5,c); } AnalyseArea(p+5,-1); }
Наведем курсор на начало CPB блока и вызовем функцию try. Это еще одна особенность IDA - с помощью своих функций мы безгранично можем расширять ее возможности. Функция try теперь будет доступна из любого выполняющегося скрипта. Или с консоли. Нажмем Shift-F2 и введем try(). Было бы утомительно проделывать это каждый раз, не поддерживай IDA макросы. Можно задать любую удобную для нас горячую клавишу для вызова этой функции. Например, Alt-C.
Однако этот метод грешит тем, что придется проанализировать все функции вручную, а это потребует больших усилий. В самом деле, неудобно работать "ручками" там, где можно использовать автономный скрипт. Попытаемся использовать тот факт, что процедура EXECUTE принимает входной аргумент в регистре BP, представляющем собой смещение CPB. Можно предположить, что использовалась конструкция MOV BP, offset Proc (LEA BP, Proc), и таким образом, найдя все вхождения MOV BP,xxxx\CALL EXECUTE мы можем автоматически расшифровать весь файл! В большинстве случаев этот способ безотказно срабатывает. Действительно, поскольку процедура расшифровки одна, то простым контекстным поиском мы можем обнаружить все ссылки на нее. Неужели все так просто? И все наши усилия по созданию динамически шифрующейся программы пропали даром? Другими словами: можно ли от всего этого защититься? Разумеется! Даже не потребуется менять алгоритм. Нужно, чтобы адрес процедуры был задан не в форме константы, а произвольным образом вычислялся. В этом случае контекстный поиск окажется бессильным. В нашем примере сингатурный поиск поможет расшифровать только часть процедур, большую часть которых составляют переходники к операционной системе такие как WriteString, ReadString и т.д,, не содержащие в себе ни йоты интересного кода. Остальные останутся зашифрованными. Приглядимся повнимательнее к тому, как работает функция main:
LEA SI,scenario Main_repeat: LODSW OR AX,AX JZ main_exit XCHG BP,AX SUB BP,5 CALL EXECUTE JMP SHORT Main_repeat
Фактически это встроенный интерпретатор, который последовательно исполняет инструкции из списка scenario. Но содержит ли scenario полный перечень процедур? Быть может, вложенные процедуры также содержат свой локальный интерпретатор?
Но существует простое и элегантное решение, которое зачастую упускают разработчики защит. В большинстве случаев все существующие процедуры расположены вплотную одна за другой. За концом одной процедуры - начало следующей. Эта банальность предельно упрощает расшифровку. Очевидным решением будет помещение между соседними процедурами случайного числа незначащих байт. К сожалению, ни один из известных мне ассемблеров или компиляторов не поддерживал такой особенности. А вручную это делать очень утомительно? Но, допустим, автор защиты пошел на такие затраты времени и все соседние процедуры разнесены от нуля до N байт. Остановит ли это хакеров? Давайте еще раз внимательно посмотрим на CPB структуру:
CPB macro Begin,End,XorMask DW offset &End - offset &Begin DW offset &Begin DB XorMask endm
Обнаруживаются две закономерности. Поле [2] численно равно смещению заголовка CPB и offset &End - offset &Begin, как правило, не больше 0xFFFF. Этой избыточной информации вполне достаточно для поиска блоков CPB и расшифровки всех существующих процедур простейшим автоматическим скриптом.
Сражение технологий взлома и защиты не прекращается ни на минуту. Каждый день рождаются все новые хитроумные скрывающие механизмы, чтобы спустя короткое время исчезнуть, освобождая место для новых. Приведенный выше пример оказался нестойким из-за ошибки в программной реализации. Существуют ли программы без ошибок? Хакеры отвечают: "Программ без ошибок не бывает. Бывает? Плохо искали". Неизвестно, чего больше в этой шутке - юмора или правды.
Язык ассемблера, однако сегодня непопулярен. Можно ли писать самошифрующиеся программы на языках высокого уровня? Удивительно, но далеко не каждый программист на этот вопрос утвердительно ответит "ДА". Обычно пожимают плечами - зачем это делать? В результате подавляющее большинство программ абсолютно не защищены и легко модифицируются злоумышленниками, число которых угрожающе растет. Доходит то того, что новые версии программ и "ломки" для них появляются практически одновременно, часто их разделяет всего несколько часов.
В России бесполезно надеяться на правовую поддержку. Спасение утопающих остается задачей утопающих, которые упорно не хотят понять, что никто не поможет им кроме них самих. Пираты не будут копировать программы, если их взлом станет невыгодным и коммерчески неокупаемым. Шифровка кода - первый шаг на пути создания защиты, без которого последняя ничего не стоит.
Создание самошифрующихся программ на языках высокого уровня - сложная, но все же разрешимая задача. Для ее решения приходится учитывать специфику конкретного компилятора и операционной системы. Заметим, что в некоторых операционных системах наложен запрет на модификацию кодового сегмента. В таком случае сделать ничего нельзя. Остается либо сменить операционку, либо спуститься на один уровень ниже, разрабатывая защиту для работы в нулевом кольце с неограниченными правами доступа.
К счастью, MS-DOS никак не препятствует модификации программой своего собственного кода, и данная процедура будет успешно работать:
void Crypt(char *Point, char *EndPoint) { while (Point<=EndPoint) Point[0]=(Point++)[0] ^ 0x77; }
Однако при этом будут наложены жесткие ограничения - программа должна компилироваться для TINY модели памяти. Будет очень жаль, если ваш любимый компилятор на это не рассчитан.
Попробуем выяснить, что влияет на выбор модели памяти. TINY - "крошечная" - размещает в одном сегменте и код, и данные. Следовательно, один и тот же указатель может иметь доступ как к собственному коду, так и к данным. Отметим, что в языке Си нет указателей, работающих с кодом, и только особенность выбранной модели памяти позволяет "дотянуться" до кода. Впрочем, некоторые компиляторы могут проверять границы и при выходе за отведенное для данных пространство генерировать исключение или останавливаться с сообщением об ошибке.
Модель SMALL размещает код и переменные в двух независимых сегментах. Используются только близкие указатели, адресуемые через DS. Сам DS строго указывает на сегмент данных.
Но с переходом к большой (LARGE) модели памяти все вновь меняется. При этом все указатели превращаются в далекие (сегмент: смещение) и могут адресовать как данные, так и код. Эту замечательную новость омрачает тот прискорбный факт, что вместе с указателями становятся далекими и вызовы процедур. В момент загрузки файла DOS вычисляет и непосредственно записывает конкретные значения сегментов во всех перемещаемых элементах. Подробнее этот процесс описан в руководстве MS-DOS для программиста: мы же просто примем к сведению, что средствами одного лишь языка высокого уровня, не используя ассемблерных вставок, работать с перемещаемыми элементами невозможно.
Открытые системы
Приведенные выше подходы по существу никакого отношения к криптографии не имеют, поскольку являются закрытыми. Т.е. система содержит в себе и шифротекст и пароль. Утаить пароль в таком случае просто невозможно. Все что можно сделать, - это бесконечно затруднить его выявление и анализ используемого криптоалгоритма. На самом деле эта задача никогда не является разрешимой, и такие системы всегда могут быть вскрыты. Технические приемы, затрудняющие анализ кода, вы найдете в соответствующей главе, а сейчас мы рассмотрим другой подход к проблеме. Пусть система не содержит пароля, а ожидает его прибытия извне. Тогда дизассемблирование и анализ криптоалгоритма нам ровным счетом ничего не дадут. Необходим пароль, а система его не содержит и предполагается, что последний недоступен для злоумышленника. Это обстоятельство настолько важно, что я подчеркну его еще раз. Охрана пароля - это сугубо организационная проблема, не имеющая отношения ни к криптографии, ни к хакерству.
Откровенно говоря, у меня никогда не вызывали уважения люди, похищающие пароли с помощью шпионажа и "социальной инженерии". Нетехнические средства, будь то легендарные копания в мусорных бачках или запрос сведений по телефону от имени технического персонала (администратора, разработчика системы) действительно являются самостоятельной наукой, даже, может быть в определенной степени искусством, требующим незаурядных психологических навыков, но... К рассматриваемому вопросу они не относятся. Атака на систему подразумевает, что пароль достоверно не известен и требуется его найти анализом открытой криптостемы.
В действительности открытых криптосистем сегодня не существует. Человек (знающий пароль) + механизм шифрования представляют собой закрытую криптосистему, которая уже содержит ключ! Как было показано выше, в этом случае криптостойкость последней всегда равна нулю и оптимальное решение сводится к выявлению ключа, которое невозможно предотвратить, а только бесконечно затруднить.
С другой стороны, на самом деле картина отнюдь не такая мрачная. Каналы передачи и хранения ключей сегодня достаточно надежны, персонал, имеющий к ним доступ, хорошо инструктирован и за каждую бумажку, брошенную в мусорную корзину, несет строгую ответственность.
Поэтому нас будет интересовать в первую очередь именно уязвимость криптоалгоритмов и конечных реализаций.
Как атаковать шифр
При атаке на шифр считается, что криптоалгоритм известен с точностью до реализации и требуется найти пароль. В качестве примера рассмотрим программу crackme0.com (file://CD:/SRC/Crackme.com). Алгоритм расшифровки ничем не защищен и его легко можно изучить. Однако это нам не дает никакой информации об исходном пароле.
CalcCRC: ; <--------------------------------¬ LODSB ; Читаем байт введенного пароля ¦ ADD AH,AL ; Суммируем ¦ LOOP CalcCRC ; --{CX}----------------------------
Эта процедура вычисляет CRC с введенного пароля. CRC очень простой и с плохим рассеиванием. Множество паролей будет иметь одно и то же CRC, поэтому нет никакой возможности предсказать на основе всего восьмибитного числа исходный пароль.
LEA SI,Crypt ; На начало зашифрованных данных Decrypt: ; <--------------------------------¬ XOR [SI],AH ; Расшифровать байт ¦ INC SI ; Следующий байт ¦ CMP SI,offset Buffer; ?Мы уже все расшифровали ¦ JB Decrypt ; --{SI<offset Buffer---------------
Шифротекст расшифровывается значением контрольной суммы от пароля! Причем всего может существовать 0x100 (256) вариантов расшифровки! Нетрудно было бы перебрать их всех и найти исходный. Даже если бы использовалось слово (0x1000), то вариантов было бы все равно немного!
Между прочим, это довольно распространенный примем в некоммерческих программах. Удивительно, как это не учитывают авторы защит! Если перебирать не пароли, а ключи (т.е. сами хеш-значения), то можно достичь скорости порядка нескольких сотен тысяч ключей в секунду. Таким образом не спасет даже использование двойного слова (0x100000000). Оценим какое наибольшее время необходимо для перебора тридцатидвухбитного ключа. Исходя из средней скорости миллион ключей в секунду, мы найдем результат не более чем за 4295 секунд т.е. приблизительно за 71 минуту. Практически за час с небольшим, не так ли? Все дело в высокой скорости перебора ключей, обусловленной простотой алгоритма.
Автоматический перебор ключей опирается на возможность контроля идентичности расшифрованного и оригинального текста. Программа, очевидно, должна была контролировать правильность ввода пользователя и подсчитывать CRC либо пароля, либо расшифрованного текста. Рассмотрим следующий код:
CmpCRC: ; <--------------------------------¬ DEC SI ; Следующий байт ¦ ADD CL,[SI] ; Суммировать ¦ CMP SI,offset Crypt ; ?Конец ¦ JNB CmpCRC ; --{SI>offset Crypt}--------------- CMP CL,0C3h ; ?Значение CRC верно JZ Crypt ; --CRC OK--> ; СRC FAIL
Ясно, что проверяется контрольная сумма исходного текста. Мы можем исследовать хеш-алгоритм и хеш-сумму исходного текста. В нашем случае она равна 0xC3.
Но, сколько существует неправильных вариантов расшифровки, для которых их контрольные суммы тем не менее совпадают? Очевидно, что больше одного! И с ростом числа вариантов расшифровки количество "ложных" ключей стремительно растет! Так, уже при 32-битных ключах основную проблему будет представлять не поиск подходящий ключей, а выборка единственного верного исходного текста среди расшифрованных вариантов! При этом у нас никаких достаточно строгих критериев, позволяющих автоматически отличить ложные варианты. Разработчик защиты добился-таки своего! Вероятность, что пользователь введет неправильный, но подходящий пароль, достаточно мала, поэтому ей можно пренебречь. Действительно, предположим, что хотя бы 1% паролей будет давать ложные срабатывания, тогда для 0x10000 (65536) ключей это составит 655 вариантов, а для 0x100000000 уже 42.949.672! Заметим, что с точки зрения криптографии все эти варианты равноценны!
Разумеется, мы в любом случае имеем хотя бы косвенное представление об исходном тексте. Но как его использовать? Можно по типу данных предугадать вероятность того или иного символа, проверить на совпадение со словарем, поискать некоторые закономерности... Но как же все это будет медленно работать! И в любом случае (особенно на коротких фрагментах) нет никакой гарантии, что даже самый надежный алгоритм всегда распознает исходный текст.
Разработчики защиты могут торжествовать! Они заставили хакера призадуматься. Хакеры же пошли другим путем. Криптографам был давно известен метод атаки по открытому тексту. В самом деле, если злоумышленник обладает хотя бы частью открытого текста, то он может сделать обратную шифрованию операцию и найти ключ! Вспомним свойство операции xor:
X xor Key = Y, Y xor Key = X, но X xor Y = Key!
В нашем примере с восьмибитным ключом достаточно достоверно знать всего лишь один байт открытого текста, чтобы получить Key, который можно применить ко всему шифротексту в целом. Но можем ли мы узнать какой-то байт чужого текста, и к тому же достоверно? Оказывается, можем, да еще как! Конечно, не достоверно, но с определенной степенью вероятности мы можем ПРЕДПОЛОЖИТЬ его наличие!
Весьма вероятно, что в приведенном шифротексте встретится инструкция INT 0x21. Ее двухбайтовый опкод 0x21CD (мы ведь помним про обратный порядок байт в слове!) превышает длину ключа, а значит, делает его на этом промежутке неслучайным. В самом деле, в уравнении X xor Y = key мы не знаем ни X, ни key, при выбранном Y они могут быть любыми. Если #Y >#Key, то мы ~~8 получим X1 xor Y1 == X2 xor Y2 == key!
Может быть это объяснение покажется туманным, поэтому приведем практический пример. С помощью утилиты hiew попробуем расшифровать секретный фрагмент, применяя операцию XOR 0x21CD.
************** рисунок 4 ************************
00000030: C3 74¦08 B4-09 BA BE 01-CD 21 C3 B0-B5 E4 BB A9 00000040: 00 20_20 2E-0C E7 51 8C-72 9E 76 82-73 89 21 A2 00000050: 4A CC^01 E7-25 7D 01 CD-21 CD 00 00-00 00 00 00 00000060: 00 00¦00 00-00 00 00 00-00 00 00 00-00 00 00 00
Обратим внимание на последовательность 0x2020. Весьма верятно что в оригинальном тексте здесь находилось 0x21CD, зашифрованное ключом 0x20! Следует учитывать, что данный метод допускает ложные срабатывания. Поэтому предпочтительнее использовать по возможности более длинные последовательности. В этом случае мы получили только один возможный ключ, а что мы стали бы делать, если бы их оказалось несколько? Если мы знаем частоту появления выбранной последовательности (а именно такие и стоит выбирать), то мы можем определить истинный ключ простым сравнением! Однако допустим, что два или более ключей имеют очень близкую к ожидаемой вероятность появления. Как быть тогда? В этом случае необходимо поискать другую последовательность, ожидаемую в шифротексте. В нашем примере это может быть 0xA0D - перенос строки. Весьма вероятно, что пример выводит какую-то строку, которая завершается возвратом каретки. Предоставляю читателю провести этот эксперимент самостоятельно, а самзамечу, что на деле редкая программа обходится без вывода текстовых сообщений. Но ведь в любом языке не так уж много слов, а тем более распространенных! Это очень уязвимое место подобных систем. Не потребуется даже утомительного перебора. Поищем такие строки, как (c), Copyright, OK, Cancel, Yes, No... Хорошо действует двойной пробел, пробел+ запятая и т.д.
Кроме того, не забываем, что нам известен CRC исходного текста, - следовательно, можно быстро определить, какой ключ из нескольких - правильный. Я не буду приводить расчетов вероятности появления ложных паролей - скажу только, что при использовании последовательностей из двух и более символов она достаточно невелика. Таким образом криптостойкость данного шифра при атаке по открытому тексту (даже если неизвестно точное расположение последнего) равна нулю!
В данном примере использовался ключ 0x20. Внешне этот ключ абсолютно ничем не примечателен, но взгляните на зашифрованный фрагмент:
************** рисунок 5 ************************
И это ЗАШИФРОВАННЫЙ текст? Как же такое могло произойти? Магическое свойство xor 0x20 - переводить английский текст в противоположный регистр - сыграло с автором защиты очень злую шутку. У каждого алгоритма есть некоторые число СЛАБЫХ паролей, которые в разной степени снижают его криптостойкость. Об этом мы говорили выше. А теперь, если вспомнить, что 0x20 - 100000b, нетрудно убедиться, что такой ключ меняет всего один бит на каждый байт! Т.е. шифротекст будет мало чем отличаться от исходного. Хорошие криптосистемы делают проверку на слабые ключи, плохие - нет. Учитывая, что число слабых ключей у иных шифров не так мало, как кажется на первый взгляд, с вероятностью использования такого ключа стоит считаться.
Выше мы отмечали, что использование 32-битного ключа дает 0x100000000 вариантов и потребует около 71 минуты перебора. А если длина ключа все 64 бита? 0x10000000000000000 вариантов потребует 28014 секунд или почти восемь часов перебора (и выдаст еще больше ложных срабатываний). Если бы мы могли достоверно знать хотя бы одну шестнадцатибайтовую последовательность, присутствующую в исходном тексте...
Крайне маловероятно, что мы располагаем такой информацией! Однако на самом деле положение вовсе не безнадежно, и у нас по-прежнему хорошие шансы найти даже такой длинный ключ. Сначала заметим, что минимально необходимый фрагмент открытого текста в действительности равен длине ключа плюс единица. Естественно, это увеличит число ложных срабатываний, но не так сильно, как кажется на первый взгляд. Представим для начала, что нам известен лишь фрагмент A. Какова вероятность того, что он совпадет с A'?
---------------¬ L--------------- , TAT-------------TAT----- - - - +---------------+------- - - - ¦ ¦ ¦<---- L ------>¦
Давайте представим себе последовательность AA. Естественно, что она будет "вмещать" в себя #A^2 элементов. У скольких элементов левая "половина" равна правой? Обозначим левую часть как L, а правую как R. Легко видеть что для каждого L существует только один равный ему R. Число совпадающих вариантов равно множеству элементов А. Тогда вероятность совпадения произвольных А и А' равна #A/#A^2 == 1/#A, где # - число элементов множества.
Однако нас интересуют только те совпавшие числа, которые находятся строго на расстоянии L друг от друга, где L как видим, равна длине ключа. Задачу подсчета данной вероятности я оставляю любопытным читателям, отмечу только, что она на несколько порядков ниже общего числа ключей, которые нам пришлось бы перебирать в противном случае. Даже если #A равна одному биту (!), у нас неплохие шансы. И гораздо более высокие, чем показал расчет любознательных читателей. Точнее говоря они МОГУТ СТАТЬ несравненно выше. Используемая нами математическая модель исходила из предпосылки равновероятности всех символов. Это очень упрощает расчеты, но не соответствует действительности. Как правило, нам известно хотя бы приблизительное распределение вероятности встречаемых символов. Это поможет отбросить часть вариантов как заведомо ложные или, (что более правильно) начать перебор с наиболее вероятных ключей, а затем переходить к менее вероятным.
Вышесказанное звучит захватывающе, однако так и не объясняет, где нам взять хотя бы 64+1 битовую последовательность для атаки по открытому тексту. Кажется, что ассемблерских команд такой длины и даже устойчивых их сочетаний не существует. Но может быть, мы плохо искали? Например, все языки высокого уровня используют стандартные библиотеки, сингатуры которых известны, а число пренебрежительно мало (по сравнению с числом возможных ключей, разумеется). Это применимо далеко не к каждой программе, но к значительному их числу.
Допустим, автор защиты это учел и использовал неизвестный доселе компилятор или полностью реализовал весь код на ассемблере и отважился выбрать ну очень длинный ключ - не будем даже уточнять, насколько длинный. Восторжествует ли он на этот раз? Увы (или ура - в зависимости от ориентации читателя)! Злоумышленник применит масочную атаку! Суть ее сводится к следующему: пусть нам не известно сколь-нибудь длинной строки из зашифрованного текста, но мы наверняка знаем много коротких! И с некоторой достоверностью - расстояние L между ними.
Алгоритм атаки следующий: пусть s0 одна из существующих в шифротексте последовательностей. Применим атаку по открытому тексту и в результате получим ОГРОМНОЕ множество подходящих, но ложных ключей. То, что ни один из них не верен это ясно из того, что длина каждого равна #s0. По условию s0 короче пароля; следовательно, ни один ключ не является законченным паролем. Однако, ясно, что настоящий пароль включает в себя некоторые элементы полученного множества. Возьмем другую известную последовательность s1 и повторим аналогичную операцию. Теперь выберем общие для f(s0) и для f(s1) элементы. Вероятнее всего, что именно из них и составлен пароль. С каждой итерацией число символов, общее для всех последовательностей стремительно уменьшается, а вместе с ним и число вероятных паролей. Когда все последовательности исчерпаны, выберем только те, которые разнесены в границах предполагаемого расстояния (если такая информация имеется). Существует множество вариантов получения пароля из заданного множества фрагментов, однако нет нужды перебирать их все. Я доставлю читателю удовольствие самому решить несложную задачку уменьшения числа возможных вариантов. (Привычка получать все в готовом виде очень вредна, а для хакера - в корне неприемлема! В моем понимании хакер - это человек, который в любой ситуации привык рассчитывать только на себя. Готовые технологии и знания - непозволительная роскошь.
Однако наводящую подсказку я все же дам. Пусть нам неизвестна вероятность ни одного из встречаемых в шифротексте символов, но для каждой последовательности Sn вероятность образующих ее элементов известна наверняка!
Перечень возможных вариантов атак по открытому тексту этим не исчерпывается. Перечислять их дальше совершенно бессмысленно. Каждая реализация требует своего, уникального подхода к атаке. И этот подход должен быть оперативно найден злоумышленником. Книги и руководства тут не помогут. Они разве что помогают определить общее направление и выразить основную идею атаки, а это я, надеюсь, уже сделал.
В заключении мы рассмотрим атаку по открытому тексту на зашифрованный архиватором arj файл. Несмотря на высокую скорость перебора существующих взломщиков требуется немало времени на поиски паролей более чем из восьми символов. В то же время, если есть хотя бы один незашифрованный файл из архива, задача решается очень просто. Часто в архивы попадают общедоступные файлы (pgp - подписи, рекламные проспекты, стандартные файлы типа dos4gw или *.DLL). Содержимое некоторых файлов иногда можно попытаться угадать. В первую очередь это относится к bat файлам. Самое главное то, что длина bat файлов обычно очень мала, а содержимое состоит из ограниченного набора словарных последовательностей.
Но перейдем собственно к реализации атаки. Для нее нам потребуется знать используемый криптоалгоритм и формат файлов архиватора. Этот формат добросовестно описывается в прилагаемой документации. А вот криптоалгоритм автором не разглашается. Что само по себе уже подозрительно. Как учит рыночная политика - если производитель о чем-то пытается умолчать, то можно не сомневаться в качестве (точнее, его отсутствии). Не должна стойкость криптосистемы зависеть от знания ее алгоритма, никак не должна. Правило, сформулированное голландцем Керкхоффом, гласит: стойкость шифра должна определяться только секретностью ключа. Роберт Янг к этому не прислушался и приложил гораздо больше услилий к сокрытию криптоалгоритма, чем к качеству реализации. В техническом описании он не раскрывает значение байта 0xB заголовка, оставляя его "зарезервированным". Между тем он активно используется при шифровке паролем!
Есть два пути для отождествления криптоалгоритма: анализ большого числа открытых и зашифрованных текстов или дизассемблирование и анализ кода программы. Я всегда выбираю последний. Это не всегда самый быстрый, но гарантированный вариант. Дизассемблирование - довольно трудоемкая работа, которая может растянуться на долгие недели, если нет соответствующих навыков. Подробнее об этом я собираюсь рассказать в следующей моей книге "Образ мышления - IDA". В рамках данной главы можно отметить, что не обязательно анализировать весь arj.exe, воспользуемся самораспакующимся архивом, который не в пример короче. Дизассемблирование последнего быстро покажет, что в arj используется система шифрования Вермана, которая сводится к следующему: для пароля S1S2S3 получим бесконечную последовательность s1s2s3s1s2s3s1s2s3, и тогда NN-ый символ открытого текста преобразуется в шифротекст простым XOR [Sn],[Nn]+ MAG_VALUЕ.
Что такое MAG_VALUE? А это и есть тот "зарезервированный" символ, назначение которого Роберт не раскрыл в документации.
Однако наивно было бы полагать, что этим можно повысить криптостойкость алгоритма шифрования. Это "волшебное значение", видимо, генерируется случайным образом для каждого файла. Назначение его не ясно. Никакого влияния на криптостойкость оно не оказывает и вряд ли может быть названо усовершенствованием алгоритма, поскольку для атаки можно использовать те же методы, что и для "чистого" шифра Вермана, и с тем же успехом.
Любопытно, что пароли abc и abcabc абсолютно идентичны! (И, кстати, об этом умалчивается в документации)! Отсюда вытекает, что выбор "неудачного" пароля может позволить "прямую" атаку на шифр!
Ниже приведен фрагмент атакующей программы, которая открывает используемый пароль. Константа MAX_AVIALABLE_PASSWORD_SIZE - это максимально возможная длина предполагаемого пароля. Как легко видеть, крипостойкость не зависит от длины. Кроме того используемый алгоритм настолько быстр, что практически не требует времени и исполняется МГНОВЕННО!
for (int a=0;a<MAX_AVIALABLE_PASSWORD_SIZE;a++) { NormalByte = (unsigned char) pNormalBuffer[a]; ProtectByte = (unsigned char) pProtectBuffer[a]; ResByte=(unsigned char) (NormalByte ^ ProtectByte) - MagValue; st=st+(char) ResByte; }
Заметим, что в архиве есть и предсказуемая информация. Даже беглое исследование показывает, что в шифруемые данные попадают и служебные структуры, формат которых может быть получен анализом упаковщика. В этом случае нам не требуется никакого другого открытого текста, и даже если пароль не удается определить точно, то по крайней мере число возможных вариантов окажется очень невелико.
Вместе с Янгом этот бой проиграли и все пользователи его криптосистемы. Зашифрованные данные оказались уязвимы без ведома их обладателей. Сколько архивов было (или могло быть) вскрыто за это время?
Ни в коем случае нельзя пользоваться неизвестными криптоалгоритмами. Надежность их весьма сомнительна. Пользователи находятся в очень незавидном положении. Мало кто из них способен проанализировать выбранную криптосистему на уязвимость, а поэтому никогда не может чувствовать себя в безопасности. Некоторых это толкает на написание своих реализаций криптостойких алгоритмов. Однако очень немногие представляют себе, насколько сложно написать действительно качественную реализацию. Примеры ошибок мы видим и продолжаем видеть в очень многих продуктах, и число их не уменьшается со временем. Как бы ни совершенствовались защиты, хакер в этом мире всегда найдет место под солнцем.
Логическая (программная) защита
Введение
Логические (программные) защиты основываются на том предположении, что код программы не будет изучен и (или) изменен. В этом случае приложение рассматривается как "черный ящик", на вход которого подается некоторая ключевая информация. Это может быть серийный номер, ключевой диск... - да все что придумает автор. Грубо все защиты можно разделить на две категории:
- основанные на знании (пароль, серийный номер);
- основанные на обладании (ключевой диск, документация).
Любопытно, что первая категория защит в основном полагается на законодательство и законопослушность пользователей. Действительно, что помешает легальному пользователю "поделиться" паролем или сообщить серийный номер всем желающим? Конечно, подобное действие можно квалифицировать как "пиратство" и наказать нарушителя. Но точно так же можно наказать за распространение любого ПО, охраняемого авторским правом, вне зависимости от наличия\отсутствия на нем защиты.
Общеизвестно, что правоохранительные органы в нашей стране относятся к пиратам очень терпимо. Нелегальное ПО можно свободно купить в любом городе в центральных магазинах и на радиорынках.
В этих условиях "спасение утопающих - дело рук самих утопающих". Разработчики ПО используют специальные методы, препятствующие нелегальному тиражированию своей продукции. Самые распространенные сегодня защиты - это пароли и серийные номера. Как уже было отмечено выше, наивно полагаться на то, что это затруднит копирование. Серийные номера содержатся в файлах read.me на дисках, печатаются на обложках, помещаются на общедоступных серверах в Интернете и публикуются в телеконференциях.
В настоящее время взлом и модификация кода программ для удаления процедуры запроса серийного номера уже не популярны. Гораздо легче сообщить покупателю серийный номер.
Эти взгляды характерны не только для пользователей, но и для хакеров. Действительно, зачем что-то ломать, если серийный номер и так известен! Однако, встречаются недобросовестные пираты (и довольно нередко), которые распространяют программы без сообщения серийных номеров. Вот тут-то и приходится браться за отладчик!
Рассмотрим простейший пример (file://CD:/SRC/BREAK00). Это программа под Win32, использующая библиотеку MFC. Под MS-DOS используются в целом те же принципы защиты, но с небольшими расхождениями. Иногда я на них буду обращать внимание, иногда нет. Не то чтобы взлом досовских приложений перестал быть актуальным, но местами различия не настолько значительны, чтобы стоило отвлекать и запутывать читателя.
Запустим break00.exe на выполнение. Программа просит ввести пароль. Чтобы сравнить его с введенным, оригинальный пароль должен как-то храниться в программе. Очевидным способом является простое посимвольное сравнение:
if ((s0=ch)!="KPNC") cout << endl << "Password fail" <<endl;
Для того чтобы найти правильный пароль, надо просто просмотреть дамп программы и поискать все текстовые строки, которые могут быть паролем. Едва ли разработчик защиты (если его так можно назвать) окажется наивным парнем, рассчитывающим, что злоумышленник не найдет открыто хранящийся пароль. Однако некоторые программы (в том числе русифицированные игры от Акелла) защищены именно так.
Можно было бы просто просматривать текст программы в любом hex-вьювере, но это утомительно, особенно для больших файлов. Поэтому воспользуемся утилитой - "текстовым фильтром", которая анализирует файл и записывает все встретившиеся текстовые строки. Рекомендую воспользоваться собственной filter.com.
Рассмотрим полученный листинг.
------------> смещение в файле ¦ -------> текстовая строка ¦ ¦ 24FA:Winit 250C:MSVCP 3020:Password OK 3030:Password fail 3040:KPNC ^^^^^^^^^ 3048:Enter password 305C:CrackMe ^^^^^^^^^^^^ 3067: Try to path code of found valid password 3094:Fatal Error 30A0: MFC initialization failed
Обратим внимание на строку, находящуюся по адресу 0x3040. Не правда ли, она могла бы быть паролем? Чаще всего (но необязательно) искомая строка располагается близко к тексту "введите пароль". Ниже мы видим еще одного "кандидата".
Давайте проверим, подойдет ли хотя бы один из них?
CrackMe01 : Try to path code of found valid password Enter password : KPNC Password OK! Press any key to continue
Несмотря на простоту, данный метод не лишен недостатков. Самый главный из них - то, что результат не гарантирован. Скорее всего,в открытом виде пароля не окажется.
Более надежным способом (но, увы, и более трудоемким) является дизассемблирование программы и анализ алгоритма защиты. Это трудоемкая и кропотливая работа, требующая не только знаний ассемблера, но и усидчивости, а также немного интуиции.
Части: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15