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

Ваш аккаунт

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

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

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

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

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

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

NAG SCREEN

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

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

Но кому понравится назойливая реклама или периодически всплывающий экран с просьбой нажать ту или иную клавишу (каждый раз разную)? "Nag" в переводе с английского - "изводить, раздражать, ныть", а также "придирки, постоянное ворчание". Нетрудно представить, как пользователи относятся к подобным защитам. С другой стороны, каждому предоставлена полная свобода выбора - или терпи Nag Screen, или заплати немного денег за регистрацию и вздохни с облегчением.

Однако деньги российский пользователь платить не любит, особенно за рубеж. Не то чтобы все были поголовно нищие (на что обычно и ссылаются), но вот менталитет у нас не тот. Оставим этические проблемы за кадром и зададимся вопросом: насколько же надежны такие защиты?

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

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

Рассмотрим типичный пример защиты file://CD/SRC/CRACK0A/Crack0A.exe. Если запустить этот примитивнейший текстовый редактор, то работе будет мешать периодически появляющееся диалоговое окно, закрыть которое в течение некоторого времени будет невозможно.

******************** Рисунок 0B *******************

Каким образом его можно убрать? Первое, что приходит в голову, - дизассемблировать приложение, найти код, создающий диалог и нейтрализовать его. Мысль, безусловно, правильная, но не дающая ответа на вопрос, как среди километров листинга дизассемблера найти нужный фрагмент. Установить точку останова на API-функцию создания диалога можно, но бесполезно. Код, вызывающий ее, находится где-то глубоко в недрах MFC42.DLL и совершенно неинтересен. На самом деле диалог запускается на выполнение функцией CDialog::DoModal(). Ее ординал равен 0x9D2. Получить его можно описанным выше способом с помощью dumpbin или IDA. Поскольку последнее подробно еще не рассматривалось, сделаем это сейчас. Загрузим файл в дизассемблер и откроем окно имен (ALT-V\N). Найдем в нем 'DoModal'.

??1CDialog!!AMPER!!!!AMPER!!UAE!!AMPER!!XZ               004020A0 
?DoModal!!AMPER!!CDialog!!AMPER!!!!AMPER!!UAEHXZ         004020A4 
                                 ^^^^^^^^ 
?Enable3dControls!!AMPER!!CWinApp!!AMPER!!!!AMPER!!IAEHX 004020A8 

Справа указан адрес ординала в таблице импорта. Было бы логично переключить дизассемблер в hex-режим (благо IDA это позволяет) и узнать, что по этому адресу находится. К сожалению, по непонятным мне причинам она отказываается это выполнить. Если в используемой вами версии этот недостаток не устранен, то на экране появится следующая белиберда:

004020A0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????" 
004020B0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????" 
004020C0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????" 

Если же заглянуть по этому адресу, скажем в hiew-е, то можно увидеть следующее:

.004020A0:  81 02 00 80-D2 09 00 80-3D 0A 00 80-6E 04 00 80 
.004020B0:  91 14 00 80-18 11 00 80-F5 12 00 80-86 13 00 80 
.004020C0:  A4 17 00 80-EF 06 00 80-B2 10 00 80-E7 18 00 80 

По адресу 0x4020A4 находится слово 0x9D2 - это и есть ординал CDialog:: DoModal(). Интересно, что некоторые версии Софт-Айса находят его неправильно! Если, исходя из здравого смысла, установить точку останова на MFC42!ORD_09D2, то... можно потратить уйму времени, но так и не выяснить, почему же отладчик не всплывает, хотя диалог все же создается! Однажды я из-за этой удручающей ошибки НуМеги потратил десяток часов в поисках антиотладочного кода, которого на самом деле (словно пресловутой черной кошки в темной комнате) никогда и не было.

На самом деле необходимо дать команду BPX MFC42!09D1 - по совершенно магической причине это сработает.

Первое всплытие мы пропускаем, поскольку в создании главного диалога нет ничего интересного. Выйдем из отладчика и немного подождем. Вызов nag-screen-а защитой вызовет исключение, и мы окажемся в самом начале процедуры DoModal. Выйдем из нее командой p ret и изучим окружающий код.

015F:0040159C  8BF1                MOV     ESI,ECX 
015F:0040159E  8B4660              MOV     EAX,[ESI+60] 
015F:004015A1  85C0                TEST    EAX,EAX 
015F:004015A3  754F                JNZ     loc_0_4015F4 
015F:004015A5  8D4C2404            LEA     ECX,[ESP+04] 
015F:004015A9  C7466001000000      MOV     DWORD PTR [ESI+60],01 
015F:004015B0  E89BFBFFFF          CALL    sub_0_401150 
015F:004015B5  8D4C2404            LEA     ECX,[ESP+04] 
015F:004015B9  C78424B0000000000000MOV     DWORD PTR [ESP+000000B0],0 
015F:004015C4  E857020000          CALL    CDialog::DoModal(void) 
015F:004015C9  FF4E60              DEC     DWORD PTR [ESI+60] 

Необходимо найти причину появления диалога и обезвредить. С другой стороны можно не разбираться в защитном механизме, а удалить процедуру вызова диалога, заменив 'E8 57 02 00 00' в строке 0х04015C4 на '90 90 90 90', - но это самый варварский способ. Посмотрим лучше чуть выше, на бросающуюся в глаза констукцию:

015F:0040159E  8B4660              MOV     EAX,[ESI+60] 
015F:004015A1  85C0                TEST    EAX,EAX 
015F:004015A3  754F                JNZ     loc_0_4015F4 

Куда ведет ветка loc_0_4015F4? Прокрутим окно немного вниз:

loc_0_4015F4 
    015F:004015F4  8BCE                MOV     ECX,ESI 
    015F:004015F6  E857030000          CALL    CWnd::Default(void) 
    015F:004015FB  8B8C24A8000000      MOV     ECX,[ESP+000000A8] 
    015F:00401602  5E                  POP     ESI 
    015F:00401603  64890D00000000      MOV     FS:[00000000],ECX 
    015F:0040160A  81C4B0000000        ADD     ESP,000000B0 
    015F:00401610  C20400              RET     0004 

Несомненно, этот условный переход вызывает ветку, завершающую процедуру без создания диалогового окна. Если JNZ заменить на безусловный переход, то ветка защиты никогда не получит управления, - следовательно, nag-screen никогда не появится. Работу кракера на этом можно считать завершенной. Программа взломана, клиенты довольны, разве хоть что-то еще осталось? Но хакеры обладают пытливой натурой, поэтому назначение переменной [ESI+60] не может их не заинтересовать. С первого взгдяда все ясно. Это переменная типа BOOL. Если она равна TRUE, то NAG-SCREEN никогда не появится. Можно даже с уверенностью дать ей символьное имя 'Registered'. Однако взглянем на код защиты еще раз:

