Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Почтовая рассылка

Подписчиков: -1
Последний выпуск: 19.06.2015

Техника и философия хакерских атак

Крис Касперски

Продолжение...

Первый шаг. От 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

Оставить комментарий

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог