Кое-что о том, как можно патчить приложения

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

Ищем, что патчить

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

Первый, самый простой, распространенный и довольно эффективный - нахождение в коде строк, имеющих какое-либо отношение к регистрации. Благо сообщения об успешной/неуспешной активации программы, о количестве оставшихся до окончания срока функционирования дней, содержимое NAG-окна, записи в About и т.п. - все хранится в программе и, как правило, в виде plain-текста, поэтому их поиск в программе будет не очень сложен. После успешного нахождения строки остается только поймать в программе код, который использует ее. Это можно сделать либо с помощью любого дизассемблера, либо с помощью OllyDbg. Обычно такие строчки встречаются в коде в виде инструкций наподобие “mov eax, prog.004FB613″ или “push prog.004FB613″, где по адресу 004FB613 как раз и лежит искомая строка. Далее путем статического или динамического метода (визуальный метод или метод трассировки) определяется, является ли найденный код важным для взлома.

Второй прием, кстати, не менее эффективный - останов на API-функциях, вызываемых в критичных для взлома участках. Для его применения нужны довольно глубокие познания в области набора API-функций для конкретной версии Windows, поэтому перед употреблением советую хорошенько изучить MSDN последней версии. Для того чтобы воспользоваться этим приемом, нужно хотя бы примерно представлять себе, что делает программа, пытаясь стрясти с тебя некоторую сумму денег за регистрацию. Как правило, она просит ввести что-нибудь вроде имени/рег. кода/e-mail. В этом случае нужно ловить место регистрации по API-функциям GetDlgItem, GetDlgItemTextA, GetWindowTextA.

Если тебе повезло и ты поймал программу в процессе ввода серийника на одной из этих API, то, выйдя из дебрей системных библиотек и немного потрассировав код, ты, скорее всего, найдешь место проверки или какой-нибудь другой манипуляции введенных тобой данных. Можно также ловить место регистрации функциями ShowWindow, MessageBoxA, MessageBoxExA, MessageBoxIndirectA и недокументированной MessageBoxTimeoutA, отвечающими за выводы различных окошек с сообщениями. Соответственно, если выдаются сообщения вида “Вы ввели неправильный код” или что-то очень похожее, то, когда вылезешь из системных дебрей, посмотри на код, находящийся выше/раньше вызова этого сообщения, чтобы найти код, критичный для взлома.

Также программа может издавать характерный звук при выводе ошибки - тут можно попробовать отловить код на MessageBeep. В случае неудачи в первых двух случаях можно попробовать поискать места чтения/записи значений из реестра, так как программисты порой очень любят хранить там регистрационные данные своей программы. Здесь тебе помогут API RegOpenKeyA, RegQueryValueA, RegQueryValueExA, RegCreateKeyA, RegSetValueA и RegSetValueExA. При анализе данных, передаваемых в реестр, всегда есть вероятность, что ты наткнешься на критичный код. Данный способ немного муторный, так как программы обычно считывают множество параметров реестра, и чем больше программа, тем больше нагоняется трафика, который нужно анализировать. Кстати, иногда программисты записывают регистрационные данные в файл. Здесь все немного проще. Существует замечательная API CreateFileA, вызываемая всегда - как при открытии какого-либо файла, так и при его создании. Аналогично, анализируя параметры вызываемой CreateFileA, можно нарваться на место для будущей модификации.

Если же программа проверяет, запустили ее с оригинального диска или нет, то, как правило, бывает достаточно брякнуться на API GetDriveTypeA. Эта функция просто проверяет тип заданного диска (в данном случае диска, с которого запущена программа). Если возвращенное значение равно пяти, значит это CD/DVD-привод. После запуска этой функции должны идти разные проверки на соответствие метки диска, наличия какого-нибудь файла и т.п. Их и нужно патчить.

Естественно, это не все приемы поиска важного для взломщика кода - лишь основные. Подробности в этом номере журнала.

Нашли? Патчим!

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

Прямой патчинг

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

  1. распаковать программу (это, думаю, не вызовет трудностей);
  2. найти код, ответственный за регистрацию;
  3. прямо в распакованной программе модифицировать найденный код определенным образом.