015F:0040159E  8B4660              MOV     EAX,[ESI+60] 
                                           ^^^^^^^^^^^^ 
015F:004015A1  85C0                TEST    EAX,EAX 
015F:004015A3  754F                JNZ     loc_0_4015F4 
015F:004015A5  8D4C2404            LEA     ECX,[ESP+04] 
015F:004015A9  C7466001000000      MOV     DWORD PTR [ESI+60],00000001 
                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
015F:004015B0  E89BFBFFFF          CALL    sub_0_401150 
015F:004015B5  8D4C2404            LEA     ECX,[ESP+04] 
015F:004015B9  C78424B0000000000000MOV     DWORD PTR [ESP+000000B0],0 
015F:004015C4  E857020000          CALL    CDialog::DoModal(void) 
015F:004015C9  FF4E60              DEC     DWORD PTR [ESI+60] 
                                           ^^^^^^^^^^^^^^^^^^ 
015F:004015CC  8D4C2468            LEA     ECX,[ESP+68] 
015F:004015D0  C78424B0000000010000MOV     DWORD PTR [ESP+000000B0],1 

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

На самом деле эта переменная предотвращает повторное вхождение в одну и ту же процедуру. Иначе говоря, пока пользователь не закроет окно диалога, новое не создается, даже если пришло время.

Конечно, можно остановиться на достигнутом, ведь программа работает и защита никогда не создаcт диалог, думая, что он уже активен. Но хакера не интересует сам факт взлома, ему должно быть интересно докопаться до самой сути, понять, как это работает.

Продолжим трассировку или сразу выйдем из процедуры, командой p ret. Софт Айс покажет, что мы находимся глубоко внутри процедуры MFC42!ORD_142B (OnWndMsg!!AMPER!!CWnd) или, другими словами, в цикле выборки сообщений. Переданное сообщение находится по адресу ss:[ebp+8]. Легко видеть, что в нашем случае это 0x113, которое более известно как WM_TIMER (это можно выяснить командой WMSG 113).

Теперь алгоритм защиты в общих чертах ясен. Приложение устанавливает таймер, который периодически посылает сообщение WM_TIMER, приводящее к появлению диалогового окна с просьбой о регистрации. Очевидно, что, блокировав его создание, мы не полностью нейтрализовали защиту. Таймер продолжает посылать сообщения, что можно проверить с помощью любого шпиона (например spyxx).

Рассмотрим таблицу импорта crack0b.exe:

USER32.dll 
              F0  GetClientRect 
             252  SetTimer 
             ^^^^^^^^^^^^^ 
             18C  IsIconic 
             195  KillTimer 
              B7  EnableWindow 
             146  GetSystemMetrics 
             19E  LoadIconA 

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

015F:004013CD  MOV     EAX,[ESI+20] 
015F:004013D0  PUSH    00 
015F:004013D2  PUSH    00002710 
015F:004013D7  PUSH    01 
015F:004013D9  PUSH    EAX 
015F:004013DA  CALL    [USER32!SetTimer] 
015F:004013E0  MOV     ECX,[ESP+0C] 
015F:004013E4  MOV     DWORD PTR [ESI+60],00000000 

Очевидно, что, удалив CALL [USER32!SetTimer] вместе с заносимыми в стек параметрами, можно полностью парализовать защиту. Ветка вывода диалога попросту никогда не получит сообщения WM_TIMER и, следовательно, управления. Анализ защиты можно считать завершенным. Как видим, существует не один путь ее взлома. И любой способ ничем не хуже другого.

Рассмотрим еще один похожий пример file://CD/SRC/CRACK0C/Crack0C.exe. Первым бросающимся в глаза отличием является NAG-Screen, всплывающий при первом запуске программы. Очевидно, что его появление связано не с таймером, а с процедурой инициализации приложения. Другое (более существенное) отличие можно увидеть, если запустить spyxx или изучить таблицу импорта. Нет сообщения WM_TIMER, и нет в импорте процедуры SetTimer. Очевидно, приложение ухитряется с высокой периодичностью вызывать nag-screen не используя таймера. Самый очевидный способ это сделать - постоянно опрашивать текущее время и через некоторые промежутки передавать управление защите. Разумеется, организовать подобное можно либо непосредственно в цикле выборки сообщений, либо в отдельном потоке. По опыту могу сказать, что разработчики защит чаще всего предпочитают последнее. В чем можно убедиться, запустив 'Process Viewer Application', который входит в поставку MS VC.

************ Рисунок pc ****************

Приложение имеет два потока. Вполне возможно, что один из них целиком принадлежит защите и не делает ничего кроме постоянного опроса времени. Для подтверждения этого нам нужно изучить код потока и проанализировать его. Установим точку останова на CreateThread. Разумеется, приложение не вызывает его непосредственно, а действует через MFC. Но сейчас это для нас не важно. Вспомним прототип функции CreateThread или обратимся к SDK:

HANDLE CreateThread( 
 LPSECURITY_ATTRIBUTES lpThreadAttributes,// pointer to security attributes 
 DWORD dwStackSize,                       // initial thread stack size 
 LPTHREAD_START_ROUTINE lpStartAddress,   // pointer to thread function 
 LPVOID lpParameter,                      // argument for new thread 
 DWORD dwCreationFlags,                   // creation flags 
 LPDWORD lpThreadId                       // pointer to receive thread ID 
 ); 

Таким образом, адрес процедуры потока можно узнать с помощью команды D ss:esp+0C и последующего дизассемблирования. Разумеется, это будет _beginthreadex модуля MSVCRT. Однако, немного потрассировав последнюю, можно добратся и до кода приложения. Здесь не помешает небольшой опыт работы с MFC и ее исследования. MicroSoft предоставляет отладочные версии и даже исходные тексты, поэтому архитектуру MFC изучить нетрудно, но очень и очень полезно. То же самое можно отнести и к другим компиляторам и библиотекам.

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

.text:00401740                 push    ecx 

ECX указывает на экземпляр класса, производного от CWinThread.

.text:00401741                 mov     eax, [ecx+6Ch] 
.text:00401744                 push    esi 
.text:00401745                 test    eax, eax 

Очевидно, что [ecx+6Ch] какой-то флаг. Но какой? На данном этапе это еще не ясно.

.text:00401747                 push    edi 
.text:00401748                 jz      short loc_0_401750 

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

.text:0040174A                 pop     edi 
.text:0040174B                 xor     eax, eax 
.text:0040174D                 pop     esi 
.text:0040174E                 pop     ecx 
.text:0040174F                 retn 

