Теоретические основы крэкинга: Глава 8. И вновь продолжается бой…

Дата публикации 1 ноя 2004

Теоретические основы крэкинга: Глава 8. И вновь продолжается бой… — Архив WASM.RU

Контролирует ценность тот, кто может ее уничтожить
Ф. Херберт, «Дюна»

Ну вот, все, что мы могли сделать простыми средствами, мы сделали. И теперь пришло время приступить к отладке. Да-да, эта глава будет практически полностью посвящена искусству дебагить, bpx’ить, трассировать  - в общем, одному из аспектов той рутинной деятельности, которая ожидает каждого крэкера на его светлом пути к торжеству высокого искусства над коммерческими интересами. Осмелюсь предположить, что Вы имеете общее представление о том, на что похож процесс отладки программ, о точках останова, пошаговой отладке с заходом внутрь процедур и без оного и прочих столь же элементарных вещах. В интерфейсах и «горячих клавишах» отдельных представителей обширного семейства отладчиков, я думаю, Вы тоже разберетесь без особого труда – скажу лишь, что мы будем ориентироваться на работу под Windows. А наиболее достойными представителями своего племени под этой ОС на сегодня являются SoftIce(бессменный чемпион в течение многих лет – но чемпион довольно капризный) и OllyDebug (сравнительно новая, но очень качественная и совершенно бесплатная программа). Поэтому перейдем к изучению идей, которые, надеюсь, позволят Вам усовершенствовать свои умения в исследовании кодов, о полезных же особенностях конкретных отладчиков я буду упоминать тогда, когда эти полезные особенности будут нами востребованы. А поскольку методы борьбы с nagscreen’ами содержанием предыдущей главы не исчерпывается, мы будем изучать техники отладки параллельно с искусством ликвидации рекламных окон.

Как я уже говорил, долгие годы первым словом, которое знаменовало рождение нового крэкера, было «bpxMessageBoxA» - то есть команда установки breakpoint’а на некую функцию WindowsAPI. И в этом, несомненно, сокрыт глубокий смысл – точки останова (они же брейкпойнты) являются одним из важнейших средств отладки, позволяющим остановить программу в нужной точке. Системные вызовы ОС являются связующим звеном между программой и операционной системой. И если во времена DOSеще существовала возможность написать программу, выполняющую некие полезные действия, выводящую результаты и при этом ни разу не обращающуюся к средствам операционной системы, то сейчас, в эпоху многозадачных операционок, блокирующих прямой доступ к абсолютному большинству аппаратных ресурсов, сделать такое практически невозможно. Чтобы зажечь один-единственный пиксель в углу экрана, программам теперь приходится идти на поклон к операционной системе с вежливой просьбой «нарисуйте, пожалуйста, белую точку по указанным экранным координатам». С другой стороны, операционная система предлагает широкий выбор различных полезных функций – от мелочей вроде строковых операций (надо отметить, что выбор этих операций в Windowsмог бы быть и побогаче) до таких высокоуровневых функций, как работа с изображениями или уже упомянутое создание диалоговых окон по шаблонам в секции ресурсов. В общем, сделано все для удобства программиста (другое дело, что разработчики ОС иногда понимают это удобство весьма странным образом) – и разработчики этим пользуются. Пользуются этим и крэкеры. Когда какая-нибудь программка лезет в реестр, чтобы провериться на истечение триального срока – она вызывает функции API. Когда читает серийник из поля редактирования – обращается к одной из оконных функций. Даже сообщение «Зарегистрируй меня!» - и то выводится при помощи API. И вот тут-то мы их и ловим – провоцируем приложение чем-нибудь выдать свою коммерческую природу, ставим точки останова – и терпеливо ждем, пока программа не попадется в расставленные сети. Однако чтобы поймать по-настоящему хитрую дичь, ловушки нужно расставлять умело.

Давайте посмотрим, каким образом в уме крэкера рождается великая идея набрать в командной строке SoftIce’а ту самую эпохальную команду – bpxMessageBoxA. Сначала крэкер изучает повадки зверя, на которого идет охота: пытается вводить невалидные серийные номера (мы ведь не в рулетку играем – так что на дурную удачу рассчитывать не приходится), вызвать заблокированные функции (некоторые программы не отключают соответствующие элементы управления) или сделать еще что-либо подобное. Конечная цель всего этого – вынудить программу проявить свою незарегистрированность каким-нибудь хорошо заметным и легко идентифицируемым способом, например – выводом стандартного окна с сообщением. Впрочем, нередко бывает и так, что специально ничего делать не надо – программа сама при запуске или в процессе работы выплюнет на экран окошко с предложением зарегистрироваться. Допустим, что это окошко имеет специфический вид «MessageBox’а обыкновенного». Что такое «MessageBoxобыкновенный», я думаю, объяснять никому не надо - единожды в жизни увидев хотя бы один MessageBox, Вы уже больше никогда не сможете забыть это ужасающее зрелище, оно будет преследовать Вас годами, лишая покоя и сна, пока Вы не решите раз и навсегда «завязать» с компьютерами. Если каким-то чудом Вам за все время работы с компьютером удалось избежать этого счастья, Вы всегда можете посмотреть на MessageBox, попытавшись обратиться из «Проводника» к дисководу, в котором нет дискеты. Посмотрели? Вот и отлично, тогда продолжим.

Одна из важнейших для крэкера областей знаний есть знания о том, при помощи каких функций WindowsAPIвыполняются те или иные действия. Чтобы успешно поставить брейкпойнт на какую-либо функцию и получить от этого полезный результат, необходимо выполнение двух условий: Вы должны знать, что делает требуемая функция (сия мысль выглядит несколько банальной – но такова правда жизни) и как функция эта называется. И вот теперь Вам потребуются именно знания, одними только хорошими идеями, как мы это делали раньше, тут не обойтись. Есть два пути к обретению необходимых знаний: Вы можете либо с головой погрузиться в изучение программирования с использованием различных разделов WindowsAPI– и после этого будете способны не только влепить брейкпойнт «не глядя», но и в первом приближении оценить функции того или иного куска кода, просто посмотрев на вызываемые системные функции и передаваемые им параметры. Именно этим путем, насколько будет возможно, я и рекомендую Вам следовать – поскольку, когда Вы перейдете от обезвреживания простых защит к исследованию более защищенных программ, навыки программирования с использованием WindowsAPIВам очень пригодятся. Однако столь обширная информация, как программирование под Windows, не уместилась бы  в рамки данной статьи – поэтому мы будем учиться в условиях, «максимально приближенных к боевым». Итак: у нас есть MessageBox с сообщением, и нам нужно куда-то поставить брейкпойнт, чтобы выявить, откуда этот MessageBox появляется.

