Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #121, ЯНВАРЬ 2009 г.

Охота на скринсейверы. Ломаем скринсейверы с 3d-нагами

Сергей «_A1!3N» Аляев




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

Чтобы получить моральное удовольствие от скринсейвера, достаточно удалить НАГ (такой «квадрат Малевича», на котором написано про регистрацию). Некоторые приемы борьбы с НАГами в обычных Win-приложениях я описал в майском номере ][, в статье «Ты – повелитель армии лоадеров!». К сожалению, в случае 3D-приложений эти методы не подходят. НАГ-скрин в 3D-программе вовсе не то же самое, что НАГ в Win-приложении. В 3D НАГ нельзя послать сообщение, так как это не объект Windows (то есть, не окно) и манипулировать им извне стандартными методами WinApi не получится. А значит, SendMessage нам не поможет.

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

Новичкам советую для начала выбрать второй путь, иначе есть риск заработать лысину и чувство собственной неполноценности. Видишь ли, криптография – это наука и с наскока проблему разных MD5, RSA не решить, а именно так и будет защищена (по закону подлости) твоя первая программа. Поэтому переходи на первый путь, только после того как протопчешь широкую тропу на втором. Однако не забывай и почитывать скучные (без картинок, но с формулами) мануалы по криптографии. Работать будем, как настоящие «джедаи», в основном с дизассемблером. Отладчики user mode (OllyDbg, MS Dbg) не особо удобны для отладки в связи со спецификой полноэкранных 3D прог.

Рекогносцировка на местности

Определимся для начала, какие признаки НАГа будем искать в коде. С точки зрения 3D-программиста, НАГ – это 3D-модель, состоящая или из двух треугольников, или одного полигона (OpenGL), или одного квадрата (OpenGL) с включенным альфа каналом для прозрачности и натянутой на него текстурой. Есть еще маленький нюанс: чтобы прозрачный НАГ корректно выводился, его надо визуализировать последним (надо отметить, что к вышеперечисленным признакам подойдет код, имитирующий снег, дым, пыль, дождь, огонь) и предустановленной ортогональной проекцией. Исходя из этого, определяемся с признаками присутствия кода НАГа:

  1. Наличие API-функций, устанавливающих ортогональную проекцию (glOrtho);
  2. Наличие API-функций, имитирующих прозрачность (glEnable(GL_BLEND), glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), glColor4f...);
  3. Нахождение кода прозрачного НАГа в конце функции визуализации.

Итак, начнем. Заходим в любой поисковик, вбиваем в него что-то подобное «Screensaver download» и качаем только красивые скринсейверы! Процесс исследования защит должен приносить эстетическое удовольствие :). Теперь устанавливаем, заходим в системную папку Windows и в ней же, в System32, копируем нужные *.scr в папку, где будем производить над ними эксперименты и... творить чудеса!

Родной OpenGL

На роль первого клиента мне попался ElectriCalm 3D Screensaver. Откроем его в IDA (если кто не знает, дизассемблер такой, можно воспользоваться другим, но лучше не надо). На все ее вопросы ответим «да». Далее тип файла выбери PE Executable (*.scr – это тот же *.exe). Подожди, пока он сделает свое дело, и открывай окно Imports. Большое количество функций с префиксом «gl_» подтверждает, что скринсейвер использует OpenGL.

Кликаем два раза на glOrtho. В таблице импорта напротив имени функции после DATA XREF: установлен адрес функции плюс смещение, откуда вызывается glOrtho. Кликни по нему.

.data:0043A2CC glOrtho dd ? ; DATA XREF: sub_405350+44

Отлично, попадаем в маленькую функцию (которую IDA обозвала sub_405350) по соответствующему адресу 0x405350. Что она делает? Ничего особенного – является оберткой glOrtho, поэтому выделяем имя функции, жмем N и в появившемся диалоге Rename переименовываем функцию в Call_Ortho. Правее заголовка функции располагаются таинственные письмена под заголовком CODE XREF: как ты, наверное, догадался, это ссылки на адреса, откуда вызывается Call_Ortho.

; CODE XREF: sub_403C10+E02
; sub_405110+2A

Кликаем на первой из них sub_403C10+E02 и попадаем в весьма интересное место:

