«Хакер»: Используем отладчик для анализа 64-разрядных программ в Windows

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

Пят­надцать лет назад эпи­чес­кий труд Кри­са Кас­пер­ски «Фун­дамен­таль­ные осно­вы хакерс­тва» был нас­толь­ной кни­гой каж­дого начина­юще­го иссле­дова­теля в области компь­ютер­ной безопас­ности. Одна­ко вре­мя идет, и зна­ния, опуб­ликован­ные Кри­сом, теря­ют акту­аль­ность. Редак­торы «Хакера» обновляют ­этот объ­емный труд, чтобы перенес­ти его из вре­мен Windows 2000 и Visual Studio 6.0 во вре­мена Windows 10 и Visual Studio 2019.

Результатом стал цикл статей «Фундаментальные основы хакерства». Перед тобой уже во второй раз обновленная вторая статья из этого цикла (на «Хакере» также доступна первая в новой редакции).

Весь цикл с учетом всех обновлений вышел в виде книги «Фундаментальные основы хакерства. Анализ программ в среде Win64». Купить ее можно на сайте «Солон-пресс».

Книга «Фундаментальные основы хакерства. Анализ программ в среде Win64»

Способности отладчиков

Первым делом надо разобраться в перечне основных функциональных возможностей типовых отладчиков (без этого невозможно их осмысленное применение):

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

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

  • отслеживание загрузки DLL и вызова из них таких-то функций, включая системные компоненты (как мы увидим далее, это основное оружие современного взломщика);

  • отслеживание вызова программных и аппаратных прерываний (большей частью уже неактуально — не так много защит балуется с прерываниями);

  • отслеживание сообщений, посылаемых приложением окну;

  • и, разумеется, контекстный поиск в памяти.

Как именно это делает отладчик, пока знать необязательно, достаточно знать, что он это умеет, и все. Куда актуальнее вопрос, какой отладчик умеет это делать.

Герои прошлого

В прошлом в качестве отладчика хакеры использовали широко известный SoftICE. Это был действительно мощный инструмент, и даже по прошествии многих лет лучше него ничего не было изобретено. Неоспоримым его преимуществом была возможность отладки ядра Windows с помощью одного компьютера. Между тем, не без давления Microsoft, в 2006 году его разработка была прекращена. А поскольку SoftICE очень сильно зависел от операционной системы Windows, в более поздних ее версиях он просто не работает. Последней версией Windows, в которой можно пользоваться SoftICE, была Windows XP SP 2. В SP 3 он уже не функционировал, а в Vista и Windows 7 и подавно.

95b6b6dfdab825c9ff6641d29a8af8a0.jpg

Хакеры, конечно, приуныли, но не стали посыпать голову пеплом, а начали изобретать альтернативные отладчики. Последовала эпоха расцвета новых отладчиков. Но какая картина на этом поле сейчас? Нет ни одного нового хорошего отладчика! Передовым среди всех был коммерческий Syser китайских разработчиков. Ему пророчили светлое будущее, возможность заменить SoftICE, но где он сейчас? Может быть, пара копий сохранилась где-то на файловых свалках, но он давно не развивается.

5c3acea2d0d21ab1c8bc415e347b04de.jpg

Сейчас по большому счету у хакера есть выбор только из двух по-настоящему годных отладчиков: WinDbg и x64dbg. Последний годится лишь для исследования приложений пользовательского режима, тогда как с помощью WinDbg можно заниматься и ядерной отладкой Windows. Но в этом случае придется использовать два компьютера, объединенных проводом или по локальной сети.

Современный инструмент кодокопателя

Когда-то хакеры пренебрегали WinDbg, но со временем он вырос и стал действительно мощным и полезным инструментом исследования кода. Не стоит забывать, что именно он используется командой разработки Windows. Для него можно изготавливать расширения путем подключаемых DLL. Начиная с Windows XP, движок отладки включен непосредственно в операционную систему. Он состоит из двух DLL: dbgeng.dll и dbghelp.dll. Кроме непосредственно средств отладки, в число которых входит сам WinDbg, его движок используется в том числе «Доктором Ватсоном» (drwtsn32.exe).

Средство отладки для Windows состоит из четырех приложений, использующих dbgeng.dll:

  • cdb и ntsd — отладчики пользовательского режима с консольным интерфейсом. Они различаются только одним: при запуске из существующего консольного окна ntsd открывает новое консольное окно, a cdb этого не делает;

  • kd — отладчик режима ядра с консольным интерфейсом;

  • WinDbg может использоваться как отладчик пользовательского режима либо режима ядра, но не одновременно. У WinDbg есть графический интерфейс.