В итоге распакованная и модифицированная программа - это, по сути, и есть крэк. Вернее, программа, просто взломанная прямым патчингом. Как видишь, все зло сведено к минимуму, сделать такой крэк очень просто даже без особых затрат времени. Чтобы прояснить, как искать критичный код и как патчить его, разберемся со всем этим делом, как говорится, на живом примере. Исследуем и взломаем реальную программу - игру HyperBalloid Complete Edition 1.20, которую можно скачать с сайта www.reflexive.net. В процессе патчинга будем пользоваться только отладчиком OllyDbg. Запускаем программу и видим NAG-окно с любезным предложением зарегистрироваться и указанием количества минут, оставшихся от trial-периода.

Сразу же попытаемся отловить процедуру регистрации, поставив бряки на описанные в начале статьи API-функции. Итак, жмем на кнопку Already Paid в NAG’е и видим окно с приглашением ввести регистрационный код.

Переходим в отладчик и ставим точки останова сразу на все указанные API: GetDlgItem, GetDlgItemTextA, GetWindowTextA, MessageBoxA, MessageBoxExA, MessageBoxIndirectA, MessageBoxTimeoutA, ShowWindow, вводя bp <имя_API_функции> в поле Command. Введем какой-нибудь, неважно какой, серийник, нажмем Submit, и, как это ни странно, увидим сообщение - якобы неправильно набран номер ;).

Отсюда сделаем вывод, что, если мы не остановились ни на одной из функций, то в игре используются иные методы взятия введенной информации и вывода результата. Что ж, не будем отчаиваться. Перезапустим программу и пойдем по первому указанному мной методу - посмотрим наличие строк в коде, имеющих отношение к регистрации. Нажав правой кнопкой мыши по любому участку кода и выбрав пункт Search for->All referenced text strings, ты сможешь увидеть окно со списком всех строк, встречающихся в программе, и с информацией о коде, который использует эти строки. Честно говоря, найти строки из NAG-скрина вряд ли повезет. При размере exe-файла 144 Кб вряд ли в нем будут находиться процедура регистрации и сам код игры. Скорее всего, в этом случае весь код вынесен из основного модуля приложения в динамически подгружаемые библиотеки.

Итак, анализируя выведенные строки (благо из-за размера exe-файла их там не очень много), я наткнулся на подозрительную:

0040631B PUSH game.0041DAA8 ASCII “radll_HasTheProductBeenPurchased”

Очень похоже на вызов функции из библиотеки, проверяющий, приобретена ли программа. Поставим точку останова на этот PUSH, то есть на адрес 0040631B, выделив строку и нажав <F2>. Запустим игру по <F9> и, как это ни странно, еще до появления каких-либо окон остановимся на этом адресе. И вот показался очень важный код.

Не нужно быть reverse engineer’ом, чтобы, взглянув на инструкцию call esi и на esi = 77E7B332 kernel32.GetProcAddress, сообразить, что из какой-то библиотеки берется адрес функции radll_HasTheProductBeenPurchased и он записывается в некоторую переменную по адресу 0042319C. Если посмотреть на строку Reflexiv.00A70000, можно сделать вывод, что эта функция берется из библиотеки ReflexiveArcade.dll. Ее мы обнаружим в папке игры в директории ReflexiveArcade.

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

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

Находим его довольно быстро - нули начинаются с адреса 004198AE. Чтобы было проще запомнить, спустимся еще чуть ниже до адреса, кратного 100h - 00419900, который и сделаем адресом нашей функции. Выделим стоку 00419900 и нажмем пробел для ввода кода по этому адресу. Вобьем в появившемся окошке mov eax, 1 и нажмем <Enter>. В регистре eax, как ты и сам знаешь, обычно содержится значение, возвращаемое функцией. В данном случае этим значением может быть только 1. Так как мы пишем функцию, а не просто кусок кода, мы должны позаботиться о том, чтобы код вернулся на то место, откуда был запущен. Поэтому нужна еще одна инструкция - ret. Вбиваем ее и жмем <Enter>.

Все. Нажмем Cancel для отмены дальнейшего ввода кода. Получена мини-функция из шести байт. Теперь вернемся к месту, где записывался адрес функции radll_HasTheProductBeenPurchased. Для этого выделим в окне регистров EIP, тыкнем по нему правой кнопкой мыши и выберем Origin. Окажемся по адресу 0040631B. В принципе, весь местный код нужно вырезать совсем: нам ни на что не сдался этот GetProcAddress. Поэтому, стоя на адресе 0040631B, нажмем пробел и введем MOV EAX,419900, то есть подставим вместо оригинального адреса функции свой. Остальные команды нам не нужны, поэтому вводим далее инструкции nop до адреса 00406329 включительно.