004049DC push 0BE2h ;Вкл. смешение
004049E1 call glEnable ;glEnable(GL_BLEND);
004049E7 push 303h
004049EC push 302h ;Установ. функ. смешения (параметры типичные для прозрачности)
004049F1 call glBlendFunc ;glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
004049F7 mov ebx, [ebp-34h]004049FA push ebx ;альфа компонент (прозрачность НАГа)
004049FB push 0
004049FD push 0
004049FF push 0 ;Установ. цвета с параметром прозр.- ALPHA
00404A01 call glColor4f ;glColor4f(RED,GREEN,BLUE,ALPHA)
00404A07 push 0DE1h ;Откл. текстурирования
00404A0C call glDisable ;glDisable(GL_TEXTURE_2D)
00404A12 call Call_Ortho ;glOrtho... вкл. орто. проекции
00404A17 push 7 ;Нарисованный примитив это четырехугольник
00404A19 call glBegin ;glBegin(GL_QUADS)

Перед нами типичный код, который собрался вывести перед носом прозрачный четырехугольник. Забегая вперед, скажу, что НАГ можно отключать уже сейчас путем модификации всего одной команды, но для чистоты научного эксперимента продолжим исследование кода. Отключение текстурирования (glDisable(GL_TEXTURE_2D)) вызывает некоторое беспокойство по поводу правильности выбора направления нашего расследования, ну да не будем пока обращать на это внимание. Попробуем заменить push 7 по адресу 0x00404A17 на push 0, что соответствует glBegin(GL_POINTS). Это заставит OpenGL выводить не квадраты, а точки. Задача glBegin – предварять список данных вершин и определять тип выводимых примитивов. Это не единственный способ рисования в OpenGL, но для вывода НАГа обычно пользуются им.

Для модификации кода можно использовать OllyDbg, но я обычно пользуюсь Hiew. Открываем в Hiew файл ElectriCalm 3D Screensaver.scr и нажимаем <Enter> до тех пор, пока не попадем в режим дизассемблера. Жмем <F5> и в поле ввода вводим нужный адрес 0x00404A17, не забыв перед ним поставить точку, означающую, что это виртуальный адрес PE-файла, а не смещение относительно начало файла. Далее – <Enter>. Сейчас курсор находится на двоичном представлении(6A 07) асм-кода push 7. Давим на <F3>, редактируем машинный код в 6A 00. Затем <F9> для сохранения изменений. Запускаем скринсейвер - фон НАГа исчез (точнее, не исчез, просто OpenGL нарисовала не квадрат, а четыре очень маленькие точки), но НАГ-письмена остались. Теперь понятно, почему отключалось текстурирование! Текст в НАГе – это не картинка-текстура, а множество примитивов квадратов, на которые натягиваются соответствующие картинки-буквы.

Все это мне подсказали внутренности функции sub_405110 (которую можно смело обозвать PrintStringLine), находящейся чуть ниже и вызывающейся три раза – как ты помнишь, в НАГе было три строчки. Зайдя в нее, ты увидишь нашу старую знакомую Call_Ortho, которая здесь выполняет ту же функцию и, вообще, близлежащий код напоминает нам код вывода прозрачного четырехугольника. В этом нет ничего странного (я же предупреждал про дождь, снег, огонь, дым, буквы и т.п.).
Сейчас мы достаточно подробно разобрали устройство кода создания НАГа. Следующий этап – взлом. Итак, можно банально занопить (забить командой NOP опкод 0x90, не преминув забить и команду засылки параметров функции push...) какую-то ключевую функцию (glBegin, glVertex). А можно сделать красиво – ведь glVertex3f принимает в качестве параметров координаты вершин четырехугольника x, y, z. Соответственно отредактировав координаты (например, 0), можно сделать НАГ очень маленьким или вывести его за пределы 3D-сцены: пусть показывается на другой планете. Я остановился на следующем варианте: четвертым параметром функции glColor4f является альфа-компонент, который задает уровень прозрачности последующих примитивов от 0.0 до 1.0 – стоит только заменить команду:

004049F7 mov ebx, [ebp-34h] ;пересылаем значение из памяти по адресу из стека [ebp-34h] в EBX
на
004049F7 xor ebx, ebx ;исключающее ИЛИ примененное к регистру EBX (EBX = 0) альфа = 0

Оставшиеся лишние байты забьем NOP. НАГ-скрин станет прозрачным, как человек-невидимка. Если у тебя конкретные проблемы с ASSEMBLERом, то редактировать код будет проще в OllyDbg. Вот тебе небольшая инструкция:

  1. Запускаем OllyDbg, загружаем ElectriCalm 3D Screensaver.scr.
  2. Давим <Ctr+G>, в появившееся окно вводим адрес искомой команды 004049F7.
  3. Давим пробел, в окно вводим нужный код на АСМе xor ebx, ebx. Далее ОК, (OllyDbg сама забьет NOP лишние байты).
  4. Правой кнопкой мыши в окне с кодом и в меню – Copy to executable -> All modifications.
  5. Жмем кнопку «Copy all».
  6. В окне правой кнопкой мыши и в появившимся меню Save file cохраняй модифицированный файл.

