February 15, 2019

Фундаментальные основы хакерства. Знакомство с отладчиком

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

INFO

Предыдущая часть: «Проверка аутентичности и базовый взлом защиты»

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

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

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

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

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

В оригинальной книге Крис в качестве отладчика использовал широко известный среди хакеров старой школы SoftICE. Это действительно мощный, до сих пор не превзойденный инструмент — его неоспоримым преимуществом была возможность отладки ядра Windows с помощью одного компьютера. Между тем не без давления Microsoft в 2006 году его разработку прекратили. А поскольку SoftICE очень сильно зависит от операционной системы Windows, в ее более поздних версиях он просто не запустится. Последней версией Windows, в которой работал SoftICE, была Windows XP SP2, в SP3 уже нет.

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

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

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

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

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

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

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

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

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

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

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

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

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

После создания окна приложения еще до вывода каких-либо данных выполнение прерывается на инструкции int 3 — это программная точка останова. Многие новички считают, что выполнение программы начинается с функции main или WinMain. Этому их учат в школе, либо они сами черпают такие сведения из учебников по 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 распознает имена системных функций, чем существенно упрощает анализ.

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

Главная команда для установки программной точки останова — bp. Для получения списка установленных точек служит команда bl, а для удаления — команда bc, параметром которой является индекс точки останова. Звездочка в качестве параметра удаляет все бряки. Команды be и bd, соответственно, включают и выключают брейк-пойнты.

Аппаратных точек останова всегда четыре, их количество не зависит от разрядности процессора (в процессоре присутствуют восемь регистров отладки (DR0 — DR7), но только первые четыре могут быть использованы для хранения адресов точек останова).

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

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

  • встроенные команды служат для отладки процесса и записываются без лидирующего символа, к таким командам относятся gbpbd;
  • метакоманды управляют работой отладчика, перед ними ставится символ точки, например .reload.symfix.cls;
  • команды-расширения, загружаемые из внешних DLL, имеют в начале символ восклицательного знака, например !heap!dh.

Поиск адреса

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

dc 0x402100

Cуществует большое количество команд для отображения содержимого памяти. Мы использовали dc, потому что она показывает значения двойных слов и символы ASCII.

Что мы видим? Неинициализированные данные. Раньше (до «Висты») кодокопателям было проще. Windows загружала образы в виртуальную память по определенному при компиляции адресу. Для проверки достаточно несколько раз запустить приложение под Windows XP (построенное соответствующим компилятором), между запусками понадобится перезагрузка системы, так как без нее и «Десятка» будет загружать одно и то же приложение по одному и тому же адресу и сравнивать адреса начала модуля, полученные с помощью того же SoftICE по команде mod -u. Соответственно, секции тоже размещались по одним и тем же адресам. А теперь нам придется самим искать секцию .rdata уже не на диске, а в памяти. Легко сказать, но сделать еще проще!

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

start      end        module         name
00cf0000   00cf6000   passCompare1   passCompare1.exe

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

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

SECTION HEADER #2
 .rdata name
    A7E virtual size
   2000 virtual address
    C00 size of raw data
   1200 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 00cf0000 + 2000

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

dc 00cf2000 00cf21f0

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

