Техника и философия хакерских атак
Продолжение...
Первый шаг. От EXE до CRK
Бесспорно, среди существующих на сегодняшний день дизассемблеров лучшим является IDA. Особенно идеально она подходит для взлома и изучения защищенных программ. Очевидно, что Break00 не является таковой в полном смысле этого слова. В нем нет ни шифрованного кода, ни "ловушек" для дизассемблеров. SOURCER или любой другой справился бы с этой задачей не хуже. Поэтому окончательный выбор я оставляю за читателем.
После того как дизассемблер завершит свою работу и выдаст километровый листинг, неопытный читатель может испугаться: как войти в эти дебри непонятного и запутанного кода? Сотни вызовов функций, множество условных переходов... Как во всем этом разобраться? И сколько времени потребуется на анализ? К счастью, нет никакой нужды разбираться во всем дизассемблированном листинге.
Достаточно изучить и понять алгоритм защитного механизма, ответственного за сверку паролей. Нужно только найти этот механизм. Можно ли это сделать как-то иначе, чем полным анализом всей программы? Разумеется, можно! Для этого нужно найти ссылки на строки "неверный пароль", "пароль ОК", "введите пароль". Ожидается, что защитный механизм будет где-то поблизости. Сами строки находятся в сегменте данных. (В старых программах под DOS это правило часто не соблюдалось. В частности, компилятор Turbo Паскаля любил располагать константы непосредственно в кодовом сегменте.)
Для перехода в сегмент данных в IDA нужно в меню "View" выбрать "Segment Windows" и среди перечисленных в появившемся окне отыскать сегмент типа "DATA". Искомые строки бросаются в глаза даже при беглом просмотре. Перевести их в удобочитаемый вид можно перемещением курсора на начало строки и нажатием "A" (от слова ASCII).
Популярные руководства по взлому рекомендуют теперь найти по перекрестным ссылкам код, который эти строки выводит. Но, увы, нас постигает глубокое разочарование. Ни одной ссылки на код нет. На самом деле, просто IDA не сумела их самостоятельно найти. Нет никакой возможности автоматически отличить константы от смещений, ни один дизассемблер не может делать это безупречно. SOURCER, возможно, показал бы куда больше интересного, но на этом его достоинства и кончаются. Кроме воистину магической способности определять перекрестные ссылки, поклонники SOURCER не могут привести ни одного аргумента в его пользу.
Но IDA позволяет найти перекрестные ссылки и самостоятельно. Для этого нужно нажать Alt-T и ввести адрес интересующей нас строки. Попробуем найти код, который выводит 'Enter password'. Нажимаем Alt-T и вводим,'403048' (без кавычек) - адрес, по которому расположена эта строка. Теперь IDA обычным контекстным поиском будет искать все идентичные вхождения по всему дизассемблированному тексту.
Мы должны быть готовы к тому, что получим множество "ложных" срабатываний (просто констант, или смещений, но в другом сегменте), а то и ничего не получим. Возможно, адрес строки задается не через константу, а хитрыми математическими вычислениями и манипуляциями. Это действительно не самый лучший метод, но неоправданно популярный и рекомендуемый многими руководствами.
Не то чтобы я был против такого подхода, но боюсь, что новичков он может только запутать. Существуют гораздо более красивые и быстрые методы, которые будут рассмотрены ниже. Но идеология их схожа - они всегда пытаются "поймать" защитный код на основе известных или предсказуемых данных. Если у нас есть строка, которая выводится, значит есть тот, кто ее выводит. Естественно предпологать, что он находится где-то в окрестностях сравнивающего механизма или непосредственно к нему относится.
Цель автора защиты - построить код так, чтобы не оставить хакеру никакой избыточной информации о работе последнего, по которой его можно вычислить. Данный пример ничего подобного не содержит, и IDA нам показывает следующий фрагмент:
004010C0 mov esi, ds:??6std!!AMPER!!!!AMPER!!YAAAV? $basic_ostream!!AMPER!!... ........ ... ... 004010E7 mov eax, ds:?cout!!AMPER!!std!!AMPER!!! !AMPER!!3V?$basic_ostream!!AMPER!!... 004010EC push 403048h ^^^^^^^^^^^^^^^ 004010F1 push eax 004010F2 call esi
Но чем является в данном случае 403048h - смещением или константой? Это можно узнать из прототипа функции basic_ostream<E, T> *tie(basic_ostream<E,T> *str). Пусть читателя не смущает небольшая разница в написании имен: причина в двух словах объяснена быть не может, и мне остается лишь отослать интересующихся этим вопросом к MSDN, который не только по-прежнему доступен в Он-Лайне, но и распространяется вместе с MS VC. Без MSDN и глубоких знаний Win32 говорить о хаке под Windows просто неэтично! Это, конечно, не означает, что каждый кракер обладает глубокими знаниями платформы, на которой он ломает. Большинство защит вскрываются стандартными приемами, которые вовсе не требуют понимания "как это работает". Мой тезка (широко известный среди спектрумистов уже едва ли не десяток лет) однажды сказал "Умение снимать защиту, еще не означает умения ее ставить". Это типично для кракера, которому, судя по всему, ничто не мешает ломать и крушить. Хакер же не ставит целью взлом (т.е. способ любой ценой заставить программу работать), а интересуется именно МЕХАНИЗМОМ: "как оно работает". Взлом для него вторичен.
Однако мы отвлеклись. Вернемся к прототипу функции basic_ostream. Компилятор языка Cи заносит в стек все аргументы справа налево. Поэтому 0x403048 будет указателем на строку (*str), которую затем функция и выводит. Таким образом, мы находимся в непосредственной близости от защитного механизма. Сделаем еще один шаг.
004010D8 mov edi, ds:??5std!!AMPER!!!!AMPER!!YAAAV? $basic_istream!!AMPER!!DU? ........ ... ... 004010F4 mov edx, ds:?cin!!AMPER!!std!!AMPER!!!!AMPER! !3V?$basic_istream!!AMPER!!DU? 004010FA lea ecx, [esp+1Ch] 004010FE push ecx 004010FF push edx 00401100 call edi
Теперь вызывается функция basic_istream, считывающая пароль со стандартного устройства ввода (клавиатуры). Прототип ее аналогичен, за исключением того что вместо выводимой строки ей передается указатель на буфер.
Я умышленно взял сложный для понимания пример. Вероятно, вы думаете, что буфер расположен по адресу [esp+1Ch]? И чтобы сравнить введенную строку с эталонной, необходимо передать указатель на [esp+1Ch]? Как бы не так! Коварный оптимизирующий компилятор использовал не регистр EBP, значение которого неизменно на протяжении всей процедуры, а указатель на верхушку стека esp! Занесение параметров в стек приводит к изменению esp, и нет никакой возможности предугадать его значение в произвольной точке кода. Приходиться отслеживать все операции со стеком и вычислять новое значение esp в уме (или с помощью хитрых скриптов для IDA, о которых мы поговорим в следующий раз).
Рассмотрим нижеследующий фрагмент. После того как [esp+1Ch] указал на буфер, содержащий введенную строку, в стек были переданы два двойных слова (см. выше). Заметим, что стек растет "снизу вверх", т.е. от старших адресов к младшим.
Команда add очищает стек от локальных параметров отработанной функции. Тогда новое значение esp равно -2*4+0x10 = +8; 0х1С - 0х8 = 0x14. Следовательно, теперь уже [esp+0x14] указывает на наш буфер!
00401102 add esp, 10h 00401105 lea eax, [esp+14h] ^^^^^^^^^^^^^^^^^^^^^^ 00401109 lea ecx, [esp+10h] 0040110D push eax 0040110E call j_??4CString!!AMPER!!!!AMPER!!QAEABV0!!AMPER! !PBD!!AMPER!!Z
Обратим внимание на подчеркнутую строку. Насколько же с первого взгляда неочевидно, куда указывает указатель eax! Попутно замечу, что даже сегодня не каждый компилятор способен генерировать такой код, ценность которого заключается практически в экономии всего одного регистра ebp. Но читатель должен быть готов, что со временем этому "научатся" все компиляторы и все операции с локальными переменными придется отслеживать указанным выше образом. К счастью, дизассемблеры не отстают в этом от компиляторов, и уже IDA 3.8b прекрасно справляется с этой задачей. Можно даже не задумываться о том, что, собственно, происходит у нее "внутри". В том-то и беда, что новые технологии могут освобождать от глубокого понимания предмета. Между тем такой подход делает человека зависимым от окружающих его инструментов. Без них он становится беззащитен перед дикой природой.
Далеко не всегда под руками оказывается последняя версия мощного дизассемблера или другого необходимого инструмента. Обычный программист в такой ситуации просто мычит и беспомощно разводит руками. Хороший хакер не почувствует в этой ситуации дискомфорта. Глубокие знания и привычка делать все самому, своими руками, не доверяя машинам, дают навыки, которые позволяют совершенно "без ничего", в прямом смысле этого слова, ломать программу используя лишь то, что имеется в распоряжении, даже если для этого приходится дизассемблировать код в уме.
Поэтому, стремясь показать, как происходит обращение к локальным переменным, я начал объяснение с версии 3.6, не "умеющей" автоматически отслеживать изменение esp. Разумеется, это не повод для обязательного ее использования. Впрочем, нет оснований и отказываться от нее. Лично мне старая, проверенная и устойчивая версия ближе, чем неустойчивая и непривычная новая 3.8b. В то же время встроенный язык позволяет неограниченно расширять возможности дизассемблера и включать в него все новые технологии и достижения на свой вкус.
Использование последней версии IDA выгодно еще и тем, что позволяет получить символьные имена всех используемых функций в популярных библиотеках по их сигнатурам. Так, в вышеприведенном примере трудно понять, что же делает функция в строке 0х040110E. IDA 3.8 уверенно распознает эту функцию как CString::operator=(char const *). Следовательно, введенный пользователем пароль заносится в переменную типа CString, хранящуюся по адресу [esp+ 10h]. Само собой теперь необходимо ожидать процедуру сравнения. Более того, мы может предположить, что она будет одним из методов CString!
004010DE mov ebx, ds:_mbscmp ........ ... 0040110E call j_??4CString!!AMPER!!!!AMPER!!QAEABV0!!AMPER!!PBD!!AMPER!!Z 00401113 mov eax, [eax] 00401115 push 403040h 0040111A push eax 0040111B call ebx 0040111D add esp, 8 00401120 test eax, eax 00401122 mov eax, ds:?cout!!AMPER!!std!!AMPER!!!!AMPER!!3V ?$basic_ostream!!AMPER!!DU?.. 00401127 push eax 00401128 jz short loc_401144
Функция _mbscmp, как следует из ее названия, сравнивает строки и имеет очевидный прототип int _mbscmp( const char *string1, const char *string2 ). В строке 0x0401113 мы получим указатель на новую переменную CString, но что же тогда представляет собой число 0x403040? Очевидно, что это указатель на эталонную строку. Посмотрим, что находится по указанному смещению:
00403040 aKpnc db 'KPNC',0
Как нетрудно догадаться, это и есть тот пароль, которого от нас ожидает программа! Для того чтобы убедиться в этом, посмотрим, как используется результат сравнения. Нет ничего проще для разработчика защиты, чем подсунуть нам "ложный" пароль, который, вместо того чтобы запустить программу, наоборот, удалит ее с диска или, что еще хуже, отформатирует сам диск. Конечно, такие приемы не относятся к числу красивых и не так уж часто встречаются, чтобы заставить нас принимать эту угрозу в расчет, но анализ использования результатов сравнения позволит нам изменить код так, чтобы программа вообще не спрашивала пароль или, на худой конец, просто воспринимала любой как правильный.
Для этого в очередной раз обратимся к MSDN, где узнаем, что функция _mbscmp возвращает false (ноль), если строки идентичны, и true в противном случае. Если бы над кодом не поработал оптимизатор, то можно было бы ожидать непосредственно после CALL-а примерно следующую конструкцию:
CALL xxxx TEST EAX,EAX JZ xxxx [JNZ xxx]
Однако оптимизатор расположил команды в несколько ином порядке - так, чтобы они выполнялись за меньшее число тактов. К сожалению, в ущерб читабельности. Условный переход находится на четыре команды ниже в строке 0х0401128. TEST EAX,EAX устанавливает флаг Zero в том случае, когда EAX == 0. Следовательно, переход JZ выполняется только тогда, когда сравниваемые строки идентичны. Думаю, что читатель сможет с удовольствием удостовериться, что код в ветке loc_401144 выводит "Password OK" и в законченном (а не демонстрационном) приложении продолжает нормальное выполнение программы.
Что будет, если мы заменим условный переход JZ на БЕЗУСЛОВНЫЙ JMP? Тогда независимо от результатов сравнения (а следовательно, и введенной строки) программа будет воспринимать любой пароль как правильный!
IDA 3.6 не может записывать отпаченный PE файл, поэтому нам придется этим заняться самостоятельно. Для этого нужно найти в файле тот же фрагмент, что мы видим в дизассемблере. HIEW позволяет искать непосредственно ассемблерные инструкции, облегчая взломщикам жизнь, но мы пойдем другим путем. Гораздо надежнее искать hex-последовательность, которую включает интересующий нас фрагмент. Для этого переключим IDA в режим показа опкода инструкций.
Строго говоря, теперь нам предстоит выбрать сигнатуру, т.е. по возможности короткую, но уникальную последовательность, которая повторяется в файле только один раз. Разумеется последовательности jz xxx (0x74 0x1A) окажется скорее всего недостаточно, поскольку ожидается, что она может встретиться более чем в одном контексте. Практика показывает, что обычно требуется последовательность не менее чем из трех инструкций. Конечно, чем короче файл, тем меньше вероятности ложных срабатываний. Давайте ограничимся всего двумя командами - push eax\jz xxx. Запишем на бумажку (или запомним) их опкод - 50 74 1A.
Теперь запускаем hiew, переводим его в hex-режим просмотра и пытаемся найти эту последовательность. Если все сделано правильно, то мы обнаружим ее по адресу 0x0401127. Удостоверимся, что это действительно единственное вхождение и больше совпадений нет. Если же в файле присутствует более одной строки, то возвращаемся в IDA и записываем более длинную последовательность. Впрочем иногда (чем больше опыта, тем чаще) можно определить, какие варианты оказались ложными, "на глаз" сравнив код в этом месте с тем фрагментом, что мы видели в дизассемблере.
Я умышленно не предупредил, что нужно сделать резервную копию файла. Собственно, если есть дистрибутив (а он,как правило, есть всегда), то потеря файла в результате простой человеческой ошибки неопасна. В противном случае резервную копию делать просто необходимо.
Так или иначе, пришло время немного "похулиганить" и изменить ту заветную пару байт, которая мешает нелегальным пользователям (а так же всем легальным, но забывшим пароль) получить доступ к программе. Как уже было показано выше, изменение условного перехода на безусловный приведет к тому, что программа будет воспринимать любой пароль как правильный. Опкод команды JMP SHORT - 0xEB. Узнать это можно из руководства Intel по микропроцессорам 80x86. Впрочем, hiew позволяет обойтись и без этого. Достаточно перейти в режим ассемблера и ввести jmps с тем же адресом перехода, что и JZ. Сохраняем проделанные изменения и выходим.
Запустим программу и попробуем ввести любое слово (желательно из нормативной лексики), пришедшее нам на ум. Если мы все сделали правильно, то на экране появится "Password OK". Если же программа зависла, значит, мы где-то допустили ошибку. Восстановим программу с резервной копии и повторим все сначала.
Если же взлом прошел успешно, то можно попробовать придумать какую-нибудь шутку. Мне в голову пришло целых две, о которых я и расскажу ниже, надеясь, что бурная фантазия читателей не останется безучастной, а предложит что-нибудь еще, гораздо более оригинальное.
Подумаем, что будет, если заменить JZ на JNZ? Ветви программы поменяются местами! Теперь, если будет введен неправильный пароль, то система воспримет его как истинный, а легальный пользователь, вводя настоящий пароль, с удивлением прочитает сообщение об ошибке.
Часто кракеры любят оставлять во взломанной программе свои лозунги или (с позволения сказать) "копирайты". Модификация подобного рода в откомпилированных исполняемых файлах довольно трудна и требует навыков, которыми вряд ли обладает начинающий. Но ведь оставить свою подпись так хочется! Для подобной операции можно использовать уже не нужный во взломанной программе фрагмент, выводящий сообщение о неверно набранном пароле. Вспомним, как расположены ветки в исполняемом файле:
--------------------------¬ ¦ Ввод и сравнение пароля ¦<--¬ +-------------------------+ ¦ ---+ JZ Password_ok ¦ ¦ ¦ +-------------------------+ ¦ ¦ ¦ ¦ ¦ ¦ ¦ "НЕВЕРНЫЙ ПАРОЛЬ" ¦ ¦ ¦ ¦ ¦ ¦ ¦ +-------------------------+ ¦ ¦ ¦ JMP enter&compare +---- ¦ +-------------------------+ ¦ ¦ ¦ L->¦ "ВЕРНЫЙ ПАРОЛЬ" ¦ ¦ ¦ L--------------------------
Что будет, если мы удалим два перехода (один условный, второй безусловный)? В этом случае последовательно отработают две ветки программы. Чтобы "убить" любую инструкцию, достаточно "забить" ее NOP (опкод который 0x90, а вовсе не 0, как почему-то думают многие начинающие кодокопатели). Обе команды в нашем примере двухбайтовые, и поэтому каждую придется заменить двумя инструкциями NOP.
Кажется, мы все сделали правильно, однако "программа выполнила недопустимую операцию и будет закрыта". К сожалению, мы забыли об оптимизирующем компиляторе. Это затрудняет модификацию программы. Но ни в коем случае не делает ее невозможной. Давайте заглянем "под капот" могучей системы Windows и посмотрим,что там творится. Запустим программу еще раз и вместо аварийного закрытия нажмем кнопку "сведения", в результате чего получим следующий результат:
Программа BREAK_X вызвала сбой при обращении к странице памяти в модуле MSVCP60.DLL по адресу 015f:780c278d.
Разочаровывающе малоинформативные сведения! Разумеется, ошибка никак не связана с MSVCP60.DLL, и указанный адрес, лежащий глубоко в недрах последней, нам совершенно ни о чем не говорит. Даже если мы рискнем туда отправиться с отладчиком, то следов причины мы не найдем. В действительности вызываемой функции передали неверные параметры, которые и привели к исключительной ситуации. Конечно, это говорит не в пользу фирмы MircroSoft: что же это за функция такая, если она не проверяет, какие аргументы ей передали! С другой стороны, именно сокращением числа проверок и вызвано некоторое ускорение win98 по сравнению с ее предшественницей. Но нужна ли такая оптимизация? Я бы твердо ответил "НЕТ". Жаль только, что Бил Гейтс меня не услышит.
Однако мы опять отвлеклись. Как же нам проникнуть внутрь Windows и выяснить, что там у нее не в порядке? В этом нам поможет другой продукт фирмы MicroSoft - MS VC. Будучи установленным в систему, он делает доступной кнопку "отладка" в окне аварийного завершения. Теперь мы можем не только закрыть некорректно работающее приложение, но и разобраться, в чем причина сбоя.
Дождемся появления этого окошка еще раз и вызовем интегрированный в MS VC отладчик. Пусть не самый мощный, но достаточно удобный во многих случаях. Как уже отмечалось, бессмысленно искать черную кошку там, где ее нет. Ошибка никак не связана с местом ее возникновения. Нам нужно выбраться из глубины вложенных функций "наверх", чтобы выяснить, что явилось причиной передачи некорректных параметров. Это можно сделать, используя адреса, занесенные в стек. В удобочитаемом виде эту информацию может предоставить мастер "Call Stack", результат работы которого показан ниже:
std::basic_ostream<char,std::char_traits<char> >::opfx(std::basic_ostre... std::basic_ostream<char,std::char_traits<char> >::put(std::basic_ostrea... std::endl(std::basic_ostream<char,std::char_traits<char> > & {...}) BREAK_X! 0040114a() CThreadSlotData::SetValue(CThreadSlotData * const 0x00000000, int 4,....
Напомню, что стек растет снизу вверх, а нам, следовательно, нужно спускаться вниз. Первые три вызова можно смело пропустить (т.к. это библиотечные функции), а четвертый break_x принадлежит пользовательскому приложению (по имени исполняемого файла). Вот это - настоящий источник ошибки. Кликнем по нему мышкой и перейдем непосредственно в окно дизассемблера.
00401142 nop 00401143 nop 00401144 call dword ptr ds:[402038h] 0040114A push 403020h
Узнаете окружающий код? Да-да, это то самое место, где мы его модифицировали. Но в чем причина ошибки? Обратим внимание, что перед вызовом функции в строке 0x0401144 не были переданы параметры! Куда же они могли подеваться? А... Это хитрый оптимизирующий компилятор расположил их так, чтобы они оказывались в стеке только в том случае, если эта ветка получает управление. Вернемся к оригинальной копии, чтобы подтвердить наше предположение:
401122 mov eax, ds:?cout!!AMPER!!std!!AMPER!!!!AMPER!!3V? $basic_ostream... 401127 push eax 401128 jz short loc_401144
Как хитро построен код. Этим искусством машинного творения действительно можно залюбоваться! Начинаешь испытывать глубокое уважение и к самим компиляторам, и их разработчикам. Оставим занимательную головоломку модификации кода любопытным читателям. Первая необдуманная мысль - переписать целиком этот фрагмент - не выдерживает никакой критики. Это слишком некрасивое решение. Наибольший интерес представляют решения с изменением минимального числа байт. Это действительно захватывающая головоломка, от которой я получил большое удовольствие.
Красивые и лаконичные решения, полученные ценой бессонных ночей, проведенных за монитором и километрами распечаток, это удел настоящих хакеров. Кракерам они не то что бы неинтересны (ведь кракеры тоже неплохие специалисты с нелинейным мышлением), но для них взлом - это все же большей частью профессия с вытекающей отсюда рыночной системой отношений. Клиенту не нужны красивые решения, клиент хочет видеть быстрый и дешевый взлом. Прямо здесь и прямо сейчас. А вместе с красотой страдает и качество.
Изменив всего один байт 0x74 на 0xEB, мы грязно взломали программу. Кракер на этом остановится, но хакер пойдет дальше. Почему "грязно"? Программа по-прежнему спрашивает пароль. И хотя не имеет значения какой, все же это может сильно раздражать, да и просто выглядет не аккуратно. Давайте модифицируем программу так, чтобы она вообще не отвлекала нас запросом пароля.
Одним из решений будет удаление процедуры ввода пароля. Обращу внимание на важный момент: вместе с процедурой необходимо удалить и заносимые в стек параметры, иначе он окажется несбалансированным и последствия, скорее всего, не заставят себя ждать. Однако компиляторы Cи строят функции так, что очистку стека выполняет не сама функция. Рассмотрим внимательно еще раз процедуру ввода строки:
4010D8 mov edi, ds:??5std!!AMPER!!!!AMPER!!YAAAV? $basic_istream!!AMPER!!DU?... ...... ... ... 4010FA lea ecx, [esp+1Ch] 4010FE push ecx 4010FF push edx 401100 call edi 401102 add esp, 10h
Стек очищается командой ADD ESP,10h. Функция его не изменяет. Поэтому нам ничем не грозит удаление этой функции, и мы без последствий можем "забить" ее командами NOP. Кроме того, можно удалить две команды push (соответственно изменив add esp,10h на add esp,08h), но это вопрос стиля. Кому-то так может показаться красивее, а другой не захочет выполнять бесполезную работу.
Совсем иначе обстоит дело с Паскалевскими компиляторами. Стек очищает непосредственно сама функция. Тогда удаление заносимых параметров становится обязательным - иначе несбалансированность стека очень быстро приведет к зависанию.
Что же еще можно улучшить? Надпись Enter password: по-прежнему выводится и выглядит небрежной кляксой на фоне опрятного взлома. Отключим ее? Заметим, что это можно сделать изменив всего один байт, - поставить в начало выводимой строки завершающий символ 0. Это не потребует изменения кода, что безопаснее. А что если мы вместо 'Enter password' запишем свой копирайт? (Должны же пользователи знать, какого доброхота им следует благодарить!). Рассмотрим подробно эту простую операцию, ибо она далеко не так проста, какой кажется на первый взгляд. Было бы неплохо, если бы строка 'Enter password' была раза в два длиннее. А в таком ограниченном объеме мало что можно записать. На деле существующие ограничения легко обойти. Рассмотрим несколько наиболее очевидных вариантов.
0403020: 50 61 73 73-77 6F 72 64-20 4F 4B 21-00 00 00 00 Password OK! 0403030: 50 61 73 73-77 6F 72 64-20 66 61 69-6C 00 00 00 Password fail 0403040: 4B 50 4E 43-00 00 00 00-45 6E 74 65-72 20 70 61 KPNC Enter pa 0403050: 73 73 77 6F-72 64 20 3A-20 00 00 00-43 72 61 63 ssword : Crac 0403060: 6B 4D 65 30-31 20 3A 20-54 72 79 20-74 6F 20 70 kMe01 : Try to p 0403070: 61 74 68 20-63 6F 64 65-20 6F 66 20-66 6F 75 6E ath code of foun 0403080: 64 20 76 61-6C 69 64 20-70 61 73 73-77 6F 72 64 d valid password 0403090: 00 00 00 00-46 61 74 61-6C 20 45 72-72 6F 72 3A Fatal Error: 04030A0: 20 4D 46 43-20 69 6E 69-74 69 61 6C-69 7A 61 74 MFC initializat 04030B0: 69 6F 6E 20-66 61 69 6C-65 64 00 00-00 00 00 00 ion failed
Во взломанной программе строки 'Password fail!' и 'KPNC' уже не нужны. И мы их можем использовать для своих нужд. Для этого нужно изменить указатель на выводимую строку. Как помним, он расположен по адресу 0х04010EC:
004010EC push 403048h
Изменим смещение 0x403048 на 0x0403030. Тогда нам нам будет доступна вся область до 0х403059 (т.е. до начала строки 'CrackMe....'). Только не забудьте конец строки отметить завершающим нулем.
С другой стороны, в сегменте данных еще много свободного места (на этом дампе оно не показано). Если уж мы изменили смещение выводимой строки, почему бы тогда не расположить необходимую нам строку в любой свободной области и не установить на нее указатель?
Но наш взлом еще не подошел к концу. Остается последний немаловажный вопрос - как мы будет распространять свое творение? exe-файлы обычно имеют очень большогй объем, и на распространение их наложены чисто законодательные ограничения.
Хорошо бы объяснить пользователю, какие именно байтики следует поменять, чтобы программа заработала, но сможет ли он понять нас? Вот для этой цели и были написаны автоматические взломщики.
Для начала нужно установить, какие именно байты были изменены. Для этого нам вновь потребуется оригинальная копия и какой-нибудь сравниватель файлов. Наиболее популярными на сегодняшний день являются c2u by Professor Nimnul и MakeCrk by Doctor Stein's labs. Первый гораздо предпочтительнее, т.к. он не только более точно придерживается наиболее популярного "стандарта" (если можно так сказать), но и позволяет генерировать расширенный xck формат.
Для запуска утилиты передадим два файла - оригинал и модифицированную версию. После чего все изменения будут записаны в файл. При некоторых различиях практически все форматы (в особенности xck) поддерживают ряд чисто текстовых информационных полей, которые абсолютно бесполезны, кроме того, что могут нести какую-то информацию. Единого формата полей нет, и форма заполнения произвольна (на вкус взломщика). Поэтому навязывать свою точку зрения я не буду.
Теперь нам потребуется другая утилита, цель которой будет прямо противоположна: используя crk файл, изменить эти самые байты в оригинальной программе. Таких утилит на сегодняшний день очень много. К сожалению, это не лучшим образом сказывается на их совместимости с различными crk форматами. Самые известные из них, скорее всего, cra386 by Professor и pcracker by Doctor Stein's labs.
Но поиск подходящей программы, поддерживающий наш формат crk, является уже заботой пользователя, решившего взломать программу. Заметим, что распространение crk файлов НЕ является нарушением и НЕ карается законом, т.к. это чисто текстовая информация и, кроме того, продукт вашего умственного труда, который автоматически попадает под защиту закона об авторских правах.
Крак можно легально распространять, тиражировать, продавать. Но вот у пользователя, решившего его использовать, проблемы с законом возникнуть уже могут, т.к. этим он ущемляет авторские права разработчиков программы. Парадоксальный, однако, у нас мир!
Для избежания проблем с совместимостью иногда используют исполняемые файлы (c2u способен генерировать и такие), которые выполняют модификацию программы автоматически. При этом они часто занимают даже меньше места! Но главный недостаток их в том, что исполняемый файл по нашим законам уже является не информацией, а орудием атаки, и следовательно, распространяться не может. Впрочем, конечный выбор я оставляю за читателем и его совестью.
Мы проделали большую работу и наверняка узнали немало нового. Это была очень простая защита, и нас ждет еще очень длинный, но интересный путь.
Как победить хэш
Первые шаги к усложнению парольных защит прикладные программисты сделали приблизительно в самом конце восьмидесятых годов. Посимвольное сравнение пароля было неэффективной и недостаточной защитой от увеличивающейся армии взломщиков, вооруженных новыми по тем временам отладчиками и дизассемблерами.
В главе, посвященной динамической шифровке, я впервые использовал в демонстрационной программе хеш функцию для сокрытия пароля. Действительно, пусть у нас имеется функция f(password) = hashe, которая в первом приближении необратима. Тогда значение hashe не дает никакой информации о самом пароле!
Однако на самом деле ничего не изменилось. Вместо того чтобы сравнивать пароли, теперь защита сравнивает хеши. Чтобы убедиться, что пароль введен правильно и получена верная хеш сумма, программа должна сравнить полученное значение хеша с эталонным! В зависимости от результата сравнения выполняется та или иная ветка программы. Сравните:
if ((s0=ch)!="KPNC") cout << "Password fail" << endl; if (hashe(&pasw[0])!=0x77) cout << "Password fail" << endl;
По сути ничего не изменилось. И в том и в другом случае достаточно изменения одного условного перехода. Ниже будет показано, как исправить ситуацию и улучшить реализацию защиты. Но сперва попробуем вскрыть этот вариант, чтобы действительно понять всю глубину ошибки разработчика защиты.
Парадоксально, но факт: многие приложения "защищены" именно таким наивным способом. Для меня остается загадочным упорное нежелание программистов внять советам хакеров и исправить допущенные ошибки. Особенно это характерно для прикладных программистов, все попытки которых затруднить взлом обычно сводятся к запутыванию алгоритма, но никак к использованию хорошо спроектированных и продуманных защит. Что естественно - качественная защита требует значительного времени на реализацию, а это в конечном счете сказывается на стоимости проекта.
Все еще открытым остается вопрос о необходимости защит. Как бы ни изощрялся автор и какие бы хитрые алгоритмы ни применял, все равно это взломают. И чем популярнее программа, тем быстрее. Усилия, затраченные на разработку защиты, окажутся выкинутыми не ветер.
Убедимся в этом очередной раз, удалив типовую защиту из предлагаемого примера. Попробуем для начала найти с помощью filter.com все текстовые строки, входящие в исполняемый файл. С удивлением рассмотрев протокол, мы не обнаружим там ничего похожего на то, что программа выводит на экран. Неужели файл зашифрован? Не будем спешить с выводами. Используем hiew для более подробного изучения файла. Отсутствие строк в сегменте данных наводит нас на мысль, что они могут находиться в ресурсах. Для подтверждения этой гипотезы просмотрим содержимое ресурсов файла.
**************** рисунок p6 **************
Как в этом случае найти код, выводящий эти строки? Заглянув в SDK, мы узнаем, что загрузить строку из ресурса можно функцией LoadStringA (и LoadStringW для уникода). Чтобы понять, какая же из двух используется в нашей программе, изучим таблицу импорта функций исполняемого файла. Для этой цели подойдет утилита dumpbin или любая другая.
Странно, но приложение не импортирует функции LoadString, более того, не импортирует ни одной функции из USER32! Как же оно при этом может работать? Рассмотрим иерахрию импорта в 'Dependency Walker'.
**************** рисунок p7 **************
На самом деле вызывается функция LoadString - метод класса CString, которая уже в свою очередь вызывает LoadStringA Win32 API. Посмотрим, какие функции MFC42.DLL импортирует наша "подопытная" программа.
MFC42.DLL 40200C Import Address Table 402140 Import Name Table 0 time date stamp 0 Index of first forwarder reference Ordinal 815 Ordinal 561 Ordinal 800 Ordinal 4160 Ordinal 540 Ordinal 1575
Какая жалость! Символьная информация недоступна и известен только ординал. Можно ли как-то определить, какая из этих функций - LoadString? Возможно, нам будет доступна символьная информация непосредственно в MFC42.DLL? Увы, там тоже не содержится ничего интересного кроме ординалов. Но как-то линкер справляется с этой ситуацией? Заглянем в MFC42.map и попробуем найти строку по контексту 'LoadString'
0001:00003042 ?LoadStringA!!AMPER!!CString!!AMPER!!!!AMPER! !QAEHI!!AMPER!!Z 5f404042 f
Замечательно! Но как же нам теперь получить ее ординал? Для этого уточним, что означает '0001:00003042'.
Preferred load address is 5f400000 Start Length Name Class 0001:00000000 00099250H .text CODE
Следовательно, 0x3042 - это смещение относительно секции .text. Воспользуемся утилитой hiew и посмотрим, что там находится. Для этого сначала вызовем таблицу объектов, выберем '.text' и к полученному смещению прибавим 0x3042. Переведем hiew в режим дизассемблера.
5F404042: 55 push ebp 5F404043: 8BEC mov ebp,esp 5F404045: 81EC04010000 sub esp,000000104 5F40404B: 56 push esi
То, что мы сейчас видим до боли напоминает пролог функции. Но почему, собственно, напоминает? Это и есть точка входа в функцию LoadStringA!!AMPER!!CString!!AMPER!!!!AMPER!!QAEHI!!AMPER!!! Разумеется, теперь нетрудно в списке ординалов найти тот, чей адрес совпадает с полученным. Однако прежде чем приступить к поиску, уточним, что мы, собственно, намереваемся искать. Вычтем из полученного адреса рекомендуемый (0x5F404042 - 0x5f400000 = 0x4042): именно это значение будет присутствовать в таблице экспорта, а не 0x5F404042, как можно было подумать с первого взгляда.
Используем ранее полученный список экспорта функций MFC42.dll Просматривать вручную двух-мегабайтовый файл рапорта было бы чрезычайно утомительно, поэтому воспользуемся контекстным поиском.
4160 00004042 [NONAME]
Оказывается, ординал этой функции 4160 (0x1040). Не кажется ли вам, что это число мы уже где-то видели? Вспомним таблицу импорта crack02.exe
MFC42.DLL 40200C Import Address Table 402140 Import Name Table 0 time date stamp 0 Index of first forwarder reference Ordinal 815 Ordinal 561 Ordinal 800 Ordinal 4160 ^^^^^^^^^^^^^ Ordinal 540 Ordinal 1575
Круг замкнулся. Мы выполнили поставленную задачу. Но можно ли было поступить как-то иначе? Неужели это никто не догадался автоматизировать?
Разумеется: с этим справляется любой хороший дизассемблер. Например, IDA. Но последняя надежно скрывает все эти операции под "капотом". Используя инструментарий подобного класса, можно даже не задумываться как это все происходит.
Но даже возможности IDA в той или иной степени ограничены. Иногда она не может получить символьную информацию (или получает ее неправильно). В этом случае приходится забывать про удобства прогресса и заниматься анализом самостоятельно. Кроме того, такой подход неизбежен, когда под рукой нет IDA, а используемый дизассемблер не поддерживает map файлы (что чаще всего и случается).
К счастью, у нас IDA под рукой, и мы избежим кропотливой и трудоемкой работы. Убедимся, что она правильно извлекла символьную информацию о импортируемых функциях. Для этого перейдем в сегмент "_rdata".
; Imports from MFC42.DLL ??1CWinApp!!AMPER!!!!AMPER!!UAE!!AMPER!!XZ ??0CWinApp!!AMPER!!!!AMPER!!QAE!!AMPER!!PBD!!AMPER!!Z ??1CString!!AMPER!!!!AMPER!!QAE!!AMPER!!XZ ?LoadStringA!!AMPER!!CString!!AMPER!!!!AMPER!!QAEHI!!AMPER!!Z ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ??0CString!!AMPER!!!!AMPER!!QAE!!AMPER!!XZ ?AfxWinInit!!AMPER!!!!AMPER!!YGHPAUHINSTANCE__!!AMPER!!!!AMPER! !0PADH!!AMPER!!Z
Переместим курсор на LoadStringA!!AMPER!!String, чтобы узнать, какой код ее вызывает. После недолгих путешествий мы должны увидеть приблизительно следующее:
0040119F push 1 004011A1 lea ecx, [esp+0Ch] 004011A5 mov [esp+2Ch], edi 004011A9 call j_?LoadStringA!!AMPER!!CString!!AMPER!! !!AMPER!!QAEHI!!AMPER!!Z
Похоже, что функция принимает всего один параметр. Уточним это с помощью MSDN - BOOL LoadString( UINT nID ). Передаваемый параметр представляет собой идентификатор ресурса. Используем любой редактор ресурсов (например, интегрированный в MS VC), чтобы узнать связанную с ним строку. Нетрудно убедиться, что эта та самая строка, которую программа первой выводит на экран. Следовательно, мы находимся в самом начале защитного механизма.
Продолжим наши исследования дальше - до тех пор, пока не обнаружим уже знакомую нам функцию ввода.
004011EA lea edx, [esp+14h] 004011EE push edx 004011EF push eax 004011F0 call ds:??5std!!AMPER!!!!AMPER!!YAAAV? $basic_istream!!AMPER!!DU...
Вычислим новое значение указателя после засылки двух аргументов в стек. Оно будет равно 14h+2*4 = 0x1C. Теперь нам становится понятен смысл следующего фрагмента:
04011F6 lea ecx, [esp+1Ch] ; указатель на введеный пароль 04011FA push ecx 04011FB call sub_4010E0 ; (1) 0401200 add esp, 14h ; 0x1C - 0x14 + 4 = 0xC 0401203 lea edx, [esp+0Ch] ; указатель на введенный пароль 0401207 push eax ; агрумент для функции (3) 0401208 push edx 0401209 call sub_4010E0 ; (2) 040120E add esp, 4 0401211 push eax 0401212 call sub_4010C0 ; (3) 0401217 add esp, 8 040121A cmp ax, 1F8h ; Сравниваем ответ 040121E jz short loc_401224 ; !ВОТ ИСКОМЫЙ ПЕРЕХОД!
Довольно витиеватый алгоритм, задействовавший три процедуры. Но даже не интересуясь, что делает каждая из них, мы с уверенностью можем сказать, что тот условный переход в строку 0x040121E и является "высшей инстанцией", которая выносит окончательный вердикт. Замена его на безусловный приводит к тому, что независимо от результата работы хеш-функции будет получать управление "правая" ветка программы.
Но это же полная катастрофа для разработчика защиты! Несмотря на все его усилия, программа по-прежнему ломается заменой всего одного байта, на поиск которого уходит несколько минут.
Попробуем усилить реализацию. Для этого нам нужно непосредственно использовать введенный пароль в программе. Лучше всего подойдет для этой цели шифровка. Поскольку исполнямый код шифровать средствами языков высокого уровня очень затруднительно, то мы зашифруем данные. Однако сделать это непосредственно не позволяет ни один популярный компилятор! Поэтому всю работу (какой бы она ни показалась трудоемкой, нам предстоить выполнить вручную). Существует по крайней мере два диаметрально противоположных подхода к решению этой задачи. Можно зашифровать данные любой утилитой в исполняемом файле, а в самой программе их расшифровывать. Это может выглядеть, например, так:
while (true) { if (!SecretString[pTxt]) break; if (Password[pPsw]<0x20) pPsw = 0; SecretString[pTxt++] = SecretString[pTxt] ^ Password[pPsw++]; }
При этом SecretString нормально выглядит в исходном тексте и зашифрована только после компиляции непосредственно в исполняемом файле. Однако для этого требуется написание утилиты автоматической зашифровки, что может быть утомительно, а кроме того, требует знания формата исполняемого файла и проведения этой операции после каждой компиляции. Однако можно включать зашифрованные данные прямо в исходный текст программы приблизительно таким образом:
char SecretString[] = "\x1F\x38\x2B\x63\x49\x4E\x38\x24\x6E\x2A\x58\x0B"
Чтобы получить подобную строку, нужно воспользоваться специально написанным для этой цели шифровальщиком. Например, таким: (полный исходный текст file: //CD:/SRC/CRACK03/Crypt.asm).
MOV DI,offset Text-1 Reset: LEA SI,Password Crypt: LODSB OR AL,AL JZ Reset INC DI XOR [DI],AL JMP Crypt
Для лучшего понимания опущена процедура форматного вывода, однако можно воспользоваться любым отладчиком, чтобы получить hex-дамп, который после незначительного редактирования будет легко включить в исходный текст.
Конечно, это утомительная работа, заметно удлиняющая и удорожающая разработку. Оправдает ли этот подход затраченное время и усилия? Попытаемся его вскрыть, чтобы ответить утвердительно: "Да, оправдает". Поскольку crack03 отличается от crack02 только одной новой процедурой, мы можем продолжить с того самого места, на котором закончили изучение последней. Остальной код полностью идентичен и мы быстро найдем следующий ключевой фрагмент:
401276 call sub_401120 40127B add esp, 8 40127E cmp ax, 1F8h 401282 jz short loc_401291
Попробуем заменить условный переход на jmp short loc_401291, предполагая, что независимо от введенного пароля программа будет корректно работать. Посмотрим, что из этого получилось:
Crack Me 03 Enter password : crack |JJ
Какое разочарование... Программа действительно воспринимает любой пароль, но работает некорректно. Мы лишь нейтрализовали механизм проверки, но отнюдь не сняли защиту. Основная (и самая тяжелая работа) еще впереди!
Теперь заменой одного-двух байт программу никак не взломаешь. Нужно разобраться, как она манипулирует паролем и как его можно найти. На эти вопросы невозможно ответить без глубокого и вдумчивого анализа механизма защиты и процедуры расшифровки. Цель хакера - сократить эти усилия до минимума. Подумаем, с чего начать изучение программы? С механизма проверки пароля? Нет конечно. Точный механизм проверки нас пока не интересует. А вот как используется пароль - это уже интересно. Чтобы это выяснить, продолжим анализ со строки 0x0401291, на которую ссылается условный переход.
00401291 loc_401291: 00401291 lea eax, [esp+10h] 00401295 lea ecx, [esp+0Ch] 00401299 push eax 0040129A push ecx 0040129B call sub_4010C0
Мы знаем, что [esp+10h] указывает на начало буфера, содержащего введенный пароль. Но что тогда [esp+0Ch]? Похоже, что это адрес буфера для возвращения результата работы процедуры. Для подтверждения этого предположения заглянем внутрь последней:
004010C0 sub esp, 28h 004010C3 push esi 004010C4 push edi 004010C5 mov ecx, 9 004010CA mov esi, 403020h 004010CF lea edi, [esp+0Ch] 004010D3 mov dword ptr [esp+8], 0 004010DB rep movsd 004010DD mov edi, [esp+38h] 004010E1 xor eax, eax 004010E3 lea esi, [esp+0Ch]
Что такое 0x0403020 - константа или смещение? Посмотрим. esi используется как указатель командой movsd, следовательно, это смещение. Посмотрим, что расположено по этому адресу:
a8Cin8NX?8Cne_7 db '8+cIN8$n*X',0Bh,'?8+cNE.=7cDMk$&&',0Bh,'L$?*c',0
Нечто абсолютно нечитабельное. Однако завершающий нуль наводит на мысль, что это может быть строкой. Команда "mov dword ptr [esp+8], 0" еще больше укрепляет нашу уверенность. Действительно, ноль в конце строки не случайность, а часть структуры. С другой стороны, зная особенности используемого компилятора, нетрудно заметить, что рассматриваемый код декомпилируется в обычную конструкцию char MyString[]="It's my string". Но почему же эта строка так странно выглядит? Быть может, она зашифрована? Эту мысль подтверждает установка регистра edi на начало пароля. Наступает самый волнующий момент - мы переходим к изучению криптоалгоритма. Если он окажется недостаточно стойким, то можно будет подобрать подходящий пароль. Обратим внимание на следующий фрагмент кода:
004010F5 mov dl, [eax+edi] 004010F8 xor dl, cl 004010FA mov [esi], dl 004010FC inc esi 004010FD inc eax 004010FE jmp short loc_4010E7
Попробуем записать его в более удобочитаемом виде, чтобы легче было отождествить алгоритм:
SecretString[pTxt++] = SecretString[pTxt] ^ Password[pPsw++];
Это хорошо известный шифр Вернама. Криптостойкость его уже рассматривалась в главе, посвященной криптографии. (равно как и методы атаки). Однако не зная, что за текст содержался в зашифрованной строке, мы имеем мало шансов быстро подобрать пароль. Быть может, удастся подобрать хеш-сумму или просто перебрать пароль? Последнее, уже внушает некоторую надежду. Если пароль окажется не очень длинным (от шести до восьми символов), то перебор скорее всего завершится гораздо быстрее словарной атаки на шифротекст. Чтобы написать переборщик паролей, необходимо с точностью до реализации знать алгоритм вычисления хеш-суммы. Возвратимся вновь к механизму проверки пароля.
04011F6 lea ecx, [esp+1Ch] ; указатель на введенный пароль 04011FA push ecx 04011FB call sub_4010E0 ; (1) 0401200 add esp, 14h ; 0x1C - 0x14 + 4 = 0xC 0401203 lea edx, [esp+0Ch] ; указатель на введенный пароль 0401207 push eax ; аргумент для функции (3) 0401208 push edx 0401209 call sub_4010E0 ; (2) 040120E add esp, 4 0401211 push eax 0401212 call sub_4010C0 ; (3) 0401217 add esp, 8 040121A cmp ax, 1F8h ; Сравниваем ответ 040121E jz short loc_401224 ; !ВОТ ИСКОМЫЙ ПЕРЕХОД!
Хеш-сумма на самом деле вычисляется дважды (что затрудняет ее реверсирование). Используемый автором алгоритм можно свести к следующему
if (f(f1(&pasw[0]),f1(&pasw[0]))== 0x1F8) ....
Как работает функция f? Изучим следующий фрагмент:
00401120 sub_401120 proc near 00401120 00401120 mov eax, [esp+4] 00401124 mov ecx, [esp+8] 00401128 and eax, 0FFh 0040112D and ecx, 0FFh 00401133 imul eax, ecx 00401136 sar eax, 7 00401139 retn 00401139 sub_401120 endp
Она умножает аргументы друг на друга и берет старшие 9 бит (0x10-0x7). Это хорошая хеш-функция. Для ее обращения потребуется разлагать числа на множители, что нельзя эффективно реализовать. С другой стороны, прямое ее вычисление очень быстрое, что упрощает перебор паролей. Однако обратим внимание на то, что на самом деле ее аргументы РАВНЫ. Таким образом обращение функции сводится к элементарному вычислению квадратного корня. После чего останется перебрать 2^7 = 0x80 (128) вариантов (т.к. эти биты были отброшены хеш-функцией). Это смехотвороное число вселяет в нас уверенность, что и пароль мы сможем найти очень быстро. Но не будем спешить. Необходимо реверсировать еще одну хеш-функцию. Посмотрим, что за сюрприз приготовил нам автор на этот раз:
00401152 loc_401152: 00401152 mov al, [esi] 00401154 cmp al, 20h 00401156 jl short loc_40117F 00401158 mov cl, [esi-1] 0040115B mov [esp+14h], al 0040115F mov edx, [esp+14h] 00401163 mov [esp+0Ch], cl 00401167 mov eax, [esp+0Ch] 0040116B push edx 0040116C push eax 0040116D call sub_401120 00401172 lea ecx, [edi+esi] 00401175 add esp, 8 00401178 shl eax, cl 0040117A or ebx, eax 0040117C inc esi 0040117D jmp short loc_401152
Чтобы разобраться в алгоритме этого непонятного кода, попробуем его построчно перевести на Си-подобный язык.
00401152 while (true) { // Цикл 00401152 char _AL = String[idx+1]; // Берем один символ 00401154 if (_AL < 0x20) break; // Это конец строки? 00401158 char _CL = String[idx]; // Берем другой смвол 0040115B chat _s1 = _AL; // Сохраняем _AL в стеке 0040115F (DWORD) _EDX = _s1; // Расширяем до DWORD 00401163 char _s2 = _CL; // Сохраняем в стеке 00401167 (DWORD) _EAX = _s2; // Расширяем до DWORD 0040116D _EAX=f(_EAX,_EDX); // Уже знакомая нам функция! 00401172 CHAR _CL = idx; // см. объяснения ниже 00401178 _EAX = _EAX << _CL; // сдвигаем влево 0040117A DWORD _EBX = _EBX | _EAX; // накапливаем единичные биты 0040117C idx++ 0040117D }
Весь код понятен, кроме странной и совершенно непонятной на первый взгяд операции "00401172 lea ecx, [edi+esi]". Поскольку ESI - это однозначно указатель на текущий символ, то что же тогда edi?
00401141 mov eax, [esp+8] 00401148 or edi, 0FFFFFFFFh 0040114B xor ebx, ebx 0040114D lea esi, [eax+1] 00401150 sub edi, eax
Этот код наглядно показывает, какие перлы иногда может выдавать оптимизатор. Попробуем разобраться что же было на этом месте в исходном коде. edi = (or edi, 0FFFFFFFFh) = -1; тогда esi = (lea esi, [eax+1]) == &String+1; и edi = (sub edi, eax) == -1-(&String) = -1 - &String. Поэтому ecx = (lea ecx, [edi+esi]) == - 1 - &String + &String+idx + 1 == idx! Это хороший пример "магического кода". Т.е. такого, который работает, но с первого взгляда абсолютно нельзя понять, как и почему.
Многие хакеры любят писать программу в похожем стиле, и для ее анализа приходится с боем продираться через каждую строку. До недавнего времени это считалось своего рода искусством. Сегодня существующие компиляторы по "дикости" кода с легкостью обгоняют человека. Поэтому все чаще возникают сомнения: что это за искусство такое, в котором машина превосходит человека?
Как бы то ни было, приблизительный исходный текст программы восстановить не составляет труда. Вероятно, в оригинале он выглядел так:
while (true) { if (String[idx+1]<0x20) break; x1 = x1 | (f (String[idx],String[idx+1]) << idx++); }
Используя полученные исходные тексты, можно написать программу, которая для всех возможных паролей вычислит хеш-сумму и распечатает только те, у которых она будет равна 1F8h, т.е. искомой.
Я настоятельно рекомендую попытаться решить эту задачу самостоятельно и только в крайнем случае прибегать к готовому решению (folder://CD/SRC/CRACK_3).
Собственно, алгоритм перебора паролей достаточно несложен и в не самой лучшей реализации может выглядеть так:
while (1) { int idx=0; while ((Password[idx++]++)>'z') Password[idx-1]='!'; if (mult(hashe(&Password[0]),hashe(&Password[0]))==0х1F8) printf ("Password - %s \n",Password); }
На самом деле для написания переборщиков только самые ленивые используют Си. Лишь тщательно продуманный и хорошо оптимизированный код может обеспечить приемлемую скорость. Языки высокого уровня, увы, ее обеспечить пока не в состоянии.
Однако прежде чем садиться за ассемблер, для начала не мешает написать простую тестовую программку на Си для выяснения всех нюансов и отладки алгоритма. Только после этого выверенный и вычищенный код можно переводить на ассемблер. В противном случае мы рискуем потратить огромное количество времени и усилий и получить неработоспособный код, который исправить уже не удастся.
Не будет исключением и этот случай. Наш простой перебощик начнет "плеваться" паролями, чья хеш сумма в точности равна 0x1F8, но настоящими паролями все они, разумеется, не являются. Их много, очень много, очень-очень много... Похоже, дальнейший перебор не имеет никакого смысла и его придется прекратить. Почему? Рассмотрим фрагмент протокола:
Password - yuO Password - xvO Password - uwO Password - wwO Password - rxO Password - vxO Password - qyO Password - uyO Password - nzO Password - pzO
Пароли ложатся настолько тесно друг к другу, что нет никакой возможности найти среди них настоящий. Кроме бессмысленных комбинаций, не раз попадаются и словарные слова. Даже если предположить, что в качестве пароля использовалось (возможно видоизмененное) осмысленное слово, то и это нам ничего не даст, т.к. подходящих вариантов по-прежнему будет очень много.
Это пик торжества разработчика защиты. Использовав ПЛОХУЮ хеш-функцию со слабым рассеянием, он обрубил нам все пути ко взлому своего приложения. Бесполезно даже обращение хеш-функции, ибо оно ничего не даст. Мы получим те же "левые" пароли - может быть, чуть быстрее.
Конечно, существует вероятность, что пользователь введет неправильный пароль, который система воспримет как достоверный, но скажет "мяу" и откажет в работе. Введем, наугад (или на вкус) любой из вероятных паролей, например 'yuO'. Вместо сообщения об ошибке на экране появится мусор некорректно работающей программы. Однако мы этого и ждали. А какова вероятность, что такое произойдет от неумышленной ошибки пользователя? Расчеты показывают, что она невелика. Практика действительно подтвержает низкую вероятность этого события.
Это одна из самых лучших стратегий защиты. Не давать взломщику возможности перебора пароля (за счет большого числа вариантов) и затруднить реверсирование хеш-функции. Как мы смогли убедиться на этом примере, злоумышленнику ничего не остается делать... Постойте, неужели в самом деле ничего? А как же атака на шифротекст?
Какой непредсказуемый поворот событий! Очередная незаметная лазейка разрушает всю воздвигнутую систему защиты. Строго говоря, даже не требуется атаки на шифротекст. Необходим лишь метод автоматического контроля, позволяющий отсеять максимально возможное число "ложных" паролей. Для этого используем тот факт, что оригинальный текст (а в нашем примере это должен быть какой-то лозунг или поздравление) с большой вероятностью содержит '_a_','_of_','_is_' и т.д. Если в расшифрованном тексте хоть одно из этих слов присутствует и нет ни одного символа, выходящего за интервал '!' - 'z', то это неплохой кандидат. Предложенный метод хотя и является крайне медленным, но, похоже единственно возможным. Добавим в существующий переборщик еще пару строк:
if (mult(hashe(&Password[0]),hashe(&Password[0]))==0х1F8) { s0=Protect(&Password[0]); if (s0.Find(" is ")!=-1) printf ("Password - %s - %s\n",Password,s0); }
Приведенная реализация не является образцом для подражания, написана исключительно с целью лучшей демонстрации материала и никак не предназначена для конечного использования.
Части: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15