Разберем по косточкам скринсейвер «3D Formula 1 Screensaver». Работать будем по шаблону. Ищем glOrtho, которая также находится в функции обертке, переименовываем ее в CallOrtho… ищем и обламываемся - ссылок на функцию слишком много, анализировать все не хочется. То же самое касается и Color4f и Vertex3f. Все это говорит о том, что программа далека от совершенства. Ну да ладно, мы сюда залезли не для оптимизации кода. Попробуем сменить тактику: вспомним третий признак прозрачного НАГа, который гласит, что его код должен быть в конце функции визуализации. В программах, использующих OpenGL, функция визуализации (RenderFunc – так я ее обзываю в IDA) должна заканчиваться API-функцией SwapBuffers или glutSwapBuffers, если прога использует библиотеку-посредника glut, но обычно более-менее серьезный софт под Windows библиотеки-посредники не используют. Что делает SwapBuffers, видно из ее названия. Когда в один буфер происходит рисование, содержимое другого отображается на экране, а функция меняет их местами (в общем, она нужна для анимации). Ищем SwapBuffers и находим:

extrn SwapBuffers:dword ; DATA XREF: sub_406320+281
; sub_417379+6FC

Всего два вызова, заглянем в каждый. Лично мне понравился второй:

00417A3F test ecx, ecx
00417A41 jnz short loc_417A54 ; <---Vot on beyte ego
00417A43 push offset unk_452710
00417A48 mov edx, [ebp+arg_0]
00417A4B push edx
00417A4C call sub_4065BB
00417A51 add esp, 8
00417A54
00417A54 loc_417A54: ; CODE XREF: RenderFunc+6C8
00417A54 call sub_4068F0
00417A59 call sub_405D40
00417A5E push 1
00417A60 call sub_405CF0
00417A65 add esp, 4
00417A68 call ds:glFlush
00417A6E call ds:wglGetCurrentDC
00417A74 push eax ; HDC
00417A75 call ds:SwapBuffers

Так как нехорошо показывать НАГ зарегистрированному пользователю, логично предположить, что в любой проге с НАГом должен быть механизм его обхода. Можно не затирать функции НАГа, а найти этот механизм и «выломать из него шестеренку». Итак, мне понравился условный переход по адресу 0x00417A41, который обходит вызов функции sub_4065BB. Заглянем сюда и увидим, что здесь вызывается и наш Call_Ortho, и glColor4f, и glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), и много других интересных функций, о которых можно почитать в мануалах по OpenGL, щедро разбросанных по Сети. Обойдем вызов sub_4065BB путем классического 75 -> EB. Шестнадцатеричное представление АСМ команды jne 000417a54 (7511) меняем 7511 на EB11. Теперь условный переход (в зависимости от значения ECX) становится безусловным Jmp. Запускаем и убеждаемся что «шестеренка выломана», а механизм вывода НАГа работает как надо.

Переспективный DirectX.

Главная проблема, с которой мы столкнемся при исследовании программ, использующих библиотеку DirectX - это технология COM. Грубо говоря, технология заключается в том, что функции библиотеки вызываются через указатель на массив указателей на функции. Напоминает механизм вызова виртуальных функций. Неудивительно, так как COM на этом и основывается. В результате, дизассемблер не имеет возможности идентифицировать вызов функции DirecX, что несколько усложняет наши исследования и заставляет пересмотреть стратегию нападения. Из приведенных трех признаков НАГа, которыми мы пользовались при исследовании OpenGL-программ, сейчас для нас актуальным остается только признак номер три. Как ты скоро увидишь, не так уж это и мало. Как найти RenderFunc? Для этого придется разобраться, куда обычно 3Д-кодеры ее вставляют.

А вставляют они ее обычно в функцию (в IDA я ее обзываю On_Idle), которая вызывается из цикла выборки сообщений в момент простоя программы. Это не единственный способ вызова RenderFunc, но вменяемые программисты чаще используют его.

Возьмем первый DirectX-пример: «Spirit of Fire 3D Screensaver». Грузим исполняемый файл в IDA.