Остальное тело потока здесь не приводится для экономии места. Его содержимое не представляет ничего интересного. Поток - "стрелочник". Ему приказали - он выполнил, т.е. вывел диалог. Да, конечно, создание диалога нетрудно и блокировать, но это не даст ответа на вопрос: кто же за всем этим стоит? Убедимся, что этот поток всецело подчинен защите, т.е. ни для чего другого больше не служит.

Собственно говоря, непосредственно к защитному механизму могла относиться только переменная [ecx+6Ch], весь остальной код потока полностью автономен и ничем не управляется.

Необходимо найти код, манипулирующий этой переменной. Очень вероятно, что он и будет ядром защитного механизма, который мы пытаемся снять. Однако это потребует анализа, возможно, непростого и утомительного. Не лучше ли просто блокировать вызов диалога, невзирая на то что защита по-прежнему работает, - ведь досадить пользователю она уже не может? Чем плох этот метод? Так (или почти так) думает большинство кракеров. Может быть думает все-таки по другому, но ломает именно так. И в этом трудно их винить, т.к. рынок требует самых быстрых и дешевых решений. Красиво это решение или нет, заказчика ни в малейшей степени не интересует. Этим и вызвана деградация технической культуры. В наши дни эти слова уже звучат как метафора. Действительно, есть техника, есть культура, - а что такое техническая культура?

Сегодня программирование уже перестало быть искусством, и только отдельные энтузиасты-одиночки все еще считают его увлекательным хобби, на которое не жалко тратить все свободное время. Если вы принадлежите к их числу, то исследование защиты несомненно стоит продолжать, в противном случае можно удалить вызовы AfxMessageBox, чтобы NAG-SCREEN не появлялся ни при запуске, ни в дальнейшем.

Задумаемся над следующим неочевидным моментом. Если защитный механизм манипулирует каким-то флагом, вполне естественно, что при каком-то условии программа считается зарегистрированной и эта переменная принимает единичное значение, блокирующее все остальные ветки защиты. (А их, забегая вперед, целых три). Но какое же условие может проверять защита? Никакой явной регистрации в программе не предусмотрено. Из источников ввода остаются только командная строка, ключевой файл и реестр. Первое - давно забытый пережиток MS-DOS и в Windows массового применения не нашло. А вот реестр очень даже вероятная кандидатура.

Читатель, вероятно, уже запускает монитор. Посмотрим, что нам даст его применение:

43      Crack0c OpenKey HKCU\SOFTWARE\CRACK0C\RegIt     NOTFOUND 

В самом деле, защита пыталась открыть явно относящийся к регистрации раздел. Попробуем его создать и запустим программу еще раз.

****************** Рисунок pD **************

О чудо! Программа признала нас зарегистрированным пользователем! Исчезли NAG-SCREEN-ы, изменилась строка с "Ждите..." на "больше не появится...". Вот что значит качественный взлом!

Конечно, реальные защиты не только проверяют существование раздела, но и проверяют его значение. Здесь же для упрощения этого не делалось. Работа с реестром уже была рассмотрена выше и никаких трудностей не представляет. Более того, даже облегчает нам распространение "кряков" к программе. Достаточно только экспортировать раздел CRACK0C в файл: запустив его, пользователь импортирует раздел в свой реестр и тем самым регистрирует приложение. Надо ли говорить, что распространение подобного файла никак не противоречит российскому законодательству, а поэтому ограничено быть не может.

Вообще же NAG-SCREEN-ы даже трудно отнести к защитам. Они принципиально не могут быть сложно защищены. Все, что нужно, - это удалить одну или несколько процедур, даже не вникая в их алгоритм. Можно, конечно противодействовать отладке и дизассемблированию, но сегодня совершенство и мощь инструментария хакера позволяют противодействовать любому антиотладочному коду. Интерактивный дизассемблер IDA вообще невозможно обмануть именно в силу его интерактивности, т.к. предполагается его тесное взаимодействие с человеком. Человека же, в отличие от автоматики, одурачить очень трудно.

Ограничение возможностей

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

Часто это оказывается безнадежным, но не всегда. Если нет никаких других путей для получения легальной копии (например, ее автор эмигрировал в Штаты или запросил такие деньги, за которые лучше ее самому написать), то можно решиться на такой отважный шаг, как самостоятельное воссоздание недостающего кода. Это сложный и трудоемкий процесс, требующий тщательного изучения алгоритма взаимодействия остального кода с отсутствующим.

Однако чаще всего код все же физически присутствует, но не получает управления. Например, просто заблокированы некоторые пункты меню, как в file://CD/SRC/CRACK0D/Crack0D.exe . Такое действительно встречается очень часто и легко программируется. Все, что нужно сделать программисту, - это пометить в редакторе ресурсов некоторые элементы управления или меню как 'Disabled'. Но что просто делается, так же просто и ломается. Необходимо воспользоваться любым редактором ресурсорв. Я предпочитаю пользоваться 'Symantex ResourceStudio 1.0', однако пригоден и любой другой. Загрузим в него наш файл. Дальнейшие действия зависят от интерфейса выбранной программы и не должны вызвать затруднений, за исключением тех ситуаций, когда выбранный редактор не поддерживает используемого формата ресурсов или некорректно работает с ними. Например, с помощью Borland Resource WorkShop мне так и не удалось выполнить эту операцию. Он необратимо портил ресурс диалога, хотя с разблокированием меню справился отлично.

Чтобы разблокировать элементы управления или меню, необходимо вызвать свойства объекта и снять пометку 'Disabled' или 'Grayed', после чего сохранить изменения. Запустим программу, чтобы проверить нашу работу. Получилось! Не исправив ни одного байта кода и даже не прибегая к помощи дизассемблера и отладчика, мы осуществили взлом!

Удивительно, что такие защиты встречаются до сих пор, и не так уж редко. Психология разработчиков - воистину великая тайна. Очень трудно понять, на что они расчитывают. Однако некоторые уже, видимо, начинают догадываться, что нет ничего проще и приятнее, чем редактировать ресурсы в исполняемом файле, поэтому прибегают к явным вызовам API типа EnableWindow(false). Т.е. блокируют элементы управления непосредственно во время работы. Разумеется, можно перехватить этот вызов отладчиком и удалить защитный код. Именно так поступит любой хакер и даже кракер. Рядовой же пользователь остановит свой выбор на программе, подобной Customizer, которая позволяет "на лету" менять свойства любого окна, а впоследствии делать это и автоматически.

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

Именно так реализована защита, например, в crack0E. Откроем файл редактором ресурсов и убедимся, что все элементы разблокированы. Выключаются они позже, на стадии инициализации диалога, функциями API. Попробуем разблокировать их инструментом типа customizer-а. С первого взгляда кажется, что все в порядке. Но попробуем нажать кнопку "hello". Защита сообщает о незарегистрированной версии и вновь блокирует кнопку. Для простого пользователя такой барьер можно уже считать непреодолимым. Однако тому, кто знаком с ассемблером и отладчиком, не трудно нейтрализовать подобную защиту.

