HTB Flight. Повышаем привилегии в Windows
the Matrix • Cyber News
Перед тобой уже во второй раз обновленная версия цикла «Фундаментальные основы хакерства». В 2018 году Юрий Язев изменил текст Криса Касперски для соответствия новым версиям Windows и Visual Studio, а теперь внес правки с учетом отладки программ для 64-разрядной архитектуры.
Читай также улучшенные версии прошлых статей цикла:
Все новые версии статей доступны без платной подписки.

Цикл «Фундаментальные основы хакерства» со всеми обновлениями опубликован в виде книги, купить ее по выгодной цене ты можешь на сайте издательства «Солон‑пресс».
В прошлой статье цикла мы узнали, как соотнести адреса байтов в виртуальной памяти с их реальным расположением на носителе. Это потребовало от нас напрячь мозг и применить математику. Между тем, как мы увидели из предыдущих статей, непосредственный взлом, когда известно месторасположение защитного механизма, представляет собой элементарную задачу, которую легко решить с помощью Hiew или другого редактора PE-файлов.
Материалы к статье на GitHub
Пароль, хранящийся в теле программы открытым текстом, — скорее из ряда вон выходящее исключение, чем правило. К чему услуги хакера, если пароль и без того виден невооруженным глазом? Поэтому разработчики защиты всячески пытаются скрыть его (о том, как именно они это делают, мы поговорим позже).
Впрочем, учитывая размер современных дистрибутивов, программист может без особого труда поместить пароль в любом завалящем файле, попутно снабдив его «крякушами» — строками, выглядящими как пароль, но паролем не являющимися. Попробуй разберись, где тут липа, а где нет, тем более что подходящих на эту роль строк в проекте средней величины может быть несколько сотен, а то и тысяч!
Давай подойдем к решению проблемы от обратного — будем искать не исходный пароль, который нам неизвестен, а ту строку, которую мы скормили программе в качестве пароля. А найдя, установим на нее бряк, и дальше всё точно так же, как и раньше. Бряк всплывает на обращение по сравнению, мы выходим из сравнивающей процедуры, корректируем JMP и...
Взглянем еще раз на исходный текст ломаемого нами примера passCompare1.cpp:
Обрати внимание — в buff читается введенный пользователем пароль, сравнивается с оригиналом, затем (при неудачном сравнении) запрашивается еще раз, но (!) при этом buff не очищается! Отсюда следует, что, если после выдачи ругательства «Wrong password» вызвать отладчик и пройтись по памяти контекстным поиском, можно обнаружить тот заветный buff, а остальное уже дело техники!
Итак, приступим (мы еще не знаем, во что мы ввязываемся, — но, увы, в жизни все сложнее, чем в теории). На этот раз запустим passCompare1.exe отдельно от отладчика. Затем подключимся к процессу из отладчика («Attach to process» в WinDbg). Обрати внимание: в окне выбора процесса отображаются все запущенные процессы и для каждого из них выводится его разрядность в столбце Platform. Вводим любой пришедший на ум пароль (например, KPNC Kaspersky++), пропускаем возмущенный вопль Wrong мимо ушей и в отладчике нажимаем Break (сочетание клавиш Alt-Del).