Значит, первое, что нужно сделать – найти RenderFunc. Ищем цикл выборки сообщений, вернее, любую API-функцию, входящую в эту конструкцию, например PeekMessage (как правило, именно PeekMessage применяется в циклах выборки совместно с RenderFunc). Итак, нашли оба вызова в одной функции sub_40A4A0, соответственно, по адресу 0x0040A4A0. Ищем On_Idle. Задача не сложная – после WaitMessage только один Call sub_403910. Переименовываем в OnIdle, заглядываем в нее и спускаемся вниз. Что у нас здесь? Ага, несколько условных переходов (в листинге я их пронумеровал от 1...3). Целых 3, если точнее. Конечно, можно, тупо начиная с конца, поочередно делать их безусловными, а можно заглянуть в функции, которые эти Jcc обходят.

00403C1B call timeGetTime
00403C21 mov ecx, ds:dword_49A17C
00403C27 test ecx, ecx
00403C29 jnz short loc_403C49 ;(1)
00403C2B mov edx, ds:dword_49A1A4
00403C31 mov ecx, ds:dword_497110
00403C37 sub eax, edx
00403C39 cmp eax, ecx
00403C3B jb short loc_403C44 ;(2)
00403C3D call sub_401110
00403C42 jmp short loc_403C49
00403C44 call sub_401390
00403C49 mov eax, ds:dword_4A0854
00403C4E mov ecx, [eax]
00403C50 push eax
00403C51 call dword ptr [ecx+18h]
00403C54 mov edx, ds:dword_4A085C
00403C5A imul edx, 4CCh
00403C60 mov eax, ds:dword_49ACD4[edx]
00403C66 test eax, eax
00403C68 mov eax, ds:dword_4A0844
00403C6D jnz short loc_403C8B ;(3)
00403C6F mov ecx, [eax]

Итак, заходим в первую sub_401110, вызываемую из 0x00403C3D, – и видим, кроме API-функций, (ExtTextOut, SetTextColor...), работающих с текстом, еще и ссылки на знакомые до боли строки «UNREGISTERED VERSION!», «Press the space bar to find out». Ничего не напоминает? Это и есть функция, рисующая НАГ. Теперь зайдем в sub_401390 из 0x00403C44. Там ты тоже увидишь ссылку на интересную строку «Remaining time: %u sec.». Все ясно – это функция-таймер, отвечающая также и за его отображение. На основании исследований, делаем заключение – для нормального функционирования скринсейвера обе функции не нужны, соответственно, Jnz под номером (1) наш искомый переключатель в мир лицензионного софта, далее классика: 75 -> EB.

Следующий пример про Атлантиду – «Atlantis 3D Screensaver». Ищем цикл выборки сообщений с помощью вышеописанного способа, быстро ее находим (начинается с 0x0042414F). Ежу понятно, что единственный вызов собственной функции (не WinAPI) – это On_Idle (адрес вызова 0x004241B3). Заходим в нее в поисках RenderFunc. Привычно спускаемся вниз и... обламываемся:

0042839A call dword ptr [eax+4Ch]

Что это? Где искать эту функцию? Для начала надо разобраться, что у нас в EAX. Поднимаемся немного выше по коду:

00428392 mov edx, [ebp+var_C] ;В EDX значение временной переменной var_C=ebp-0Ch
00428395 mov eax, [edx] ;В EAX указатель на что-то

далее ищем что в var_C, ползем в самое начало On_Idle:

004281CC mov [ebp+var_C], ecx ;Инициализация временной переменной [ebp+var_C] извне On_Idle

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

004241AD mov ecx, [ebp+var_4C8] ;Присваивание ECX значением из временной переменной

Посмотрел? Тогда продолжаем подниматься выше по коду:

00423EA4 mov [ebp+var_4C8], ecx ;Инициализация временной переменной [ebp+var_4C8] из вне sub_423E9B

Опять этот ECX и опять нужно выходить из функции (sub_423E9B) наверх. В общем, скоро ты всплывешь здесь:

0044C395 mov ecx, offset unk_56EF30 ;В ECX значение переменной unk_56EF30

Поздравляю! Ты только что познакомился с тем, как выглядит объектно-ориентированное программирование в самом низу, на уровне ASSEMBLER'а. Значение, так назойливо передающееся в ECX во все нами пройденные функции, это аналог this в С++, а функция, которую мы ищем, виртуальная (адрес функции = (this+4Ch)). Все это, конечно, прекрасно, но как найти адрес виртуальной функции? this=unk_56EF30 не инициализирован. К сожалению, тут не обойтись без отладчика. Грузим скрин в OllyDbg, трассируем прогу, пока в 0x0056EF30 (правой кнопкой в окне данных, Go to -> Expression, ввести адрес 0056EF30) не появится значение – у меня это 0x18744900. Переставляем байты с учетом специфики процессоров Intel (младший байт по младшему адресу), получаем указатель 0x00497418, прибавляем смещение 0x00497418 + 0x4C = 0x00497464, переходим по нему и, наконец, получаем адрес 0x00454a87 виртуальной функции. Находим ее в IDA: небольшой размер sub_454A87 наводит на мысль, что НАГ прячется в другой функции, вызываемой отсюда. Попробуем определить, в какой, путем банального NOPа. Как всегда, начнем с конца функции.