Обратимся к MSDN и введем в строке поиска "Disable Window". Среди полученных функций будет только одна, непосредственно относящаяся к Win32 API, - EnableWindow. Можно загрузить отладчик и установить на последнюю точку останова или поискать перекрестные ссылки на нее же в дизассемблере. Но этому я, надеюсь, уже научил читателя. Давайте усложним себе задачу и попробуем обойтись без этих чудес прогресса. В конечном счете гораздо интереснее работать головой, чем техникой.

Очевидно, что сообщение "Это незарегистрировнная копия" выдается защитным механизмом. Для этого он должен передать процедуре AfxMessageBox смещение этой строки. Разумеется, речь идет о смещении в памяти, а не в файле. Однако для PE файлов его легко узнать, например, с помощью HIEW. Эта утилита - единственная из всех мне известных шестнадцатиричных редкторов, позволяющая просматривать локальные смещения для PE файлов.

Находим строку "Это незарегестрированная копия", не забыв сменить кодировку, и переключаем Hiew в режим отображения локальных смещений. В нашем случае это будет 0х00403030. Не забывая про обратный порядок байтов в слове, ищем последовательность '30 30 40 00'. Если все сделать правильно, то получим только одно вхождение. Дизассемблируем прямо в hiew-е найденный код:

.00401547: 8B4660                  mov       eax,[esi][00060] 
.0040154A: 85C0                    test      eax,eax 
.0040154C: 7516                    jne      .000401564   -------- (1) 
.0040154E: 6830304000              push      000403030 ;" !!AMPER!!00" 
                                             ^^^^^^^^^ 
.00401553: E8C2020000              call     .00040181A   -------- (2) 
.00401558: 6A00                    push      000 
.0040155A: 8D4E64                  lea       ecx,[esi][00064] 
.0040155D: E8B2020000              call     .000401814   -------- (3) 
.00401562: 5E                      pop       esi 
.00401563: C3                      retn 

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

Найдем переменную, которая управляет выполнением программы. Очевидно, что это [esi+0x060]. Необходимо найти код, который управляет ее значением. Если его изменить на противоположное, то программа автоматически зарегистрируется.

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

На этот раз нам везет, и hiew выдает следующий любопытный фрагмент:

.004013D3: 8B4C240C                     mov       ecx,[esp][0000C] 
.004013D7: C7466000000000               mov       d,[esi][00060],00000 
.004013DE: 5F                           pop       edi 

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

.004013D7: C7466000000000               mov       d,[esi][00060],00001 

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

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

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

return SomeResult*(!FlagReg1 ^ FlagReg2); 

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

К счастью, программисты часто оказываются слишком ленивы, чтобы детально проработать эту архитектуру. И рождают перлы типа Crack0F. Рассмотрим этот защитный механизм. Перед нами две заблокированных кнопки. Очевидно, для локализации защиты нужно найти вызовы EnableWindow.

j_?EnableWindow!!AMPER!!CWnd!!AMPER!!!!AMPER!!QAEHH!!AMPER!!
Z proc near ; CODE XREF: sub_0_401360+D4p 
                                        ; .text:004015CFp 
              jmp     ds:?EnableWindow!!AMPER!!CWnd!!AMPER!!!!AMPER!
!QAEHH!!AMPER!!Z 
j_?EnableWindow!!AMPER!!CWnd!!AMPER!!!!AMPER!!QAEHH!!AMPER!!Z endp 

Их всего два. Как раз по числу элементов управления. Пока защита не предвещает ничего необычного и ее код выглядит вполне типично:

.text:0040142A                 mov     eax, [esi+68h] 
.text:0040142D                 lea     ecx, [esi+0ACh] 
.text:00401433                 push    eax 
.text:00401434                 call    j_?EnableWindow!!AMPER!!CWnd!
                                       !AMPER!!!!AMPER!!QAEHH!!AMPER!!Z ; 

и, аналогично, другой фрагмент:

.text:004015C8                 mov     eax, [esi+60h] 
.text:004015CB                 lea     ecx, [esi+6Ch] 
.text:004015CE                 push    eax 
.text:004015CF                 call    j_?EnableWindow!!AMPER!!CWnd!!AMPER!!
                                       !!AMPER!!QAEHH!!AMPER!!Z ; 

Попробуем найти, как уже было показано выше, '46 60', т.е. [esi+60] и '46 68'- [esi+68]. Полученный результат должен выглядеть следующим образом

.00401385: C7466001000000               mov       d,[esi][00060],000000000 

и

.004012CC: C7466801000000               mov       d,[esi][00068],000000000 

Кажется, что защита использует два независимых флага. С первого взгляда их нетрудно изменить на ненулевое значение. Ожидается, что это заставит защиту работать. Ну что ж, попытаемся это сделать.

Как будтобы все работает, не правда ли? Но попробуем нажать на левую кнопку:

************* рисунок pe ***********

Пустой диалог выглядит странно, не так ли? Похоже, что защита взломана некорректно и приложение работает неверно. И дело не только в том, что сложно найти то место, где код ведет себя неправильно (это по большому счету не так уж и трудно). Главная сложность - убедиться в работоспособности (неработоспособности) программы. В данном примере это тривиальная задача, но она не будет такой в банковских, научных, инженерных приложениях. Если неправильно работает только одна, редко вызываемая ветка, то тестирование поломанного приложения - дело безнадежное.

Однако разработчики защит часто упускают из виду, что компилятор мог расположить все флаги близко от друг друга, значительно облегчая поиск кракеру. В самом деле, в нашем примере фигурируют две переменные типа DWORD - [esi+60] и [esi+68]. Нетрудно заметить, что между ними образовалась "дырка" размером ровно в двойное слово. Может быть, эта переменная - еще один флаг защиты? Попробуем найти '46 64':

.004015B3: C7466400000000               mov       d,[esi][00064],000000000 

Что будет, если ноль заменить на единицу? Попробуем, и... сработало! Ранее пустой диалог теперь приветствует нас "Hello, Sailor!". Защита пала! Очевидно, что разработчик использовал по крайней мере три флага и конструкцию типа:

s0.SetAt(0,s0[0]*(!RegFlag_1 ^ RegFlag_3)); 

Но кто может гарантировать, что нет четвертого или пятого флага? На самом деле число переменных класса ограничено, и не так трудно проанализировать их все. Кроме того, обычно флаги регистрации - это глобальные переменные. Последних же в грамотно спроектированной программе на объективно-ориентированном языке очень и очень немного.

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

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

