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

Ваш аккаунт

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

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

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

Техника и философия хакерских атак II (фрагмент [2/3]) часть 2

Защита взлома? Взломана! Но вот понята ли? Ведь мы та и не узнали принцип ее работы. А вдруг в защитном механизме присутствует дополнительная проверка, которая в случае неверно введенного пароля переводит программу в демонстрационный режим и по истечении стольких-то дней просто прекращает работу и хорошо, если еще не осуществляет форматирование винчестера! Так давайте проанализируем весь защитный механизм целиком, начиная с первой строки функции sub_401000 и заканчивая командой возврата (если вы новичок в дизассемблировании, то настоятельно рекомендую прочитать "Фундаментальные основы хакерства" и "Образ мышления - дизассемблер IDA", - там все эти вопросы подробно описаны):

.text:00401000 sub_401000      proc near	       ; CODE XREF: start+AF p
.text:00401000 
.text:00401000 var_29C	       = byte ptr -29Ch
.text:00401000 
.text:00401000    sub  esp, 29Ch
.text:00401000 ;  выделяем память для локальных переменных
.text:00401000 ;
.text:00401006    mov  ecx, offset dword_408A50
.text:0040100B    push ebx
.text:0040100C    push esi
.text:0040100D    push offset aCrackme00hEnte      ;"crackme 00h\nenter passwd:"
.text:00401012    call ??6ostream@@QAEAAV0@PBD@Z   ; ostream::operator<<(char const *)
.text:00401012 ;  руководствуясь прототипом функции ostream::operator<<(char const *),
.text:00401012 ;  распознанным автоматическим анализатором IDA, определяем назначение
.text:00401012 ;  ее аргументов, заносимых (как известно) в стек справа на лево.
.text:00401012 ;  offset aCrackme00hEnte - указатель на выводимую строку, а push edx
.text:00401012 ;  и push esi - вовсе не аргументы функции, как это кажется на
.text:00401012 ;  первый взгляд, а не имеющие к ней никакого отношения, временно
.text:00401012 ;  сохраняемые в стеке. Смещение же, загружаемые в регистр ECX
.text:00401012 ;  есть ни что иное, как указатель на экземпляр объекта basic_ostream
.text:00401012 ;  расположенный в памяти по адресу 408A50h.
.text:00401012 ;  
.text:00401017    lea     eax, [esp+2A4h+var_29C]
.text:0040101B    mov     ecx, offset dword_408A00
.text:00401020    push    eax
.text:00401021    call    ??5istream@@QAEAAV0@PAD@Z ; istream::operator>>(char *)
.text:00401021 ;  теперь вызывается функция istream::operator>>(char *),
.text:00401021 ;  считывающая пароль со стандартного устройства ввода (клавиатуры)
.text:00401021;   прототип ее аналогичен, за исключением того что вместо 
.text:00401021 ;  адреса выводимой строки ей передается указатель на приемный буфер,
.text:00401021 ;  дислоцирующийся в данном случае в переменной var_29C
.text:00401021
.text:00401026    lea     esi, [esp+2A4h+var_29C]
.text:00401026 ;  загружаем в ESI указатель на буфер, содеращий введений пароль
.text:00401026
.text:0040102A    mov     eax, offset aMy_good_passwo ; "my.good.password"
.text:0040102A ;  загружаем в EAX указатель на: строку, похожую на эталонный пароль
.text:0040102A ;
.text:0040102F loc_40102F:			       ; CODE XREF: sub_401000+51 j
.text:0040102F    mov     dl, [eax]
.text:00401031    mov     bl, [esi]
.text:00401033    mov     cl, dl
.text:00401035    cmp     dl, bl
.text:00401035 ; проверка очередных символов введенного и эталонного пароля на
.text:00401035 ; идентичность друг другу
.text:00401035
.text:00401037    jnz     short loc_401057
.text:00401037 ;  если символы не идентичны, то прыгаем на loc_401057
.text:00401037 ;
.text:00401039    test    cl, cl
.text:0040103B    jz      short loc_401053
.text:0040103B ;  если достигнут конец эталонного пароля и при этом не было
.text:0040103B ;  обнаружено ни одного расхождения, прыгаем на loc_401053
.text:0040103B ; 
.text:0040103D    mov     dl, [eax+1]
.text:00401040    mov     bl, [esi+1]
.text:00401043    mov     cl, dl
.text:00401045    cmp     dl, bl
.text:00401047    jnz     short loc_401057
.text:00401047 ;  проверка очередных символов введенного и эталонного пароля на
.text:00401047 ;  идентичность друг другу и, если символы не идентичны,
.text:00401047 ;  прыгаем на loc_401057
.text:00401047 ;
.text:00401049    add     eax, 2
.text:0040104C    add     esi, 2
.text:0040104C ;  перемещаемся на два символа вперед в каждой из строк
.text:0040104C ; 
.text:0040104F    test    cl, cl
.text:00401051    jnz     short loc_40102F
.text:00401051 ;  продолжать цикл до тех пор, пока не будет достигнут конец
.text:00401051 ;  эталонного пароля или не встретится хотя бы одно расхождение
.text:00401053 
.text:00401053 loc_401053:			       ; CODE XREF: sub_401000+3B j
.text:00401053 ;  (сюда мы попадаем при идентичности обоих паролей)
.text:00401053    xor     eax, eax
.text:00401053 ;  обнуляем EAX, EAX и:
.text:00401053 ;
.text:00401055    jmp     short loc_40105C
.text:00401055 ;  :и прыгаем на loc_40105C
.text:00401055 ;  
.text:00401057 ; ------------------------------------------------------------------
.text:00401057 
.text:00401057 loc_401057:			       ; CODE XREF: sub_401000+37 j
.text:00401057 ; (сюда мы попадаем при обнаружении различий в паролях)
.text:00401057    sbb     eax, eax
.text:00401059    sbb     eax, 0FFFFFFFFh
.text:00401059 ;  записываем в EAX значение 1
.text:0040105C 
.text:0040105C loc_40105C:			       ; CODE XREF: sub_401000+55 j
.text:0040105C ; (эта ветка получает управление в обоих случаях)
.text:0040105C    pop     esi
.text:0040105D    pop     ebx
.text:0040105D ;  восстанавливаем ранее сохраненные регистры
.text:0040105D ;
.text:0040105E    test    eax, eax
.text:00401060    jz      short loc_40107A
.text:00401060 ;  и вот он - анализ результата сравнения паролей!
.text:00401060 ;  как мы помним, если результат ноль - пароли совпадают и,
.text:00401060 ;  соотвесвтенно, наоборот.
.text:00401060
.text:00401062    push    offset aWrongPassword ; "wrong password\n"
.text:00401062 ;  (ветка "неправильный пароль" получает управление при ненулевом
.text:00401062 ;   значение регистра EAX)
.text:00401062
.text:00401067    mov     ecx, offset dword_408A50
.text:0040106C    call  ??6ostream@@QAEAAV0@PBD@Z ; ostream::operator<<(char const *)
.text:00401071    xor     eax, eax
.text:00401073    add     esp, 29Ch
.text:00401079    retn
.text:0040107A ; ---------------------------------------------------------------------
.text:0040107A 
.text:0040107A loc_40107A:			       ; CODE XREF: sub_401000+60 j
.text:0040107A    push   offset aPasswordOkHell ;"password ok\nhello, legal user!\n"
.text:0040107A ;  (ветка "правильный пароль" получает управление при нулевом значении
.text:0040107A ;   регистра EAX)
.text:0040107A
.text:0040107F   mov     ecx, offset dword_408A50
.text:00401084   call   ??6ostream@@QAEAAV0@PBD@Z ; ostream::operator<<(char const *)
.text:00401089   xor     eax, eax
.text:0040108B   add     esp, 29Ch
.text:00401091   retn
.text:00401091 ; вот мы и достигли конца защиты. Ну что мы теперь можем сказать?
.text:00401091 ; во-первых, защитный механизм несмотря на свою простоту содержит
.text:00401091 ; огромное количество условных переходов, можно сказать - ими кишит
.text:00401091 ; но только один из них отвечает за анализ результата проверки
.text:00401091 ; идентичности паролей, а другие - осуществляют саму эту проверку
.text:00401091 ; поэтому, никогда не стоит пытаться угадать "нужный" нам условный
.text:00401091 ; переход "на глаза". в частности, инверсия переходов, контролирующих
.text:00401091 ; выход за пределы сравниваемой строки, привела бы к зависанию
.text:00401091 ; программы!
.text:00401091 ; во-вторых, проанализировав защиту, мы не только убедились в том,
.text:00401091 ; никаких дополнительных проверок истинности введенного пароля в ней
.text:00401091 ; нет, но и открыли для себя массу способов ее взлома. ниже будет
.text:00401091 ; перечислена лишь часть из них:
.text:00401091 ; 1) можно просто "подсмотреть" эталонный пароль, зная его адрес:
.text:00401091 ;    (для этого достаточно перейти по ссылке в строке 40102A)
.text:00401091 ;
.text:00401091 ; 2) можно сравнивать введенный пароль не с эталонным паролем,
.text:00401091 ; а: с самим собой, всего лишь заменив mov eax, offset aMy_good_passwo 
.text:00401091 ; на lea     esi, [esp+2A4h+var_29C] в строке 40102A и добавив один
.text:00401091 ; NOP для сохранения прежней длины машинных команд.
.text:00401091 ;
.text:00401091 ; 3) можно забить двумя NOP'ами условный переход в строке 00401037
.text:00401091 ; тем самым навсегда отучив защиту находить различия в паролях
.text:00401091 ; а если изменить условный переход на противоположный? 
.text:00401091 ; т.е. инвертировать его? а вы попробуйте!!!
.text:00401091 ; 
.text:00401091 sub_401000      endp

