Способы обхода защиты и взлома программ

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

Способ взлома программ 1. Поиск введенного пароля в памяти

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

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

РЕКОМЕНДУЕМ:
Взлом приложений для Андроид с помощью отладчика

Давайте подойдем к решению проблемы от обратного — будем искать не оригинальный пароль, который нам неизвестен, а ту строку, которую мы скормили программе в качестве пароля. А найдя, установим на нее бряк, и дальше все точно так же, как и раньше. Бряк всплывает на обращение по сравнению, мы выходим из сравнивающей процедуры, корректируем JMP и…

Взглянем еще раз на исходный текст ломаемого нами примера passCompare1.cpp:

Обратите внимание — в buff читается введенный пользователем пароль, сравнивается с оригиналом, затем (при неудачном сравнении) запрашивается еще раз, но (!) при этом buff не очищается! Отсюда следует, что, если после выдачи ругательства Wrong password вызвать отладчик и пройтись по памяти контекстным поиском, можно обнаружить тот заветный buff, а остальное уже дело техники!

Итак, приступим (мы еще не знаем, во что мы ввязываемся, — но, увы, в жизни все сложнее, чем в теории). На этот раз запустим passCompare1.exe отдельно от отладчика. Затем подключимся к процессу из отладчика (Attach to process в WinDbg). Хочу обратить ваше внимание: в окне выбора процесса отображаются все запущенные процессы и для каждого из них выводится его разрядность в столбце Platform. Вводим любой пришедший на ум пароль (например, KPNC Kaspersky++), пропускаем возмущенный вопль Wrong мимо ушей и в отладчике нажимаем Break (сочетание клавиш Alt + Del).

Окно со списком процессов для выбора
Окно со списком процессов для выбора

Попробуем отыскать в памяти введенный пароль:

Пояснения

Первый параметр после команды s — флаг -a — определяет цель поиска как набор ASCII-символов. Второй параметр — смещение, откуда начать искать. Вообще-то начинать поиск с нулевого смещения — идея глупая. Судя по карте памяти, здесь расположен служебный код и искомого пароля быть не может. Впрочем, это ничему не вредит, и так гораздо быстрее, чем разбираться, с какого адреса загружена программа и откуда именно начинать поиск.

Третий параметр — верхний предел поиска, то есть «докуда». Здесь у нас стоит максимальное 32-битное знаковое число, таким образом мы охватываем весь возможный диапазон 32-битного процесса.

Последний параметр — собственно искомая строка. Обратите внимание, что мы ищем не всю строку, а только ее часть (KPNC Kaspersky++ против KPNC Kaspersky). Это позволяет избавиться от ложных срабатываний, возникающих из-за ссылок на внутренние буфера.

Результат (у вас значения, скорее всего, получатся другими, и они будут меняться при каждом перезапуске приложения):

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

Начинаем думать.

На помощь приходит карта памяти — зная владельца региона, которому принадлежит буфер, можно очень многое сказать об этом буфере. Наскоро набив уже знакомую команду !dh passCompare1, мы получим приблизительно следующее (выбраны сведения только о секциях .data и .rdata):

Заодно определим базовый адрес модуля приложения: lmf m passCompare1 (в моем конкретном случае он равен 0xDE0000, а у вас значение, скорее всего, будет другим). Узнаем, куда в памяти загружена секция .rdata: 0xDE0000 + 0x2000 == 0xDE2000, а также куда загружена секция .data: 0xDE0000 + 0x3000 == 0xDE3000. Это гораздо выше найденных адресов расположения буферов с введенным паролем. Следовательно, найденные адреса не указывают в области .data и .rdata.

Думаем дальше. Адрес 0x147fd80 выходит далеко за пределы ломаемого приложения, и вообще непонятно, чему принадлежит. Почесав затылок, мы вспомним о такой «вкусности» Windows, как куча (heap). С помощью команды !heap посмотрим, где она начинается:

Из этого заключаем, что адрес 0x147fd80 явно находится в куче.

Разбираемся дальше. Поскольку стек растет снизу вверх (то есть от старших адресов к младшим), адрес 0xf9f810 находится в стеке. Уверенность подогревает тот факт, что большинство программистов размещает буфера в локальных переменных, ну а локальные переменные, в свою очередь, размещаются компилятором в стеке.