Первое, что Вам надо уяснить – это то, что одно и то же действие нередко может выполняться несколькими разными функциями WinAPI. Например, наш любимый MessageBoxможет создаваться не только функцией MessageBoxA, но и функциями MessageBoxExAи MessageBoxIndirectA. Наиболее широкими возможностями в области управления внешним видом MessageBox’а обладает функция MessageBoxIndirectA, наименьшими – собственно MessageBoxA (но зато последнюю гораздо удобнее вызывать – ведь у нее всего четыре параметра). В действительности MessageBoxA – не самостоятельная функция, а лишь удобная «обертка» для вызова MessageBoxExA. Да и сама MessageBoxExAдалеко не самодостаточна: в WindowsXP, например, она реализована через вызов функции MessageBoxTimeoutA. Что же у нас получилось: мы охотились за одной функцией создания окна с сообщением, а нашли целое гнездо функций, имеющих близкое назначение! Да, именно так – ради удобства программистов и компактности кода в WindowsAPIвходит немало функций, частично дублирующих друг друга. И если Вы хотите при помощи точек останова отследить выполнение той или иной операции – Вам нужно ставить точки останова на все функции API, которые эту операцию могут выполнять. В нашем примере, если Вы поставите точку останова на MessageBox, но забудете про экзотичный MessageBoxIndirect, Вы можете просто не обнаружить точку, в которой выводится сообщение. Поэтому рекомендую Вам пользоваться следующим правилом: точки останова лучше ставить не на отдельные функции API, а на всю группу функций, выполняющих близкие по смыслу действия.

Но как узнать, какие функции являются «близкими по смыслу» - спросите Вы. В этом обычно нет ничего сложного. Для начала Вам понадобится раздобыть документацию по программированию под Windows. В принципе, подойдет Win32 SDK(и даже он нужен не весь, а лишь справочные файлы по функциям WindowsAPI). Нужные файлы поставляется совместно с компиляторами под Windowsот Borlandили Microsoft, также нередко встречаются в Интернете (в том числе – частично переведенными на русский язык). Недостаток Win32 SDKв том, что он давно не обновляется – и, соответственно, не содержит информации по APIпоследних версий Windows. Более обширным источником, несомненно, является MSDNLibrary– ежеквартально обновляемый сборник документации по программированию под Windowsс использованием компиляторов, созданных этой фирмой. Встречается MSDNлибо в составе VisualStudio, либо отдельно, и занимает, как правило, несколько CD. Если Вы не испытываете хронический дефицит места на жестком диске, я бы рекомендовал использовать именно MSDN, причем как можно более новой версии. Помимо более свежей и подробной информации о системных вызовах Windowsтам Вы найдете еще и подробную информацию по классам MFC (что может сильно пригодиться Вам при исследовании программ, созданных с использованием MFC).

Как только у Вас на «винчестере» появится требуемая документация, поиск близких по смыслу команд для Вас перестанет представлять какую-либо сложность. Вам нужно будет лишь нажать кнопку «Group» в окне справки или заглянуть в конец справочной статьи и изучить назначение функций, имена которых стоят после слов «Seealso». Разумеется, можно (и нужно!) применить и другие приемы работы с документацией – просмотреть дерево словарных статей, воспользоваться контекстным поиском по заголовкам или просто выполнить поиск текста по всей справочной системе. Для тех, у кого пока нет под рукой нужной документации, я составил небольшой «поминальник» наиболее часто используемых функций WinAPI и областей, в которых эти функции используются. Однако не думайте, что Вы сможете обойтись лишь этим списком – функции WinAPIимеют не только имена, но и параметры, понимание назначения которых ничуть не менее важно, чем знание имен функций. Так что описание перечисленных функций читать все равно придется – свою же задачу я вижу в том, чтобы указать, какие разделы стоит изучить в первую очередь.

Упрощенный вывод сообщений

MessageBox, MessageBoxEx, MessageBoxIndirect, MessageBeep (эта функция не выводит сообщение, а только издает соответствующий звуковой сигнал)

Создание и уничтожение окон

CreateWindow(наиболее популярная функция создания окон), CreateWindowEx

CloseWindow (функция закрытия окна), DestroyWindow

Создание и уничтожение диалогов

CreateDialog, CreateDialogIndirect, CreateDialogIndirectParam, CreateDialogParam (этифункциитолькосоздаютдиалог)

DialogBox, DialogBoxIndirect, DialogBoxIndirectParam, DialogBoxParam (эта группа функций позволяет создавать модальные диалоги; управление не возвращается программе, пока диалог не будет закрыт)

EndDialog, DestroyWindow

Чтение и изменение текстов окон

GetWindowText, GetDlgItemText (чтение текста окна или элемента диалога)

GetWindowTextLength (чтение длины текста окна)

GetDlgItemInt (чтение текста элемента диалога как 32-битного числа)

SetWindowText, SetDlgItemText, SetDlgItemInt (установка нового текста окна или элемента диалога)

Изменение видимости, позиции и прочих подобных свойств окна

EnableWindow (активация/деактивацияокна)

ShowWindow, ShowWindowAsync (изменение видимости и состояния окна, в частности – позволяет минимизировать или наоборот развернуть окно)

SetWindowPos, MoveWindow (изменение положения окна)

SetWindowWord (устаревшая и практически не используемая функция), SetWindowLong – две функции, позволяющие модифицировать весьма широкий спектр параметров окна. GetWindowWord, GetWindowLong – функции чтения этих параметров.