Листинг 9 дизассемблерный листинг защитной процедуры с подробными комментариями

Многие хакеры любят оставлять во взломанной программе свои лозунги или с позволения сказать "копирайты". Модификация исполняемых файлах довольно трудна и требует определенных навыков, отсутствующих у основной массы начинающих.

Но ведь оставить свою подпись так хочется! Что ж, для подобной операции можно использовать фрагмент, выводящий сообщение о неверно набранном пароле, ставший ненужным после взлома программы. Вспомним, как были расположены различные ветки программы в только что исследованном нами файле:


Рисунок 1 - 0х06 блок-схема защитной процедуры

Что будет, если мы удалим команду возврата из процедуры, расположенную по адресу 0401079h? Тогда, при вводе неверного пароля, защита хотя и обложит нас матом (в смысле скажет "вронг пысворд"), но не сможет завершить свою работу и продолжит свое выполнение с радостным воплем "password ok". Заменив "wrong password" на нечто вроде "hacked by мной любимым", мы открыто заявим миру о себе, причем эта надпись будет выдаваться только у нелегальных пользователей, т. е. тех, кто не знает пароль и стало быть, вам - хакеру - теперь сильно обязан. (Должны же пользователи знать, какого доброхота им следует благодарить!). Сказано - сделано!

Загружаем программу в .HIEW, переходим по адресу 401079h (для этого вы должны выполнить следующую последовательность операций: <ENTER> для перехода в HEX-режим, если только он у вас не установлен режимом по умолчанию, <F5> для ввода адреса перехода, затем собственно сам адрес, предваренный точкой, что указывает HIEW'у, что это именно адрес, а не смещение в файле) и, нажав <F3> для активация режима редактирования, заменяем байт RETN (код C3h) на код команды NOP - 90h, а вовсе не 00h, как почему-то думают многие начинающие кодокопатели).

Кажется, мы все сделали правильно, однако: "программа выполнила недопустимую операцию и будет закрыта". Ах, да! Мы совсем забыли об оптимизирующем компиляторе. Это затрудняет модификацию программы. Но ни в коем случае не делает ее невозможной. Давайте заглянем "под капот" могучей системы Windows и посмотрим, что там творится. Запустим программу еще раз и вместо аварийного закрытия нажмем кнопку "сведения", в результате чего нам сообщат, что: "Программа crackme.C5F11EA6h.exe вызвала сбой при обращении к странице памяти в модуле MSVCP60.DLL по адресу 015F:780C278D". Разочаровывающее малоинформативные сведения! Разумеется, ошибка никак не связана с MSVCP60.DLL, и указанный адрес, лежащий глубоко в недрах последней, нам совершенно ни о чем не говорит. Даже если мы рискнем туда отправиться с отладчиком, то причину сбоя все равно не найдем: этой функции передали неверные параметры, которые и привели к исключительной ситуации. Конечно, это говорит не в пользу фирмы Microsoft: что же это за функция такая, если она не проверяет корректные ли ей аргументы передали! С другой стороны, излишние проверки не самым лучшим образом сказываются на быстродействии и компактности кода. Но нужна ли нам такая оптимизация? Я бы твердо ответил "НЕТ". Жаль только, что команда разработчиков Windows меня не услышит.

Однако мы отвлеклись. Проникнуть внутрь Windows и выяснить, что именно у нее не в порядке нам поможет другой продукт фирмы Microsoft - MS Visual Studio Debugger. Будучи установленным в системе, он добавляет кнопку "отладка" к окну аварийного завершения. С ее помощью мы можем не только закрыть некорректно работающее приложение, но и разобраться, в чем причина ошибки.

Дождемся появления этого окошка еще раз и вызовем интегрированный в 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> > & {...}) 
crackme.C5F11EA6h! 00401091()
CThreadSlotData::SetValue(CThreadSlotData * const 0x00000000, int 4,.... 

Листинг 10 просмотр содержимого стека вызов функций в отладчике

Поскольку стек растет кверху, мы, соответственно, должны спускаться книзу. Первые три вызова можно смело пропустить (это библиотечные функции, не содержащие в себе ничего интересного), а четвертый - crackme.C5F11EA6h принадлежит нашему приложению. Вот это и есть непосредственный источник ошибки. Кликнем по нему мышкой и перейдем непосредственно в окно дизассемблера.

0040105E   test        eax,eax
00401060   je          0040107A
00401062   push        408080h
00401067   mov         ecx,408A50h
0040106C   call        004014F5
00401071   xor         eax,eax
00401073   add         esp,29Ch
00401079   nop
0040107A   push        408090h
0040107F   mov         ecx,408A50h
00401084   call        004014F5
00401089   xor         eax,eax
0040108B   add         esp,29Ch
00401091   ret

Листинг 11 прибытие на место происшествия

Узнаете окружающий код? Да-да! Это то самое место, где мы слегка его изменяли. Но в чем причина ошибки?! Обратим внимание, что удаленному нами RET'у предшествует команда очистки стека от локальных переменных: ADD ESP, 29CH. И эта же самая команда повторяется перед "настоящим" завершением функции в строке 40108Bh. Но ведь при повторной очистке стека, его балансировка нарушается и вместо адреса возврата из функции на вершину стека попадает всякая ерунда, приводящая к непредсказуемому поведению взломанного нами приложения. Как это избежать? Да очень просто - достаточно всего лишь удалить одну из команд "ADD ESP, 29Ch" забив его NOP'ами или же заменить 29Ch на нуль (при добавлении к чему бы то ни было нуля, его значение не изменяется).

После этого, взломанная программа перестает капризничать и начинает нормально работать, что следующий листинг и подтверждает:

> crackme. C5F11EA6h.exe 
enter passwd:xxxx
hacked by KPNC
password ok
hello, legal user!

Листинг 12 теперь любой введенный пароль защита воспринимает как правильный

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

Одним из решений будет удаление процедуры ввода пароля. Обращу внимание на важный момент: вместе с процедурой необходимо удалить и заносимые в стек параметры, иначе он окажется несбалансированным и последствия, скорее всего, не заставят себя ждать. Возвращаясь к дизассемблерному листингу ломаемой программы, мы видим, что функция ввода пароля расположена по адресу 401021h, а команда передачи аргумента (у данной функции он всего один) по адресу -401020h. Для полного отключения защиты оба вызова должны быть затерты NOP'ами. И тогда код программы будет выглядеть так:

.00401000: 81EC9C020000                 sub       esp,00000029C ;"  O?"
.00401006: B9508A4000                   mov       ecx,000408A50 ;" @SP"
.0040100B: 53                           push      ebx
.0040100C: 56                           push      esi
.0040100D: 6850804000                   push      000408050 ;" @ИP"
.00401012: E8DE040000                   call     .0004014F5   -------- (1)
.00401017: 8D442408                     lea       eax,[esp][00008]
.0040101B: B9008A4000                   mov       ecx,000408A00 ;" @S "
.00401020: 90                           nop
.00401021: 90                           nop
.00401022: 90                           nop
.00401023: 90                           nop
.00401024: 90                           nop
.00401025: 90                           nop
.00401026: 8D742408                     lea       esi,[esp][00008]
.0040102A: B86C804000                   mov       eax,00040806C ;" @Иl"

Листинг 13 вид взломанного кода программы (изменения выделены жирным шрифтом)

Сохраняем изменения в файле, запускаем его и: это работает!!! Несмотря на то, что строка "enter password" все еще видна, сам пароль более не запрашивается, а работа программы - не приостанавливается. Можно ли удалить строку "enter password"? Конечно, почему бы и нет! Причем, совершенно незачем затирать NOP'ами выводящую ее процедуру. Вполне достаточно "всобачить" один-единственный ноль в начале строки, или: использовать эту строку для вывода своего "копирайта". действительно, строка "wrong password" слишком коротка и далеко не всякое имя в ней запишешь. Уж лучше использовать "enter password" под "hacked by",а "wrong password" целиком отдать под запись своего "графити".

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

Для начала нужно установить: какие именно байты взломанного файл были изменены. Для этого нам потребуется оригинальная копия исходного файла и какой-нибудь "сравниватель" файлов. Наиболее популярными на сегодняшний день являются C2U by Professor Nimnul и MakeCrk by Doctor Stein's labs. Первый гораздо предпочтительнее, т. к. во-первых, он лучше "переваривает" не совсем стандартные crk-файлы, а, во-вторых, позволяет генерировать расширенный xck формат.

Для запуска C2U в командной строке следует указать имена двух файлов - оригинала и его "хакнутой" версии. После того, как утилита завершит свою работу все обнаруженные различия будут записаны в crk/xcrk-файл.

Теперь нам потребуется другая утилита, цель которой будет прямо противоположна: используя crk файл, изменить эти самые байты в оригинальной программе. Таких утилит на сегодняшний день очень много. К сожалению, это не лучшим образом сказывается на их совместимости с различными crk форматами. Самые известные из них, скорее всего, cra386 by Professor и pcracker by Doctor Stein's labs. Но поиск подходящей программы, поддерживающий ваш формат crk, является уже заботой пользователя, решившего взломать программу. Попутно отметим, что распространение crk файлов не является нарушением и не карается законом, т. к. такие файлы представляют собой не орудие взлома, а лишь информация о том, как этот самый взлом осуществить. Согласитесь, если мы скажем, что: "выстрел из пистолета в висок приводит к смерти человека", никто из следователей не сможет привлечь нас к ответственности. Аналогично, фраза "а у Сидорова чемоданы с золотом под кроватью лежат" не попадает под статью о соучастии в ограблении, если таковое вдруг произойдет (конечно, при том условии, что грабители не отстегнули вам за наводку часть награбленного). Крак можно легально распространять, тиражировать, продавать. А вот у пользователя, решившего ваш крак использовать, проблемы с законом возникнуть вполне могут, т. к. этим он ущемляет авторские права разработчиков программы. Парадоксальный, однако, у нас мир!

Для избежания проблем с совместимостью иногда используют исполняемые файлы (C2U способен генерировать и такие), которые выполняют модификацию программы автоматически (и зачастую занимают меньше места!). Но главный недостаток их в том, что исполняемый файл по нашим законам уже является не информацией, а орудием преступления, и следовательно, легально распространяться не может.

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

шаг третий - дао регистрационных защит

...идем мы [Andrew Dolgov] с Сергеем Кожиным (кто не в курсе - это автоp parmatosser'a) на пойнтовкy к немy. Такой диалог:

Я: Ты бы дал мне нормальный ключ, а то этот пиратский генератор как-то не катит.

Он: Hафиг? Я сам им пользуюсь, он меньше и pработает быстрее.

фидошное

Мир давно привык к тому, что популярные технологии далеко не всегда оказываются хорошими. Вот и в сфере условно-бесплатного программного обеспечения наибольшее распространение получили защиты, генерирующие регистрационный номер на основе имени пользователя (регистрационные защиты). Суть этого механизма заключается в том, что на основе некоторой функции f(name) разработчик преобразует регистрационное имя клиента в регистрационный номер и за некоторую плату отсылает его клиенту. Защита же в свою очередь проделывает с регистрационным именем ту же самую операцию, а затем сравнивает сгенерированный регистрационный номер с регистрационным номером, введенным пользователем. Если эти номера совпадают, то все ОК и, соответственно, wrong reg num в противном случае (см. рис. 0x00F).

Таким образом, защитный механизм содержит в себе полноценный генератор регистрационного кода и все, что требуется хакеру: найти процедуру генерации и, подсунув ей свое собственное имя, просто подсмотреть возращенный результат! Другая слабая точка: компаратор, т. е. процедура, сравнивающая введенный и эталонный регистрационный номера. Если на оба плеча компаратора подать один и тот же регистрационный номер (не важно введенный пользователем или сгенерированный защитой), он, со всей очевидностью, скажет "ОК" и защита примет любого пользователя как родного. Еще один способ взлома: проанализировав алгоритм генератора отладчиком и/или дизассемблером, хакер сможет создать свой собственный генератор регистрационных номеров.

Все, чем может досадить хакеру автор защиты, - затруднить анализ и реконструкцию алгоритма генерации. Первое осуществляется оригинальными приемами программирования, противостоящих отладке и/или дизассемблированию, а второе - "размазыванием" кода по десяткам процедур, активным использованием глобальных переменных и запутанным взаимодействием различных фрагментами кода.

Стоит ли и говорить, что запутывание алгоритма малоэффективно и отдает "ребячеством", а подавляющее большинство антиотладочных приемов бессильно против современных отладчиков; кроме того, далеко не все антиотладочные приемы удается реализовать на языках высокого уровня. Спускаться же на уровень ассемблера практически никто из разработчиков не хочет.

Причем, если генератор реализован в одной-двух процедурах (а чаще всего генераторы реализуются именно так!), хакеру нет никакой нужды тратить время на его анализ, и можно прибегнуть к тупому "выкусыванию" кода генератора и копированию его тела в свою собственную программу-оболочку, позволяющую передавать генератору произвольные параметры, в роли которых обычно выступают имена пользователя, company name и прочие регистрационные данные. Впрочем, "выдергиванию" кода легко помешать. Действительно, если рассредоточить код генератор по множеству служебных функций со сложным взаимодействием и неочевидным обменом данных, то без кропотливого анализа всей защиты, выделение всех, относящихся к ней компонентов, окажется невозможным! (Кстати, с точки зрения закона создание собственных генераторов намного более предпочтительнее, чем несанкционированное выдирания фрагментов "живого" кода из чужой программы).


Рисунок 2 - 0х00F принцип работы регистрационной защиты

Рассмотрим простую реализацию данного защитного механизма на примере программы crackme.58DD2D69h. До сих пор для изучения защитного кода мы пользовались одним лишь дизассемблером, но это не единственный возможный подход к задаче. Не меньшим успехом у хакеров пользуются и отладчики. Отметим, что отладка - более агрессивный способ исследования: в этом случае взлом программы осуществляется "в живую" и со стороны защиты возможны любые "подлянки". Антиотладочный код может запросто "завесить" вашу систему и вообще, выкинуть то, чего вы от него никак не ожидаете. С другой стороны, отладчик обладает многими замечательными (в плане взлома) возможностями, о реализации которых в дизассемблерах пока приходится только мечтать. В первую очередь это относится к точкам останова (по-английски break point), с которыми мы чуть позже с успехом и воспользуемся.

Самым популярным среди хакеров отладчиком был, есть и остается отладчик Soft-Ice от компании NuMega, представляющий собой профессионально-ориентированный инструмент, и потому вызывающий большие трудности у новичков в его освоении. Однако потраченные усилия стоят того! Разумеется, никто не ограничивает свободу читателя в выборе инструментария, - вы можете использовать Microsoft Windows Debugger, Borland Turbo Debugger, Intel Enhanced Debugger, DeGlucker или любой другой отладчик по своему вкусу . Рядовые задачи они решают не хуже айса, а узкоспециализированные отладчики (такие, например, как CUP и Exe Hack) в своих областях даже обгоняют soft-ice. Но уникальность "Айса" как раз и заключается в том, что он покрывает рекордно широкий круг задач и платформ. Существуют его реализации для MS-DOS (ну вдруг кому ни будь понадобится старушка!), Windows 3.1, Windows 9x и Windows NT. Все эти версии Айса несколько различаются межу собой по набору и синтаксису команд, однако эти отличия не столь принципиальны, чтобы вызывать какие либо проблемы. На всякий случай: здесь описывается soft-ice 2.54 под Windows NT.

Итак, загружаем отладчик (под NT это можно сделать в любое время, а в Windows 9x только на стадии загрузки компьютера) и запускаем ломаемое приложение, которое немедленно запрашивает у нас имя и регистрационный номер. Поскольку, регистрационный номер нам доподлинно не известен, приходится набрать что-нибудь "от балды".


Рисунок - 3 0х00A реакция защиты на неверно введенный регистрационный номер

Защита, обложив нас матом, сообщает, что "regnum" есть "wrong" и никакой регистрации нам не видать! А чего мы ждали?! Угадать регистрационный номер ни с первой, ни со второй, ни даже с тысячной попытки нереально (регистрационные номера по обыкновению до безобразия длинны) и тупым перебором взломать программу нам не удастся. На это, собственно, и рассчитывал автор защиты. Однако у нас есть преимущество: знание ассемблера позволяет нам заглянуть внутрь кода и проанализировать алгоритм генерации регистрационных номеров. То есть, атаковать защиту не в лоб, а, обойдя укрепленные позиции, напасть с тыла.

Сразу же возникает вопрос: как определить местонахождение генератора, не прибегая к полному анализу исследуемой программы? Давайте представим себе, что генератор это взяточник, а мы - ОБХСС. Роль денег будет играть регистрационное имя, вводимое пользователем. Код, позарившийся на взятку, очевидно и будет самим генератором! То есть, в основе взлома по сути своей лежит перехват обращения к исходным регистрационным данным, избежать которого защита в принципе не может (телепатических возможностей существующие процессоры увы лишены).

Для осуществления такого перехвата нам потребуется всего лишь установить на регистрационное имя так называемую точку останова (break point). Процессор на аппаратном уровне будет контролировать этот регион памяти и при первой же попытке обращения к нему, прервет выполнение программы, сообщая отладчику адреса машинной команды, рискнувшей осуществить такой доступ. Естественно, для установки точки останова требуется знать точное расположение искомой строки в памяти. Спрашиваете, как мы его найдем? Начнем с того, что содержимое окна редактирования надо как-то считать. В Windows это осуществляется посылкой окну сообщения WM_GETTEXT с указанием адреса буфера-приемника. Однако, низкоуровневая работа с сообщениями - занятие муторное и непопулярное. Гораздо чаще программисты используют API-функции, предоставляющие приятный и удобный в обращении высокоуровневый интерфейс. В Platform SDK можно найти по крайней мере две таких функции: GetWindowText и GetDlgItemText. Статистика показывает, что первая из них встречается чуть ли не на порядок чаще, что и не удивительно, т. к. она более универсальна чем ее "коллега".

Перехватив вызов функции, читающей содержимого окна, мы сможем подсмотреть значение переданного ей указателя на буфер, в который и будет скопирована наша строка. Очевидно, что это и есть тот самый адрес, на который мы стремимся установить точку останова! Теперь любой код, обращающийся к этой области, вызовет отладочное исключение и "разбудит" отладчик. Благодаря этому мы обнаружим защитный механизм в сколь угодно большой программе так же быстро как и в маленькой.

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

Поскольку исследуемое нами приложения написано на Microsoft Visual C++ с применением библиотеки MFC (что видно по копирайтам, содержащимся в теле файла, и содержимому таблицы импорта), то представляется достаточно маловероятным, чтобы программист, разрабатывающий его, использовал прямые вызовы win32 API. Скорее всего, он, как истинный поклонник объективно ориентированного программирования, сосредоточился исключительно на MFC-функциях, и употребил CWnd::GetWinowText или производные от него методы. К сожалению, неприятной особенностью библиотеки MFC является отсутствие символических имен функций в таблице экспорта и она экспортирует их лишь по порядковому номеру (так же называемому ординалом - от английского ordinal). При наличии сопутствующих библиотек мы без труда определим какому именно ординалу соответствует то или иное имя, однако, вся проблема как раз и заключается в том, что далеко не всегда такие библиотеки у нас есть. Ведь не можем же мы устанавливать на свой компьютер все версии всех компиляторов без разбора?!

Зацепку дает тот факт, что CWnd::GetWindowText по сути своей является сквозным "переходником" от win32 API функции GetWindowTextA. Поскольку все, что нам сейчас требуемся, - это выяснить адрес регистрационной строки, то не все ли равно перехватом какой именно функции это делать? Материнская функция-обертка работает с тем же самым буфером, что и дочь. Это типичное не только для MFC, но и для подавляющего большинства других библиотек. В любом случае на нижнем уровне приложений находятся вызовы win32 API и поэтому нет никакой нужды досконально изучать все существующие библиотеки. Достаточно иметь под рукой SDK! Однако не стоит так же бросаться и в другую крайность, отвергая идею изучения архитектуры высокоуровневых библиотек вообще. Приведенный пример оказался "прозрачен" лишь благодаря тому, что функции GetWindowTextA передается указатель на тот же самый буфер, в котором и возвращалась введенная строка. Но в некоторых случаях функции GetWindowTextA передается указатель на промежуточный буфер, который впоследствии копируется в целевой. Так что ознакомление (хотя бы поверхностное) с архитектурой популярных библиотек очень полезно.

как узнать имя функции по ординалу

Если динамическая библиотека экспортирует свои функции по ориналу и только по ординалу, то непосредственно определить имена функций невозможно, поскольку их там нет. Однако при наличии соответствующей библиотеки (обычно поставляющейся вместе со средой разработки) наша задача значительно упрощается. Ведь как-то же определяют линкеры ординалы функций по их именам! Так почему же нам не проделать обратную операцию? Давайте воспользуемся уже полюбившейся нам утилитой DUMPBIN из комплекта поставки Platform SDK, запустив ее с ключом /HEADERS и, естественно, именем анализируемой библиотеки. В частности, для определения ординала функции CWnd::GetWindowText мы должны найти в каталоге \Microsoft Visual Studio\VC98\MFC\Lib файл MFC42.lib и натравить на него DUMPBIN:

> dumpbin /HEADERS MFC42.lib > MFC42.headers.txt
> type MFC42.headers.txt | MORE
  Version      : 0
  Machine      : 14C (i386)
  TimeDateStamp: 35887C4E Thu Jun 18 06:32:46 1998
  SizeOfData   : 00000033
  DLL name     : MFC42.DLL
  Symbol name  : ?GetWindowTextA@CWnd@@QBEXAAVCString@@@Z
		: (public: void __thiscall
         CWnd::GetWindowTextA(class CString &)const )
  Type         : code
  Name type    : ordinal
  Ordinal      : 3874

...затем в образовавшемся файле находим нужное нам имя и смотрим всю информацию по нему и, среди всего прочего - ординал (в данном случае: 3874h)

Но вернемся к нашим баранам. Нажатием <Ctrl-D> вызываем soft-ice и даем ему команду "bpx GetWindowTextA" Откуда, спрашиваете взялась буква 'A'? Это суффикс, указывающий на ее принадлежность к ANSI-строкам. Функции, обрабатывающие Unicode-строки, имеют префикс 'W' (в Windows 9x они не реализованы и представляют собой лишь "заглушки", а ядро Windows NT, наоборот, работает исключительно с уникодом и уже ANSI - функции представляют собой переходники; более подробно об этом можно прочитать в Platform SDK), выходим из отладчика повторным нажатием <Ctrl-D> или аналогичной по действию командой "x" и вводим в ломаемое приложение свое имя и произвольный регистрационный номер, подтверждая серьезность своих намерений нажатием <Enter>. Если отладчик был правильно настроен, то он тут же "всплывает". В противном случае вам следует внимательно изучить прилагаемое к нему руководство или на худой конец его русский перевод, который без труда можно найти в сети.

В общем, будет считать, что все перипетии борьбы с отладчиком уже позади и сейчас мы находимся в точке входа в функцию GetWindowTextA. Как узнать адрес переданного ей буфера? Разумеется, через стек. Рассмотрим ее прототип, приведенный в SDK:

int GetWindowText( 
    HWND hWnd,        // handle to window or control with text 
    LPTSTR lpString,  // address of buffer for text 
    int nMaxCount     // maximum number of characters to copy 
    ); 

Листинг 14 прототип функции GetWindowText

Поскольку, все win32 API функции придерживаются соглашения stdcall и передают свои аргументы слева направо, то стек, на момент вызова функции, будет выглядеть так:


Рисунок 4 - 0x00B состояние стека на момент вызова функции GetWindowText

часть 1 | часть 2 | часть 3 | часть 4

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

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