Здесь надо внести некоторые пояснения. Обычно нет смысла тратить время на самый последний Call. Не забывай о тотальной победе объектно-ориентированного программирования над процедурным, благодаря которой тело современной функций нашпиговано вызовами конструкторов, деструкторов и прочими инициализирующими и очищающими функциями. Начнем с предпоследней функции (помни про параметры), правим, запускаем… - мимо! Нопим следующий вызов (0x00454B0C) с параметром (0x00454B08), запускаем и попадаем. Так-с, НАГа нет, но нет и часов и FPS'а. Кого-то устроит этот результат, но не нас. Все равно найдем НАГ! Заходим в sub_428670 и спускаемся вниз, не забыв отметить про себя длину листинга – около 3 метров. Здесь будем использовать метод исключения. Суть его в следующем: как я уже упоминал, в программе должен быть условный переход, который отличает зарегистрированного пользователя от остальных. Его-то и будем искать и превращать в безусловный переход. Для этого нужно, начиная с конца функции, выбирать только условные переходы – те, которые исключают большой участок кода, где есть вызовы функций (без них ничего не нарисуешь).

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

00429712 jz loc_429A91

После правки в скринсейвере пропадает таймер, но не НАГ. Следующим идет еще метр кода:

004292C3 jz loc_429680

Вот и нет НАГа!

И последний на сегодня пример – «Christmas Time 3D Screensaver» на новогоднюю тему – будет самым сложным. Сложность заключается в том, что On_iDle и RenderFunc будут совсем не детских размеров. Это несколько усложнит поиск кода создания НАГа. Но только «несколько», так как человека, знающего, чего он ищет и где и как оно выглядит, остановить сложно.

Грузим скрин в IDA, ищем On_Idle. Для этого находим цикл, начиная с адреса 0x0041295F. Замечаем, что с циклом что-то не так, а именно – в теле цикла очень большой участок кода. Похоже, изначально On_Idle была определена как inline функция, и все ее содержимое компилятор вставил в цикл. Словом, On_Idle нашли, следующий пункт – RenderFunc. Для поиска RenderFunc используем метод исключения: начнем с Jcc по адресу 0x00413344. Правим, запускаем, – мимо. Правим два перехода по адресам 0x004132DC, 0x004132EE. Оба они ссылаются на один адрес, – и опять мимо. Выбираем Jcc с адресом 0x00413282, ну вот и попали! Значит, одна из двух вызываемых функций из исключенного кода RenderFunc. Зайдем и посмотрим в каждую. Заходим в sub_406C07 - пять локальных переменных, штук пять вызовов функций – нет, это RenderFunc. Заходим в sub_4091FA, вот оно - около 50 локальных переменных, команды сопроцессора, ссылки на строки типа «Bliss», «Lamp_off01.tga»... А дальше – метод исключения, который со второго раза должен показать на условный переход по адресу 0x0040AA18.

Кто следующий?

Надеюсь, у меня получилось показать, как легко и непринужденно (практически без применения отладчика) можно лишить 3Д-приложение НАГ-скрина. Сложностью будет только применение протекторов, но так как люди, отвечающие за выбор протектора (не знаю кто, разработчики или издатели) страдают хроническим отсутствием фантазии, то, как правило, им является ASProtect. Для снятия его часто достаточно stripper'а. НАГ-скрин в 3D-программе – слишком заметная конструкция, чтоб хоть сколько-то на него рассчитывать, поэтому выход один – не использовать или натягивать НАГ-текстуру на ключевую модель. Самое главное: не делать НАГ прозрачным. Как следствие, – возможность располагать код НАГа в любом месте функции визуализации 3D-сцены.

INFO

Я набросал небольшой список литературы для внеклассного чтения (пригодится, если в статье встретились какие- то незнакомые тебе слова типа «цикл выборки сообщений», «альфа канал», «ортогональная проекция», «забить командой NOP опкод 0x90»...):

  1. Петзолд Ч. Программирование для Windows 95.
  2. Касперски К. Фундаментальные основы хакерства. Искусство дизассемблирования.
  3. Девис Т., Нейдер Дж., Шрайнер Д. OpenGL. Руководство по программированию.
Содержание