Ну что, попробуем поставить бряк по первому адресу?

На втором запросе пароля снова вводим KPNC Kaspersky++. Жмем Enter и дожидаемся сиюминутной активации отладчика. Бряк произошел на второй из этих строк:

Смотрим, что находится в регистре esi:

Впрочем, этого и следовало ожидать. Попробуем выйти из текущей функции по Shift + F11. И мы снова попадем на эту же строку. Вновь посмотрим содержимое этого регистра:

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

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

Как раз то, что мы ищем!

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

Выводы

Итак, мы познакомились с одним более или менее универсальным способом взлома защит, основанных на сравнении пароля (позже мы увидим, что он подходит и для защит, основанных на регистрационных номерах). Его основное достоинство — простота. А недостатки… недостатков у него много:

  • если программист очистит буфера после сравнения, поиск введенного пароля ничего не даст, разве что останутся системные буфера, которые так просто не затереть, но отследить перемещения пароля из системных буферов в локальные не так-то легко;
  • служебных буферов много, и очень трудно определить, какой из них «настоящий». Программист же может располагать буфер и в сегменте данных (статический буфер), и в стеке (локальный буфер), и в куче, и даже выделять память низкоуровневыми вызовами типа VirtualAlloc или… да мало ли как разыграется его фантазия. В результате подчас приходится просеивать все найденные вхождения тупым перебором.

Способ взлома программ 2. Бряк на функции ввода пароля

Взлом приложения с GUI

Настала пора разнообразить наш объект взлома. Теперь попробуем заломить приложение с графическим интерфейсом. В качестве тренировки разберем passCompare3. Это то же самое, что и passCompare1.exe, только с графическим интерфейсом на основе MFC Dialog Based App (см. в скачиваемых материалах к статье).

Взлом программ. MFC Application Wizard
Старый добрый MFC Application Wizard

Также обратите внимание на то, что работа с текстом в этом примере организована по-другому. Если раньше мы работали с базовым типом char, то здесь используется обертка — класс CString, что, скорее всего, при взломе профессиональных приложений будет встречаться нам чаще. Кроме двух кнопок, идущих в заготовке по умолчанию, добавьте на форму элемент Edit Control. Свяжите его с переменной m_password и создайте событие обработки нажатия на кнопке OK. Это и будет ключевая процедура приложения, проверяющая введенный пароль на равенство эталонному:

Кажется, никаких сюрпризов не предвидится.

При всем желании метод прямого поиска пароля в памяти элегантным назвать нельзя, да и практичным тоже. А собственно, зачем искать сам пароль, спотыкаясь о беспорядочно разбросанные буфера, когда можно поставить бряк непосредственно на функцию, его считывающую? Хм, можно и так… да вот угадать, какой именно функцией разработчик вздумал читать пароль, вряд ли будет намного проще.

На самом деле одно и то же действие может быть выполнено всего лишь несколькими функциями и их перебор не займет много времени. В частности, содержимое окна редактирования обычно добывается при помощи либо функции GetWindowTextW (чаще всего), либо функции GetDlgItemTextW (а это значительно реже). Все версии Windows NT и младше предпочитают работать с юникодом, поэтому на конце функций работы с текстом W (wide), а не A (ASCII).

Раз уж речь зашла об окнах, запустим наш GUI «крякмис» и установим точку останова на функцию GetWindowTextW (bp User32!GetWindowTextW). Хотя эта функция — системная, точка останова не будет глобальной и не затронет все приложения в системе, а будет функционировать только в контексте данного приложения.

Вводим какой-нибудь пароль (KPNC Kaspersky++, по обыкновению), нажимаем клавишу Enter, и отладчик незамедлительно всплывает:

Может случиться так, что сначала произойдет «левая» активация отладчика. Ее можно пропустить многократным нажатием F5 до тех пор, пока отладчик вновь не всплывет на этой же функции. А лучше, чтобы не пропустить нужный момент, как только мы попадем в GetWindowTextW, выйти из нее по нажатию Shift + F11 и посмотреть, куда попадаем. Если вокруг находятся вызовы перерисовки элементов, то, значит, это процедура перерисовки формы и нам не сюда.