Загрузка ресурсов

LoadImage (универсальная функция загрузки изображений, иконок и курсоров), LoadBitmap, LoadIcon, LoadCursor

FindResource, FindResourceEx, LoadResource (функциизагрузкиресурсовлюбоготипавпамять)

Отображение изображений и текстов на экране

BitBlt, StretchBlt, MaskBlt (функции копирования BITMAP’ов на экран)

DrawText, TextOut, TabbedTextOut (обычный вывод текста)

GrayString (редко используемая функция, выводит на экран строку со стилем надписи на неактивном управляющем элементе)

Работа с файлами

OpenFile (устаревшая функция), CreateFile (основная функция открытия файлов; несмотря на свое название способна открывать файлы и даже директории)

ReadFile, ReadFileEx (функции чтения из файлов)

WriteFile, WriteFileEx (функции записи в файл)

SetFilePointer (перемещениепофайлу)

GetFileTime, GetFileAttributes, SetFileTime, SetFileAttributes (чтениеимодификациявременисозданияиатрибутовфайлов/директорий)

SetEndOfFile (изменение размеров файла)

Операциисреестром

RegOpenKey, RegOpenKeyEx, RegCreateKey, RegCreateKeyEx (открытиеисозданиеключейреестра)

RegQueryInfoKey (запрос информации о ключе, в частности – для проверки факта существования подключа)

RegQueryValue,RegQueryValueEx (чтение значений из реестра)

RegSetValue, RegSetValueEx (запись ключей в реестр)

RegCloseKey (закрытие ключа реестра)

Чтение и запись INI-файлов

GetProfileSection, WriteProfileSection, GetProfileInt, GetProfileString, WriteProfileString, WriteProfileInt (функции для работы с файлом Win.ini, в настоящее время считаются устаревшими, но иногда используются)

GetPrivateProfileSection, GetPrivateProfileSectionNames, WritePrivateProfileSection, GetPrivateProfileInt, GetPrivateProfileString, GetPrivateProfileStruct, WritePrivateProfileString, WritePrivateProfileInt, WritePrivateProfileStruct (функцииработысобластьюреестра, отведеннойдляхранениянастроекпрограмм, либоспроизвольнымINI-файлом; этагруппафункцийсчитаетсяустаревшей)

Работа с датой и временем

GetSystemTime, GetLocalTime, GetSystemTimeAsFileTime (чтениетекущеговремени)

SetSystemTime, SetLocalTime(установка нового времени)

LocalTimeToFileTime, FileTimeToLocalTime, FileTimeToSystemTime, SystemTimeToFileTime (преобразованиеформатавремени)

CompareFileTime (сравнение двух переменных, хранящих время)

GetFileTime, SetFileTime (запись и чтение времени создания, последней модификации и последнего доступа к файлу)

Процессы и потоки: создание и управление

WinExec (устаревшая функция запуска исполняемых файлов), CreateProcess (функция, обычно используемая для запуска исполняемых файлов), ShellExecute, ShellExecuteEx (пара «альтернативных» функций для запуска приложений (применительно к исполняемым файлам) или открытия, печати и т.п. папок и документов).

 ExitProcess(«стандартное» завершение процесса, эта функция способна завершить только тот процесс, внутри которого она вызвана), TerminateProcess (принудительное завершение процесса; эта функция способна «убить» любой процесс (в NT – при наличии соответствующих привилегий), что иногда используется защитами для подавления крэкерского софта)

CreateThread (штатная функция создания нового потока), CreateRemoteThread (эта функция «живет» только под NT-подобными и на самом деле в защитах практически не используется. Зато очень, очень (я не забыл сказать «очень»?) широко используется самими крэкерами для внедрения в чужой процесс. Так что обойти ее вниманием в этом поминальнике было бы несправедливо)

ExitThread, TerminateThread(штатное завершение и аварийное уничтожение потоков соответственно)

Загрузкаивыгрузка DLL

LoadLibrary, LoadLibraryEx (функциизагрузкидинамическихбиблиотек)

LoadModule (устаревшая функция загрузки DLL)

GetProcAddress (функция, возвращающая адрес функции или переменной, объявленной в экспорте DLL, по имени этой функции/переменной (разумеется, соответствующая DLLдолжна быть подгружена текущим процессом). Эта функция широко используется как в защитах чтобы вызов какой-либо функции не «светился» в дизассемблированном листинге, а также для приемов типа push<желаемый адрес возврата>; jmp <адрес функции, полученный через GetProcAddress>, используемых для сокрытия точки, откуда была вызвана функция)

FreeLibrary (функция принудительной выгрузки DLL)

Надо сказать, список этот отнюдь не полный: в нем нет, к примеру, функций выделения блоков памяти (извлечь практическую пользу из информации о распределении памяти удается довольно редко, и те, области, где это актуально, отнюдь не относятся к «основам» крэкинга) и функций работы со строками (эти функции почти всегда дублируются в коде программы ради повышения быстродействия). Нет в этом списке и специфических библиотечных функций Microsoft’овской MFCи Borland’овских VCL/RTL – если включить в список и их, «поминальник» стал бы совершенно неподъемным. Все это мы оставим за рамками – тем более, что средства разработки не стоят на месте, и, возможно, через пару лет будут актуальны совершенно другие API. Попробуйте понять, из чего я исходил, составляя этот «поминальник» - и, поняв это, Вам станет заметно легче найти подход к ранее неизвестной системе программных интерфейсов. Я выбрал WinAPIлишь по той причине, что это – «основа основ» программирования под платформу Win32, которая в настоящее время наиболее распространена. Однако даже к изложенному выше стоит относиться с известной долей осторожности – поскольку линейка Windows 9xугасает, в ближайшем будущем вполне могут стать актуальными специфические для линейки NTсистемные вызовы, а Вы будете искать правды среди стандартных функций Win32 – и не найдете ее. Прецедент уже имел место – в многих крэкерских руководствах пятилетней давности рекомендовалось для гарантированного «отлова» считывания текста из окна устанавливать брейкпойнт на функцию hmemcpy. И это работало – но только под Windows 9x, поскольку в линейке NTвместо вызова этой функции для повышения скорости использовались inline-вставки. До тех пор, пока все семейство NT-подобных ограничивалось доисторической NT 3.51 и неудобоваримой NT 4, проблемы как бы не существовало. Но вот на наши десктопы пришли более симпатичные Windows 2000 и WindowsXP – и противоречие между старыми руководствами и наблюдаемой реальностью встало в полный рост.