Попробуем отыскать в памяти введенный пароль:
Первый параметр после команды s — флаг -a — определяет цель поиска как набор ASCII-символов. Второй параметр — смещение, по которому начать искать. Вообще‑то начинать поиск с нулевого смещения — идея глупая. Судя по карте памяти, здесь расположен служебный код и искомого пароля быть не может. Впрочем, это ничему не вредит, и так гораздо быстрее, чем разбираться, с какого адреса загружена программа и откуда именно начинать поиск.
Третий параметр — верхний предел поиска, то есть докуда надо искать. Так как в 64-битной Windows адресное пространство процесса ограничено 8 Тбайт, верхний лимит составляет 0x7FFFFFFFFFF. Последний параметр — собственно искомая строка. Обрати внимание, что мы ищем не всю строку, а только ее часть (KPNC Kaspersky++ против KPNC Kaspersky). Это позволяет избавиться от ложных срабатываний, возникающих из‑за ссылок на внутренние буфера.
Целых два вхождения! Почему два? Предположим, что при чтении ввода с клавиатуры символы сперва попадают в системный буфер, который и дает ложное срабатывание. Тем не менее не ставить же, не разобравшись, сразу обе точки останова. В данном случае четырех отладочных регистров процессора хватит, а как быть, если бы мы нашли десяток вхождений? Да и в двух бряках немудрено заблудиться с непривычки! Как отфильтровать помехи?
На помощь приходит карта памяти — зная владельца региона, которому принадлежит буфер, можно очень многое сказать об этом буфере. Наскоро набив уже знакомую команду !dh passCompare1, мы получим приблизительно следующее (выбраны сведения только о секциях .data и .rdata):
Заодно определим базовый адрес модуля приложения: lmf m passCompare1 (в моем конкретном случае он равен 0x7ff7d78f0000, а у тебя значение, скорее всего, будет другим). Узнаем, куда в памяти загружена секция .rdata:
И куда загружена секция .data:
Это гораздо выше найденных адресов расположения буферов с введенным паролем. Следовательно, найденные адреса не указывают в области .data и .rdata.
Думаем дальше. Адрес 0x1dcd30f2580 выходит далеко за пределы ломаемого приложения, и вообще непонятно, чему он принадлежит. Почесав затылок, мы вспомним о такой «вкусности» Windows, как куча (heap). С помощью команды !heap посмотрим, где она начинается:
Из этого заключаем, что адрес 0x1dcd30f2580 явно находится в куче.
Разбираемся дальше. Поскольку стек растет сверху вниз (то есть от старших адресов к младшим), адрес 0x2f10effe30 явно находится в стеке. Уверенность подогревает тот факт, что большинство программистов размещает буфера в локальных переменных, ну а локальные переменные, в свою очередь, размещаются компилятором в стеке.
Ну что, попробуем поставить бряк по первому адресу?
На втором запросе пароля снова вводим KPNC Kaspersky++. Жмем Enter и дожидаемся сиюминутной активации отладчика. Бряк произошел на второй из этих строк:
Смотрим, что находится в регистре rsi:
Впрочем, этого и следовало ожидать. Попробуем выйти из текущей функции по Shift-F11. И мы снова попадем на эту же строку. Вновь посмотрим содержимое этого регистра:
Ага, один символ откусан. Следовательно, мы находимся в сравнивающей процедуре. Выйдем из нее нажатием на F5, так как при нажатии на Shift-F11 мы перейдем на следующую итерацию перебора символов.
И вот мы в теле уже хорошо нам знакомой (развивай зрительную память!) процедуры сравнения оригинального и введенного пользователем паролей. На всякий случай для пущей убежденности выведем значение указателей [RDX+RCX] и RCX, чтобы узнать, что с чем сравнивается:
Как раз то, что мы ищем!
Ну а остальное мы уже проходили. Записываем адрес условного перехода (ключевую последовательность для поиска), с помощью сведений из прошлой статьи находим на носителе адрес инструкции, соответствующей спроецированной в памяти, правим исполняемый файл, и всё окей.
Итак, мы познакомились с одним более или менее универсальным способом взлома защит, основанных на сравнении пароля (позже мы увидим, что он подходит и для защит, основанных на регистрационных номерах). Его основное достоинство — простота. А недостатки... недостатков у него много:
Настала пора разнообразить наш объект взлома. Теперь попробуем заломить приложение с графическим интерфейсом. В качестве тренировки разберем passCompare3. Это то же самое, что и passCompare1.exe, только с графическим интерфейсом на основе MFC Dialog Based App (ищи в скачиваемых материалах к статье).