Варианты блокировки больше рассматриваться не будут, во избежание повторений. Они слишком просты и элементарно ломаются. Гораздо интереснее искать пути выхода из ситуации, когда кода, реализующего данную операцию, попросту нет. Конечно, зачастую легче полностью переписать программу заново, чем разобраться в взаимодействии с недостающим кодом и воссоздать его. Это настолько сложная тема, что не может быть исчерпывающие рассмотрена в рамках данной книги.

Рассмотрим достаточно простой пример подобной защиты: fiel: //CD/SRC/CRACK10/Crack10.exe Это простой текстовой редактор, который при попытке сохранения отредактированного файла выводит диалоговое окно, информирующее об отсутствии такой возможности в демо-версии.

Найдем этот вызов и дизассемблируем его:

.text:00401440 
.text:00401440 NagScreen       proc near ; DATA XREF: .rdata:00403648o 
.text:00401440                 push    0 
.text:00401442                 push    0 
.text:00401444                 push    offset unk_0_404090 
.text:00401449                 call    j_?AfxMessageBox!!AMPER!!!!AMPER!
                                       !YGHPBDII!!AMPER!!Z 
.text:0040144E                 xor     eax, eax 
.text:00401450                 retn    4 
.text:0040144E NagScreen       endp 
.text:0040144E 

Допустим, можно удалить вызов j_?AfxMessageBox!!AMPER!!!!AMPER!!YGHPBDII!!AMPER!!Z, но чего мы этим добьемся? Нет никаких сомнений в том, что код, обрабатывающий запись файла на диск, отсутствует. Впрочем, есть ненулевая вероятность, что он находится сразу после retn или где-нибудь поблизости. Это бывает при использовании следующих конструкций:

BOOL CCRACK10Doc::OnSaveDocument(LPCTSTR lpszPathName) 
{ 
 AfxMessageBox("Это ограниченная версия. Пожалуйста, приобретайте полную"); 
 return 0; 
 return CCRACK10Doc::OnSaveDocument(lpszPathName); 
} 

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

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

Однако гораздо большей проблемой, чем написание своего кода, станет его внедрение в уже откомпилированный exe-файл. Под MS-DOS эта проблема уже была хорошо изучена, но Windows обесценила большую часть прошлого опыта. Слишком велика оказалась разница между старой и новой платформами. С другой стороны, Windows принесла и новые возможности такой модификации. Например, помещение кода в DLL и простой вызов его оттуда. Подробное рассмотрение таких примеров требует целой отдельной книги, поэтому рассматриваемый здесь прием специально упрощен.

Вернемся к защите. Перейдем по единственной перекрестной ссылке, чтобы узнать, кто вызывает этот код.

.rdata:00403644                 dd offset j_?OnOpenDocument!!AMPER!!CDocument 
.rdata:00403648                 dd offset sub_0_401440 
                                   ^^^^^^^^^^^^^^^^^^^ 
.rdata:0040364C                 dd offset j_?OnCloseDocument!!AMPER!!CDocument 

Что представляют собой перечисленные смещения? Программисты, знакомые с MFC, безошибочно узнают в них экземпляр класса CDocument. Это можно подтвердить, если прокрутить экран немного вверх и, перейдя по одной из двух перекрестных ссылок, посмотреть на следующий фрагмент:

401390 sub_0_401390    proc near 
401390                 push    esi 
401391                 mov     esi, ecx 
401393                 call    j_??0CDocument!!AMPER!!!!AMPER!!QAE!!AMPER!!XZ 
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
401398                 mov     dword ptr [esi], offset off_0_4035C8 
40139E                 mov     eax, esi 
4013A0                 pop     esi 
4013A1                 retn 
4013A1 sub_0_401390    endp 

Становится ясно, что sub_0_401440 - это виртуальная функция CDocument::OnSavеDocument()! Но разработчик не передает управления последней, а выводит диалоговое окно и отказывается от записи.

А что если заменить sub_0_401440 на вызов функции по умолчанию OnSaveDocument? Для этого сначала необходимо узнать, импортируется ли эта функция программой или нет. Воспользуемся для этой цели IDA и изучим секцию rdata. К нашему глубокому сожалению, OnSaveDocument в таблице импорта отстутствует. Можно, конечно, вызвать любую функцию непосредственно из DLL или загрузить ее LoadLibrary. Это, разумеется, потребует немало места для размещения нового кода в файле. Но, к счастью, оно там с избытком имеется. Компилятор выравнивает прологи всех функций по границе 0x10 байт для оптимизации выполнения программы, поэтому остается много "дыр", которые взломщик может использовать для своих целей.

Это действительно очень просто, достаточно иметь минимальные навыки программирования под Windows. Однако, первая же попытка реализации сталкивается с серьезной трудностью. Чтобы вызвать функцию по адресу, необходимо наличие GetProcAddress, а приложение не импортирует ее. Печально на первый взгляд, но легко исправимо. Достаточно лишь слегка изменить таблицу импорта, чтобы включить недостающий вызов.

Обычно компиляторы всегда оставляют в файлах много пустого места, позволяющего немного расширить таблицу импорта. Чтобы это сделать, нужно знать формат PE файла, который описан, например, в MSDN. Покажем это на примере. Скопирум файл crack10.exe в myfile.exe . Теперь запустим HIEW 6.x (не ниже) и перейдем в секцию импорта. В самом ее начале расположен массив IMAGE_IMPORT_DESCRIPOR. Подробности о его структуре можно подчерпнуть в SDK или MSDN. Двойное слово, стоящее в начале, - это RVA (relative virtual address) указатель на структуру IMAGE_THUNK_DATA. Он-то нам и нужен. Преобразовать rva в локальное смещение внутри PE файла можно сложением его с image base, которую можно узнать из заголовка файла.

Что собой предстваляет IMAGE_THUNK_DATA? Это массив указателей на RVAFunctionName. Наглядно его можно представить, если изучать эту структуру в любом подходящем для вас шестнадцатиричном редакторе, например hiew. Что может быть интереснее копания в PE файле вручную, а не с помощью готового инструмента просмотра? Конечно, последнее намного проще и, может быть, приятнее, но не дает никаких полезных навыков. Хакер должен расчитывать не на технику, а только на свои руки и голову. Кракер же может, особо себя не утруждая, воспользоваться готовым редактором для таблиц экспорта\импорта (например PEKPNXE Криса Касперски) и всего лишь отредактировать одну строку, что не требует дополнительных объяснений. Ручная же работа с PE файлами, напротив, пока еще не слишком хорошо описана, а сам формат лишь отрывочно документирован. Единственным маяком в мире Windows был и остается заголовочный файл winnt.h, который содержит все необходимые нам структуры. (но, увы, не содержит комментариев к ним). Поэтому назначение некоторых полей придется выяснить самостоятельно. Для начала загрузим исследуемый файл в hiew. Можно было бы сразу вызвать секцию импорта, но в первый раз попытаемся для интереса найти ее вручную.