Как Вы можете видеть, имена функций являются сокращенным описанием тех действий, которые эти функции выполняют. Таким образом, если Вы предполагаете существование некой функции WinAPI и хотите поставить на нее брейкпойнт, но не знаете (или просто забыли), как она называется, Вы можете легко это узнать, если владеете принятой программистами под Win32 терминологией и английским языком (или хотя бы русско-английским словарем). Кроме того, если всем другим отладчикам Вы предпочитаете SoftIce, у Вас есть возможность воспользоваться командой EXP<текстовая_строка>, которая позволяет вывести на консоль отладчика все функции, имена которых начинаются с указанной Вами строки. Например, команда EXPMessageBoxпокажет Вам все MessageBox’ы, какие только встречаются в символьной информации, импортированной из DLL (список DLLдля импортирования информации должен быть заранее прописан в инициализационном файле отладчика). Надо отметить, что команда EXPимеет несколько более широкие возможности, чем простой вывод списка функций, начинающихся с определенных символов – Вы можете также просмотреть список модулей, по которым имеется символьная информация, проверить, присутствует ли нужная функция в некотором модуле и многое другое, о чем можно прочесть в руководстве по этому отладчику.

Вы наверняка заметили, что в «поминальнике» отсутствуют функции MessageBoxAи MessageBoxExA, о которых я упоминал в начале статьи, а вместо них описаны лишь MessageBox, MessageBoxEx. Разумеется, это не опечатка. Если Вы уже попробовали «на вкус» SoftIce’овую команду EXPи приложили ее к нашему измученному брейкпойнтами (то ли еще будет!) MessageBox’у, то наверняка заметили, что в списке функций, экспортируемых из USER32.DLLприсутствуют MessageBoxAи MessageBoxW (а вот просто MessageBox’а нет и в помине). Откуда же взялись буквы Aи Wв именах функций и что они вообще значат?

Расшифровка этих букв проста: A– это ANSI, W – это WIDE. А появились эти буквы после того, как в Microsoftвзяли курс на внедрение кодировки UNICODE, и возникла необходимость как-то отличать «старые» варианты функций, работающие с традиционными текстовыми строками, от «новых», использующих кодировку UNICODE. А символы в UNICODE имеют тип WСHAR длиной в 16 бит против обычных восьми – поэтому к именам функций добавили букву W, а не U, как можно было бы предположить. Вот и получается, что если функция принимает или передает строковые параметры, программист должен указать, какой из вариантов функции нужно использовать. Разумеется, если функция WinAPIне получает и не возвращает никаких символьных параметров, буквы Aи Wей совершенно ни к чему. Так что единственная причина того, что в моем «поминальнике» начисто отсутствуют упоминания об ANSI- и UNICODE-вариантах одной и той же функции весьма банальна. Если бы я подробно расписывал оба варианта имени каждой функции, мне бы пришлось набирать этот список почти в два раза дольше, а Вам – во столько же раз дольше его читать, и при этом Вы бы не получили никакой новой информации. Поэтому я и решил не упражняться в крючкотворстве, а вместо этого объяснить причины использования окончаний Aи W. Надеюсь, что после моих объяснений Вы сможете подставить нужные буквы в имена функций самостоятельно.

Вряд ли Вам пригодится на практике эта информация, однако стремление к истине обязывает меня сообщить  Вам великую тайну. Знайте, что все-таки они существуют – я имею ввиду MessageBox’ы без буквы Aили W. Ибо давным-давно, когда на Земле жили 16-разрядные динозавры Windows 3.1 и WindowsforWorkgroups, скрывавшиеся в недрах этих ОС функции ничего не ведали ни об ANSI, ни об UNICODE – а потому не нуждались в буквах Aи W. И от тех древних времен в include-файлах все еще сохранились строчки вроде lstrcmp equ <lstrcmpA> - в целях совместимости и упрощения переноса старых исходников под новые компиляторы.

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

 - Если программа проверяет, сколько дней она работает – весьма вероятно, что она считывает текущую дату. Поэтому смотрим в раздел «Работа с датой и временем» и выбираем оттуда функции, предназначенные для чтения времени.

- Программа показывает MessageBox – следовательно, есть смысл поставить точки останова на функции создания MessageBox’ов.

- При истечении триального срока программа завершается – значит можно попытаться отловить вызов функции ExitProcess.

После расстановки брейкпойнтов на упомянутые функции и запуска программы отладчик скорее всего всплывет где-то внутри той ветки программы, которая ответственна за появление nagscreen’а и проверку триала. После этого Вы можете ликвидировать сам nagscreen(просто забив nop’ами все, что относится к вызову функции, создающей MessageBox) а затем методом обратной трассировки в уме (о сути этого метода я подробно расскажу чуть позже) добраться от точки вызова ExitProcessдо условия, по которому программа определяет, что триальный срок еще не кончился (и продолжает работать) или уже истек (и тогда вызывается ветка с ExitProcess). После этого, внеся исправления в исполняемый файл, Вы сможете организовать себе «вечный триал».

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

  1. Появление MessageBox’ов любых типов
  2. Создание диалогов на основе ресурсов
  3. Вызовы функций отображения окон (успешно работает только в том случае, если окон сравнительно немного – иначе становится сложно отследить среди всех вызовов соответствующих функций те, которые создают нужное окно)
  4. Считывание текущего времени (в тех случаях, когда программа имеет ограничение по времени использования, и nagscreenсодержит информацию о том, когда истечет испытательный срок)
  5. Создание таймеров (например, для активации управляющего элемента)
  6. Изменение состояния окон Windows(например, активация кнопки или помещение в заголовок окна какой-либо надписи)
  7. Вызовы вспомогательных функций, использующихся для изменения текста окон (к примеру, если в заголовке nagscreen’а присутствует надпись «Осталось Nдней пробного периода», можно предположить, что для подстановки числа дней в надпись может использоваться функция wsprintf).