Также обрати внимание на то, что работа с текстом в этом примере организована по‑другому. Если раньше мы работали с базовым типом char, то здесь используется обертка — класс CString, что, скорее всего, при взломе профессиональных приложений будет встречаться нам чаще. Кроме двух кнопок, идущих в заготовке по умолчанию, добавь на форму элемент Edit Control. Свяжи его с переменной m_password и создай событие обработки нажатия на кнопке OK. Это и будет ключевая процедура приложения, проверяющая введенный пароль на равенство эталонному:
Кажется, никаких сюрпризов не предвидится.
При всем желании метод прямого поиска пароля в памяти элегантным назвать нельзя, да и практичным тоже. А собственно, зачем искать сам пароль, спотыкаясь о беспорядочно разбросанные буфера, когда можно поставить бряк непосредственно на функцию, его считывающую? Можно и так... да вот угадать, какой именно функцией разработчик вздумал читать пароль, вряд ли будет намного проще.
На самом деле одно и то же действие может быть выполнено всего лишь несколькими функциями и их перебор не займет много времени. В частности, содержимое окна редактирования обычно добывается при помощи либо функции GetWindowTextW (чаще всего), либо функции GetDlgItemTextW (а это значительно реже). Все версии Windows NT предпочитают работать с юникодом, поэтому на конце функций работы с текстом W (wide), а не A (ASCII).
Раз уж речь зашла об окнах, запустим наш GUI «крякмис» и установим точку останова на функцию GetWindowTextW — bp User32!GetWindowTextW. Хотя эта функция системная, точка останова не будет глобальной и не затронет все приложения в системе, а будет функционировать только в контексте данного приложения.
Вводим какой‑нибудь пароль (KPNC Kaspersky++, по обыкновению), нажимаем клавишу Enter, и отладчик незамедлительно всплывает:
Как видно, мы попали в функцию USER32!GetWindowTextW. Из нее надо выйти на более высокий уровень, нажав Shift-F11. Теперь мы попали в функцию mfc140u!CWnd::GetWindowTextW:
Теперь надо еще потрассировать эту функцию нажатиями Shift-F11. Наконец, мы попадем в функцию, которая является обработчиком нажатия кнопки OK на форме или Enter на клавиатуре:
Сейчас мы можем узнать значение в регистре RAX:
Хорошо, видим введенный пароль. Есть контакт! Только почему после каждого символа стоит точка? Думаю, ты уже догадался, что она означает двухбайтовую природу символа перед ней. Отхлебнув пивка, кваса или лимонада (по желанию), вспоминаем, что, хоть класс CString и может работать с типами char (однобайтовое представление символов) и wchar_t (многобайтовое представление до четырех байт, то есть юникод в UTF-8, -16 или -32), это зависит от настроек компилятора. А именно от того, какой символ включен: MBCS — char, UNICODE — wchar_t. Чаще всего используется второй набор символов, так как по умолчанию включены именно широкие символы.
Сейчас надо аккуратно трассировать программу, по F8 зайти внутрь следующей функции. По дороге мы обнаружим, что наш пароль занял дополнительные буфера, непонятно зачем. А следующая функция, куда мы провалимся, сравнивает строки:
Обрати внимание вот на этот оператор из листинга:
После его выполнения значение в регистре RCX будет указывать на буфер с эталонным паролем:
И правда! Интуиция нас не подвела, эталонный пароль тут как тут.

Введенная пользователем строка и эталонный пароль — как на блюдечке с голубой каемочкой! Замечательно! Вот так, безо всяких ложных срабатываний, элегантно, быстро и красиво, мы победили защиту!
Этот способ универсален, и впоследствии мы еще не раз им воспользуемся. Вся соль — определить ключевую функцию защиты и поставить на нее бряк. В Windows все поползновения (обращения к ключевому файлу, реестру и прочее) сводятся к вызову функций API, перечень которых хоть и велик, но все же конечен и известен заранее.
Источник
Наши проекты:
- Кибер новости: the Matrix • Cyber News
- Хакинг: /me Hacker
👁 Пробить человека? Легко через нашего бота: Мистер Пробиватор