00cf20d0  00000000 00cf1153 00cf11f7 00000000  ....S...........
00cf20e0  00000000 00000000 00000000 00000000  ................
00cf20f0  00cf3018 00cf3068 65746e45 61702072  .0..h0..Enter pa
00cf2100  6f777373 003a6472 4f47796d 6170444f  ssword:.myGOODpa
00cf2110  6f777373 000a6472 6e6f7257 61702067  ssword..Wrong pa
00cf2120  6f777373 000a6472 73736150 64726f77  ssword..Password
00cf2130  0a4b4f20 00000000 00000000 00000000   OK.............
00cf2140  00000000 5bc10a90 00000000 00000002  .......[........
00cf2150  00000048 00002224 00001424 00000000  H...quot;..$.......

Выровняем вывод так, чтобы наш пароль отобразился с начала строки. Для этого к текущему адресу 0xCF2100 надо прибавить 8 — количество символов для смещения. Это будет его фактический адрес. Проверим наше предположение:

dc 00cf2100+8
00cf2108 4f47796d 6170444f 6f777373 000a6472 myGOODpassword..

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

Сказано — сделано. Поставим аппаратный бряк, так как программный подарит нам ошибку доступа к памяти, возникающую из-за попытки записи в секцию, доступную только для чтения, каковой .rdata и является. А программному бряку надо модифицировать память.

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

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

00cf107d b90821cf00   mov     ecx, offset passCompare1!`string' (00cf2108)
00cf1082 8a10         mov     dl, byte ptr [eax]
00cf1084 3a11         cmp     dl, byte ptr [ecx]
00cf1086 751a         jne     passCompare1!main+0x62 (00cf10a2)       [br=1]
00cf1088 84d2         test    dl, dl
00cf108a 7412         je      passCompare1!main+0x5e (00cf109e)

В силу архитектурных особенностей процессоров Intel бряк срабатывает после инструкции, выполнившей «поползновение», то есть EIP указывает на следующую выполняемую команду. В нашем случае — jne passCompare1!main+0x62, а к памяти, стало быть, обратилась инструкция cmp dl, byte ptr [ecx]. А что находится в dl? Поднимаем взгляд еще строкой выше — dl, byte ptr [eax].

Можно предположить, что ECX содержит указат��ль на строку оригинального пароля (поскольку он вызвал всплытие отладчика), тому подтверждение — первая строчка листинга, где в этот регистр помещается смещение 0xCF2108, по которому, как мы помним, находится эталонный пароль. В EAX в таком случае будет указатель на введенный пользователем пароль. Проверим наше предположение.

0:000> dc ecx
00cf2108  4f47796d 6170444f 6f777373 000a6472  myGOODpassword..

0:000> dc eax
0115fd44  434e504b 000a2b2b 7773cc59 00000001  KPNC++..Y.sw....

И правда — догадка оказалась верна. Теперь вопрос: а как это заломить? (Не уверен, что в современном хакерском жаргоне есть слово «заломить», но Юрий ссылается на классиков. — Прим. ред.) Вот, скажем, JNE можно поменять на JE. Или еще оригинальнее — заменить EAX на ECX. Тогда оригинальный пароль будет сравниваться сам с собой!

Продолжим трассировку и с помощью команды t пройдем по условному переходу (или внутрь функции). Мы тут же попадаем на инструкцию, выталкивающую Wrong Password на вершину стека:

00cf10a7 85c0         test    eax, eax
00cf10a9 7463         je      passCompare1!main+0xce (00cf110e)
00cf10ab 0f1f440000   nop     dword ptr [eax+eax]
00cf10b0 681821cf00   push    offset passCompare1!`string' (00cf2118)
00cf10b5 e856ffffff   call    passCompare1!printf (00cf1010)

Знакомые места! Помнишь, как мы посещали их дизассемблером? После этой инструкции вызывается функция printf. А если поднять взгляд на три строчки выше, мы обнаружим инструкцию test eax, eax, которая выполняет проверку на совпадение. Алгоритм действий прежний — запоминаем адрес команды TEST для последующей замены ее на XOR или записываем последовательность байтов.

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

Тем не менее в «Десятке» мы даже не можем надеяться, что расположенные по определенным адресам байтики, найденные в памяти с помощью отладчика, будут по тем же адресам располагаться в файле на диске. Ибо в «Висте» и последующих системах в дело вступает механизм ASLR (address space layout randomization), который случайным образом изменяет расположение в адресном пространстве процесса важных структур данных. ASLR в некоторых случаях вполне успешно борется с переполнением буфера, возвратом в библиотеку и другими типами атакам. Лиха беда начало!