Разумеется, не следует принимать приведенный список как «истину в последней инстанции» и даже как «руководство к действию» - реальность бывает весьма разнообразна, и втиснуть ее в семь нумерованных пунктов, ничего не потеряв, вряд ли возможно. Однако как отправная точка или в качестве информации к размышлению, я думаю, этот список будет небесполезен. Однако скажу, что полезность предлагаемой последовательности подтверждена  практикой – последовательная проверка каждого из пунктов этого списка во многих случаях позволяла мне легко добраться либо до процедуры создания/отображения нужного окна, либо до цикла обработки сообщений, получаемых этим окном.

Теперь Вам, пожалуй, известно о функциях WinAPIдостаточно, чтобы приступить к рассмотрению примеров, демонстрирующих, как эти знания применить для решения практическим задачам крэкинга. Так что в порядке эксперимента давайте немного взломаем какую-нибудь распространенную программу, например, Notepad. Это будет довольно несложный, но весьма поучительный эксперимент, демонстрирующий одну из техник ликвидации nagscreen’ов в «настоящих» программах. Итак, возьмем обыкновенный Блокнот (я взял тот, который входит в состав WindowsXP), скопируем его в надежное место, чтобы в случае чего можно было восстановить файл из резервной копии (вообще, возьмите себе за правило каждый успешный шаг отмечать резервными копиями файлов, дампами успешно модифицированных кусков и т.д. – очень неприятно бывает терять из-за ошибок результаты длительной и кропотливой работы) и запустим его. Теперь нажмите в Блокноте Ctrl-F (оно же «Правка|Найти…»), легонько стукните по клавиатуре левой пяткой (или просто наберите любой текст) и нажмите кнопку «Найти далее». Поскольку нашего «любого» текста в поле редактирования нет (потому что там вообще ничего нет), Блокнот ругнется сообщением вроде «Не удается найти "340t80543t"». Будем считать, что это и есть nagscreen, который нам предстоит ликвидировать.

Очевидно, что этот «nagscreen» очень сильно похож на MessageBox – поэтому вполне логичным было бы начать поиск нашего окошка с установки брейкпойнта на функции вывода стандартных окон с сообщениями (MessageBoxA, MessageBoxW, MessageBoxExA – и далее по списку). Грузим подопытную программу в отладчик (я в качестве отладчика буду использовать OllyDebug, но те, кто предпочитает SoftIce, тоже смогут без всяких сложностей повторить этот эксперимент) и устанавливаем точки останова. В SoftIceэто делается очень просто: «BPXимя_функции» - и так много-много раз, для всех подходящих функций из «поминальника». В OllyDebugэта операция выполняется несколько сложнее, зато Вы получите весьма неожиданный, но очень приятный сюрприз. Но приятные сюрпризы будут позже, а пока щелкните правой кнопкой мыши в окне кода и вызовите всплывающее меню. В этом меню вызовите пункт «Searchfor > Name (label) incurrentmodule» - этот пункт заставляет отладчик пробежаться по загруженному модулю (в нашем случае – по EXE’шнику Блокнота) и найти в нем все ссылки на «именованные» адреса (в частности, такими адресами являются вызовы WinAPI). Отладчик покажет окно с весьма немаленьким списком функций, на которые удалось найти ссылки в коде программы, и теперь в этом списке нам нужно найти наши МessageBox’ы. А вот и наступило время для обещанного приятного сюрприза: в списке нашлась только одна подходящая функция из «поминальника» - MessageBoxW. Так что в то время, как счастливые обладатели SoftIceсбивали в кровь руки, разбрасывая брейкпойнты на функции, которые вообще не используются в программе, пользователи OllyDebugбез всяких усилий получили информацию о том, с какого вызова APIлучше всего начать подбираться к нашему nagscreen’у. Пользователям же SoftIceчтобы получить список вызываемых программой функций APIпридется воспользоваться каким-нибудь дополнительным софтом, позволяющим просмотреть список импортируемых функций (благо таких программ немало).

В SoftIceбрейкпойнт на функцию ставится элементарно: BPXMessageBoxWи никаких гвоздей; в OllyDebugэта операция немного сложнее: найдя нужную строчку в списке импортированных функций, щелкните правой кнопкой мыши и в всплывшем меню выберите пункт «Followimportindisassembler». После этого отладчик мгновенно перенесет Вас в глубины одной из важнейших системных библиотек операционной системы прямо к первому байту функции MessageBoxW. После этого останется лишь нажать клавишу F2 и увидеть, как начальный адрес этой функции окрасится в красный цвет. На этом подготовительные операции закончены – пора отпустить программу на волю и начать охоту на наш «nagscreen».

Попробовав опять заставить Блокнот искать «то, не знаю что», мы с радостью видим, что наша точка останова, о необходимости которой я так долго говорил, наконец-то выполнила свою задачу и «тормознула» программу, как только та попыталась выполнить функцию MessageBoxW. В окне отладчика Вы увидите что-то вроде этого:

Код (Text):
  1. <code>
  2. 77D70956   >  6A 00         push    0
  3. 77D70958   .  FF7424 14     push    dword ptr ss:[esp+14]
  4. 77D7095C   .  FF7424 14     push    dword ptr ss:[esp+14]            ; |Title = "????"
  5. 77D70960   .  FF7424 14     push    dword ptr ss:[esp+14]            ; |Text = "????"
  6. 77D70964   .  FF7424 14     push    dword ptr ss:[esp+14]            ; |hOwner = 00010015
  7. 77D70968   .  E8 03000000   call    USER32.MessageBoxExW             ; \MessageBoxExW
  8. 77D7096D   .  C2 1000       retn    10</code>