Заголовок PE файла начинается не в начале файла. Там расположена DOS-овская заглушка, которая нам совсем не интересна. Сам же PE файл начинается с одноименной сигнатуры. Двенадцатое (считая от нуля) двойное слово - это image base, который нам потребуется для вычислений, связанных с RVA: в нашем случае он равен 0x400000, что типично для Win32 файлов.

Теперь нам необходимо найти адрес таблицы импорта. Он стоит вторым в директории (первый - таблица экспорта). Под директорией здесь понимается структура, расположенная в конце OPTIONAL HEADERа и содержащая необходимую нам информацию. Я не привожу точного описания ее формата, отсылая читателя к MSDN и winnt.h . Настоящая книга не предназначена для пересказа существующей документации, и было бы бессмыслено тратить на это десятки страниц. Замечу, что на стадии подготовки книги это вызвало некоторые возражения у тех людей, которые не владели английским даже на уровне чтения технических текстов со словарем и не позаботились приобрести хотя бы электронную документацию, которая свободно поставляется с любым компилятором и доступна в Интернете на сайтах производителей, в первую очередь, MicroSoft. Увы, тут просто ничего не скажешь.

Итак, предположим, что мы уже выяснили, что таблица импорта располагается по адресу 0x40000+0x3A90=0x43a90. Перейдем к ее рассмотрению, а точнее к рассмотрению IMAGE_THUNK_DATA, которое мы уже затронули выше. Формат его данных очевиден из содержания:

.00403E30:  F6 3E 00 00-02 3F 00 00-16 3F 00 00-C6 3E 00 00 
.00403E40:  E6 3E 00 00-26 3F 00 00-44 3F 00 00-BE 3E 00 00 
.00403E50:  6A 3F 00 00-A8 3E 00 00-9A 3E 00 00-86 3E 00 00 
.00403E60:  36 3F 00 00-56 3F 00 00-D8 3F 00 00-00 00 00 00 

Поразмыслив над ним минутку-другую можно догадаться, что его элементы - двойные слова - это RVA указатели. На эту мысль наталкивает то, что само значение, например 0x3EF6 - находится недалеко от текущей позиции, глубоко в таблице импорта. Кроме того, все близкие к друг другу и однонаправленно возрастающие значения элементов очень похожи на типичный массив указателей.

Заглянув в документацию, мы можем убедиться в правильности нашей догадки. Попытаемся теперь, не обращаясь к документации, угадать - это указатели НА ЧТО? Логично предположить, что на непосредственно импортируемые функции. Все еще не обращаясь к документации, перейдем по одному из указателей:

0403F00:  6D 00 83 00-5F 5F 73 65-74 75 73 65-72 6D 61 74  m Г __setusermat 
                                                            ^ 
0403F10:  68 65 72 72-00 00 9D 00-5F 61 64 6A-75 73 74 5F  herr  Э _adjust_ 
0403F20:  66 64 69 76-00 00 6A 00-5F 5F 70 5F-5F 63 6F 6D  fdiv  j __p__com 
0403F30:  6D 6F 64 65-00 00 6F 00-5F 5F 70 5F-5F 66 6D 6F  mode  o __p__fmo 

Это действительно имена функций, а слово, стоящее перед ними, очевидно, ординал! Однако мы едва не упустили одну важную деталь - ведь существуют функции, которые экспортируются только по ординалу и символьная информация попросту не доступна. Неужели тогда ДВОЙНЫЕ СЛОВА-указатели будут расточительно указывать на СЛОВА-ординалы? Разумеется, нет: фирма MicroSoft в стремлении к оптимизации предотвратила такой вариант. В этом случае все элементы IMAGE_THUNK_DATA представляют собой не указатели, а непосредственно ординалы функций. Чтобы загрузчик мог распознать эту ситуацию, старший бит двойного слова равен единице. В результате, получается массив наподобие следующего:

.00403B00:  B2 10 00 80-86 11 00 80-FA 09 00 80-D0 09 00 80 
.00403B10:  63 16 00 80-52 0F 00 80-41 04 00 80-4F 14 00 80 
.00403B20:  5C 09 00 80-12 0D 00 80-B4 14 00 80-B6 14 00 80 
.00403B30:  A5 0A 00 80-EF 0F 00 80-5A 12 00 80-BB 14 00 80 
.00403B40:  A9 14 00 80-52 16 00 80-A6 0B 00 80-4B 0C 00 80 

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

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

Взглянем на наш файл. RVA адрес первой структуры IMAGE_THUNK_DATA равен 0x3B00. Учитывая, что image base 0x400000, получаем локальное смещение 0x403B00. Как узнать, из какого модуля импортируются эти функции? Для этого заглянем в поле Name IMAGE_IMPORT_DESCRIPTOR, (четвертое двойное слово от начала). В нашем случае оно указывает на стоку 'MFC42.DLL' Именно в эту таблицу мы и должны добавить запись для OnSaveDocument. Разумеется, в таблице не будет свободного места, а за ее концом находится начало следующей. Кажется, ситуация неразрешимая... но подумаем немного. На каждую IMAGE_THUNK_DATA указывает всего одна ссылка. А что будет, если мы переместим одну из них в другое свободное место (которое наверняка найдется) и в освободившееся простанство внесем новую запись?

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

Благодаря выравниванию адресов на границе секций данных и ресурсов практически всегда есть бездна никем не занятого пространства. Переместим выделенную структуру, например, по адресу 0х404110. Для этого нужно скопировать блок с адреса 0х403E24 по 0х403E6B и записать его на новое место. Теперь освободившееся место можно использовать по своему усмотрению. Но прежде необходимо скорректировать ссылку на перемещенный фрагмент. Для этого найдем в IMAGE_IMPORT_DESCRIPTOR прежний RVA адрес и исправим его на новый.

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

.00403B00:  B2 10 00 80-86 11 00 80-FA 09 00 80-D0 09 00 80 
                     ^^          ^^          ^^          ^^ 
.00403B10:  63 16 00 80-52 0F 00 80-41 04 00 80-4F 14 00 80 
.00403B20:  5C 09 00 80-12 0D 00 80-B4 14 00 80-B6 14 00 80 
.00403B30:  A5 0A 00 80-EF 0F 00 80-5A 12 00 80-BB 14 00 80 
.00403B40:  A9 14 00 80-52 16 00 80-A6 0B 00 80-4B 0C 00 80 