Следовательно, WinDbg представляет собой только оболочку для отладки с помощью движка.

Вспомогательный файл dbghelp.dll используется внешними тулзами для исследования внутренностей Windows, например отладчиком OllyDbg, программой Process Explorer за авторством Марка Руссиновича и прочими.

У WinDbg есть две версии: классическая и UWP. Первая устанавливается вместе с набором тулз Debugging Tools for Windows. Этот набор содержит две версии WinDbg. Одна предназначена для отладки 32-разрядных приложений, другая — 64-разрядных. Версию UWP можно скачать из Windows Store, она имеет только 32-битную версию. Обе 32-разрядные версии абсолютно равноценны, не считая того, что в UWP продвинутый пользовательский интерфейс Windows 10. Кстати, весьма удобный при работе на большом экране.

Для наших экспериментов я буду применять UWP-версию. Разницы в их использовании практически нет, могут разве что немного различаться команды в пользовательском интерфейсе, именно надписи на элементах интерфейса, но не команды встроенного языка.

Способ 0. Бряк на оригинальный пароль

С помощью WinDbg загрузим ломаемый нами файл — passCompare1.exe — через пункт меню Launch Executable или Open Executable в классическом приложении. В дальнейшем я не буду приводить аналоги команд.

Файлы с примерами можно скачать с GitHub.

Сразу после открытия исполняемого файла WinDbg загрузит приложение, в окне Disassembly отладчика появятся дизассемблированные команды, а в окне Command отобразятся результаты их выполнения.

После создания окна приложения еще до вывода каких-либо данных выполнение тут же прерывается на инструкции int 3 — это программная точка останова. Часто новички считают, что выполнение программы начинается с функции main или WinMain. Этому их учат в школе, либо они сами черпают такие сведения из учебников по C/C++. Конечно, это неправда.

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

Первым делом загрузим отладочную информацию для компонентов операционной системы. Для этого в командную строку введем

.symfix d:\debugSymbols

Эта команда определяет папку, указанную в параметре, куда отладчик при необходимости загрузит отладочные символы для подсистем Windows. Затем надо отправить команду для загрузки или обновления файлов:

.reload

После этого WinDbg загрузит нужные данные с серверов Microsoft.

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

Давай попробуем натравить отладчик на дебажную версию passCompare1 и при достижении первой точки останова поставим бряк на функцию main:

bp passCompare1!main

Теперь продолжим выполнение командой g. Когда отладчик достигнет поставленной точки останова (начало функции main), в окне дизассемблера увидим, что листинг разделен на сегменты, в заголовке которых находятся имена функций, в частности main.

В релизной версии исполняемого файла этого не будет. Если же мы поставим точку останова по адресу начала модуля и прибавим адрес точки входа, то мы попадем не в начало функции main, а в системный загрузчик — функцию mainCRTStartup, подготовленную компилятором.

Кроме Microsoft, мало кто предоставляет отладочные символы, не будем привыкать к легкой жизни, тем более WinDbg специально предназначен для отладки программ без отладочной информации и исходного кода. Будем применять его по назначению! Между тем, если приглядеться к окошку дизассемблера повнимательнее, можно заметить, что в отличие от дизассемблера dumpbin, который мы использовали в прошлой статье, WinDbg распознает имена системных функций, чем существенно упрощает анализ.

df1709d215f4fe5eb4c3c533722efd7a.png

Точки останова могут быть двух типов: программные и аппаратные. С первыми мы уже встречались. В программе их может быть любое количество. Для своей работы они модифицируют память исполняемого процесса, то есть в том месте, где должен стоять бряк, отладчик запоминает ассемблерную инструкцию и заменяет ее int 3. Из-за того что программная точка останова изменяет память, ее можно установить не везде. В этом заключается ее основной недостаток. Главной командой для установки программной точки останова является bp. Для получения списка установленных точек служит команда bl, а для удаления — команда bc, параметром которой является индекс точки останова. Звездочка в качестве параметра удаляет все бряки. Команды be и bd, соответственно, включают и выключают брейк-пойнты.

Количество аппаратных точек останова, независимо от разрядности процессора, всегда четыре. Хотя в процессоре присутствуют восемь регистров отладки (DR-0 — DR-7), только первые четыре могут быть использованы для хранения адресов точек останова. Аппаратные бряки могут ставиться в любое место памяти процесса. Таким образом, они лишены недостатка программных бряков. Остальные регистры предназначены для хранения условий доступа — срабатывания точек останова, например чтение, запись, исполнение. Малое количество — основной недостаток аппаратных бряков. Для установки аппаратной точки останова используется команда ba с тремя параметрами: тип доступа, размер и адрес.