Нам остается только сохранить все изменения в программе и протестировать ее. Выделяем весь код с 00401000 по 00419FFF, выбираем в контекстном меню Copy to executable->Selection и указываем в появившемся окне файл, куда хотим сохранить пропатченную версию игры. После этого можно закрывать отладчик и пробовать запустить игру. Вуаля! Она прекрасно запустилась и, обращаю на это твое внимание, без всяких приглашений зарегистрироваться. При выходе из игры нас мило благодарят за приобретение.

“Нет, нет, что вы! Вам спасибо”. Сделаем некоторые выводы. Мало того, что лентяи программисты из Reflexive не делают дополнительных проверок, так они еще и называют экспортируемые (!) функции из dll как radll_HasTheProductBeenPurchased. Крайне безответственно с их стороны. Ну что ж, их лень - наши сэкономленные деньги.

Кстати, не могу не заметить, что подобным образом ломается любая игра с сайта www.reflexive.net.

Патчинг загрузчиком

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

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

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

.data

; заголовок окна с сообщением об ошибке

Msg db "Fatal Error", 0

; сообщение об ошибке

Error db "Program not found",0

; имя файла программы

program db "victim.exe",0

; записываемый в память процесса байт

write_buffer db 90h

; адрес, по которому будет

; осуществляться считывание/запись

check_addr DWORD 401050h

.data?

; переменная, в которую производится

; считывание байта процесса

buffer dw ?

; структура информации о процессе

process_info PROCESS_INFORMATION &lt;&gt;

; структура информации о параметрах

; создающегося процесса

startup_info STARTUPINFO &lt;&gt;

.code

start: ;начало программы

; запускаем нужные нам программы.

invoke CreateProcess,addr program, NULL, NULL, NULL, FALSE,

CREATE_NEW_CONSOLE OR NORMAL_PRIORITY_CLASS, NULL, NULL, addr startup_info,

addr process_info

;если результат выполнения равен 0,

; то программа не найдена и не запустилась

.if eax == 0

; информируем об ошибке

invoke MessageBox, NULL, addr Error, addr Msg, MB_OK

; и выходим

invoke ExitProcess,0

.endif

; главный цикл

.while true

; считываем память процесса по

; адресу check_addr в буфер buffer размером в 1 байт

invoke ReadProcessMemory, process_info.hProcess, check_addr, addr buffer,

1, NULL

; проверка на успешность считывания

.if eax != 0

; проверка на распакованность

; программы по этому адресу

.if bufr != 00h

;ждем проверку целостности кода

invoke Sleep,300

; приостанавливаем процесс

invoke SuspendThread, addr process_info.hThread

; записываем 1 байт write_buffer

; по адресу check_addr

invoke WriteProcessMemory, process_info.hProcess, check_addr, addr

write_buffer, 1, NULL

; продолжаем выполнение программы

invoke ResumeThread, addr process_info.hThread

; закрываем хэндл процесса и

; завершаем свой процесс

invoke CloseHandle, process_info.hThread

invoke ExitProcess, 0

.endif

.endif

.endw

; конец кода
<p style="text-align: justify;">end start

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

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

Inline-патчинг

Это довольно сложный метод. Возможно, он чем-то напомнит тебе прямой патчинг, но они схожи только по подходу к проблеме, а отличаются и принципами работы, и большинством случаев в области применения: inline-патчинг делается для программ, круто обработанных пакерами/протекторами, которые, вероятно, даже невозможно распаковать с ходу. Смысл метода в том, что в место программы, которое передает управление на OEP (на оригинальный Entry Point), то есть в место, получающее управление, когда вся программа уже распакована, встраивается код, который уже будет модифицировать некоторые байты, отучать от регистрации - собственно, производить взлом. Отработав свое, патчащий код возвращает управление на OEP уже взломанной программе. Для чего нужен этот метод? Ну, хотя бы для того, чтобы уменьшить размер крэка. Если ломаешь программу прямым патчингом, приходится раздавать крэк либо в виде взломанного exe’шника, что ужасно, особенно если вспомнить многомегабайтные Delphi-монстры, либо в виде программы-патча вместе с распаковщиком, что тоже, скорее всего, будет весить немало. Сложность метода inline-патчинга, как ты понимаешь, заключается в том, чтобы вычислить адрес прыжка на OEP и грамотно создать код, модифицирующий программу. Подробно о том, как реализуется этот метод, вместе с его примерами читай на www.cracklab.ru.

На дорожку

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

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