Перерисовка элементов:

Если же после выхода из USER32!GetWindowTextW мы попадаем в passCompare3!CWnd::GetWindowTextW, делаем еще один выход. Следуя логике, мы в результате попадаем в обработчик нажатия кнопки OK на форме или Enter на клавиатуре, прямо на первую строчку приведенного ниже листинга:

Жмем F10, чтобы сделать шаг вперед в трассировке программы. Теперь можем узнать значение в регистре eax:

Хорошо, введенный пароль, есть контакт. Только почему после каждого символа стоит точка? Думаю, вы уже догадались, что она означает двухбайтовую природу символа перед ней. Отхлебнув пивка, кваса или лимонада (по желанию), вспоминаем, что, хоть класс CString и может работать с типами char (однобайтовое представление символов) и wchar_t (многобайтовое представление до четырех байт, то есть юникод в UTF-8, -16 или -32), это зависит от настроек компилятора. А именно от того, какой символ включен: MBCS — char, UNICODE — wchar_t. Чаще всего используется второй набор символов, так как по умолчанию включены именно широкие символы.

Судя по всему, где-то рядом притаился эталонный пароль. Сделаем пару шагов внутри процедуры. Мы попадаем на строку 00ef2850 8bce mov ecx, esi, которая находится в конце приведенного выше листинга.

Проверим содержимое регистра ecx:

И правда! Интуиция нас не подвела, эталонный пароль тут как тут.

Обход защиты программы. Работа приложения
Работа приложения

Изменяем тип данных

А что, если бы программист для сохранения введенного пароля воспользовался не классом CString, а по старинке — массивом широких символов wchar_t? Давайте посмотрим семпл passCompare35. Он отличается от предыдущего только изменением типа данных считываемой строки и использованием перегруженного метода для ее считывания:

Как видите, у этого метода появился параметр размерности строки, он же величина строкового буфера, добавьте его объявление в начало программы: const int MAX_PASSWORD_SIZE = 0x666;.

Натравим отладчик на исполняемый файл. Поставим бряк на функцию GetWindowTextW, как в прошлом примере. Теперь, если проследить выполнение программы после всплытия отладчика, мы не обнаружим эталонный пароль на прежнем месте.

Что же делать? Как теперь его искать? Мы пойдем другим путем, но в том же направлении. Когда мы окажемся в passCompare3!CWnd::GetWindowTextW после вызова User32!GetWindowText, у нас уже будет считанный из элемента управления буфер, содержащий строку. Если на этот буфер поставить бряк, то мы доберемся до места, где пароли сравниваются.

Вот там мы поймаем эталонный пароль. Но как узнать адрес буфера? Есть по меньшей мере два пути. Первый — воспользоваться командой kp, она выведет стек вызовов всех функций с их параметрами. На вершине будет последняя вызванная функция, в которой мы сейчас находимся, с параметрами:

Обход защиты программы. WinDbg с окном Locals
WinDbg с окном Locals

Второй способ — воспользоваться сведениями из окна Locals (полезная штука): View → Locals. Если бы программист оставил пароль в локальных переменных какой-либо функции, мы бы просто увидели его в окне Locals. Удобно, безусловно. Итак, адрес буфера с паролем узнали (в вашем случае он будет другим), осталось легким движением руки поставить бряк:

Продолжим выполнение. Отладчик тут же вспыхивает снова в функции passCompare35!CpassCompare35Dlg::OnBnClickedOk, прямо на последней строке:

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

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

РЕКОМЕНДУЕМ:
Безопасность UEFI

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

Этот способ универсален, и впоследствии мы еще не раз им воспользуемся. Вся соль — определить ключевую функцию защиты и поставить на нее бряк. В Windows все поползновения (обращения к ключевому файлу, реестру и прочее) сводятся к вызову функций API, перечень которых хоть и велик, но все же конечен и известен заранее.

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (5 оценок, среднее: 5,00 из 5)
Загрузка...
Понравилась статья? Поделиться с друзьями:
Добавить комментарий