И это есть ни что иное, как код функции MessageBoxWс комментариями, которые в него подставил OllyDebug (поскольку SoftIceникаких комментариев, кроме заранее определенных пользователем, подставлять не умеет, пользователям Айса придется немного напрячь воображение). При помощи кнопки F8 начинаем трассировать этот код («Ура! Мы отлаживаем ядро!») без захода в процедуры, и, дотрассировав до адреса 77D70968 с удивлением видим, что отладчик остановился, зато на экране появился наш MessageBox. А это значит, что наше исходное предположение насчет MessageBox’овой сущности «nagscreen’а» было верно. Закроем этот MessageBoxи продолжим трассировать до тех пор, пока не выйдем из процедуры по команде retn 10.

Пока Вы продавливаете до пола несчастную клавишу F8, я сделаю небольшое лирическое отступление. Вы наверняка уже прочувствовали, что много раз подряд нажимать F8  довольно скучно. А ведь MessageBoxW - это далеко не самая длинная процедура, нередко встречаются шедевры, в дизассемблированном виде занимающие десятки и сотни строк! И у Вас возник естественный вопрос – «неужели в таком большом и сложном отладчике нет какой-нибудь маленькой кнопки, чтобы сразу добраться до точки выхода из процедуры». Не подумайте о разработчиках плохого - они позаботились о нас, поэтому столь нужная кнопка в обеих отладчиках есть! В OllyDebugэта «кнопка» (она находится не только на панели инструментов, но и продублирована соответствующим пунктом меню) называется «Executetillreturn» и вызывается комбинацией Ctrl-F9. Заодно обратите внимание и на пункт меню «Executetillusercode»: если Вы когда-нибудь заплутаете в недрах чужой DLLи захотите одним махом выполнить весь код до того знаменательного момента, когда управление вернется в основную программу – смело жмите Alt-F9, и OllyDebugВам поможет. В SoftIceдля того, чтобы добраться до точки выхода из функции, служит команда pret, по умолчанию «подвешенная» на клавишу F12. Вы можете подумать, что если есть команда pret, которая заставляет программу работать до первого ret’а, то должны существовать команды pmov, ppushи т.п. Увы, это не так – разработчики SoftIceсочли, что и одного отслеживания ret’ов более чем достаточно.

Но вернемся к нашему Блокноту. После того, как Вы успешно выйдете из процедуры, OllyDebugпокажет примерно следующее:

Код (Text):
  1. <code>
  2. 01001F8C  |.  FF75 14       push    [arg.4]          ;  notepad.01009800
  3. 01001F8F  |.  FF75 10       push    [arg.3]
  4. 01001F92  |.  E8 58FFFFFF   call    notepad.01001EEF
  5. 01001F97  |.  FF75 18       push    [arg.5]          ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
  6. 01001F9A  |.  FF75 0C       push    [arg.2]          ; |Title = "Блокнот"
  7. 01001F9D  |.  56            push    esi              ; |Text = "Не удается найти "oierg""
  8. 01001F9E  |.  FF75 08       push    [arg.1]          ; |hOwner = 000D063A ('Найти',
  9.                                                      ; class='#32770',parent=004C0360)
  10. 01001FA1  |.  FF15 4C120001 call    dword ptr ds:[<&USER32.MessageBo>; \MessageBoxW
  11. 01001FA7  |.  56            push    esi              ; /hMemory = 0009BCB0
  12. 01001FA8  |.  8BF8          mov     edi, eax         ; |
  13. 01001FAA  |.  FF15 F4100001 call    dword ptr ds:[<&KERNEL32.LocalFr> \LocalFree</code>

Если у Вас вместо OllyDebug установлен SoftIce, Вы все равно увидите нечто подобное, но уже без подсказок о параметрах функций MessageBoxи LocalFree – SoftIceизлишней дружелюбностью не страдает, а потому услужливо выискивать и расшифровывать для Вас параметры системных вызовов не станет. А жаль. Один из способов сделать так, чтобы MessageBoxне больше появлялся, заключается в забивании nop’ами «лишних» команд – а именно вызова call    dword ptr ds:[<&USER32.MessageBoxW>]. Однако мало ликвидировать лишь сам CALL – нельзя забывать и о параметрах вызываемой функции. Потому что, если эти параметры не прибить тоже – они попадут в стек и так там и останутся до тех пор, пока процедуре, внутри которой эти параметры были положены на стек, не вздумается выполнить команду ret (или retn). Вот тут-то и начнется самое веселое: по-хорошему в момент выполнения команды retна вершине стека должен находиться адрес возврата, а в действительности окажется мусор, который должен был «уйти» в функцию MessageBoxWи благополучно исчезнуть в ее недрах. В зависимости от Вашей удачливости, настроения программы и текущей фазы Луны Вы получите либо банальный GPF (почти наверняка), либо какой-нибудь экзотический спецэффект вплоть до форматирования винчестера. Насчет форматирования - это, конечно, шутка, но шутка с долей правды – представьте, что случится, если на стеке в качестве адреса возврата окажется адрес какого-нибудь осмысленного куска кода. Так что после успешной ликвидации системного вызова не поленитесь разобраться и с PUSH’ами – пусть волшебная команда XCHGEAX,EAX (более известная как NOP) станет Вашим лучшим другом. Если же Вы питаете суеверный страх перед командой NOPили Вам просто лень много-много раз набирать код 90 – просто пропишите по адресу 01001F97 команду jmp 01001FA7; результат будет тот же самый, а пальцы устанут значительно меньше.

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

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

На практике такую проверку можно выполнить следующим образом: засечь значение регистра ESPна входе в процедуру и на выходе из нее, вычислить разность между этими значениями (тут полезно помнить, что соглашения вызова PASCALи STDCALLпредусматривают удаление параметров процедуры со стека внутри процедуры, а CDECL – коррекцию стека после завершения процедуры, уже в теле программы). Если после внесения модификаций разность между значениями ESPне изменилась – значит, все сделано правильно, если изменилась – ищите ошибку в своих действиях.

Разумеется, совершенно необязательно замерять разность между значениями ESPименно в начале и в конце процедуры. К примеру, если процедура, внутри которой мы «стираем» обращение к другой процедуре, вызывается при помощи обычного CALL, можно промерить значения ESPнепосредственно перед выполнением этого CALLи сразу после него. Чтобы корректно проверить, не «застряло» ли чего-нибудь в стеке, или наоборот, не удалили ли мы чего-нибудь лишнего,  необходимо соблюсти следующие условия:

1. При любом ходе исполнения кода (т.е. независимо от того, какие ветки срабатывают между точками, в которых проверяется положение стека) изменение величины ESPдолжно быть одинаковым.

2. Вы должны быть уверены, что все операции по помещению параметров на стек, относящиеся к проверяемому вызову, находятся между точками замеров. И вот здесь-то Вас могут подстерегать довольно неприятные неожиданности. Языки высокого уровня не позволяют по-настоящему изощренно работать со стеком (компиляторы С\C++, правда, позволяет динамически зарезервировать на стеке некоторую область и даже соорудить в этой области объект – но это максимум, на что может рассчитывать программист). Даже «очень оптимизирующие» компиляторы обычно генерируют помещение параметров на стек в непосредственной близости от вызова функции, которая эти параметры принимает – поэтому найти их не так уж сложно. Но вот ассемблер… Да, ассемблер способен изменить ситуацию коренным образом. Вместо банального MyFunction (my_param) Вы вольны написать что-то вроде

Код (Text):
  1. <code>
  2. push my_param
  3. <сотня строчек кода, не относящегося к делу>
  4. call MyFunction</code>

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

Раз уж зашла речь об антидизассемблерных приемах (хотя на самом деле эти приемы направлены не столько на то, чтобы сбить с толку дизассемблер, сколько на то, чтобы запутать крэкера, пытающегося осмыслить код), еще немного уклонюсь от основной темы главы и расскажу о паре хитростей, иногда использующихся для помещения параметров на стек. Основаны эти приемы на том, что регистр ESPмало чем отличается от регистров общего назначения, а область стека – от всех прочих областей памяти. Зная, что над регистром ESPвполне возможно производить арифметические операции сложения и вычитания, а также обращаться к содержимому стека при помощи косвенной адресации командами вроде mov [ESP+8],eax, нетрудно догадаться, что команду pusheaxможно заменить, к примеру, последовательностью

Код (Text):
  1. <code>
  2. sub esp,4
  3. mov [esp+4],eax</code>

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

Надеюсь, что я не слишком напугал Вас живописаниями тех ужасов, которые Вы можете встретить (а можете и не встретить)  на своем пути. С функциями WinAPI, как правило, все бывает гораздо проще – число параметров в любой момент можно посмотреть в документации, «ленивые» компиляторы складируют эти параметры непосредственно перед вызовом, и удаление «лишнего» вызова не представляет собой совершенно никакой сложности – примерно как убрать MessageBoxиз Блокнота.

А напоследок я расскажу об одном очень-очень простом способе, в некоторых случаях позволяющем найти место, в котором локализован вызов nagscreen’а. Суть способы очень проста: Вы загружаете программу и начинаете трассировать ее без захода внутрь функций, при каждом нажатии клавиши F8 запоминая текущий адрес (на практике почти всегда достаточно запоминать только адреса выполняемых call’ов). При выполнении одной из таких функции появится наш nagscreen. Снова загружаем программу, вспоминаем, каким был адрес того call’а, который вызвал появление окна и «прогоняем» программу до этого адреса в ускоренном режиме.

Перейдем ко второму шагу: зная, что создание nagscreen’а сидит где-то в глубинах вызываемой функции, войдем внутрь этой функции при помощи команды трассировки с заходом в функцию. Теперь начинаем трассировать содержимое этой функции, все так же запоминая адреса исполняемых команд. Рано или поздно мы наткнемся на очередной вызов, после которого выскочит nagscreen. Этот процесс постепенного погружения в код можно продолжать до тех пор, пока Вы не доберетесь до вызова WinAPI или другой библиотечной функции, создающей окно; не попадете в цикл обработки сообщений или ожидания закрытия окна nagscreen’а (что будет означать «перелет», но из этого результата тоже можно извлечь определенную пользу) либо не придете к выводу, что данная техника в Вашем случае неприменима.

Вообще говоря, эта методика применима не только для поиска вызовов nagscreen’ов, выводимых сразу после запуска. Если Вы знакомы с численными методами решения уравнений, Вы наверняка заметили некоторую аналогию с методом Ньютона: последовательно двигаясь вглубь кода и проверяя эффекты от вызовов подпрограмм, мы констатируем «недолет» либо «перелет» и постепенно сужаем область, в которой находится интересующий нас блок команд. Метод Ньютона изначально был предназначен для поиска решения на некотором промежутке значений, и, по аналогии, предлагаемый метод также может быть использован для поиска некоей функции «на промежутке кода». В качестве границ промежутка удобно использовать «знаковые» и легко отслеживаемые события, такие, как чтение текста из управляющего элемента, обращение к ячейке памяти, получение информации из реестра или из файла и отображение результатов этих действий в виде nagscreen’а, сообщения о неверном серийном номере и т.п. Проще говоря, если Вы ввели серийный номер, нажали кнопку «ОК» и программа в ответ сказала что-то вроде «Неправильно ты, дядя Федор, серийники вводишь» - значит, проверка правильности серийного номера лежит где-то между считыванием введенного серийника и выводом сообщения. И если Вам удастся зафиксировать оба этих события при помощи брейкпойнтов, трассируя и изучая код, лежащий между этими бряками Вы с большой вероятностью обретете желаемый адрес процедуры проверки серийника. А уж что Вы с этим адресом сделаете – зависит лишь от Ваших исходных целей и изобретательности.

Предлагаемый мной способ имеет несколько существенных ограничений: во-первых, трассируемый код должен исполняться последовательно (т.е. создание окна по таймеру или какому-либо иному событию таким способом отследить не получится или, по крайней мере, будет достаточно сложно). Во-вторых, очень желательно, чтобы создание и отображение nagscreen’а происходило в главном потоке программы (если nagscreenотображается в отдельном потоке, придется сначала выискивать место создания этого потока). И, в-третьих, если Вы активно практикуете дзен-крэкинг (то бишь Ваш любимый modusoperandi – «я не знаю, как это работает, но я все равно это сломаю»), Вы можете совершить следующую ошибку: «отключая» nagscreen, можно копнуть слишком глубоко и случайно «вынести» не процедуру отображения nagscreen’а, а более универсальную процедуру отображения окна вообще, которая по сути ни в чем не виновна. Для того, чтобы Вам было понятнее, о чем идет речь, продемонстрирую идею следующим псевдокодом:

Код (Text):
  1. <code>
  2. ShowNagScreen proc
  3.   …
  4.   invoke ShowAnyWindow, nag_screen_handle
  5.   …
  6. ShowNagScreen endp
  7.  
  8. ShowAnyWindow proc hwnd:DWORD
  9.   invoke ShowWindow, hwnd, SW_SHOWNORMAL
  10. ShowAnyWindow endp</code>

Предотвратить создание nagscreen’а в этом случае можно тремя способами:

  1. Убрать вызов самой процедуры ShowNagScreen
  2. В коде процедуры ShowNagScreenубрать вызов ShowAnyWindow
  3. Удалить вызов WinAPI’шной функции ShowWindow внутри ShowAnyWindow

Первый и второй способы, в принципе, вполне корректны (причем второй даже несколько лучше – в реальной процедуре создания nagscreen’а могут выполняться какие-нибудь дополнительные операции), а вот третий… Да, своего мы бы, конечно, добились – но вместе с nagscreen’ом исчезли бы и все другие окна, которые отображаются процедурой ShowAnyWindow. А если учесть, что в современных программах вызовы WinAPIнередко упрятаны глубоко в недра всевозможных библиотек, вопрос о том, как бы случайно не перестараться с поиском «корня зла» и от избытка чувств не пропатчить библиотечную функцию – отнюдь не праздный. И универсального решения этого вопроса, по-видимому, не существует (если, конечно, не считать таким решением доскональный анализ кода программы).

Однако я могу предложить Вашему вниманию два подхода, которые с достаточно высокой вероятностью позволяют определить, используется ли функция исключительно для единственной цели (в частности – для отображения nagscreen’а), или же является универсальной в рамках приложения. Первый подход основывается на том, что библиотечные и просто широко используемые функции, как правило, вызываются многократно из разных областей исполняемого кода, в то время, как ссылки на узкоспециализированные процедуры (такие, как проверка серийного номера на валидность или вывод сообщения об ограничениях в программе) обычно присутствуют лишь в двух-трех экземплярах на всю программу, и во время исполнения кода приложения срабатывают считанные разы. Выполнить проверку подозрительной функции на количество вызовов можно как при помощи дизассемблера, так и прямо в процессе отладки. Поскольку большинство дизассемблеров позволяют получить список ссылок на процедуру (а также точек, в которых эти ссылки находятся), анализ при помощи дизассемблера сводится к простому поиску нужной процедуры в выходном листинге дизассемблера  и визуальной оценке количества ссылок. Не могу еще раз не проагитировать Вас за использование OllyDebug: этот отладчик содержит несколько весьма удобных инструментов, скрывающихся внутри пункта меню «Findreferencesto…». В данном контексте нам, несомненно, особенно интересен подпункт «Selectedaddress», позволяющий найти все ссылки на команду под курсором. То есть, для выполнения описанной проверки Вы можете даже обойтись без дизассемблера; чтобы найти все прямые ссылки на некий адрес (например, на адрес первого байта функции), достаточно установить курсор на этот адрес и нажать Crtl-R. Если ссылок немного – значит, Вы нашли то, что искали. А вот если их количество перевалит за 5-8 штук – у Вас есть веские основания подозревать, что проверяемая функция выполняет некие общие функции, и потому Вам нужно подняться по дереву вызовов на уровень выше либо вообще искать нужный код в другой области. Раз уж речь зашла о дереве вызовов, добавлю еще следующее: даже удостоверившись в том, что проверяемая функция A вызывается один-единственный раз из функции B, не поленитесь посмотреть, что «растет» на дереве вызовов сверху – может оказаться, что сама функция Bвызывается в программе десятки раз. В этом случае вывод очевиден – Вы слишком увлеклись «глубоким бурением». Выполнить подобную проверку при помощи отладчика ничуть не сложнее – нужно всего лишь поставить точку останова на подозрительную функцию и запустить программу «с нуля». По количеству срабатываний точки останова, а также соотнося эти срабатывания с всплыванием nagscreen’а, Вы сможете сделать вывод о том, является функция «знаковой» для защиты или же просто исполняет некие более общие функции, прямого отношения к защитным механизмам не имеющие.

Другой способ выявления в общей массе библиотечных и других «общих» функций основывается на следующей особенности современных компиляторов: при сборке проекта функции размещаются в исполняемом файле в том порядке, в каком их обрабатывает компилятор, а код всевозможных библиотек помещается в начало либо в конец исполняемого файла. Кроме того, поскольку большинство проектов в настоящее время разбиты на модули, которые транслируются раздельно, получается так, что функции из одного модуля (обычно, к тому же, логически связанные, как того требуют принципы модульного программирования) располагаются по близким адресам. Как следствие, вызов функции из другого модуля или библиотеки в окне отладчика выглядит как очень длинный переход в далекие области памяти, в то время, как переходы внутри «своего» модуля являются сравнительно короткими (в смысле величины, на которую изменяется значение EIPпри переходе).

Вот и подошла к концу очередная глава. Надеюсь, что Вы узнали об основах дебаггинга достаточно, чтобы попробовать самостоятельно что-нибудь взломать. Раз уж мы начали с Блокнота – попробуйте сотворить что-нибудь эдакое с Калькулятором (операция деления на ноль – достаточно интересная область для экспериментов), а уж затем можно перейти от «учебных целей» и к «настоящим» задачам. Возможно, исследование «большого» приложения у Вас тоже пройдет как по маслу – но вполне может быть, что защита окажется достаточно серьезной, и в процессе анализа кода у Вас возникнут сложности. И потому следующая глава как раз и будет посвящена различным тонкостям и хитростям отладки, а также борьбе с некоторыми антиотладочными приемами.

© CyberManiac

0 996
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532