Видно, что все они импортируются по ординалу. И нам необходимо только добавить еще один. Находим в файле MFC42.map функцию OnSaveDocument и на основе полученного смещения определяем ординал с помошью dumpbin или любой другой аналогичной утилиты: получаем, что ее ординал 0x1359. Дописываем ее в конец таблицы. Запускаем dumpbin, чтобы удостовериться, что он заметил проделанные изменения. Однако это далеко не конец нашей работы, а скорее только ее начало. Что нам даст новая запись в IMAGE_THUNK_DATA? Честно говоря, ничего. Нам нужно узнать адрес функции после загрузки, а как это сделать? Для этого существует еще одно поле в IMAGE_IMPORT_DESCRIPTOR - это пятое двойное слово, указывающее адрес массива, в каждый элемент которого загрузчик операционной системы запишет реальный адрес импортируемой функции. В нашем случае для MFC42.DLL такая структура расположена по адресу 0x40300C. Рассмотрим ее более детально, но сначала обратим внимание на то, что адрес 0x40300C находится за пределами секции импорта и принадлежит уже секции .rdata . Это обстоятельство на самом деле очень важно, т.к. иначе загрузчик просто не смог бы получить доступ к памяти на запись, а следовательно, изменить значение. Таким образом, эта таблица перемещаема только в пределах .rdata . Но что она собой представляет? Гораздо проще и быстрее выяснить это самостоятельно, чем искать в документации среди множества бесполезной для нас сейчас информации. Рассмотрим ее более детально:

.00403000:  8C 3F 00 00-78 3F 00 00-00 00 00 00-B2 10 00 80 
                                                ^^ 
.00403010:  86 11 00 80-FA 09 00 80-D0 09 00 80-63 16 00 80 
.00403020:  52 0F 00 80-41 04 00 80-4F 14 00 80-5C 09 00 80 
.00403030:  12 0D 00 80-B4 14 00 80-B6 14 00 80-A5 0A 00 80 
.00403040:  EF 0F 00 80-5A 12 00 80-BB 14 00 80-A9 14 00 80 

Не правда ли, эти таблицы идентичны? И та и другая перечисляет ординалы. Однако между ними все же есть существенная разница. Первая сохраняется неизменной на всем протяжении работы, а последняя замещается реальными адресами импортируемых функций уже на стадии загрузки. И именно ее приложение использует для вызовов типа CALL DWORD PTR [0x403010].

Очевидно, что в случае импорта по имени все элементы таблицы будут указателями на ASCIIZ-строки с именем и ординалом функции. Заглянув в MSDN, можно с гордостью констатировать тот факт, что мы нигде не ошиблись в наших предположениях. Со временем большинство исследователей недр Windows все реже и реже заглядывают в документацию, поскольку многое и так достаточно очевидно и не требует разъяснения.

Печально, что это служит примером для начинающих и неопытных хакеров, которые отказываются от документации вообще. В результате они тычутся вслепую или начинают задавать глупые вопросы наподобие таких: "какой функцией Windows открывет файл. Я установил на OpenFile точку останова, а она не сработала. Почему?" Действительно, общий объем документации для разработчика Win32 столь велик, что даже беглый просмотр заголовков отнимет не один месяц времени. Это верно. Еще про Windows 3.1 говорили, что нужно не меньше года обучения, чтобы стать полноценным программистом под эту платформу. Насколько же все усложнилось с тех пор! Самое обидное, что на этом фоне делается основной упор на каркасные библиотеки типа MFC, технологии OLE и ActiveX, а системному программированию просто не остается места - ни в умах разработчиков, ни в документации. Лозунг "все, что Вам нужно, уже сделано компанией MicroSoft" сейчас очень популярен, но многих людей (включая и меня) он приводит в ярость. Программисты старшего поколения до сих пор любят все делать своими руками и не передают выполнения своей программы чужому коду, пока его не изучат.

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

Мы ушли далеко в сторону. Но не будем возращаться назад. Признаюсь, я вас обманул и завел в тупиковый путь. Надеюсь, что читатель уже это осознал. В самом деле, чтобы добавить еще одну запись в вышеуказанную секцию, надо ее чуточку раздвинуть и... получить GPF. ~~9 На нее и вправду указывает множество ссылок из разных частей кода. Изменить их все не представляется возможным, особенно там, где использовалась косвенная адресация.

Впрочем, если быть до конца честным, большинство компиляторов генерируют хорошо известные:

.004018D0: FF25C4314000                 jmp       MFC42.4612 
.004018D6: FF2590314000                 jmp       MFC42.4610 
.004018DC: FF2594314000                 jmp       MFC42.6375 
.004018E2: FF2510304000                 jmp       MFC42.4486 
.004018E8: FF2514304000                 jmp       MFC42.2554 
.004018EE: FF2518304000                 jmp       MFC42.2512 
.004018F4: FF251C304000                 jmp       MFC42.5731 
.004018FA: FF2520304000                 jmp       MFC42.3922 
.00401900: FF2524304000                 jmp       MFC42.1089 

Таким образом, на каждый элемент имеется всего одна ссылка, которая к тому же легко может быть найдена и скорректирована. Выходит, я дважды обманул читателя. На самом деле это не тупик, а просто хлопотный, но очевидный путь. Я встречал многих хакеров, которые им соблазнились и даже писали для коррекции ссылок специальную программу или скрипт к IDA.

Однако можно пойти более короткой дорогой. Кто нас заставляет добавлять элемент в существующую таблицу, когда можно создать свою и разместить ее где угодно! Это в самом деле очень просто.

Поскольку сразу за концом IMAGE_IMPORT_DESCRIPTOR следует IMAGE_THUNK_DATA, то очевидно, что добавить еще одну запись можно только в том случае, если переместь одну из двух на свободное место. Первая несравненно короче, поэтому и найти бесхозное пространство для нее легче. Строго говоря, нам необходимо разместить ее в пределах таблицы импорта, и никто не разрешит перемещать ее в секцию .data - получится перекрывание секций, и последствия не заставят себя ждать... hiew "заругается" на такой файл. И, пожалуй, все. Действительно, если изучить код загрузчика Windows, становится ясно, что ему совершенно все равно, в какой секции расположена таблица импорта, и, более того, совершенно безразличен размер последней, а точнее, его соответствие с реальным. Конец определяется null-записью.

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

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

Скопируем IMAGE_IMPORT_DESCRIPTOR в любое свободное место секции данных и изменим на нее ссылку в Import Directory. Теперь нам необходимо создать в ней новую запись. Начнем с четвертого двойного слова, указывающего на имя функции. Можно сослаться на уже существующую строку 'MFC42.DLL' или создать свою и указать на нее. Последнее дает нам больше свободы и независимости. Поэтому поступим именно так:

.004041D0:  4D 46 43 34-32 2E 44 4C-4C 00 00 00-00 00 00 00  MFC42.DLL 