Итак, мы рассмотрели небольшой список команд внутреннего языка отладчика WinDbg. Наверняка ты обратил внимание на их запись. В языке отладчика присутствуют три вида команд:

  • встроенные команды служат для отладки процесса и записываются без лидирующего символа, к таким командам относятся g, bp, bd;

  • метакоманды управляют работой отладчика, перед ними ставится символ точки, например: .reload, .symfix, .cls;

  • команды-расширения, загружаемые из внешних DLL, имеют в начале символ восклицательного знака, например: !heap, !dh.

Поиск адреса

Давай попробуем наскоро найти защитный механизм и, не вникая в подробности его функционирования, напрочь отрубить защиту. Вспомним, по какому адресу расположен в памяти оригинальный пароль. Заглянем в дамп секции .rdata, где хранится пароль. Исходный пароль myGOODpassword находится по смещению 0x140002280. Попробуем вывести находящиеся по этому адресу в памяти данные:

dc 0x140002280

Существует большое количество команд для отображения содержимого памяти: da, db, dd и прочие. Мы использовали dc, потому что она показывает значения двойных слов и ASCII-символы. Что мы видим? Неинициализированные данные.

Раньше (до Windows Vista) кодокопателям было проще. Windows загружала образы в виртуальную память по определенному при компиляции адресу. В этом легко убедиться: запустим приложение в Windows XP, затем при помощи того же SoftICE узнаем адреса секций (команда mod -u) и выпишем их. После перезагрузки системы и повторного запуска приложения увидим, что они останутся неизменными.

Начиная с Windows Vista, программы после перезапуска системы получают случайные адреса, а не те, что определены при компиляции. Поэтому нам придется самим искать секцию .rdata уже не на диске, а в памяти. Легко сказать, но сделать еще проще!

Найдем, по какому адресу расположен наш модуль в памяти. Для этого в отладчике введем lmf m passcompare1. Второй параметр — имя модуля, адрес которого надо определить. В результате на своем компе я получил:

start             end                 module name
00007ff7`159b0000 00007ff7`159b8000   passCompare1 passCompare1.exe

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

!dh passCompare1 

Вывод команды довольно объемный. !dh — в некотором роде аналог команды map32 из SoftICE, при этом первая предоставляет больше сведений. Найдем в выводе описание целевой секции .rdata:

SECTION HEADER #2
  .rdata name
    101C virtual size
    2000 virtual address
    1200 size of raw data
    1400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

Здесь нас интересует четвертая строчка — виртуальный адрес. Следом можно найти, где в памяти располагается .rdata, для этого надо сложить начальный адрес модуля и виртуальный адрес секции. Посмотрим, что там находится:

dc 0x7ff7159b0000 + 2000 

Уже теплее, читаемые символы. Пройдем глубже в секцию и распечатаем диапазон адресов:

dc 0x7ff7159b2000 0x7ff7159b2300

А вот и наш пароль по адресу 0x7ff7159b2280! Дамп памяти процесса:

00007ff7`159b2250  159b4040 00007ff7 159b40e0 00007ff7  @@.......@......
00007ff7`159b2260  ffffffff ffffffff ffffffff ffffffff  ................
00007ff7`159b2270  65746e45 61702072 6f777373 003a6472  Enter password:.
00007ff7`159b2280  4f47796d 6170444f 6f777373 000a6472  myGOODpassword..
00007ff7`159b2290  6e6f7257 61702067 6f777373 000a6472  Wrong password..
00007ff7`159b22a0  73736150 64726f77 0a4b4f20 00000000  Password OK.....
00007ff7`159b22b0  00000140 00000000 00000000 00000000  @...............

ba85347d8c4ad5c9c2b883c4197e9d36.png

Есть контакт! Задумаемся еще раз. Чтобы проверить корректность введенного пользователем пароля, защита, очевидно, должна сравнить его с оригинальным. А раз так, установив точку останова на чтении памяти по адресу 0x7ff7159b2280, мы поймаем за хвост сравнивающий механизм. Сказано — сделано.

Поставим аппаратный бряк, он же бряк по доступу (break on access), так как при программном будет ошибка доступа к памяти, возникающая по причине попытки записи в секцию, доступную только для чтения, какой .rdata и является. А программному бряку надо модифицировать память.

ba r4 0x7ff7159b2280

Первый параметр — тип доступа: r — чтение; второй параметр — количество байтов, подвергаемых операции; последний параметр — адрес.

По команде g продолжим отладку и введем любой пришедший на ум пароль, например KPNC++. Отладчик незамедлительно «всплывет» в библиотечной функции сравнения строк strcmp:

00007ffb`5d375670 488b01               mov     rax, qword ptr [rcx]
00007ffb`5d375673 483b040a             cmp     rax, qword ptr [rdx+rcx]
00007ffb`5d375677 75bf                 jne     ucrtbase!strcmp+0x8 (7ffb5d375638)
00007ffb`5d375679 4e8d0c10             lea     r9, [rax+r10]
00007ffb`5d37567d 48f7d0               not     rax

