И вновь продолжается бой…
Контролирует ценность тот, кто может ее уничтожить
Ф. Херберт, «Дюна»
Ну вот, все, что мы могли сделать простыми средствами, мы сделали. И теперь пришло время приступить к отладке. Да-да, эта глава будет практически полностью посвящена искусству дебагить, 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» в окне справки или заглянуть в конец справочной статьи и изучить назначение функций, имена которых стоят после слов «See also». Разумеется, можно (и нужно!) применить и другие приемы работы с документацией – просмотреть дерево словарных статей, воспользоваться контекстным поиском по заголовкам или просто выполнить поиск текста по всей справочной системе. Для тех, у кого пока нет под рукой нужной документации, я составил небольшой «поминальник» наиболее часто используемых функций 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<текстовая_строка>, которая позволяет вывести на консоль отладчика все функции, имена которых начинаются с указанной Вами строки.
Например, команда EXP MessageBox покажет Вам все 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’ов проще всего, отслеживая при помощи точек останова следующие типы событий: Появление MessageBox’ов любых типов Создание диалогов на основе ресурсов Вызовы функций отображения окон (успешно работает только в том случае, если окон сравнительно немного – иначе становится сложно отследить среди всех вызовов соответствующих функций те, которые создают нужное окно) Считывание текущего времени (в тех случаях, когда программа имеет ограничение по времени использования, и nagscreenсодержит информацию о том, когда истечет испытательный срок) Создание таймеров (например, для активации управляющего элемента) Изменение состояния окон Windows(например, активация кнопки или помещение в заголовок окна какой-либо надписи) Вызовы вспомогательных функций, использующихся для изменения текста окон (к примеру, если в заголовке 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. В окне отладчика Вы увидите что-то вроде этого: 77D70956 > 6A 00 push 0 77D70958 . FF7424 14 push dword ptr ss:[esp+14] 77D7095C . FF7424 14 push dword ptr ss:[esp+14] ; |Title = "????" 77D70960 . FF7424 14 push dword ptr ss:[esp+14] ; |Text = "????" 77D70964 .
FF7424 14 push dword ptr ss:[esp+14] ; |hOwner = 00010015 77D70968 . E8 03000000 call USER32.MessageBoxExW ; \MessageBoxExW 77D7096D . C2 1000 retn 10 И это есть ни что иное, как код функции MessageBoxWс комментариями, которые в него подставил OllyDebug (поскольку SoftIce никаких комментариев, кроме заранее определенных пользователем, подставлять не умеет, пользователям Айса придется немного напрячь воображение). При помощи кнопки F8 начинаем трассировать этот код («Ура! Мы отлаживаем ядро!») без захода в процедуры, и, дотрассировав до адреса 77D70968 с удивлением видим, что отладчик остановился, зато на экране появился наш MessageBox. А это значит, что наше исходное предположение насчет MessageBox’овой сущности «nagscreen’а» было верно. Закроем этот MessageBox и продолжим трассировать до тех пор, пока не выйдем из процедуры по команде retn 10. Пока Вы продавливаете до пола несчастную клавишу F8, я сделаю небольшое лирическое отступление. Вы наверняка уже прочувствовали, что много раз подряд нажимать F8 довольно скучно. А ведь MessageBoxW - это далеко не самая длинная процедура, нередко встречаются шедевры, в дизассемблированном виде занимающие десятки и сотни строк! И у Вас возник естественный вопрос – «неужели в таком большом и сложном отладчике нет какой-нибудь маленькой кнопки, чтобы сразу добраться до точки выхода из процедуры». Не подумайте о разработчиках плохого - они позаботились о нас, поэтому столь нужная кнопка в обеих отладчиках есть! В OllyDebug эта «кнопка» (она находится не только на панели инструментов, но и продублирована соответствующим пунктом меню) называется «Executetillreturn» и вызывается комбинацией Ctrl-F9. Заодно обратите внимание и на пункт меню «Executetillusercode»: если Вы когда-нибудь заплутаете в недрах чужой DLL и захотите одним махом выполнить весь код до того знаменательного момента, когда управление вернется в основную программу – смело жмите Alt-F9, и OllyDebug Вам поможет. В SoftIce для того, чтобы добраться до точки выхода из функции, служит команда p ret , по умолчанию «подвешенная» на клавишу F12.
Вы можете подумать, что если есть команда pret, которая заставляет программу работать до первого ret’а, то должны существовать команды pmov, ppush и т.п. Увы, это не так – разработчики SoftIceсочли, что и одного отслеживания ret’ов более чем достаточно. Но вернемся к нашему Блокноту. После того, как Вы успешно выйдете из процедуры, OllyDebug покажет примерно следующее: 01001F8C |. FF75 14 push [arg.4] ; notepad.01009800 01001F8F |. FF75 10 push [arg.3] 01001F92 |. E8 58FFFFFF call notepad.01001EEF 01001F97 |. FF75 18 push [arg.5] ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL 01001F9A |. FF75 0C push [arg.2] ; |Title = "Блокнот" 01001F9D |. 56 push esi ; |Text = "Не удается найти "oierg"" 01001F9E |. FF75 08 push [arg.1] ; |hOwner = 000D063A ('Найти', ; class='#32770',parent=004C0360) 01001FA1 |. FF15 4C120001 call dword ptr ds:[<&USER32.MessageBo>; \MessageBoxW 01001FA7 |. 56 push esi ; /hMemory = 0009BCB0 01001FA8 |. 8BF8 mov edi, eax ; | 01001FAA |. FF15 F4100001 call dword ptr ds:[<&KERNEL32.LocalFr> \LocalFree Если у Вас вместо 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) Вы вольны написать что-то вроде push my_param <сотня строчек кода, не относящегося к делу> call MyFunction А если как следует поразмыслить и поиграть с значениями регистров ESP и EBP, можно сотворить такое, что все ныне существующие дизассемблеры вывихнут свои виртуальные мозги, пытаясь разобраться, где у такой функции лежат параметры и где - локальные переменные. Правда, у подобных «антидизассемблерных» техник есть и обратная сторона – все это довольно долго пишется, тяжело отлаживается и защищает только от новичков и лентяев (надеюсь, что после прочтения этой главы Вы в число таких новичков и лентяев не попадете). Вот и получается, что замерять значения регистра ESP лучше всего на первой и на последней команде процедуры, когда вышеописанные «фокусы» со стеком еще себя не проявили, либо свое уже отработали. Раз уж зашла речь об антидизассемблерных приемах (хотя на самом деле эти приемы направлены не столько на то, чтобы сбить с толку дизассемблер, сколько на то, чтобы запутать крэкера, пытающегося осмыслить код), еще немного уклонюсь от основной темы главы и расскажу о паре хитростей, иногда использующихся для помещения параметров на стек.
Основаны эти приемы на том, что регистр ESP мало чем отличается от регистров общего назначения, а область стека – от всех прочих областей памяти. Зная, что над регистром ESP вполне возможно производить арифметические операции сложения и вычитания, а также обращаться к содержимому стека при помощи косвенной адресации командами вроде mov [ESP+8],eax, нетрудно догадаться, что команду push eax можно заменить, к примеру, последовательностью sub esp,4 mov [esp+4],eax И это только простейший способ замены одной-единственной команды… А если таких команд – много? А если загрузку значения на стек выполнять не целиком и одномоментно, а по одному байтику? А если функция, которая будет обрабатывать эти значения, вызывается не при помощи CALL, а каким-нибудь более изощренным образом? Подумайте на досуге о том, какие приемы могли бы помочь Вам преодолеть все эти сложности (кое-что я продемонстрирую Вам в следующей главе). Надеюсь, что я не слишком напугал Вас живописаниями тех ужасов, которые Вы можете встретить (а можете и не встретить) на своем пути. С функциями WinAPI, как правило, все бывает гораздо проще – число параметров в любой момент можно посмотреть в документации, «ленивые» компиляторы складируют эти параметры непосредственно перед вызовом, и удаление «лишнего» вызова не представляет собой совершенно никакой сложности – примерно как убрать MessageBox из Блокнота. А напоследок я расскажу об одном очень-очень простом способе, в некоторых случаях позволяющем найти место, в котором локализован вызов nagscreen’а. Суть способы очень проста: Вы загружаете программу и начинаете трассировать ее без захода внутрь функций, при каждом нажатии клавиши F8 запоминая текущий адрес (на практике почти всегда достаточно запоминать только адреса выполняемых call’ов). При выполнении одной из таких функции появится наш nagscreen. Снова загружаем программу, вспоминаем, каким был адрес того call’а, который вызвал появление окна и «прогоняем» программу до этого адреса в ускоренном режиме. Перейдем ко второму шагу: зная, что создание nagscreen’а сидит где-то в глубинах вызываемой функции, войдем внутрь этой функции при помощи команды трассировки с заходом в функцию.
Теперь начинаем трассировать содержимое этой функции, все так же запоминая адреса исполняемых команд. Рано или поздно мы наткнемся на очередной вызов, после которого выскочит nagscreen. Этот процесс постепенного погружения в код можно продолжать до тех пор, пока Вы не доберетесь до вызова WinAPI или другой библиотечной функции, создающей окно; не попадете в цикл обработки сообщений или ожидания закрытия окна nagscreen’а (что будет означать «перелет», но из этого результата тоже можно извлечь определенную пользу) либо не придете к выводу, что данная техника в Вашем случае неприменима. Вообще говоря, эта методика применима не только для поиска вызовов nagscreen’ов, выводимых сразу после запуска. Если Вы знакомы с численными методами решения уравнений, Вы наверняка заметили некоторую аналогию с методом Ньютона: последовательно двигаясь вглубь кода и проверяя эффекты от вызовов подпрограмм, мы констатируем «недолет» либо «перелет» и постепенно сужаем область, в которой находится интересующий нас блок команд. Метод Ньютона изначально был предназначен для поиска решения на некотором промежутке значений, и, по аналогии, предлагаемый метод также может быть использован для поиска некоей функции «на промежутке кода». В качестве границ промежутка удобно использовать «знаковые» и легко отслеживаемые события, такие, как чтение текста из управляющего элемента, обращение к ячейке памяти, получение информации из реестра или из файла и отображение результатов этих действий в виде nagscreen’а, сообщения о неверном серийном номере и т.п. Проще говоря, если Вы ввели серийный номер, нажали кнопку «ОК» и программа в ответ сказала что-то вроде «Неправильно ты, дядя Федор, серийники вводишь» - значит, проверка правильности серийного номера лежит где-то между считыванием введенного серийника и выводом сообщения. И если Вам удастся зафиксировать оба этих события при помощи брейкпойнтов, трассируя и изучая код, лежащий между этими бряками Вы с большой вероятностью обретете желаемый адрес процедуры проверки серийника.
А уж что Вы с этим адресом сделаете – зависит лишь от Ваших исходных целей и изобретательности. Предлагаемый мной способ имеет несколько существенных ограничений: во-первых, трассируемый код должен исполняться последовательно (т.е. создание окна по таймеру или какому-либо иному событию таким способом отследить не получится или, по крайней мере, будет достаточно сложно). Во-вторых, очень желательно, чтобы создание и отображение nagscreen’а происходило в главном потоке программы (если nagscreenотображается в отдельном потоке, придется сначала выискивать место создания этого потока). И, в-третьих, если Вы активно практикуете дзен-крэкинг (то бишь Ваш любимый modusoperandi – «я не знаю, как это работает, но я все равно это сломаю»), Вы можете совершить следующую ошибку: «отключая» nagscreen, можно копнуть слишком глубоко и случайно «вынести» не процедуру отображения nagscreen’а, а более универсальную процедуру отображения окна вообще, которая по сути ни в чем не виновна. Для того, чтобы Вам было понятнее, о чем идет речь, продемонстрирую идею следующим псевдокодом: ShowNagScreen proc … invoke ShowAnyWindow, nag_screen_handle … ShowNagScreen endp ShowAnyWindow proc hwnd:DWORD invoke ShowWindow, hwnd, SW_SHOWNORMAL ShowAnyWindow endp Предотвратить создание nagscreen’а в этом случае можно тремя способами: Убрать вызов самой процедуры ShowNagScreen В коде процедуры ShowNagScreen убрать вызов ShowAnyWindow Удалить вызов WinAPI’шной функции ShowWindow внутри ShowAnyWindow Первый и второй способы, в принципе, вполне корректны (причем второй даже несколько лучше – в реальной процедуре создания nagscreen’а могут выполняться какие-нибудь дополнительные операции), а вот третий… Да, своего мы бы, конечно, добились – но вместе с nagscreen’ом исчезли бы и все другие окна, которые отображаются процедурой ShowAnyWindow. А если учесть, что в современных программах вызовы WinAPI нередко упрятаны глубоко в недра всевозможных библиотек, вопрос о том, как бы случайно не перестараться с поиском «корня зла» и от избытка чувств не пропатчить библиотечную функцию – отнюдь не праздный.
И универсального решения этого вопроса, по-видимому, не существует (если, конечно, не считать таким решением доскональный анализ кода программы). Однако я могу предложить Вашему вниманию два подхода, которые с достаточно высокой вероятностью позволяют определить, используется ли функция исключительно для единственной цели (в частности – для отображения nagscreen’а), или же является универсальной в рамках приложения. Первый подход основывается на том, что библиотечные и просто широко используемые функции, как правило, вызываются многократно из разных областей исполняемого кода, в то время, как ссылки на узкоспециализированные процедуры (такие, как проверка серийного номера на валидность или вывод сообщения об ограничениях в программе) обычно присутствуют лишь в двух-трех экземплярах на всю программу, и во время исполнения кода приложения срабатывают считанные разы. Выполнить проверку подозрительной функции на количество вызовов можно как при помощи дизассемблера, так и прямо в процессе отладки. Поскольку большинство дизассемблеров позволяют получить список ссылок на процедуру (а также точек, в которых эти ссылки находятся), анализ при помощи дизассемблера сводится к простому поиску нужной процедуры в выходном листинге дизассемблера и визуальной оценке количества ссылок. Не могу еще раз не проагитировать Вас за использование OllyDebug: этот отладчик содержит несколько весьма удобных инструментов, скрывающихся внутри пункта меню «Findreferencesto…». В данном контексте нам, несомненно, особенно интересен подпункт «Selectedaddress», позволяющий найти все ссылки на команду под курсором. То есть, для выполнения описанной проверки Вы можете даже обойтись без дизассемблера; чтобы найти все прямые ссылки на некий адрес (например, на адрес первого байта функции), достаточно установить курсор на этот адрес и нажать Crtl-R. Если ссылок немного – значит, Вы нашли то, что искали. А вот если их количество перевалит за 5-8 штук – у Вас есть веские основания подозревать, что проверяемая функция выполняет некие общие функции, и потому Вам нужно подняться по дереву вызовов на уровень выше либо вообще искать нужный код в другой области.
Раз уж речь зашла о дереве вызовов, добавлю еще следующее: даже удостоверившись в том, что проверяемая функция A вызывается один-единственный раз из функции B, не поленитесь посмотреть, что «растет» на дереве вызовов сверху – может оказаться, что сама функция Bвызывается в программе десятки раз. В этом случае вывод очевиден – Вы слишком увлеклись «глубоким бурением». Выполнить подобную проверку при помощи отладчика ничуть не сложнее – нужно всего лишь поставить точку останова на подозрительную функцию и запустить программу «с нуля». По количеству срабатываний точки останова, а также соотнося эти срабатывания с всплыванием nagscreen’а, Вы сможете сделать вывод о том, является функция «знаковой» для защиты или же просто исполняет некие более общие функции, прямого отношения к защитным механизмам не имеющие. Другой способ выявления в общей массе библиотечных и других «общих» функций основывается на следующей особенности современных компиляторов: при сборке проекта функции размещаются в исполняемом файле в том порядке, в каком их обрабатывает компилятор, а код всевозможных библиотек помещается в начало либо в конец исполняемого файла. Кроме того, поскольку большинство проектов в настоящее время разбиты на модули, которые транслируются раздельно, получается так, что функции из одного модуля (обычно, к тому же, логически связанные, как того требуют принципы модульного программирования) располагаются по близким адресам. Как следствие, вызов функции из другого модуля или библиотеки в окне отладчика выглядит как очень длинный переход в далекие области памяти, в то время, как переходы внутри «своего» модуля являются сравнительно короткими (в смысле величины, на которую изменяется значение EIP при переходе). Вот и подошла к концу очередная глава. Надеюсь, что Вы узнали об основах дебаггинга достаточно, чтобы попробовать самостоятельно что-нибудь взломать. Раз уж мы начали с Блокнота – попробуйте сотворить что-нибудь эдакое с Калькулятором (операция деления на ноль – достаточно интересная область для экспериментов), а уж затем можно перейти от «учебных целей» и к «настоящим» задачам.Возможно, исследование «большого» приложения у Вас тоже пройдет как по маслу – но вполне может быть, что защита окажется достаточно серьезной, и в процессе анализа кода у Вас возникнут сложности. И потому следующая глава как раз и будет посвящена различным тонкостям и хитростям отладки, а также борьбе с некоторыми антиотладочными приемами. [C] CyberManiac Содержание Далее
Если бряк оказался вдруг…
Наверное, Вы уже попытались что-нибудь взломать. Может быть даже, Вам это удалось – за счет знаний и способностей к анализу, благодаря интуиции, или же в силу Вашего трудолюбия и настойчивости. Возможно также, что Вам просто очень повезло с первой в жизни программой, и защита оказалась слабее, чем в большинстве других программ. Однако тех, кто не смог с первой попытки одержать победу над мегабайтами кода, гораздо больше. Кто-то споткнулся об антиотладочные приемы, кому-то «повезло» встретиться с запакованной программой, кто-то принял близко к сердцу огромнейшие возможности, предоставляемые OllyDebug и SoftIce, и погрузился в изучение этих инструментов, отложив до времени собственно копание в коде. Некоторые отступили, не добравшись до подходящей зацепки, с которой можно было бы начать «раскручивать» защиту. Свежие ощущения, новые знания, предвкушение будущих побед – все, что знаменовало рождение крэкера, осталось в светлом прошлом, куда Вы сможете вернуться лишь в мечтах. В общем, одни радуются своей первой победе, другие – переводят дыхание и с тоской глядят на заоблачные выси, которые не удалось достичь. Если Вы попали в число «других», значит, у нас есть кое-что общее – свою первую программу я взломал далеко не с первой попытки. Надеюсь, после этих слов у Вас появился повод для оптимизма – возможно, именно Вам в будущем суждено написать свои собственные «Теоретические основы…». Однако сейчас Вас, наверняка, больше интересует другой вопрос – «почему мне не удалось сломать программу?» Причем нередко этот вопрос обретает еще более конкретную формы – «почему я ставлю брейкпойнты, а они не срабатывают?» и «как отлаживаемая программа может обнаружить мои точки останова?» И вопросы о неработающих (или «странно» работающих) брейкпойнтах – это отнюдь не повод упрекнуть в невнимательности начинающего крэкера, но основание для подробного разговора об особенностях Win32 API, тонкостях работы точек останова и антиотладочных приемах.
Брейкпойнты – лучшие друзья крэкера, готовые в любой момент прийти Вам на помощь.
Однако эти друзья отнюдь не всемогущи; как и живым людям, им присущи определенные слабости и врожденные особенности. И чтобы «поладить» с точками останова, нужно обладать знаниями об этих особенностях и слабостях – это, в конечном итоге, позволит Вам при помощи нехитрых приемов отлавливать весьма замысловатые ситуации и успешно обходить защитные механизмы, направленные на «вырубание» брейкпойнтов. Но прежде чем приступать к познанию столь высоких сфер, как внутреннее устройство и принципы функционирования точек останова, разберемся с куда более приземленными причинами возможной неработоспособности брейкпойнтов. Самой простой (и, к сожалению, отнюдь не самой редкой) причиной такого поведения наших верных друзей являются ошибки в коде отладчиков. Да-да, вы не ослышались, крэкерам нередко приходится тратить часы на поиски несуществующих защит именно из-за недоработок в используемом инструментарии. «SoftIce не ставит бряк на функции», «Symbol loader не останавливает программу после загрузки» и другие подобные проблемы, с которыми сталкивался едва ли не каждый пользователь этого отладчика, уже который год отравляют жизнь крэкерам. При некотором упорстве и настойчивости эти проблемы иногда удается обойти разными «шаманскими» приемами, например, использованием аппаратного брейкпойнта вместо обычного или указанием адресов в явном виде, но даже такие «танцы с бубном» не всегда оказываются эффективны против сущностей, скрывающихся по ту сторону отладчика. Никакие конкретные рекомендации тут, понятное дело, дать невозможно – программные глюки бесконечно разнообразны, и без точного знания причины с ними можно бороться разве что методом терпеливого перебора всех «обходных путей», какие только придут Вам в голову. Если Вас не прельщает сей метод – есть смысл поискать другую версию продукта (поскольку глюки, присущие одной версии программы, могут полностью отсутствовать в другой, пусть даже более старой), либо вообще подумать об обновлении инструментария. Ненамного отстают по популярности среди авторов защит всевозможные приемы определения присутствия отладчиков, от откровенно примитивной проверки наличия определенных файлов/ключей реестра (отдельные разработчики защит даже удаляют эти ключи, нимало не утруждая свой беспросветно могучий интеллект мыслями о том, что SoftIce можно приобрести легально и использовать не для взлома их поделок) до довольно изощренных антиотладочных приемов, использующих особенности конкретных отладчиков.
Примерами таких особенностей могут служить «черные ходы» в SoftIce для взаимодействия с Bounds Checker’ом или нездоровая реакция на вызов IsDebuggerPresent в OllyDebug и всех остальных отладчиках, использующих Debug API. Кстати, признаки наличия отладчика могут быть не только информационными, но и физическими: программа может «догадаться» о том, что ее ломают, по ненормально большому времени выполнения тех или иных процедур. Задумайтесь над тем, сколько времени уходит на выполнение десятка команд в «ручном режиме», когда Вы исступленно давите кнопку F8 в OllyDebug - и Вы сразу поймете, что я имею в виду. К этой же группе можно отнести использование в защитных механизмах отладочных регистров процессора: поскольку эти регистры используются отладчиком для установки брейкпойнтов, одновременная их эксплуатация программой и отладчиком невозможна, если попытаться проделать такое, либо отладчик «забудет» аппаратные точки останова, либо защитные процедуры выдадут некорректный результат со всеми вытекающими из этого последствиями. Большинство антиотладочных приемов, разумеется, давно и хорошо известны, и их описание несложно найти в руководствах по крэкингу и написанию защит. Впрочем, авторы защит на такие приемы обычно всерьез не рассчитывают, поскольку идентифицировать (а часто – и обойти) антиотладочный код в дизассемблерном листинге обычно несложно (например, если прикладная программа пытается оперировать с отладочными регистрами, это очевидный признак того, что в коде «что-то нечисто»), а некоторые крэкерские инструменты среди своих функций имеют отслеживание популярных антиотладочных приемов (примером может служить старая утилита FrogsIce, которая умела выявлять и побеждать множество защитных трюков, направленных против SoftIce). Наиболее популярным среди начинающих крэкеров, по-видимому, еще долго будет оставаться вопрос: «я поставил брейкпойнты на GetWindowText’ы и GetDlgItemText’ы, и все равно не могу поймать момент чтения серийника из окна». Действительно, формально все вроде бы сделано правильно, и все подходящие функции из «поминальника» обвешаны точками останова, как новогодняя елка – игрушками, но отладчик все равно не подает ни малейших признаков активности.
При этом все точки останова находятся на своих местах и вполне успешно срабатывают – но, увы, не по тому поводу, который Вам интересен. В общем, у неопытного кодокопателя может сложиться впечатление, что серийный номер считывается при помощи телепатии или, как минимум, весьма недокументированным способом, чтобы обнаружить который нужно иметь не меньше семи пядей во лбу. Однако в действительности никаких телепатических датчиков в Вашем компьютере нет (а если даже и есть, то вряд ли они используются для чтения текста из диалоговых окон), да и подозревать недокументированные приемы я бы тоже не торопился, поскольку существует куда более простое объяснение этого явления. В Windows с давних пор сосуществуют два различных механизма, позволяющих управлять окнами и некоторыми другими объектами. Об одном из этих механизмов – системных вызовах Windows API я уже говорил, и даже дал в предыдущей главе небольшой список наиболее употребительных функций с комментариями по поводу области их применения. Другая же сторона Windows до настоящего момента как-то оставалась в тени, за исключением эпизодических упоминаний «по поводу». Вы, наверное, уже догадались, что это за «другая сторона Windows»: я говорю о широко используемых в нашей любимой ОС сообщениях (хотя, если быть до конца точным, сообщения в том или ином виде присутствуют в большинстве современных операционных систем).
Если функции WinAPI безраздельно властвуют в темном и мрачном царстве невизуальных объектов, таких, как файлы, процессы, средства синхронизации и прочее, то в «оконной» области ситуация отличается разительным образом. Сравнительно небольшой набор системных вызовов общего назначения («создать-включить-удалить окно») с лихвой компенсируется огромным разнообразием системных сообщений (в англоязычной документации – «messages»; общее число документированных сообщений уж перевалило за тысячу), подчас дублирующих функции WinAPI (например, сообщение WM_GETTEXT, которое способно читать текст окна не хуже, чем уже известная Вам функция GetWindowText).
Некоторые типы управляющих элементов, такие, как обычные или выпадающие списки, вообще не имеют полноценной «обвязки» функциями Win32 API и управляются с ними именно при помощи сообщений. Вы не сможете добавить в такой список строчку или перейти к нужной позиции, вызвав WinAPI’шную функцию с названием вроде ComboBoxAddString или ComboBoxSetPos – таких функций в системных библиотеках Windows просто нет. Зато есть сообщения CB_ADDSTRING и CB_SETCURSEL соответственно, воспользовавшись которыми, Вы легко выполните задуманное. То есть, сообщения играют роль параллельного механизма управления объектами ОС, на работу которого совершенно не влияют традиционные брейкпойнты, которые мы щедрой рукой сеяли в предыдущей главе. Поскольку сообщение – не функция, брейкпойнт на него поставить нельзя. Но очень хочется. А если очень хочется – значит, все-таки можно, хотя и не так просто, как хотелось бы. Прежде всего, Вам нужно определиться, что именно Вы хотите отловить – момент и точку отправки сообщения, либо подпрограмму обработки этого сообщения. Если Ваc интересует второй вариант и Вы являетесь поклонником SoftIce – считайте, что о Вас уже позаботилась фирма NuMega (ныне - Compuware). Встроенная в SoftIce команда BMSG как раз для этого и предназначена, но чтобы успешно ее использовать, Вам понадобится узнать хэндл окна, которому предназначено сообщение. Если нужные данные у Вас имеются – просто набирайте BMSG <хэндл_окна> <код_сообщения>, и ждите, когда «всплывет» отладчик. Разумеется, команда BMSG, как и любая другая команда установки брейкпойнтов, позволяет создавать условные точки останова, срабатывающие, например, при поступлении сообщений только с определенными значениями wParam и lParam. А что делать тем, кто пользуется другими отладчиками, в которых аналог BMSG отсутствует? Ответ на этот вопрос находится, как ни странно, именно в руководстве по SoftIce. В частности, там написано, что действие команды BMSG может быть воспроизведено установкой условного брейкпойнта на оконную процедуру, причем в качестве условия нужно указать следующее: IF (esp->8)==<имя_сообщения>; адаптация этого условия под синтаксис, принятый в конкретном отладчике, обычно сложности не представляет, хотя вместо символьного имени сообщения скорее всего придется подставить его код (коды сообщений можно найти в файлах windows.inc, winnt.h или Messages.pas – в зависимости от того, компилятор какого языка у Вас есть под рукой; те, кто не обзавелся подходящим компилятором и не планируют им обзаводиться в ближайшем будущем, могут заглянуть в файл messages.lst из состава InqSoft Window Scanner). Такой подход несколько сложнее, чем вызвать команду BMSG с нужными параметрами, но зато он дает одно немаловажное преимущество.
В предыдущем абзаце я сделал замечание насчет необходимости знать хэндл окна для успешного применения этой команды. Однако хэндл окна может быть Вам известен далеко не во всех случаях. Рассмотрим, к примеру, ситуацию, когда Вам нужно оттрассировать обработчик сообщения WM_INITDIALOG. Вы не сможете просто взять и посмотреть нужный Вам хэндл, поскольку окно только-только создано и могло еще даже не появиться на экране. Конечно, можно «заморозить» все исследуемое приложение и затем предпринять поиск среди всех окон в системе, но не кажется ли Вам, что это несколько сложнее, чем хотелось бы? Кроме того, при каждом запуске программы хэндлы меняются, что тоже отнюдь не упрощает отладку. А вот оконная процедура всегда находится на одном и том же месте (справедливости ради надо отметить, что создание «плавающей» по адресному пространству от запуска к запуску процедуры теоретически возможно, хотя я такое и не встречал). И потому адрес этой процедуры можно просто записать на бумажке, чтобы затем восстанавливать соответствующий бряк без каких-либо сложностей. Нам осталось только раздобыть адрес этой самой процедуры. Тут тоже, в принципе, ничего сложного нет – разумеется, если под рукой имеются соответствующие инструменты (к примеру, Microsoft Spy++ или все тот же InqSoft Window Scanner). Наведите «прицел» программы на интересующее Вас окно и прочитайте желанный адрес оконной процедуры собственно окна (обычно этот адрес обозначается как WndProc) или оконной процедуры, сопоставленной классу окна. Иной путь получения адреса оконной процедуры заключается в том, чтобы при помощи API-шпионов обнаружить системный вызов, при помощи которого производится регистрация класса окна (функции WinAPI RegisterClass и RegisterClassEx) либо непосредственно создание окна (список соответствующих функций я приводил в предыдущей главе). Операция эта выполняется в три этапа:
Запускаем под API-шпионом, настроенным на отслеживание процедур создания окон, и ждем появления нужного окна.
Как только окно появится – останавливаем работу шпиона и при помощи любого сканера окон получаем хэндл этого окна.
Если адрес оконной процедуры находится среди параметров функции создания окна - ищем в логе, сгенерированном API-шпионом, функцию, которая возвращает значение нашего хэндла, и считываем ее параметры, среди которых находим искомый адрес оконной процедуры.
Если адрес оконной процедуры находится в структуре, указатель на которую передается в функцию регистрации классов – считываем адрес, откуда был произведен вызов этой функции.
Затем загружаем программу в отладчик, ставим точку останова на этот адрес (или чуть выше – на том месте, где происходит запись в стек указателя на структуру, это уж как Вам больше понравится) и запускаем программу. Как только исполнение программы прервется на нашем брейкпойнте – находим в памяти структуру, указатель на которую передается в RegisterClass[Ex] и аккуратно переписываем содержимое поля этой структуры, содержащее адрес оконной процедуры для регистрируемого класса.
Вас может смутить сложность четвертого пункта, который выполняет весьма несложные функции, но при этом его описание едва ли не длиннее предыдущих трех. Казалось бы, что нам мешает просто вытащить из лога API-шпиона значение указателя на структуру, по-быстрому снять дамп нужной области и прочитать желанный адрес? В принципе, ничего не мешает – но вот истинное содержимое структуры WNDCLASSEX Вы таким способом вряд ли прочитаете – потому что скорее всего в момент снятия дампа эта структура уже давно будет затерта другими данными. Дело в том, что регистрация класса – событие разовое, и потому память под структуру, описывающую класс, редко выделяют статически; обычно же программист обходится для этих целей куском стека. Так что когда Вы заберетесь своим дампером в адресное пространство процесса, в том месте, где находилась желанная структура, давно уже будут лежать другие данные. И единственным решением в данном случае мог бы быть интеллектуальный API-шпион, которому можно было бы объяснить правила извлечения полей структур из памяти. К сожалению, на данный момент API-шпионы с такими свойствами мне не известны. Другой распространенной причиной, по которой может «не работают» брейкпойнты, является маскировка действий защиты под что-нибудь совершенно безобидное или нетривиальная реализация защитных механизмов. В повседневной жизни мы очень часто руководствуемся правилом «если что-то выглядит, как утка и крякает, как утка – значит, это и есть утка». Более того, данное правило - один из столпов того, что мы называем здравым смыслом.
Однако Вы наверняка замечали, что правило это – не без изъяна, и не так уж редко видимая картина мира отнюдь не соответствует истинной. В крэкинге это противоречие между видимым эффектом и скрытым от невооруженного глаза назначением защитного кода может быть доведено до предела, поскольку, взламывая программу, крэкер не просто копается в машинном коде, но ведет интеллектуальный поединок с автором защиты. И со стороны противника можно ожидать всего – блефа в виде процедур-«пустышек», имитирующих защиту, сверхсложных схем, решающих простейшие задачи, ловких имитаций, призванных повести крэкера по ложному пути, и, наконец, многоуровневой системы проверок, которые не слишком сложно реализуются, но достаточно долго и нудно обезвреживаются. При написании защит редко задаются вопросами оптимальности, скорости и расхода ресурсов – все эти добродетели программирования приносятся в жертву защищенности.
Я уже приводил пример того, как программа считывала дату своей установки под видом поиска плагинов в своей директории, и, разумеется, этим список возможных приемов маскировки одних действий под другие не исчерпывается. Программа eXeScope, например, в качестве сообщения об ограничении в незарегистрированной версии выдает окно, внешним видом точь-в-точь повторяющее стандартный MessageBox, но в действительности нарисованное визуальными средствами в Delphi. Отображение файла в память вместо обычного чтения в буфер – прием известный, и, тем не менее, чтение файла лицензии таким способом вполне может поставить в тупик начинающего крэкера. Я уж не говорю о таких изощренных техниках, как парсинг ini-файлов «вручную» (после чего можно очень долго возиться с точками останова на GetPrivateProfile* - разумеется, с нулевым результатом) или экспорт кусков реестра при помощи утилиты regedit во временный файл с последующим анализом этого файла (что позволяет обойтись без вызова функций работы с реестром внутри программы). Однако наиболее интересным для читателя, я думаю, будет рассмотрение причин, по которым точки останова просто исчезают из отлаживаемой программы.
Я мог бы просто назвать причину таких мистических исчезновений и изложить типовой способ решения этой проблемы, но, думаю, Вам будет гораздо интереснее понять причины, по которым «теряются» брейкпойнты. А уж теоретические знания помогут Вам самостоятельно найти подходы к решению этой проблемы еще до того, как Вы доберетесь до готовых рецептов. Очевидно, что прежде чем разбираться в защитных приемах, подавляющих точки останова, нужно сначала понять физический смысл этих самых точек, то есть узнать, что они собой представляют, как устанавливаются и по каким признакам программа может догадаться об их наличии. А поскольку точки останова – изобретение отнюдь не новое, рассказ о них следует начать с исторического экскурса в седую древность. В свое время самым популярным отладчиком для «Спектрума» был MONS (впрочем, некоторые люди, включая меня, предпочитали MON) – восьмикилобайтное порождение программистской мысли, способное загружаться в ОЗУ с любого адреса и управляемое из командной строки (прямо как SoftIce – внешнее сходство этих двух отладчиков вообще сложно не заметить). И, разумеется, MONS позволял ставить брейкпойнты – еще бы, не имея в своем арсенале такой возможности, этот отладчик вряд ли стал бы столь популярен. Но поскольку процессор Z80, на основе которого был сделан «Спектрум», никаких отладочных средств не предоставлял, авторам MONS пришлось реализовывать точки останова чисто программными средствами. Реализация эта красотой отнюдь не блистала – «установка брейкпойнта» по-Спектрумовски заключалась в подстановке в нужное место кода трехбайтной команды CALL xxxx, которая передавала исполнение в недра самого отладчика и таким образом приостанавливала исполнение пользовательского кода. Старые команды, код которых затирался брейкпойнтом, копировались в специальный буфер и дополнялись командой JP (аналог jmp из набора команд x86) для возврата к следующей команде, не испорченной CALL’ом. Исполнение в пошаговом режиме выглядело не менее оригинальным – исполняемая команда перебрасывалась в отдельный буфер, дополнялась все тем же JP, после чего отладчик передавал управление в этот буфер.
Если еще вспомнить, что в Z80 существовали недокументированные команды, которые были известны далеко не всем отладчикам (и потому могли обрабатываться некорректно), отлаживаемая программа даже при абсолютно корректной работе могла испортить код отладчика, а под сам отладчик могло элементарно не хватить свободной памяти, и потому его загружали на место «ненужных» данных – Вы поймете, что представляла собой отладка в старые добрые времена.
Разработчики линейки x86 проявили больше заботы о программистах. В этой линейке процессоров вместо самодельной «затычки» в виде команды вызова подпрограммы для отладочных целей ввели отдельное прерывание с номером 3, которое вызывалось однобайтной командой (опкод команды int 3 – СС), в отличие от всех прочих прерываний, которые менее чем двумя байтами вызвать не получится. Другим полезным нововведением стала возможность исполнять код в пошаговом режиме через управление флагом трассировки (эта возможность, впрочем, мало актуальна для отладчиков пользовательского уровня под современные ОС). Однако, несмотря на такой, казалось бы, очевидный прогресс в развитии средств отладки, обыкновенные точки останова все так же, как и десятилетия назад, модифицируют исполняемый код, а потому легко обнаруживаются даже простейшими способами, например, проверкой контрольной суммы всех байтов (не говоря уже о CRC32 и использовании иных, еще более сложных и надежных хэш-функций). Самостоятельно убедиться в том, что точки останова модифицируют код, Вы можете за считанные секунды: откомпилируйте при помощи любого ассемблера следующие две строчки, возьмите OllyDebug и загрузите в него откомпилированный код.
addr1: mov eax,addr1 mov al, byte ptr [eax]
Если Вы просто выполните этот код в пошаговом режиме, то в регистре AL окажется число 0B8h.В этом нет ничего удивительного, B8 – это опкод команды mov eax, <число>. А теперь попробуйте поставить брейкпойнт на команду mov eax,addr1 и снова оттрассируйте этот код. После выполнения второй команды Вы увидите, что в регистре AL находится число 0CCh, хотя код в окне отладчика внешне совершенно не изменился (если, конечно, не считать изменением подсветку адреса, на который поставлен брейкпойнт).
Самое интересное, что отладчики могут «приукрашать реальность» не только в окне кода, но и при просмотре данных. Давайте проделаем еще один весьма поучительный в этом смысле эксперимент: загрузим наш пример из двух команд, поставим точку останова на первую и запишите адрес этой точки останова. Затем берем InqSoft Window Scanner и читаем байт по записанному адресу. Получаем, разумеется, 0CCh. А теперь взглянем на ту же область глазами отладчика (в OllyDebug это пункт меню Follow in dump|Selection) – и очень сильно удивляемся. Отладчик показывает нам совсем не то, что реально читается из памяти в регистр AL, a то, что должно было бы находиться по указанному адресу, если бы мы не поставили точку останова. Но и это еще не все! Посмотрите на динамические подсказки под окном кода – там-то как раз содержимое памяти отображается как надо.
Вот так «умные» отладчики помогают самообманываться начинающим крэкерам: отсутствие видимых изменений в коде наводит человека, не знакомого с тайнами устройства брейкпойнтов, на мысль о том, что прерывание исполнения программы в точке останова происходит по воле неких таинственных сил, с которыми отладчик находится в телепатической связи. Хотя на самом деле «классические» точки останова – это ни что иное, как обычные memory patch’и – а потому и обнаруживаются теми же самыми способами, что и любые другие исправления в коде. Кстати, из того, что обычный (не аппаратный) брейкпойнт является ничем иным, как исправлением программы, есть одно интересное следствие. Дело в том, что SoftIce’у в общем-то без разницы, каким образом в программе появилась команда int 3 – главное, что он может на этой команде остановиться не хуже, чем на настоящем брейкпойнте. А после того, как отладчик остановится, можно внести любые поправки в содержимое регистра EIP и код программы, после чего продолжить исполнение как ни в чем не бывало (собственно, в OllyDebug тоже можно проделать такую операцию при помощи пункта New origin here из всплывающего меню). Польза от такого эрзац-брейкпойнта (после срабатывания которого, к тому же, нужно вручную восстанавливать код, который находился на месте int 3 и править EIP), на первый взгляд кажется весьма сомнительной, но она есть.
Я уже упоминал глюк в SoftIce, когда отлаживаемая программа после загрузки Symbol loader’ом начинает немедленно выполняться, хотя крэкеру хотелось бы ее в этот момент притормозить. Так вот, если в Entry point исполняемого файла воткнуть опкод 0CCh, у подопытной программы не будет ни единого шанса избегнуть процесса отладки – поскольку первой командой окажется наш int 3, принудительно активирующий отладчик. Теперь, когда мы знаем, что точки останова обнаружить можно (и даже знаем, как их можно обнаружить), можно вернуться к основному вопросу этой главы – «почему точки останова не срабатывают». В нашем случае этот вопрос можно даже конкретизировать – «какими способами подопытная программа может удалить из себя точку останова». В различных источниках мне неоднократно встречалось предложение использовать для этой цели коды коррекции ошибок, предваряя все «критичные ко взлому» участки программы вызовом функции проверки и восстановления кода. В случае изменения кода из-за появления точек останова процедура восстановления должна откорректировать «неправильные» байты. Теоретически такая схема вполне возможна, но на практике алгоритмы коррекции ошибок довольно сложны в реализации и не слишком производительны, так что народные массы эту идею не приняли. А вот более простой вариант восстановления кода из «резервной копии», расположенной в другом конце программы (или прямого вызова этой резервной процедуры вместо основной), таки имел место во времена ДОСа; впрочем я не удивлюсь, если выяснится, что такой прием до сих пор в ходу – реализация очень проста, а какой-никакой эффект все-таки имеется. На практике дело обстоит еще хуже – для удаления некоторых точек останова не нужны ни коды коррекции ошибок, ни резервные копии. И именно к таким точкам останова относятся всеми нами любимые BPX’ы на вызовы функций WinAPI (и, если смотреть шире, на вызовы практически любых функций). Поскольку «брейкпойнт на функцию» - это на самом деле всего лишь брейкпойнт на первый байт этой функции, самый простой из приемов, удаляющих точки останова, выглядит следующим образом: заранее узнать адреса нужных функций при помощи GetProcAddress, прочитать их первые байты (если речь идет о внутренних функциях программы – то просто прочитать содержимое соответствующей ячейки) и сохранить значения этих эталонных байтов.
Затем перед особо критичными вызовами нужно лишь сравнивать первые байты процедур с эталонным, и, если обнаружится несоответствие, восстанавливать их. Сам факт того, что процедура начинается с опкода 0CCh говорит о том, что на эту процедуру поставлена точка останова, что может побудить программу предпринять некоторые действия по самозащите. Если учитывать, что многие процедуры начинаются стандартной последовательностью команд push ebp; mov ebp, esp (в шестнадцатиричном редакторе эти команды выглядят как последовательность 55 8B EC), то «действия по самозащите» могут быть простой записью в первые три байта процедуры той самой стандартной последовательности 55 8B EC. После этой операции точка останова, разумеется, исчезнет. Разумеется, выявить защиту от брейкпойнтов, основанную на проверке содержимого неких адресов в памяти, не слишком сложно – нужно лишь поставить аппаратную точку останова на чтение/запись первого байта функции и посмотреть, где этот «капкан на защиту» сработает. Другой способ постановки бряков на импортированные из DLL функции основан на том, что вызов импортированной функции почти всегда выполняется не напрямую, а через «переходник». На практике вызовы через «переходник» обычно выполняются одним из двух способов. Первый способ:
call <переходник_к_MyFunc> ; Вызов функции API … переходник_к_MyFunc: jmp MyFunc
(этот способ вызова функций наиболее распространен; переходники вида «jmp истинный_адрес_функции» обычно собраны в конце программы) Второй способ:
mov edi, dword ptr ds:[элемент_в_таблице] call edi … элемент_в_таблице: dd <истинный_адрес_функции_MyFunc>
(данная техника вызова функций обычно встречается в продуктах Microsoft) Идея заключается в том, что в первом случае точку останова можно поставить не на первый байт функции, а на переходник, через который вызывается эта функция. В первом случае это будет обычный BPX на адрес команды jmp MyFunc, во втором случае придется прибегнуть к аппаратной точке останова на чтение двойного слова по адресу «элемент_в_таблице».
Поскольку это не будут брейкпойнты на функцию в прямом смысле слова, этот метод имеет одно существенное ограничение: если нужная функция вызывается не через «переходник», а непосредственно по значению ее адреса (получаемому, например, при помощи GetProcAddress), то такой брейкпойнт, понятное дело, не сработает. Разумеется, также существует возможность, что программа попытается проверить целостность «переходников», но как такие попытки обнаруживать, Вы уже знаете. Если некая процедура вызывается внутри программы стандартным образом, то большинство современных компиляторов генерирует последовательность команд push для помещения параметров функции на стек, собственно переход к процедуре выполняется при помощи команды CALL, а после того, как функция отработает, управление возвращается на команду, следующую за CALL. Однако если программист имеет достаточно высокую квалификацию, он может внести заметное разнообразие в эту картину при помощи «ручного» вызова функций средствами ассемблера. Хотя великий Intel завещал нам вызывать процедуры и функции при помощи специально для этого придуманной команды CALL, отдельным гражданам закон не писан (надо отметить, в число этих граждан входят не только авторы защит, но и любители предельной оптимизации, а также фанаты нетрадиционного программирования). И вот эти странные граждане сочинили несколько имитаций несчастной команды CALL, и эти имитации давно и прочно вошли в арсенал разработчиков защит. Из универсальных нетрадиционных средств вызова подпрограмм прежде всего нужно назвать следующие:
push <адрес возврата> jmp <адрес процедуры>
или
push <адрес возврата> push <адрес процедуры> ret
Более сложные техники неявной передачи управления основаны на умышленном создании и обработке исключительных ситуаций, вызове прерываний и эксплуатации особенностей конкретных ОС. Эти техники сами по себе представляют весьма значительный интерес – с точки зрения как крэкера, так и программиста, однако их количество практически бесконечно, а сложность нередко выходит далеко за пределами «основ».
Чтобы Вы имели представление о том, насколько обширна эта тема, сообщу, что любые функции WinAPI (да и вообще любого другого API), в параметрах которых фигурирует callback-функция, могут служить инструментом неочевидного вызова пользовательского кода. Вместо рассмотрения всего этого бесконечного разнообразия возможных приемов (большинство из которых Вы, возможно, вообще никогда не встретите) мы углубимся в исследование возможностей приведенной выше пары базовых «заменителей CALL», понимание которых в итоге дает ключ к «раскалыванию» многих других способов неочевидного вызова процедур. Прежде всего следует отметить, что помещение на стек адреса возврата в этих методах отделено от собственно вызова процедуры, что позволяет вклинить между двумя этими действиями практически любой код, например, кусок вызываемой процедуры – и, соответственно, вызывать эту процедуру не с первого байта, а «с середины». Например, вот таким образом:
push <адрес возврата> push ebp mov ebp, esp jmp MyProc+3 ; (1) …
MyProc: push ebp mov ebp,esp … ; При вызове процедуры в точке (1) будет выполнен переход в эту точку
Как видите, хотя приведенный кусок кода по функциональности полностью аналогичен тривиальному call MyProc, явным образом процедура MyProc нигде не вызывается. Причем при помощи макросов можно добиться того, что все вызовы процедуры MyProc в программе будут выглядеть именно таким образом! Так что, сколько бы Вы ни ставили брейкпойнтов на адрес MyProc, ни один из них никогда не сработает – по той простой причине, что управление на этот адрес просто никогда не передается, хотя все внешние признаки могут говорить о том, что процедура MyProc успешно отработала. Впрочем, обмануть такой защитный прием обычно не составляет никакой сложности – нужно лишь ставить точку останова не на первую команду в процедуре, а где-нибудь подальше, например, после третьей (можно даже на команду выхода из процедуры, но при этом следует помнить, что процедура может содержать несколько точек выхода) или вообще в какой-нибудь подпрограмме, вызываемой этой процедурой (вторым способом я нередко пользуюсь, когда ставлю точки останова на функции WinAPI). Другая проблема, которую порождают нетрадиционные способы вызова процедур, заключается в том, что адрес возврата может быть совершенно любым, а не только адресом команды, следующей за командой вызова.
Этот прием встречается довольно часто, когда автор защиты хочет скрыть адрес какой-либо функции, к примеру, проверки корректности введенного серийного номера. Рассуждая логически, он понимает, что необходимо максимально осложнить крэкеру нахождение связи между появлением сообщения о неверном серийнике и процедурой, этот серийник проверяющей. А поскольку традиционным приемом поиска такой процедуры является BPX MessageBox с последующим наблюдением, куда вернется программа из MessageBox’а – программист делает вывод, что хорошо бы сделать так, чтобы программа вернулась «не туда», т.е. как можно дальше от процедуры проверки серийника. В этом случае даже поставив брейкпойнт «куда надо», мы не узнаем, по какому поводу был произведен вызов функции – на стеке будет лежать совершенно другой адрес возврата. И особенно неприятно для начинающего крэкера, когда в качестве адреса возврата оказывается что-нибудь вроде адреса функции ExitProcess. В общем случае «победить» такой прием можно либо через долгую медитацию с массированным применением шестнадцатиричного редактора/дизассемблера для поиска всех точек, в которых программа явно или неявно оперирует адресами нужных функций WinAPI, либо через поиск «модифицированным методом Ньютона», описанным в предыдущей главе, либо применив средства трассировки кода, имеющиеся в SoftIce или OllyDebug. Собственно, трассировка в таких случаях является орудием, по свойствам приближающимся к термоядерной бомбе: редкий код способен выдержать такой удар, но чтобы получить желаемый эффект, требуется весьма серьезное техническое обеспечение (процесс трассировки требует немалых объемов памяти и достаточно быстрого процессора) и грамотный выбор области применения. Так что, прежде чем пускать в ход «тяжелое вооружение», есть смысл подумать о решении проблемы более простыми средствами. Одним из таких более простых средств является исследование содержимого стека на предмет «застрявших» в нем полезных данных и адресов. Суть метода заключается в следующем: любой кусок кода в программе существуют не сами по себе, но находятся во взаимодействии с чем-то, и каждая из процедур может быть как вызываемой (из процедуры более высокого уровня), так и вызывающей «подчиненные» ей процедуры.
И даже когда программист скрыл точку вызова конкретной процедуры (что, как я продемонстрировал, не так уж сложно), то спрятать от пытливого взора следы, оставленные выше- и нижележащими процедурами, ему могло и не удаться. А если «могло не удаться» - есть смысл попробовать отыскать эти следы. По традиции, для начала разберемся, что эти следы из себя представляют. Представьте себе следующую широко распространенную ситуацию: A=>B=>C, где «A=>B» расшифровывается как «процедура A вызывает процедуру B». При вызове процедуры B стандартными средствами, т.е. командой CALL, в момент начала исполнения процедуры B на вершине стека будет лежать адрес возврата из процедуры B в процедуру A. Аналогичный процесс происходит при вызове процедуры C процедурой B. Получается, что если мы поставим брейкпойнт на точку входа в процедуру C, мы в этот момент сможем наблюдать в стеке следующую картину (вершина стека - вверху):
Адрес возврата из процедуры C в процедуру B
Адрес возврата из процедуры B в процедуру A
Немного усложним картину, и допустим, что в процедуры B и C передаются некие параметры (порядок передачи параметров для нас в данном примере несущественен). Стек в этом случае будет выглядеть следующим образом:
Адрес возврата из процедуры C в процедуру B
Параметры, переданные в процедуру C
Адрес возврата из процедуры B в процедуру A
Параметры, переданные в процедуру B
На практике процедуры обычно занимаются чем-то более сложным, чем простой вызов других процедур с параметрами, а потому довольно часто резервируют на стеке место под локальные переменные. Допустим, что процедуры A и B используют локальные переменные, место под которые выделяется на том же стеке, и посмотрим, что после этого будет твориться в стеке:
Адрес возврата из процедуры C в процедуру B
Параметры, переданные в процедуру C
Область локальных переменных процедуры B
Адрес возврата из процедуры B в процедуру A
Параметры, переданные в процедуру B
Область локальных переменных процедуры A
А теперь представим, что автор защиты на этапе B=>С подменил адрес возврата из процедуры С в процедуру B своим собственным значением, и возврат теперь происходит не в B, а некую процедуру D.Что от этого изменится? Да только одна, самая верхняя строчка! А вот адрес возврата в процедуру A, локальные переменные и параметры вызова какими были, такими и останутся, и это можно использовать в качестве зацепки, позволяющей выявить все этапы пути от процедуры A к процедуре C. Другое дело, что информация, лежащая в стеке, никак не структурирована, поэтому Вам придется самому угадывать, что там – локальные переменные, что – параметры вызовов, а что – адреса возврата. И хотя процесс проверки этих догадок может быть весьма трудоемким, лучше иметь хотя бы такую беспорядочную информацию, чем не иметь никакой. Иногда бывает полезно посмотреть, что находится выше вершины стека – там нередко тоже удается обнаружить следы деятельности процедур, отработавших перед тем, как мы остановили программу.
[C] CyberManiac
Содержание
Далее
Слишком хорошо – тоже не хорошо.
Как Вы уже успели убедиться, обращение с брейкпойнтами – занятие далеко не такое простое, как может показаться с первого взгляда. В предыдущей главе мы разбирались в том, почему точки останова иногда не делают того, что мы от них ожидаем, значительная же часть этой главы посвящена полностью противоположной проблеме: что делать, если точки останова срабатывают слишком часто. После всего написанного в предыдущей главе такая ситуация может показаться Вам невероятной, но, представьте себе, такое тоже бывает.
Если Вы входите в число поклонников отладчика SoftIce, то Вы не могли не заметить одну милую особенность этого инструмента: если Вы поставили брейкпойнт непосредственно на системную функцию, SoftIce будет «всплывать» при каждой попытке исполнить эту функцию независимо от того, внутри какого процесса функция была вызвана. Такое поведение отладчика, несомненно, бывает полезным при отладке драйверов, хук-процедур и самой операционной системы. Но наша беда (или счастье – это уж с какой стороны посмотреть) в том, что мы отлаживаем «обычную» программу, и постоянные выпадения в отладчик по совершенно неинтересным поводам – это далеко не то, о чем мы мечтали. Вот если бы удалось сделать так, чтобы брейкпойнты работали внутри только одного процесса… Те, кто начал знакомиться с SoftIceпо пакету NuMegaDriverStudio 2.6, скорее всего не увидят в этом никакой проблемы: BPX <имя_функции> IFPID=<ID_нужного_процесса> - и дело в шляпе. Набрав это заклинание, они, в общем-то, будут полностью правы, ибо это вполне хороший и, в общем-то, самый короткий путь к цели. И я никогда не скажу дурного слова про тех, кто последует этим путем. Однако этот путь – далеко не единственный из возможных, и если Вам интересны иные способы решения проблемы постановки брейкпойнта – добро пожаловать в музей истории SoftIce.
Мое общение с этим отладчиком началось с версии 3.23 для Windows 9x, случайно обнаруженной на свежем «хакерском» диске. Пусть по нынешним меркам тот СофтАйс совершенно не производит впечатления - тогда это был шедевр! Впрочем, то был шедевр не без недостатков, самым главным из которых была его неразборчивость в срабатывании брейкпойнтов.
Как я отмечал, поставить брейкпойнт на функцию WinAPI, срабатывающий исключительно внутри нужного процесса (по-научному брейкпойнты с такими свойствами называются «address-contextsensitive», они же «контекстно-зависимые»), напрямую было невозможно. Чтобы почувствовать, насколько серьезной была проблема, представьте себе следующую картину: бряк, поставленный на функции чтения из реестра (которая называется RegQueryValue[Ex], надеюсь, Вы еще не забыли мой «поминальник»), работает настолько хорошо, что отлавливает абсолютно все попытки чтения из реестра, независимо от того, выполняет их отлаживаемая Вами программа или какая-либо другая. Любые попытки осмысленной отладки в такой ситуации заведомо обречены на провал, единственное, чем Вы будете заниматься – это нажимание клавиш Ctrl-D, ибо Windowsнастолько сильно любит читать данные из реестра, что делает это много раз в секунду (если хотите своими глазами посмотреть на эту странную любовь, RegMon Вам в этом поможет). Так что же, поставить бряк на RegQueryValueExи получить от этого удовлетворительный результат совсем никак невозможно? Как бы не так! «Если нельзя, но очень хочется, то все-таки можно». Начнем с небольшого, но очень важного определения: адресный контекст (он же контекст процесса) – это все виртуальное адресное пространство, выделенное данному процессу. «Военная хитрость», при помощи которой мы «проапгрейдим» контекстно-независимый брейкпойнт до контекстно-зависимого, основана на том факте, что хотя SoftIce и игнорирует контекст в момент срабатывания контекстно-независимых брейкпойнтов (в число которых входят и брейкпойнты на функции WinAPI), сам текущий адресный контекст от этого никуда не исчезает. И потому в момент срабатывания условного брейкпойнта отладчик вполне способен прочитать любые данные из существующего в этот момент виртуального адресного пространства. Проще говоря, если брейкпойнт сработал внутри программы X, то отладчик сможет «увидеть» адресное пространство программы X вместе со всеми данными, содержащимися в этом пространстве, а заодно и содержимое всех регистров, каким оно было в момент срабатывания брейкпойнта.
А если отладчик «видит» все адресное пространство процесса и способен читать исполняемый код программы, значит, можно попытаться идентифицировать процессы по особенностям их исполняемого кода! На практике эта «идентификация по особенностям исполняемого кода» выглядит весьма прозаично: нужно забраться при помощи шестнадцатиричного редактора внутрь секции кода (впрочем, для идентификации можно использовать и любые другие заведомо не изменяющиеся при работе программы данные), выдернуть оттуда первый попавшийся DWORD (назовем его My_DWORD) и запомнить виртуальный адрес (My_Addrсоответственно), по которому этот DWORD находился. Дальнейшие операции, выполняемые уже в отладчике, ненамного сложнее: BPXимя_функции_WinAPIIF(*My_Addr)==My_DWORD. Все. Думаю, с пониманием того, что делает эта команда, ни у кого сложностей не возникло: мы соорудили условную точку останова, которая в момент срабатывания проверяет, не лежит ли по адресу My_Addrдвойное слово, равное My_DWORD. И если такое двойное слово по нужному адресу обнаруживается, то отладчик приостанавливает исполнение программы. Поскольку случайно встретить пару программ, у которых в секции кода по одним и тем же виртуальным адресам находились бы одинаковые DWORD’ы, вряд ли возможно, такой простейший способ различения процессов в абсолютном большинстве случаев отлично срабатывает. Сложности могут возникнуть только в двух случаях: при отладке упакованных программ и если нужно параллельно отлаживать два экземпляра («экземпляр» следует понимать как «instance») одного и того же приложения – поскольку код обеих процессов идентичен, в общем случае различить их по содержимому адресного пространства затруднительно. В настоящее время применение описанного выше метода для определения, в контексте какого из процессов сработал брейкпойнт, в общем-то, не требуется – в современных версиях SoftIce(вообще, сам отладчик SoftIce, особенности работы с ним и различия между разными его версиями – это отдельная большая тема, которая будет закрыта не раньше, чем прекратится развитие самого SoftIce) эта проблема решается безо всяких ухищрений.
Однако поскольку особенности отдельных инструментов имеют весьма слабое отношение к теоретическим вопросам крэкинга, которым посвящена данная работа, я в дальнейшем не буду акцентировать внимание на этих особенностях – разобравшись в предлагаемом материале, Вы сами найдете способ с максимальной эффективностью использовать эти особенности. Для того же, чтобы избежать путаницы при изложении материала этой главы, мы будем считать, что все брейкпойнты, о которых ниже пойдет речь, являются контекстно-зависимыми, то есть работают исключительно в рамках того процесса, в котором они установлены. Таким образом, мы «уравняем в правах» SoftIce и отладчики третьего кольца защиты («ring3 debuggers»), к которым, в частности, относится OllyDebug. Само по себе создание условного брейкпойнта, срабатывающего исключительно при появлении по некоему адресу известного значения – прием весьма часто употребляемый и полезный во множестве ситуаций. Самое известное из применений этой техники: «остановить программу, если переменная равна некоему значению» мы рассматривать не будем по причине крайней его банальности, такие вещи Вы способны проделывать самостоятельно, и, возможно, даже с закрытыми глазами. Куда менее очевидна возможность использования брейкпойнтов чтобы «притормозить» упакованную программу сразу после начала ее исполнения. Возможно, Вы уже встречали в статьях по крэкингу сокращение «OEP», которое расшифровывается как OriginalEntryPoint, «оригинальная («истинная») точка входа». Возможно, Вы также читали о том, что распаковка сжатых исполняемых файлов включает в себя поиск адреса этой самой OEP. Если же Вы ничего такого еще не читали и не встречали, то Вам необходимо запомнить следующие базовые сведения:
1. Entrypoint – это точка, с которой начинается исполнение программы после загрузки. Адрес точки входа в Win32-приложениях хранится в PE-заголовке исполняемого файла. 2. Абсолютное большинство упаковщиков и навесных защит изменяют значение адреса точки входа таким образом, чтобы управление передавалось распаковщику или защитному модулю соответственно. 3.
Originalentrypoint – это entrypointисполняемого файла до того, как файл был сжат/зашифрован и значение точки входа было модифицировано упаковщиком. 4. Для успешной распаковки программы в общем случае требуется узнать адрес OEP, остановить программу в тот момент, когда распаковщик передаст управление на OEP и в этот момент снять дамп со всех секций процесса. Также довольно часто после снятия дампа приходится восстанавливать таблицу импорта, в некоторых случаях может потребоваться ручная правка параметров секций.
Один из приемов отыскания OEP заключается в следующем: нужно поставить на функции WinAPI брейкпойнты таким образом, чтобы один из них сработал заведомо недалеко от точки входа в программу. Выяснив, откуда был произведен этот первый вызов WinAPI’шной функции, Вы сделаете первый (и самый важный) шаг на пути к адресу OEP. После этого обратной трассировкой в уме или каким-либо иным методом Вы будете «раскручивать» программу, как бы заставляя ее исполняться «задом наперед», и таким образом шаг за шагом подбираясь к OEP. Идея, лежащая в основе этого метода поиска OEP, крайне проста: современные компиляторы устроены так, что в коде абсолютного большинства программ неподалеку от точки входа содержится как минимум один вызов функции WinAPI (обычно такой функцией является GetModuleHandle или GetCommandLine). Другой важный момент состоит в том, что практически каждый компилятор генерирует в начале программы специфический «начальный» (startup) код, который производит некие действия по инициализации данных, необходимых для работы стандартных библиотек. Причем этот код у разных компиляторов (а часто – у разных версий одного и того же компилятора или даже при разных опциях компиляции) имеет достаточно специфический и легко узнаваемый вид, причем повлиять на содержание этого startup-кода разработчик программы обычно не может (или может, но в ограниченных пределах). Поэтому чтобы Вам было легче искать OEP, я настоятельно рекомендую изучить, какой код помещают в начало программы наиболее распространенные компиляторы – так Вы быстро научитесь определять OEP исключительно по внешнему виду кода, находящегося в окрестностях первого вызова функции WinAPI. Однако как отличить вызов нужной нам функции из только что распакованной программы от всех прочих вызовов этой же функции, которые могут выполняться в процессе работы распаковщика? Как раз для этого Вы можете воспользоваться проверкой значения, находящегося по некому известному адресу внутри кода программы (обозначим этот адрес буквой A).
Извлечь нужное значение дампером из запущенной программы обычно не составляет никакой сложности. Поскольку известные мне упаковщики распаковывают код в направлении от меньших адресов к большим (и с достаточной точностью можно предположить, что так работает большинство распаковщиков), лучше выбирать эталонное значение ближе к концу программы. После того как такое значение у Вас будет, нужно лишь поставить условную точку останова на нужную функцию, а в качестве дополнительного условия указать равенство значения по адресу Aэталонному значению, которое Вы извлекли из распакованной программы. В результате Ваш брейкпойнт сработает не раньше, чем будет распакована соответствующая часть программы, так что довольно велика вероятность того, что, продолжая отладку в пошаговом режиме, Вы вернетесь в код программы, а не в недра распаковщика. Наблюдая за работой запакованных программ, Вы могли заметить, что они после распаковки всегда располагаются в памяти по одним и тем же адресам. Применив против такой программы какую-нибудь утилиту вроде ProcDump, Вы даже можете прочитать параметры секций программы, частности - начальный адрес и размер. После этого вполне естественным кажется вопрос: «а что, если проверять, принадлежит ли адрес возврата, лежащий на стеке, промежутку адресов, в котором расположен код программы?» И вопрос этот отнюдь не праздный. Дело в том, что нередки упаковщики и навесные защиты, в которых блок, выполняющий дешифровку и распаковку кода, располагается в области, не пересекающейся с той, в которой в итоге будет расположен исполняемый код программы. Поэтому, поставив брейкпойнт на функцию и в качестве условия указав что-нибудь вроде ([esp]>401000) && ([esp]<501000) Вы добьетесь того, чтобы Ваш брейкпойнт активизировался лишь в том случае, если при выходе из функции предполагается возврат в код отлаживаемой программы. Данный метод, также как и описанный выше, может использоваться для поиска OEP через обратную трассировку от первого вызова функции WinAPI.
Однако этим возможности условных точек останова, проверяющих адрес возврата, отнюдь не ограничиваются. Очень часто возникает необходимость посмотреть параметры вызова некой часто используемой функции в тех случаях, когда вызов этой функции был сделан из одной или нескольких точек, и при этом проигнорировать все прочие вызовы (которых может быть очень много). Если решать задачу «в лоб», то необходимо найти все ссылки на данную функцию и установить по брейкпойнту на каждый интересующий нас вызов. Обычно такой подход вполне удовлетворителен, но мы не будем искать легких путей и посмотрим, как ту же задачу можно решить при помощи одного единственного брейкпойнта. Вы уже знаете, что для распознавания, откуда был сделан вызов функции, можно использовать адрес возврата, лежащий на вершине стека, и потому без труда напишете соответствующее условие для брейкпойнта. Такое условие может выглядеть следующим образом:
([esp]==ret_addr1) || ([esp]==ret_addr2) || …,
где ret_addr1 и ret_addr2 – адреса возврата. Все это достаточно очевидно, и Вы можете задать вопрос, зачем нужно было изобретать очередной велосипед, если традиционный подход дает ничуть не худшие результаты? Первая причина - «человеческий фактор»: работать с большим количеством точек останова не всегда удобно даже в насквозь визуальном OllyDebug, а уж «рулить» десятком-другим брейкпойнтов в SoftIce– занятие, что называется, на любителя. Так что если есть возможность значительно уменьшить число брейкпойнтов и облегчить себе жизнь, почему бы этой возможностью не воспользоваться? Кроме того, перед Вами может встать задача, обратная по отношению к вышеприведенной: отслеживать все вызовы процедуры, за исключением нескольких. И в этом случае сформировать строку с условием вида
([esp]!=ret_addr1) && ([esp]!=ret_addr2) && …,
в которой ret_addr1, ret_addr2 – адреса возврата, при которых точка останова не должна срабатывать, представляется гораздо более простым, чем искать в коде программы все подозрительные ссылки на интересующую Вас процедуру и «обвешивать» их брейкпойнтами. Однако более важным представляется применение условных брейкпойнтов для обнаружения неочевидных вызовов функций.
Я уже рассказывал в предыдущей главе о приемах, при помощи которых разработчик защиты может вызывать процедуры неявным образом, из-за чего такие обращения к процедурам становятся «невидимы» для дизассемблеров и прочих инструментов анализа «мертвого кода». В качестве дополнительного средства маскировки авторами защит могут параллельно использоваться как обычные вызовы процедур, так и неочевидные; я встречал такое в приложении к MessageBoxA и к функции чтения из реестра, но вообще эта техника может быть применена к любой достаточно часто вызываемой процедуре, используемой в защитных механизмах. В результате в дизассемблированном коде мы увидим несколько ничем не примечательных явных вызовов – но, скорее всего, не заметим самого интересного. Поставив «обычный» брейкпойнт на вызываемую функцию, мы можем столкнуться с тем, что эта функция вызывается десятки раз, и потому тоже не сможем определить, задействована ли эта функция в защитном механизме – интересующие нас неявные вызовы потеряются среди десятков и сотен вызовов явных. Вот если бы был какой-нибудь способ отделить явные вызовы от неявных… И такой способ есть. Базовые идеи, используемые для различения явных и неявных вызовов, могут быть сформулированы следующим образом: 1. Практически все явные вызовы функций легко обнаруживаются дизассемблером или вспомогательными инструментами для поиска ссылок (к примеру, в OllyDebug эта операция элементарно выполняется из контекстного меню: Findreferencesto|selectedcommand). Построив список явных вызовов, можно записать условие-фильтр, в котором будут перечислены все адреса возврата после явных вызовов, и использовать этот фильтр в качестве условия срабатывания для точки останова. Проще говоря, нам нужно будет установить условный брейкпойнт с фильтром вида ([esp]!=ret_addr1) && ([esp]!=ret_addr2) && …, который бы «пропускал» явные вызовы, но срабатывал на всех остальных, то есть неявных. 2. Набор «штатных» команд ассемблера, предназначенных для вызова подпрограмм, сравнительно невелик, поэтому, проанализировав несколько байт, предшествующих адресу возврата, можно строить предположения о способе вызова.
В частности, если опкод, находящийся на пять байт «выше» адреса возврата, равен 0E8h (CALLxxxx), скорее всего вызов был сделан стандартным способом. Если же в окрестностях адреса возврата ни одна из разновидностей команды CALL не обнаружена – либо вызов был неявным, либо внутри отработавшей процедуры присутствовал код, искажающий адрес возврата. 3. Код, предшествующий вызову подпрограммы, может оставлять легко идентифицируемые следы: определенные состояния флагов, значения переменных или соотношения между регистрами. В некоторых случаях, проанализировав эти следы, можно делать выводы, из какой точки могла (или не могла) быть вызвана та или иная подпрограмма.
Поясню эту мысль на следующем коде:
cmp eax, ebx ja _no_call call MyProc
_no_call: …
Нетрудно заметить, что если вызов процедуры MyProc был сделан из вышеприведенного куска кода, то в момент входа в процедуру значение регистра eaxдолжно быть меньше либо равно ebx (в противном случае выполнилась бы команда ja, обходящая call). И если это соотношение между регистрами в каких-то случаях окажется недействительным, для крэкера будет совершенно очевидно, что в этих «подозрительных» случаях вызов подпрограммы был произведен откуда угодно, но только не из продемонстрированного кода. Разумеется, при большом желании, возможно построить код программы таким образом, что приложение вышеприведенные идей не даст положительного результата. Однако это потребует от программиста дополнительных и довольно серьезных усилий – разработчику придется проанализировать собственный код так, как это сделал бы крэкер (и отнюдь не факт, что это у него получится – принципиально разные цели порождают разный образ мышления). Затем все найденные уязвимые участки придется переписать, причем наверняка - на ассемблере, поскольку высокоуровневые языки обычно не дают необходимой гибкости. После всего этого все переписанные связки между процедурами придется заново отладить протестировать, чтобы убедиться в полной корректности работы исправленного кода.
В общем, объем работ нешуточный, а результат… Результат, как водится, заведомо неизвестен – никто не может гарантировать, что разработчик не упустит какой-либо важный момент, или крэкер не найдет особо нетрадиционный подход, после которого все усилия автора защиты пойдут прахом. Так что обычно разработчики обычно не озабочиваются сокрытием «тонких эффектов» при неявных вызовах, и Вы, при случае, можете обратить это себе на пользу. Увы, в настоящее время применить вторую и третью идею в полном объеме, мы обнаружим ограниченность имеющихся программных средств. Да, наши любимые отладчики отлично справляются с относительно простыми фильтрами, однако более глубокий анализ ситуации в момент срабатывания брейкпойнта, невозможен либо, по меньшей мере, крайне затруднителен. Действительно, попробуйте написать условие, которое проверяло бы наличие всех возможных разновидностей команды CALL в районе адреса возврата – и Вы поймете, что я имею в виду. А ведь иногда возникает необходимость не только (и не столько) остановить программу при тех или иных значениях регистров, но в автоматическом режиме собрать статистику по срабатыванию брейкпойнта – частота появления тех или иных адресов возврата, типичные значения параметров функции, на которую поставлен брейкпойнт и тому подобное. И тут становится очевидным, что даже «статистические» команды SoftIce и модификатор DO в командах установки брейкпойнтов являются лишь слабым подобием того, в чем рано или поздно возникает потребность у каждого крэкера. Идеальным решением была бы встраивание в отладчики собственного скриптового языка, обеспечивающего полный доступ ко всем возможностям отладчика (что уже частично реализовано в плагинах для OllyDebug и различных «сторонних утилитах» для SoftIce). Если же существующие реализации средств скриптинга не предоставляют необходимых возможностей, мы будем вынуждены обходиться программными «затычками», реализация которых аналогична устройству описанных в предыдущей главе точек останова в Spectrum’овских отладчиках.
Поскольку применяются такие «брейкпойнты» (а по сути - патчи) довольно широко, а с необходимостью «перехватить» исполнение программы в нужной точке рано или поздно сталкивается любой крэкер, мы подробно рассмотрим эту технологию в главе, посвященной патчингу. До настоящего момента мы как-то обходили вниманием точки останова, срабатывающие при попытке доступа к определенным областям памяти вообще, и аппаратные брейкпойнты в частности. Вы, возможно, даже начали беспокоиться из-за того, что, говоря о точках останова, я так долго не упоминал волшебное слово «BPM». И вот пришло время поближе узнать, что такие брейкпойнты собой представляют и какую практическую пользу из них можно извлечь. «Законный» способ установки брейкпойнтов на области памяти основывается на использовании специальных отладочных регистров, обозначаемых как DR0-DR7. Каждый из брейкпойнтов может отслеживать любой (но только один) из следующих типов обращения к памяти: запись, чтение, запись или чтение, исполнение кода. Операции «чтение» и «исполнение» процессор считает принципиально разными, несмотря на то, что здравый смысл говорит нам: прежде чем исполнить код, нужно его прочитать. «Это невозможно понять, это нужно запомнить» - поэтому временно отложите здравый смысл в сторонку и запомните это правило. По этой же причине одновременно отслеживать запись, чтение и исполнение при помощи одного-единственного брейкпойнта у Вас не получится. Это первое существенное ограничение, наложенное инженерами из Intel на использование отладочных регистров. Впервые отладочные регистры появились в процессорах 80386 именно для отслеживания обращений к памяти, но в Pentiumвозможности этих регистров были распространены и на порты ввода-вывода. Поскольку собственно адреса точек останова (или номера портов, обращение к которым будет отслеживаться) задаются в регистрах DR0-DR3, таких брейкпойнтов может быть не более четырех – это второе ограничение. Еще одна проблема состоит в том, что отладочные регистры позволяют установить брейкпойнт только на байты, слова (WORD) или двойные слова (DWORD), аппаратных брейкпойнтов на обращения к более крупным блокам памяти не предусмотрено.
Если Вам нужно отследить обращения к переменным «длинных» нецелочисленных типов (Double, Extended), Вы моежете поставить брейкпойнт в середину переменной; в этом случае любое обращение к такой переменной «зацепит» брейкпойнт. Также важно помнить следующее: если Вы устанавливаете бряк на слово, адрес брейкпойнта будет автоматически выровнен на ближайший «снизу» четный адрес, а если Вам нужен бряк на DWORD – приготовьтесь к тому, что процессор выровняет адрес брейкпойнта на адрес, кратный четырем. И, наконец, самый неприятный для крэкера факт – работа с отладочными регистрами возможна только из нулевого кольца защиты, так что любителям запустить свои шаловливые ручки в недра системы и сотворить там что-нибудь эдакое придется сначала повозиться с написанием соответствующего софта. Впрочем, пусть Вас согревает тот факт, что разработчикам защит добраться до отладочных регистров и испортить Вам удовольствие будет ничуть не проще. Если у Вас возник живой интерес к теме внутреннего устройства аппаратных точек останова, могу порекомендовать обратиться к первоисточникам, то бишь к фирменной документации Intel. Пожалуй, пора заканчивать с живописаниями грядущих трудностей и прочими ужасами, а то, добравшись до последней главы этой работы, Вы можете решить, что крэкинг – это очень сложно, больно и трудно (хотя в действительности это не совсем так). Поэтому давайте лучше устроим сеанс позитивного мышления и погрузимся в созерцание всего того доброго, светлого и прекрасного, которое привносят в нашу жизнь аппаратные брейкпойнты. Ибо, воистину, тяжела была бы наша жизнь, не будь в ней аппаратных точек останова. Нетрудно догадаться, что изначально аппаратные брейкпойнты предполагалось применять для поиска ошибок, связанных с некорректной работой с переменными, и отлавливать ситуации, чреватые переполнением буфера (для этого сразу после последнего байта буфера ставился «защитный» брейкпойнт, срабатывание которого сигнализировало о попытке записать или прочитать лишние данные). Однако крэкеры даже столь милым и безобидным штукам, как аппаратные точки останова, нашли нетрадиционное применение, превратив их в главную «ударную силу» против самомодифицирующегося и упакованного кода. Если Вы пробовали поставить брейкпойнт на упакованный код, Вы не могли не заметить, что после этого действа программа отчего-то перестает корректно распаковываться (впрочем, с вероятностью приблизительно 1/256, программа распакуется даже после такого издевательства, но брейкпойнт, разумеется, работать не будет).
Прочитав предыдущую главу, Вы наверняка осознали всю глубину и тяжесть Вашей ошибки, чистосердечно раскаялись в этом ужасном деянии и, положив руку на руководство по отладчику, трижды произнесли торжественное обещание никогда больше так не поступать. А потом, подобно классику, задались вопросом: «что делать?» Разыскивая ответ на этот глубоко философский вопрос, Вы могли обнаружить в руководстве по SoftIce раздел, повествующий о команде BPM и ее параметрах, либо добраться до таинственных пунктов Breakpoint|Hardware, on… в контекстном меню OllyDebug. Если Вы еще не проделали этих операций – прочитайте документацию по отладчику, посмотрите, как правильно ставить аппаратные точки останова на чтение, запись и исполнение и немного потренируйтесь на первой попавшейся программе, дабы убедиться, что такие точки останова действительно существуют и даже работают. А потом освежите в памяти эксперимент, в котором мы при помощи дампера наблюдали изменения в коде, вызываемые командой BPX, и попытайтесь повторить его над к аппаратными точками останова. Как и следовало ожидать, аппаратные брейкпойнты не оставляют в коде программы никаких следов. Их не видно – но наши маленькие аппаратные друзья существуют и работают! И как бы программа ни утюжила свой код проверками, сколько бы ни высчитывала контрольные суммы – против аппаратных точек останова эти приемы бесполезны, так что теперь Вы сможете сколько угодно отлаживать программу, не опасаясь, что защитные процедуры в один миг изничтожат любовно расставленные Вами бряки. Интересно, что аппаратные брейкпойнты отлично работают не только с «обычными» участками памяти, но и со стеком. В принципе, ничего удивительного в этом нет – память, зарезервированная под стек, с точки зрения процессора и ОС ничем принципиально не отличается от памяти, предназначенной для кода или данных. Однако для некоторых начинающих крэкеров, разум которых опутан предрассудками, сама идея установки аппаратного брейкпойнта на чтение/запись данных в стек выглядит чем-то странным и противоестественным.
А ведь установка аппаратного брейкпойнта на стек – это отнюдь не оригинальничание, а вполне действенная техника, часто используемая при «ручной» распаковке сжатых программ, а также позволяющая обнаруживать подмену адресов возврата в стеке. Знания об аппаратных брейкпойнтах позволяют нам по-новому взглянуть на проблему подмены адресов возврата. Вспомните последний пример из предыдущей главы – тот, где функция A вызывает функцию B, функция Bвызывает функцию C, а функция Cделает «финт ушами», подменяя адрес возврата, и возвращается в функцию D (а не в функцию B). Теперь, когда Вы вооружены необходимой информацией о том, как отслеживать обращения к определенным адресам памяти, для Вас не составит труда обнаружить попытки процедуры подправить свой адрес возврата – просто «накройте» брейкпойнтом на чтение/запись двойное слово, хранящее адрес возврата, и Вы без проблем найдете команду, которая выполняет подмену. Аппаратные точки останова внутри стека могут помочь Вам победить еще один довольно неприятный защитный прием – переход со «сбросом» части стека. В вышеприведенном примере демонстрировалось искажение адреса возврата, лежащего на стеке, однако автор защиты может и не возиться с искажением адресов, а волевым решением «выбросить» со стека часть параметров, переменных и адресов возврата (это может быть сделано, к примеру, командой ADDESP,xxxx или несколькими PUSH’ами), после чего переход в нужную точку кода выполнить простым JMP. Поразмыслив над содержимым стека, Вы даже можете приблизительно определить границы стековых фреймов. Если предположить, что стековые фреймы «сбрасываются» целиком (в принципе, это не обязательно, но такой код проще в отладке), то Вы можете составить список возможных значений регистра ESP после «сброса» фреймов. Затем методом «научного тыка» протестируйте каждое из этих значений, устанавливая аппаратные брейкпойнты на чтение/запись двойного слова из каждого из этих значений ESP, а также на 4 байта ниже; можно также добавить брейкпойнты на предполагаемые адреса локальных переменных.
В результате Вы получите одну из трех ситуаций:
Если после «сброса» стека программа попытается выполнить возврат из процедуры, она «споткнется» о брейкпойнт, стоящий по адресу ESP.
Если программа попытается вызвать подпрограмму, и при этом поместит какое-либо значение (параметр или адрес возврата) на стек, команда, выполняющая запись данных в стек, вызовет срабатывание брейкпойнта по адресу ESP-4.
Если брейкпойнт установлен на локальную переменную, исполнение программы будет прервано при первом же обращении к этой переменной.
Смысл всех этих действия заключается в том, чтобы как можно раньше остановить программу после «сброса» стека и узнать, что пытался скрыть автор защиты при помощи этого приема. Поскольку современные программы стеком пользуются достаточно активно, можно надеяться, что первое срабатывание нашей «ловушки» будет не слишком далеко отстоять от той точки, в которую был выполнен переход. Если выяснится, что адресов, на которых срабатывают ловушки, несколько, Вам нужно выбрать тот из них, обращение к которому выполняется раньше других – очевидно, что он наиболее близок к искомой точке. Описанный метод также традиционно используется для определения OEP упакованных программ: заклинание «BPMESP-4», которое следует набирать в SoftIce сразу после загрузки распаковываемой программы, так или иначе упоминается в большинстве статей, посвященных ручной распаковке кода. А вот смысл сего заклинания, увы, поясняется намного реже, и сейчас я попытаюсь исправить это недоразумение. Реализация абсолютного большинства навесных защит такова, что значение ESP в момент завершения работы защитного модуля и передачи управления на OEP в точности равно значению ESPсразу после загрузки запакованной программы. Причины этого лежат где-то в глубинах сознания разработчиков защит, поскольку уменьшение значения ESP перед исполнением программы на величину, кратную четырем, никаких отрицательных эффектов не вызывает, поскольку место под стек обычно резервируется с запасом (а вот увеличение значения ESP уже может быть чревато).
Однако большинство упаковщиков считают, что значение ESP лучше передавать пользовательскому коду в неизменном виде, а потому свято блюдут принцип «сколько на стек положено – столько должно быть снято», причем окончательная коррекция стека скорее всего будет проведена непосредственно перед выполнением перехода на OEP. И если эта коррекция выполняется командами чтения данных из стека (POP, POPAD и т.п.), а не простой записью в ESP ранее сохраненного значения, Ваш брейкпойнт сработает в этот знаменательный момент. Таким образом, Вам останется лишь отсеять ложные срабатывания брейкпойнта, если таковые будут, и трассировать код до тех пор, пока управление не будет передано из распаковщика в основную программу. Мой рассказ об аппаратных точках останова был бы неполон, если бы я не упомянул об одной специфической разновидности брейкпойнтов останавливающих исполнение программы при обращении к произвольной области памяти. Такие брейкпойнты в SoftIce под ОС Windows 9x создавались при помощи команды BPR(в современных версиях этого отладчика команда BPR, к сожалению, отсутствует, что особенно странно в свете того, что в арсенале OllyDebug такие точки останова имеются). Внешне использование таких брейкпойнтов ничем не отличается от работы с обычными аппаратными точками останова, если не считать возможности «накрыть» брейкпойнтами практически неограниченное количество участков памяти совершенно любого размера (что выгодно отличает данный тип брейкпойнтов от «обычных» аппаратных, которых может быть не более четырех). Гораздо больший интерес представляет знание о том, каким образом реализован этот тип брейкпойнтов, которое, возможно, пригодится Вам в будущем (к примеру, если Вы захотите написать собственный отладчик). Как Вы знаете, процессоры линейки x86 в защищенном режиме (некоторые источники называют этот режим расширенным) содержат множество средств, облегчающих создание многозадачных программ и позволяющих защитить данные от некорректных операций над ними. Для нас особенно интересной представляется возможность изменять атрибуты защиты отдельных страниц памяти, то есть разрешать или запрещать определенный тип действий (запись, чтение, исполнение кода) над информацией, хранящейся на той или иной странице памяти.
При этом любая попытка выполнить запрещенную операцию, к примеру, записать данные на страницу, для которой запись запрещена, вызовет исключительную ситуацию. Более того, чтобы ради этого нехитрого действа Вам не пришлось выбираться в нулевое кольцо защиты, в WindowsAPI включены функции VirtualProtectи VirtualProtectEx, позволяющие изменять атрибуты страниц памяти (правда, флаг запрета на исполнение кода на платформе x86 бесполезен, но необходимости в таком запрете обычно и не возникает). Как видите, создать «область останова» не так уж трудно, основную сложность в реализации таких «областей останова» представляет обработка исключительных ситуаций, возникающих при обращении к данным из защищенной области. Основным препятствием в практической реализации является то, что обычно размеры «области останова» не выровнены на границы страниц и кратны размерам страницы. По этой причине возникает необходимость в написании достаточно изощренного кода, распознающего, к какой ячейке памяти произошло обращение и обеспечивающего корректное продолжение работы программы после ошибки нарушения прав доступа к странице (по сути требуется написать нечто среднее между простым дизассемблером и виртуальной машиной). Уместно будет упомянуть, что некоторые защиты манипулируют атрибутами страниц, чтобы противодействовать отладке, memorypatching’у и снятию дампов. Большинству патчеров и дамперов эти ухищрения глубоко безразличны, однако если Вы планируете заняться написанием собственных утилит для патчинга и/или снятия дампов (что я Вам настоятельно рекомендую), обязательно проверьте, как Ваши изделия будут реагировать на нестандартные атрибуты страниц. Поэтому если у Вас возникнут какие-либо проблемы при операциях с памятью чужого процесса, есть смысл поинтересоваться атрибутами страниц, с которыми Вы работаете (это можно сделать при помощи той же VirtualAllocEx). На этом наше знакомство с теорией и практическими аспектами применения точек основа в основном закончено, и теперь мы можем во всеоружии приступить к рассмотрению методов трассировки кода.Именно этому и будет посвящена следующая глава.
[C] CyberManiac
Содержание
Далее
Трассировка во сне и наяву.
Трассировка - одна из основ, на которых держится крэкинг. О трассировке обычно говорят вскользь как о чем-то общеизвестном и само собой разумеющемся, но при этом имеющем нечеткие, плохо формализуемые правила. Читая предыдущие главы, Вы неоднократно встречали фразы "трассируем процедуру…", "трассируйте код до тех пор, пока…" и т.п., и при этом вряд ли задумывались о том, что нужно делать и как вообще трассируют код. Так что же такое отладка - наука это или искусство? Поставив себе целью разобраться в методах трассировки, мы прежде всего должны определиться, что такое трассировка и для чего она нужна. И лишь после того, как будут определены цели трассировки, возможно будет говорить о "технической" реализации методов, посредством которых эти цели могут быть достигнуты.
Прежде всего договоримся о терминологии. Само слово "трассировка" часто употребляется в различных смыслах - от синонима "отладки" до обозначения процесса пошагового исполнения программы (см. команды вроде "trace into" и "trace over" в некоторых отладчиках). Я не буду оригинален и введу еще одно значение слова "трассировка", которое и будет использоваться на протяжении данной главы: трассировка - это процесс определения "траектории" исполнения кода с некой заранее заданной точностью и наблюдение за изменениями во время исполнения этого кода. Вот такое определение - незамысловатое, но с замахом на глобальность. И, разумеется, без подробной расшифровки выглядящее достаточно туманно - как и любое другое достаточно широкое определение. Сам процесс трассировки может быть направлен на получение следующих знаний: Определение траекторий исполнения кода, как всех теоретически возможных, так и активирующихся только при неких начальных условиях. Наблюдение за изменениями каких-либо параметров (значений регистров, переменных, флагов) и определение их влияния на порядок исполнения команд. Нахождение точек ветвления и условий, приводящих к активации или деактивации того или иного участка траектории исполнения кода.
С пониманием того, что подразумевается под "заранее заданной точностью", я думаю, никаких сложностей у Вас не возникнет - эти слова означают всего лишь то, что при трассировке код анализируется исключительно лишь в той мере, которая нужна для решения практических задач.
Если Вам необходимо полное понимание некоего алгоритма (то есть "заранее заданная точность" - максимальна), Вам придется анализировать код целиком; если же Вам нужен лишь ответ на вопрос "почему программа выдает сообщение "Неверный серийный номер" вместо поздравления с регистрацией", Вас скорее всего устроит экспресс-анализ четырех-пяти точек ветвления, находящихся "выше" процедуры, сообщающей об ошибке, а все, что происходит между этими точками, Вы вольны проигнорировать. Трассировка может заключаться в поиске двух-трех точек в программе и анализе десятка прилежащих к ним команд. Как такое возможно? Очень просто: для примера в качестве критерия "заранее заданной точности" мы берем факт чтения или записи данных в некую ячейку памяти и находим в листинге все явные обращения к соответствующему адресу. После этого мы можем смело сказать "при некоторых неизвестных условиях в данную ячейку могут быть записаны значения, равные нулю либо единице". Если речь идет о глобальном "флаге зарегистрированности", то весь дальнейший взлом сводятся к выяснению, какое из состояний означает зарегистрированность программы (автор программы мог проявить оригинальность, сделав код 1 признаком отсутствия лицензии) и исправлению одного бита в программе.
Иногда используется даже совсем уж вырожденный вариант трассировки: в коде программы выбирается некая контрольная точка, на нее ставится брейкпойнт, а затем при помощи тестового запуска определяется, проходит траектория исполнения программы через эту точку или нет. В основе всех приведенных примеров лежит трассировка, хотя степень детализации различается очень сильно. Каких-то готовых правил, позволяющих выбрать глубину "погружения в код", по-видимому, не существует - действовать приходится по ситуации. Однако на первых порах лучше применять "экспресс-анализ" и использовать трассировку не столько для исследования тонкостей работы кода, сколько для поиска логических блоков, поиска управляющих конструкций и понимания алгоритма в общих чертах.
Именно такой подход лежит в основе дзен-крэкинга с его принципом " я не знаю, как это работает, но я могу это сломать", а если учесть, что таким "наскоком" вполне успешно ломаются недорогие утилиты, то этот метод, несомненно, будет серьезным подспорьем для начинающего крэкера.
Теперь рассмотрим, что представляет собой траектория исполнения кода. По традиции, разбирать это понятие мы будем на конкретном примере, а именно на куске кода, генерирующем таблицу констант для алгоритма CRC32:
mov eax,255
_loop_i: mov edx,eax mov cl,8
_loop_j: shr edx,1 jnc @@1 xor edx,0EDB88320h @@1: dec cl jnz _loop_j
mov [CRC32_Table+eax*4],edx dec eax jns _loop_i
Распечатайте этот код на листе бумаги (в принципе, можно выполнять все операции мысленно, но это будет не так удобно, как работать с "твердой копией"). Возьмите карандаш и напротив каждой из команд нарисуйте кружок - это будут точки возможной траектории исполнения кода. Теперь по полученным точкам попробуем построить совокупность всех возможных траекторий исполнения кода. Как известно, большинство процессоров исполняет команды последовательно (или же успешно изображают последовательное исполнение), одну за другой, по направлению от "нижних" адресов памяти к "верхним". И такой порядок исполнения может быть нарушен лишь командами переходов/возвратов, вызова прерываний, а также различными исключительными ситуациями. Вооружившись этим знанием, предположим, что исполнение данного куска кода начинается с команды mov eax,255 и начнем соединять наши кружочки стрелками в том порядке, в каком будут исполняться команды. Когда Вы доберетесь до команды jnc @@1, у Вас наверняка возникнет вопрос, куда нарисовать стрелку - ведь для флага SF может находиться в одном из двух возможных состояний, и в зависимости от этого состояния следующей исполняемой командой будет либо xor edx,0EDB88320h, либо dec cl. Выход из этой ситуации прост - рисуйте обе стрелки. После того, как Вы закончите это упражнение, должен получиться приблизительно такой рисунок:
Что мы видим? Прежде всего - то, что в каждый кружок входит как минимум одна стрелочка, а это означает, что каждая из нарисованных нами команд получает управление явным образом. Если бы после завершения нашего высокохудожественного шедевра обнаружилось, что какой-либо кружок "висит в воздухе", это было бы веским основанием для проведения подробного расследования на тему "зачем нужен программный блок, который не получает управления явным образом" (техническую сторону таких расследований мы рассматривали в предыдущих двух главах). Далее: окинув беглым взглядом картинку, Вы легко заметите два цикла, один из которых вложен в другой и одну конструкцию ветвления, "обходящую" команду xor edx,0EDB88320h при выполнении некоего условия (в языках высокого уровня такая последовательность обычно реализуется конструкциями вида IF <условие> THEN <действия>). Вот так при помощи карандаша и бумаги можно за считанные минуты выделить логические единицы внутри довольно абстрактной процедуры. Несмотря на то, что метод кажется очень простым и даже в чем-то "игрушечным", в действительности такие схемы с кружочками-стрелочками очень удобны, особенно если Вы сравнительно недавно занялись исследованием программ, и Ваш глаз еще не натренирован на вычленение управляющих конструкций в бесконечных листингах. А если Вам не хочется слишком уж часто прибегать к помощи карандаша и бумаги, есть смысл обзавестись дизассемблером IDA - последние версии этого продукта тоже умеют проставлять стрелочки напротив кода (правда, там это реализовано не так удобно, как на нашем рисунке). Или же можно написать собственную программу, которая бы на основе листинга рисовала такие вот картинки; если не пытаться сразу создать дизассемблер (и не просто дизассемблер, а как минимум аналог W32Dasm), а анализировать уже готовые листинги, программа даже получится не слишком сложной. Надо сказать, что инструменты, способные на основе ассемблерного листинга построить картинку, подобную приведенной выше, в настоящее время очень редки и потому более чем востребованы общественностью.
Кстати, наш набор стрелочек на самом деле - отнюдь не изобретение "для личного пользования", а наглядное изображение весьма научной штуковины под названием "граф" (имеется в виду математический термин, а не дворянский титул). Так что если Вам близок раздел математики под названием "теория графов", Вы можете попробовать приложить для анализа нашей картинки всю мощь этой теории; особенно актуально это для тех, кого заинтересует тема визуализации и автоматического анализа кода. При желании можно пойти еще дальше: нарисованные кружки-команды разбить на логически связанные группы, обвести каждую группу прямоугольником или ромбиком, вписать внутрь этих фигур краткие комментарии и в итоге получить обыкновенную блок-схему (она же "flowchart" в англоязычной литературе), иллюстрирующую исследуемый код. С недавних пор IDA поддерживает и эту технику представления кода - но, как обычно, со своей "спецификой". Проще говоря, блок-схемы в IDA мало похожи на то, что обычно называется блок-схемами в учебниках информатики. Да и потому пользоваться создаваемыми в IDA графиками (которые на данный момент невозможно экспортировать ни в один формат) не так удобно, как "классическими" блок-схемами. Кроме того, на сегодняшний день блок, ответственный за отображение блок-схем в IDA, является сторонней разработкой и совершенно не интерактивен, т.е. возможности пользователя по активной работе с такими блок-схемами практически нулевые - даже чтобы написать комментарий к блоку, Вам придется распечатать схему на бумаге. Оба этих метода, рисунок из кружочков со стрелочками и блок-схема, в действительности изображают одно и то же - команды и возможные порядки их исполнения, и являются "рабочим материалом" для самого древнего метода работы с листингом - трассировки в уме. Суть метода трассировки в уме очень проста: Вы на время превращаете свой мозг в некий "виртуальный процессор", и начинаете мысленно "исполнять" команды подобно тому, как это делал бы процессор настоящий.
Надо отметить, что в старые времена, когда компьютеры были большими и медленными, а машинное время было ресурсом весьма ограниченным, именно трассировка в уме была основным способом "отладки" программ - и программисты просиживали часами над огромными листингами с карандашиками в руках, пытаясь определить, в какой точке программа уклонилась с пути истинного. Нечто подобное предстоит научиться делать и Вам - с той существенной разницей, что Вы в любой момент можете проверить свои теории при помощи отладчика. Разумеется, по сравнению с машиной Ваше "быстродействие" будет ничтожным, да и отслеживать состояние регистров и ячеек памяти у Вас вряд ли получится - но в этом и нет необходимости. Главной целью такого "мысленного исполнения кода" должно быть определение "ключевых точек", вычленение логических блоков внутри трассируемого кода, приблизительное определение назначения этих блоков и наблюдение за тем, как состояние регистров, флагов и переменных отражается на пути исполнения программы. Как я уже говорил, обычно мы изучаем не весь код построчно, а лишь те его участки, которые могут привести к интересующим нас эффектам, причем задача чаще всего стоит следующим образом: по известному эффекту необходимо найти траекторию исполнения кода, которая приводит к появлению этого эффекта. Предположим, что Вы успешно обнаружили, где в программе расположена код реализации нужного эффекта (вывод MessageBox'а, запись единицы в регистр EAX и т.п.), и Вам хочется понять, каким образом программа передает управление на этот код и каким образом этого можно избежать или наоборот - получать такой результат при любых исходных данных. Для решения таких задач обычно используется обратная трассировка в уме. Идея обратной трассировки очень проста: мы начинаем читать листинг "задом наперед", то есть движемся от следствия (которое нам известно) к причине. По ходу дела отмечаем ключевые точки, к которым относятся: Вызовы функций Win32 API, а также других стандартных функций, какие сможет распознать Ваш дизассемблер или Вы сами. Обращения к глобальным переменным, которые чаще всего выглядят как чтение или запись данных по указанному явным образом адресу. Вызовы подпрограмм, непосредственно за которыми следует проверка некоего условия (внешне выглядят как связка команд CALL-CMP-Jxx). Перечисленные три группы отличаются от всех прочих кодов тем, что их назначение сравнительно легко идентифицируется.
Действительно, если Вы видите вызов API' шной функции чтения командной строки, для Вас будет очевидно, что следующие за вызовом команды почти наверняка будут оперировать именно с текстом командной строки, а не с фазами Луны или курсом доллара. И вот тому пример: call GetCommandLineA mov edi, eax cmp byte ptr [edi], 22h jnz short loc_401B08 Нетрудно догадаться, что этот кусок кода проверяет, является ли первый символ командной строки двойной кавычкой. Далее, согласно принципам дзен-крэкинга, следует помедитировать о том, что может последовать за такой проверкой. А за ней обычно следует поиск закрывающей кавычки и дальнейший синтаксический разбор строки - пропуск разделяющих пробелов, вычленение нужного параметра из строки (обычно определяется смещение первого символа в строке и длина этого параметра) и т.п. Нечто подобное как раз и проделывал тот патч, из которого я взял код для примера. Для нас в данном случае интересен не подробный анализ кода, я хотел продемонстрировать несколько иное, а именно: как всего лишь один известный системный вызов позволяет приблизительно оценить назначение процедуры, в которой он содержится. Причина интереса к глобальным переменным тоже достаточно очевидна. Современные программисты, как правило, придерживаются принципов структурного и объектного программирования, которые предполагают минимальное использование общедоступных объектов - каждая процедура должна "видеть" лишь те данные, которые ей нужны для работы. Поэтому разработчики программ обычно дают глобальный статус двум типам данных: тем, которые используются настолько широко, что их неудобно передавать в каждую процедуру, где они требуются (например: настройки программы, таблицы констант, тексты сообщений и т.п.), и всевозможным отладочным переменным, которые полагается удалять при выпуске релиза программы. Регистрационные данные нередко занимают промежуточное положение: с одной стороны, это своеобразная "настройка программы", а с другой они близки к отладочной информации в том смысле, что добавление защиты нередко производится уже после написания программы чисто механическим путем ("если переменная не равна нулю, то нарисовать поверх отчета слово UNREGISTERED"). Как Вы понимаете, использование глобальной переменной для хранения статуса программы в наше время - редкая и счастливая для крэкера случайность.
Гораздо чаще разработчики, начитавшись руководств "как защитить свою программу от хакеров за 1 час", усваивают, что глобальную переменную использовать в качестве "переключателя" нехорошо. Вот функция, вызываемая к месту и не к месту - это совсем-совсем другое дело. Сказано-сделано, и в программе появляются многочисленные куски вида "if (!RegistaProggie()) ShowMessage ("Wanna getta munnee!")" (если кто не понял текст сообщения, приблизительный перевод с нетрадиционного английского звучит как: "хочу бабки!"). А во что такие куски превращаются при ассемблировании? Правильно - в цепочку CALL-CMP-Jxx, о которой я говорил парой абзацев выше. Вот такие-то интересные кусочки мы и будем высматривать при обратной трассировке. Разумеется, одним лишь поиском регистрационных процедур дело не ограничивается - цепочки "вызов-проверка-ветвление" могут быть проверкой на корректность введенных данных (как-то раз мне пришлось поправить программу игры "Жизнь", которая могла, но отчего-то не хотела работать с полями больше, чем 100*100), и конструкцией SWITCH (она же CASE в Паскале), да и много чем еще. Причем, если у функции есть параметры, они могут стать отличной подсказкой, позволяющей установить назначение этой функции (особенно хорошо этот прием работает с функциями, выполняющими преобразование строк). Для этого нужно под отладчиком исследовать, что именно передается в функцию и какой результат она возвращает. Нередко это даже оказывается проще, чем догадаться, что хранится в глобальной переменной. Но вернемся к описанию алгоритма обратной трассировки в уме. Итак, мы встретили команду ветвления или перекрестную ссылку. Переходим к точке, куда/откуда ведет ссылка, а затем пытаемся определить, при каких условиях эта ссылка "срабатывает" и что происходит, когда условие срабатывания ссылки не выполняется, причем для "не сработавшей" ветки исходник нужно читать уже не "снизу вверх", а в порядке исполнения команд.
Следуя этой схеме, Вы, скорее всего, доберетесь до начала либо до конца процедуры, если не запутаетесь во всех этих ветвлениях и переходах. По сути, алгоритм обратной трассировки в уме рекурсивен (команды условного перехода или перекрестные ссылки часто порождает два возможных пути чтения кода), а человеческое сознание мало приспособлено к выполнению рекурсивных алгоритмов "вручную", так что Вам лучше сначала потренироваться на простых примерах. Добравшись до начала процедуры либо до точки выхода из нее, Вам придется либо анализировать все возможные варианты, откуда и почему могла быть вызвана процедура (т.е. пробежаться по всем ссылкам на эту процедуру), либо прибегнуть к отладчику. Второй путь, конечно, нарушит "чистоту идеи" трассировки в уме, но зато даст ответ максимально простым и быстрым способом. Теоретически процесс обратной трассировки в уме Вы можете продолжать до тех пор, пока не доберетесь до начала программы, но на практике мне редко приходилось подниматься по "дереву процедур" более чем на четыре уровня вверх. Нельзя не отметить одну тонкость: даже если Вы нашли точку выхода из процедуры, убедитесь, что эта точка единственная: компиляторы могут генерировать код с более чем одним выходом из процедуры\функции, причем эти "дальние" ветки могут быть даже более интересными, чем "ближние". Дело в том, что любая проверка может быть многоступенчатой: к примеру, сначала проверяется, введены ли вообще какие-либо данные, затем - насколько эти данные корректны, и лишь в последнюю очередь - соответствуют ли эти данные какому-либо узкоспециализированному критерию. Трассируя программы, Вам придется немало поводить пальцем по распечатке или экрану дисплея, взбираясь по дереву вызовов и переходов. А если к тому же Ваш дизассемблер "не умеет" выделять цветом команды переходов, Вы можете запросто пропустить что-нибудь важное. В общем, блуждание по бескрайним полям кода - дело, требующее внимания, сосредоточенности, и при этом достаточно утомительное.
Однако если Вы решаете типовую задачу "как заставить программу выполнить некий код независимо от правильности исходных данных", Вам может помочь все та же карта всех возможных путей исполнения кода, построение которой я демонстрировал в самом начале статьи. Как только Вы составите такую карту, Вам останется лишь выполнить три несложных действия: Выбрать в программе и отметить на этой карте исходную точку. В качестве исходной точки лучше всего выбирать код, имеющий непосредственное отношение к исследуемой защите, который может быть легко идентифицирован и вызван из программы. Если речь идет о старых добрых серийных номерах, вводимых с клавиатуры, то лучше всего искать код чтения серийника из окна - этот код обычно легко "ловится" в отладчике и заведомо вызывается защитой (надо же ей как-то узнавать, с какими параметрами пользователь пытается зарегистрироваться). Отметить конечную точку, путь к которой требуется найти (например, вызов MessageBoxA, сообщающий об успешной регистрации). Найти на карте путь (а лучше - все возможные пути) из начальной точки в конечную. Как только такой путь будет найден, можете начинать соответствующим образом патчить код и выяснять, что из этого получится. Интересно, что выполнять второй пункт можно не только вручную (это чем-то похоже на детскую головоломку "найди путь в лабиринте"), но и в автоматизированном режиме - в этом случае потребуется решить типовую задачу из курса теории графов. Теоретически это позволяет поставить взлом защит, использующих только переключатель "зарегистрировано/не зарегистрировано" даже не на поток - на конвейер! Даже если у Вас возникнут проблемы с вторым пунктом (например, программа никак не сообщает об успешной регистрации или же Вы просто не знаете, как это сообщение выглядит и потому не можете его найти), это не повод для отчаяния. Пусть Вы не знаете, на каком из возможных путей исполнения находится нужный Вам код - зато никто не отнимет у Вас знания о том, на каком пути этот код не находится! А значит, последовательно форсируя при помощи отладчика исполнение каждой возможной ветки кода, Вы можете постепенно отбрасывать "неправильные" ветки, пока не наткнетесь на правильную (или не придете к выводу о невозможности решить Вашу задачу таким способом). Попробовав трассировать в уме сколько-нибудь сложную процедуру, вырванную из середины программы, Вы заметите, что хотя возможные пути исполнения кода и известны, но сказать, какой путь изберет программа, если ее запустить, Вы не можете.
И никто не может - поскольку при трассировке в уме Вам неизвестны исходные данные, которые и заставляют программу выбирать из всех возможных путей единственный актуальный. Именно здесь и пролегает граница между разглядыванием препарированного "мертвого" кода и вождения пальцем по распечатке и наблюдением за "живой" программой, обрабатывающей "настоящие" данные. Так что пришло время поговорить о методах трассировки программ "вживую". Исторически первым способом трассировки "вживую" был многократный вызов в отладчике функции "исполнить текущую команду" с заходом в подрограммы или без такового. Проще говоря, программист сидел перед монитором и давил нужную кнопку, наблюдая, как бегает по коду курсор, и меняются значения регистров. Более продвинутые (или просто более ленивые) программисты смекнули, что для определения траектории важны не все команды, а лишь те, в которых программа делает выбор, в какую сторону ей "свернуть". А потому для определения траектории нужно наставить брейкпойнтов на команды условных вызовов и переходов, а все, что находится между этими командами, можно исполнять в автоматическом режиме. Интерес программиста был прост: наблюдая за траекторией "забега" программы, отследить момент, когда программа уклонится в неправильную сторону, а потом найти причину отклонения и исправить программу так, чтобы программа "бегала" по предназначенному ей пути. Обычно отслеживание траектории исполнения по "ключевым точкам" использовалось для получения ответов на вопросы "после какой точки программа начинает работать некорректно", а пошаговое исполнение - чтобы точно определить, какие именно команды формируют неверные данные. И методы эти за все годы, прошедшие со времен их появления, ничуть не утратили актуальность и широко применяются в практически неизменном виде. Технический прогресс не стоял на месте: расставлять брейкпойнты и гонять по ним программу вручную было неудобно, и программистам не могла не прийти в голову мысль "а почему бы не усовершенствовать отладчик таки образом, чтобы собственно трассировка выполнялась в автоматическом режиме".
Прообразом современных средств трассировки был режим "анимации" (то есть замедленного исполнения) программы - при некотором навыке в мелькании содержимого регистров и переменных можно было попытаться отловить полезную информацию и "притормозить" программу в нужный момент. Впрочем, пользы от этого режима в те времена было немного - изучать в режиме анимации сложные программы было неудобно, а анимировать программы, работающие с графикой - и вовсе невозможно (в те времена отладчики функционировали исключительно в текстовом режиме). Однако в некоторых современных отладчиках режим анимации все-таки сохранился, и, надо отметить, толку от него куда больше, чем во времена DOS. Значительно возросшее быстродействие современной техники превратило анимацию из слайд-шоу для особо терпеливых, сопровождавшегося лихорадочным мерцанием экрана, в весьма динамичное действо, наблюдение за которым доставляет лишь удовольствие. Некоторые люди считают, что исполнение кода в режиме анимации никакой практической пользы не приносит, но я придерживаюсь несколько иного мнения: в этом режиме хорошо заметны длинные циклы, а также циклы со счетчиком, особенно если счетчик расположен в одном из регистров. Начинающим, думаю, интересно будет "вживую" понаблюдать работу распаковщика исполняемых программ - лучше один раз увидеть работу несложной процедуры распаковки, чем сто раз прочитать "книжное" описание этого процесса. Для просмотра этого шоу лучше всего взять небольшую программу, сжатую UPX'ом, поскольку "навесная" процедура распаковки UPX - одна из самых простых, и даже если у Вас не получится полностью разобраться в ней при помощи отладчика/дизассемблера, все сложные моменты можно разъяснить по исходным текстам, находящимся в свободном доступе. Читая эту главу, Вы могли заметить, что многие из описанных методов, несмотря на всю их внешнюю простоту, довольно неудобны для практического применения, поскольку требуют от пользователя отличной памяти и внимательности, а также способностей к рекурсивному "чтению" кода.
Попробуйте "пробежаться" отладчиком по достаточно сложной процедуре, при этом строя траекторию исполнения кода в уме. Если у Вас получилось - значит, либо процедура оказалась не очень сложной, либо большинству людей остается лишь позавидовать Вашей памяти. Так или иначе, но программисты пришли к идее автоматической трассировки, то есть пошагового исполнения программы с одновременным "запоминанием" всех сделанных шагов. Основным препятствием на пути к осуществлению этой идеи долгое время был сравнительно небольшой объем оперативной памяти старых ЭВМ и недостаточная мощность процессора. Даже такой старый процессор, как 8086, способен был выполнять десятки и сотни тысяч команд в секунду - представьте, какой объем памяти потребовался бы, чтобы запомнить всего лишь последовательность адресов исполненных команд, не говоря уже о состоянии регистров. Кроме того, на одну команду, выполненную отлаживаемой программой в режиме трассировки, приходятся десятки и сотни команд, выполненных отладчиком - и отсюда возникает заметное падение производительности трассируемой программы. В общем, до некоторого времени реализация такого способа трассировки машинного кода была практически нереальна. Однако когда объем памяти компьютеров начал измеряться мегабайтами, воплощение этой идеи наконец стало возможным. Режим трассировки появился сначала в SoftIce, а затем и в OllyDebug, причем по возможностям трассировки и связанных с ней функций OllyDebug определенно превзошел все прочие известные автору отладчики. Прежде всего следует отметить, что OllyDebug, в отличие от SoftIce, запоминает трассировочную информацию более "интеллектуально" - то есть сохраняет не только список адресов команд, но и модификации регистров. К сожалению, хранить информацию обо всех изменениях в адресном пространстве процесса OllyDebug не может (это потребовало бы совершенно невообразимого объема ОЗУ), но если обращение к переменной производится по указателю на нее, отладчик вполне способен запомнить значение регистра-указателя.
Такое поведение отладчика облегчает задачу отслеживания состояний регистров: если Вас интересует, в какой момент том или ином регистре "появилось" некое число, Вы можете решить эту задачу простым поиском этого числа в текстовом файле. Да-да, именно в текстовом файле - OllyDebug обладает совершенно уникальной на сегодняшний день возможностью сохранять практически любые промежуточные данные из отладчика на жесткий диск, и в число таких данных входит отчет об исполнении программы в режиме трассировки. Вы можете безо всяких ухищрений сохранить список всех исполненных команд, их адресов, а также всю дополнительную информацию об изменениях в регистрах. Пример такого отчета Вы можете увидеть ниже: Address Thread Command Registers and comments Flushing gathered information 01006AEC Main xor ebx, ebx EBX=00000000 01006AEE Main push ebx pModule = NULL 01006AEF Main mov edi, dword ptr ds:[<&KERNEL32.GetModuleHandleA>] EDI=77E7AD86 01006AF5 Main jnz short NOTEPAD.01006B16 EAX=01000000 01006AF7 Main mov ecx, dword ptr ds:[eax+3C] 01006AFC Main add ecx, eax 01006AFE Main cmp dword ptr ds:[ecx], 4550 ECX=000000E8 01006B01 Main jnz short NOTEPAD.01006B15 ECX=010000E8 Как распорядиться столь подробным отчетом - зависит только от Ваших целей и изобретательности. Я могу лишь подсказать самые общие направления поиска. Прежде всего, стоит проанализировать значения, находящиеся в первой колонке, то есть адреса команд и "наложить" список адресов выполненных команд на листинг дизассемблирования программы (для W32Dasm, который формирует этот листинг в виде текстового файла, технически осуществить это несложно). Такое слияние "мертвого" листинга с отчетом отладчика о работе "живого" кода способно значительно облегчить понимание того, в какой точке программа повела себя "не так", и как наставить ее на путь истинный. Также бывает удобным "спроецировать" конкретную траекторию исполнения кода на схему всех возможных путей исполнения кода (например, обвести часть стрелочек красным карандашом) - сочетая этот прием с патчингом кода и/или модификацией данных для принудительной активации тех или иных траекторий, можно последовательно отсеивать траектории, не ведущие к желаемой цели. Здесь следует сделать небольшое отступление и рассказать об одном принципиальном преимуществе текстового представления дизассемблерных листингов перед всевозможными упакованными двоичными форматами.
Текст - это один из самых старых и универсальных способов передачи информации, и для его обработки создано огромное количество всевозможных утилит. Инструменты работы с текстовыми файлами сами по себе очень стары, но, тем не менее, практически не устаревают, алгоритмы, в них использующиеся, "вылизаны" и доведены до совершенства поколениями программистов, и потому было бы неразумно отвергать столь огромный пласт программистской культуры. Конечно, использование узкоспециализированных двоичных форматов в дизассемблерах позволяет сэкономить дисковое пространство, ускорить обработку данных и хранить вместе с листингом различную дополнительную информацию, например, списки перекрестных ссылок, но за это приходится расплачиваться неоправданной интеграцией собственно дизассемблера и средства просмотра дизассемблированного текста. А просмотрщики дизассемблерных листингов, увы, обычно проектируются по остаточному принципу. Если рассматривать наиболее популярные в настоящее время W32Dasm и IDA Pro, то можно обнаружить не всегда удобную навигацию, ориентацию на исключительно текстовый режим работы (как я уже говорил, построение блок-схем в IDA Pro реализовано в виде программы сторонних разработчиков, не поддерживающей интерактивную работу с кодом), и некоторые другие недостатки. Авторы дизассемблеров, впрочем, вполне осознают недостатки двоичных форматов и полезность прямой работы с текстом, а потому предусмотрели возможность экспорта в текстовый формат и даже "обычного" текстового поиска в окне дизассемблера. В IDA Pro реализован даже более сложный вариант поиска с использованием регулярных выражений и специальный скриптовый язык, который по идее должен дать пользователю возможность самому добавить в дизассемблер недостающую функциональность. Однако хороший просмотрщик текстов, набор двоичных утилит *NIX'ового происхождения и навыки в программировании способны творить с "сырыми" листингами такие чудеса, какие традиционным дизассемблерам и не снились - от простого просмотра с подсветкой синтаксиса до форматирования "лесенкой" текстов на ассемблере.
А поскольку наш отчет о трассировке как раз имеет текстовый формат и является, по сути, специфической формой дизассемблерного листинга, к нему вполне применимы все изложенные выше соображения о работе с текстом. Поиск повторяющихся последовательностей адресов позволяет обнаружить программные циклы, а также узнать, сколько раз эти циклы выполнились. Ну и, разумеется, Вы сможете ответить на вопрос, который начинающие весьма часто задают, но на который весьма редко получают ответ. Вопрос этот такой: "как мне узнать, когда в регистре появляется нужное число?" (иногда встречалась еще более странная формулировка: "как установить брейкпойнт на регистр"). Простой поиск нужного числа в тексте отчета позволит Вам ответить на этот вопрос, хотя полезность такого действия в большинстве случаев весьма сомнительна (по крайней мере, у меня ни разу не возникало такой потребности). Увы, в каждой бочке меда есть своя ложка дегтя, а в нашем случае - даже не одна. При пошаговой отладке (а, стало быть, и в процессе трассировки - тоже) накладные расходы на исполнение одной команды в десятки и сотни раз превышают собственно время исполнения команды, из-за чего скорость исполнения кода в режиме трассировки падает катастрофически. О расходах памяти на хранение информации о порядке исполнения команд я уже упоминал, однако это еще не все: если внутри отладчика эта информация подвергается упаковке для более компактного хранения, то когда Вы сохраняете эту информацию на диск в виде текста, результирующий файл может получиться просто огромным. И он будет тем огромнее, чем большее количество команд было исполнено в режиме трассировки. Так что в ближайшем будущем нам, увы, не светит возможность запустив программу под отладчиком в режиме трассировки, беспрепятственно снимать с нее "жизненные показатели", выискивая подозрительные значения регистров и "нехорошие" команды переходов. Трассировка - это метод, пригодный для анализа сравнительно небольших кусков кода, чего, впрочем, обычно более чем достаточно.Также есть принципиальные проблемы с трассировкой программ, активно работающих с "железом", многопоточных программ и с софтом, использующим коммуникацию между процессами - все эти группы ПО, даже не содержащие защит, во время трассировки могут вести себя очень капризно. [C] CyberManiac Содержание Далее
Теоретические основы крэкинга
Что тебя смутит – то ложь
Е. Летов
За пять лет более или менее активного занятия крэкингом (а именно столько времени прошло с того знаменательного момента, как мне удалось впервые в жизни взломать программу) мне удалось выработать набор методов, позволяющих сравнительно быстро найти подход к большинству программ. Возьму на себя смелость утверждать, что именно эта система наиболее доступна для начинающих, поскольку она базируется не на раскрытии алгоритма работы защиты в целом, а на анализе сравнительно небольших участков кода и установлении взаимосвязи между ними.
Несмотря на то, что крэкеры, в большинстве своем, работают под одной хорошо известной операционной системой и пользуются сходными инструментами, я буду делать упор не на описание этих инструментов и правил работы с ними, а на универсальные идеи (хотя в качестве примеров я, понятное дело, буду приводить то, что наиболее доступно для самостоятельных экспериментов). Думаю, что Вы сами определитесь, какие инструменты Вам больше по душе.
Предлагаемую мной систему не следует воспринимать как единственно правильную, это не «истина в последней инстанции», но именно эта система помогла мне взломать десятки программ, так что ее эффективность неоднократно проверена и подтверждена длительной практикой. Моя точка зрения такова: одна лишь практическая эффективность может служить критерием того, какие идеи и технические приемы могут и должны применяться крэкером в его «профессиональной» деятельности. Именно поэтому я настоятельно рекомендую отрешиться от любых утверждений о «некрасивости» тех или иных приемов борьбы с защитами и ориентироваться лишь на достижение конечной цели – независимо от того, является ли этой целью раскрытие алгоритма работы какого-либо куска кода, или же простое снятие триальных ограничений в программе.
Так или иначе, статей от том, «что делать», то есть как взломать конкретную программу или тип защиты, во много раз больше, чем руководств «как и почему надо делать именно это». Образно говоря, статьи о том, «что делать» - это уровень начальной школы, «почему был выбран именно такой прием» - уровень выпускных классов.
Но ведь есть еще и высшая школа - изучение идей, которые не привязаны к каким-либо программам и типам защит, но могут быть адаптированы для решения любой конкретной задачи. Выше – только «научная работа», то есть область чистого творчества, генерация оригинальных идей, и никакой «учебник» в этой области принципиально невозможен. По моему глубокому убеждению, основная проблема, возникающая у начинающих крэкеров, заключается в огромном количестве пособий уровня «делай, как я», совершенно не объясняющих, почему автор пособия поступил именно так. В результате начинающий крэкер «на ура» ломает новые версии одной и той же программы, но теряется перед подобной же защитой, но реализованной слегка иначе. Разумеется, существуют и весьма качественные «учебники», предлагающие именно систему, а не только набор технических приемов (те же + ORC Cracking Tutorialz или руководство от the Keyboard Caper’а) – но абсолютное большинство их них написаны на английском языке. Поскольку каждый человек имеет право получать необходимую информацию на своем родном языке (а для меня, как и для многих других, родным языком является русский), рано или поздно должны были появиться русскоязычные тексты, систематизирующие опыт крэкинга. Именно такой материал я и старался написать, а насколько хорошо это у меня получилось - решать вам.
Содержание
Далее