Итак, имя экспортируемого модуля мы уже записали. Теперь необходимо создать массив IMAGE_THUNK_DATA ("массив" громко сказано, всего лишь одну запись).

.004041E0:  59 13 00 80-00 00 00 00-00 00 00 00-00 00 00 00  Y А 

Понятно, что 0x1359 и есть импортируемая функция OnSaveDocument, а старший бит 0x8000 указывает, что последняя импортируется по ординалу. Остается создать таблицу адресов - точнее, таблицу создавать нет никакой необходимости. Несмотря на то что каждый ее элемент должен по теории ссылаться на соответствующую функцию, оптимизация загрузчика привела к тому, что он никак не использует начальные значения таблицы адресов, а вносит записи в том порядке, в котором они перечислены в таблице имен (IMAGE_THUNK_DATA). Поэтому достаточно лишь найти незанятое пространство и установить на него указатель в последнем поле IMAGE_IMPORT_DESCRIPOR.

Однако тут мы наталкиваемся на серьезные ограничения. Загрузчику на запись доступна только .rdata, в которой - скажем так - свободного места не густо. Более того, ни один элемент нельзя перемещать, поскольку ссылки на него разбросаны по всему коду программы. Остается только надеяться, что в результате выравнивания в конце таблицы найдется немножко пространства для наших целей. И действительно, несколько десятков байт свободно. Для нас этого более чем достаточно.

0403FC0:  57 69 6E 64-6F 77 00 00-55 53 45 52-33 32 2E 64  Window  USER32.d 
0403FD0:  6C 6C 00 00-AA 01 5F 73-65 74 6D 62-63 70 00 00  ll  к_setmbcp 
0403FE0:  00 00 00 00-00 00 00 00-00 00 00 00-00 00 00 00 
0403FF0:  00 00 00 00-00 00 00 00-00 00 00 00-00 00 00 00 

Остается только скорректировать IMAGE_THUNK_DATA. Финальный вариант может выглядеть так:

0404160:  E0 41 00 00-00 00 00 00-00 00 00 00-D0 41 00 00  рA          ¦A 
0404170:  E0 3F 00 00-00 00 00 00-00 00 00 00-00 00 00 00  р? 

Убедимся с помощью dumpbin, что он исправно работает.

MFC42.DLL 
           403FE0 Import Address Table 
           4041E0 Import Name Table 
                0 time date stamp 
                0 Index of first forwarder reference 

                 Ordinal  4953 

Если заглянуть отладчиком по адресу 0x403FE0, то там мы обнаружим готовый к употреблению адрес функции OnSaveDocument. Проверим, что это действительно так. Дизассемблируем (командой u в soft-ice) этот регион памяти. При этом отладчик должен вывести в прологе ординал функции. Это убеждает нас, что все работает. Остается эту функцию всего лишь вызвать. Для этого вернемся далеко назад, когда мы нашли перекрытую функцию OnSaveDocument. Нам стоит переписать ее. Рассмотрим код еще раз:

.00401440: 6A00                         push      000 
.00401442: 6A00                         push      000 
.00401444: 6890404000                   push      000404090 
.00401449: E812070000                   call      AfxMessageBox 
.0040144E: 33C0                         xor       eax,eax 
.00401450: C20400                       retn      00004 

Очевидно, что ее нужно переписать, - например, следующим образом:

.00401440: FF742404                     push      d,[esp][00004] 
.00401444: 90                           nop 
.00401445: 90                           nop 
.00401446: 90                           nop 
.00401447: 90                           nop 
.00401448: 90                           nop 
.00401449: 2EFF15E03F4000               call      d,cs:[000403FE0] 
.00401450: C20400                       retn      00004 

Для понимания этого обратимся к SDK. Вот какой прототип имеет функция

virtual BOOL OnSaveDocument( LPCTSTR lpszPathName ); 

Отсюда вытекает строка push dword [esp][00004], остается объяснить вызов функции. Как мы помним, загрузчик в ячейку 0x403FE0 записал ее адрес, - он и был использован для вызова. И это все! Мы дописали недостающий код. Этот момент очень важен. Читатель может упрекнуть меня за выбор искусственной ситуации. Действительно, часто ли встречаются подобные примеры в жизни? Даже применительно к MFC используемая функция с большой степенью вероятности может быть перекрыта функцией разработчика. Как быть тогда?

Но не спешите. Пусть функция перекрыта, тогда положение осложняется лишь тем, что хакеру сперва нужно будет понять ее алгоритм, а затем воссоздать недостающий код и... поместить его в собственную DLL, а оттуда уже аналогичым образом сделать вызов. При этом нет надобности изощряться и втискивать код в скудные клочки пустого места, беспорядочно разбросанные по файлу. Можно выбрать любое симпатичное средство разработки (например MS VC) и написать на нем недостающую функцию, используя всю мощь MFC и объективно-ориентированного Си++. Это гораздо легче и, кроме того, по-просту удобно.

Для модификации старых exe для MS-DOS обычно использовался только ассемблер. С одной стороны, это было приятно (разумеется, для поклонников этого языка), а с другой - утомительно. Кроме того, в Windows гораздо легче понять взаимодействие различных фрагментов программы, т.к. здесь очень много избыточной информации, а объективно-ориентированные языки (которые доминируют последнее время) оперируют в основном локальными структурами и переменными. К тому же здесь меньше тех ужасных глобальных объектов общего использования, которые непонятно для кого предназначены и как используются. Особенно если программист в погоне за минимизацией требуемой памяти использует одну и ту же переменную повторно, когда предыдущей процедуре она уже не нужна. Допустим, при старте программы пользователь ввел пароль, который был сравнен с некоторой эталонной строкой. Ясно, что во время работы программы эта область памяти может быть отведена под нужды других процедур, если пароль сравнивается только один раз. Из этого следует, что мы получим множество перекрестных ссылок и долго будем чесать в затылке, размышляя "почему это с паролем-то так интенсивно работают?"

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

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

.00401410: 8B442404                     mov       eax,[esp][00004] 
.00401414: 8B5014                       mov       edx,[eax][00014] 
.00401417: F7D2                         not       edx 
.00401419: F6C201                       test      dl,001 
.0040141C: 7411                         je       .00040142F 
                                        ^^^^^^^^^^^^^^^^^^^ 
.0040141E: 6A00                         push      000 
.00401420: 6A00                         push      000 
.00401422: 6854404000                   push      000404054 ; << строка 
.00401427: E834070000                   call      AfxMessageBox 
.0040142C: C20400                       retn      00004 ;" 
.00401430: 8B4130                       mov       eax,[ecx][00030] 
.00401433: 8B4808                       mov       ecx,[eax][00008] 
.00401436: E81F070000                   call      Serialize 
.0040143B: C20400                       retn      00004 

Части: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15

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

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