В силу архитектурных особенностей процессоров Intel бряк срабатывает после инструкции, выполнившей «поползновение», то есть RIP указывают на следующую выполняемую команду. В нашем случае (выделенная строка) — jne ucrtbase!strcmp+0x8, а к памяти, стало быть, обратилась инструкция cmp rax, qword ptr [rdx+rcx]. А что находится в RAX? Поднимаем взгляд еще строкой выше — mov rax, qword ptr [rcx]. Можно предположить, что RCX содержит указатель на строку оригинального пароля (поскольку он вызвал всплытие отладчика), но не будем спешить с выводами, в сравнении еще присутствует указатель [rdx+rcx]. Куда указывает он? Проверить это в отладчике проще простого. Сначала проверим, куда указывает RCX:

0:000> dc rcx
00000093`bf5cfbd0  434e504b 000a2b2b 00000000 00000000  KPNC++..........

Оказывается, RCX указывает на введенный пользователем пароль! А куда указывает второй указатель?

0:000> dc [rdx+rcx]
00007ff7`159b2280  4f47796d 6170444f 6f777373 000a6472  myGOODpassword..

Здесь как раз притаился оригинальный пароль! Картина начинает приобретать очертания.

Теперь вопрос:, а как это заломить? Вот, скажем, JNE можно поменять на JE. Или еще оригинальнее — заменить RCX [RDX+RCX]. Тогда оригинальный пароль будет сравниваться сам с собой!

Выйдем из текущей функции, для этого надо два раза нажать кнопку Step Out в отладчике. В результате мы окажемся в функции main, сразу после сравнения строк:

0007ff7`159b10b2 488d15c7110000     lea     rdx, [passCompare1!`string' (7ff7159b2280)]
00007ff7`159b10b9 488d4c2420        lea     rcx, [buff{[0]} (rsp+20h)]
00007ff7`159b10be e88c0d0000        call    passCompare1!strcmp (7ff7159b1e4f)
00007ff7`159b10c3 85c0              test    eax, eax
00007ff7`159b10c5 7458              je      passCompare1!main+0xaf (7ff7159b111f)

В первой строчке приведенного листинга в регистр RDX записывается указатель на эталонный пароль. Чтобы проверить это, выполни команду dc 7ff7159b2280. Во второй строчке в регистр RCX помещается указатель на содержимое строкового буфера, в который записывается введенный пользователем пароль. После вызова strcmp следует test, проверяющий на ноль регистр EAX. Знакомые места! Помнишь, мы посещали их дизассемблером? Далее следует JE, совершающий прыжок на основе предыдущего условия.

Алгоритм действий прежний — запоминаем адрес команды TEST для последующей замены ее на XOR или записываем последовательность байтов.

Погоди! Не стоит так спешить! Можно ли быть уверенным, что эти байтики по этим самым адресам будут находиться в исполняемом файле? В Windows XP и версиях до нее на это в подавляющем большинстве случаев можно было хотя бы надеяться, но проверка не была лишней. Хотя системный загрузчик размещал модули по заранее определенным адресам, существовали хитрые защитные механизмы, загружавшие один и тот же модуль по двум разным адресам одновременно. В Windows 10 такой трюк не прокатывает, винда видит, что это один и тот же модуль, и размещает его в памяти лишь единожды.

Тем не менее в Windows 10 мы даже не можем надеяться, что находящиеся по определенным адресам байты, найденные в памяти с помощью отладчика, будут по тем же адресам находиться в файле на диске. Когда программа выполняется, все ее секции проецируются в адресное пространство виртуальной памяти, которое кардинально отличается от начальной. В Vista и последующих системах в дело вступает механизм ASLR (Address Space Layout Randomization), который, используя MMU (Memory Management Unit), случайным образом изменяет расположение в адресном пространстве процесса важных структур данных. ASLR в некоторых случаях вполне успешно борется с переполнением буфера, возвратом в библиотеку и другими типами атак.

Авторы: Крис Касперски, Юрий Язев

© Habrahabr.ru