Отладчики и устройство отладчика

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

Но прежде чем мы двинемся дальше, давайте обсудим разницу между отладчиком белого ящика(white-box debugger) и отладчиком черного(black-box debugger). Большинство платформ разработки или IDE, содержат встроенный отладчик, который позволяет разработчикам отслеживать процесс выполнения программы, имея исходный код, и контролируя очень многое. Это называется отладкой белого ящика. Эти отладчики полезны во время разработки ПО, но реверс-инженер, или охотник за багами, редко имеет доступ к исходным кодам, и ему приходится использовать отладчики черного ящика, во время пошагового выполнения программы (трассировки). Отладчик черного ящика предполагает, что исследуемая программа полностью непрозрачна для хакера, и единственная доступная информация - это дизассемблированный код. При этом методе нахождения ошибок, более сложном и затратном по времени, хорошо подготовленный реверс-инженер в состоянии понять устройство программы на очень высоком уровне. Иногда ребята, ломающие программы, могут получить более глубокие знания и понимание того, как работает программа, нежели разработчик ее создавший!

Очень важно различать два подкласса отладчиков черного ящика: уровня пользователя и уровня ядра. Уровень пользователя (как правило т.н. ring 3 (кольцо третьего уровня, прим.)) - это режим процессора, в котором запускаются ваши пользовательские приложения. Приложения уровня пользователя выполняются с наименьшим количеством привилегий. Когда вы запускаете calc.exe чтобы что-то посчитать, вы порождаете процесс на уровне пользователя; если вы будете пошагово выполнять (тресировать) этот процесс, это будет отладка на уровне пользователя. Уровень ядра (ring 0) - это наибольший уровень привилегий. Это уровень, где работает ядро операционной системы вместе с драйверами и другими низкоуровневыми компонентами. Когда вы анализируете сетевой трафик (сниффаете, от sniffing - "нюхать", прим.) с помощью Wireshark, вы взаимодействуете с драйвером, который работает на уровне ядра. Если вы хотите остановить драйвер, и исследовать его состояние в любой точке, то вам понадобится отладчик уровня ядра.

Сейчас я приведу вам короткий список отладчиков уровня ядра, часто используемых реверс-инженерами и хакерами: WinDbg от Microsoft и OllyDbg, бесплатный отладчик от Oleh Yuschuk. При отладке под Linux, обычно используют GNU Debugger (gdb). Все три отладчика достаточно мощны, и каждый предлагает такие возможности, которые не доступны другим отладчикам.

Однако в последние годы стала проявляться тенденция в интеллектуальной отладке, особенно на платформе Windows. Интеллектуальный отладчик поддерживает скрипты (сценарии), поддерживает расширенные возможности, например такие, как вызов перехвата, и что самое главное, имеют много возможностей используемых для охоты на баги(bug hunting) и реверс-инженеринга. Два новых лидера в этой сфере: PyDbg от Pedram Amini и Immunity Debugger от Immunity inc.

Регистры общего назначения центрального процессора.

Регистр - это небольшой объем памяти находящийся прямо на центральном процессоре, и доступ к нему - быстрейший метод для процессора, чтобы получить данные. В наборе инструкций архитектуры x86 используются восемь регистров общего назначения: EAX, EDX, ECX, ESI, EDI, EBP, ESP и EBX. Большинство регистров доступны процессору, но мы рассмотрим их только в конкретных обстоятельствах, когда они потребуются. Каждый из восьми регистров общего назначения разработан для своей конкретной работы, и каждый выполняет свою функцию, которая позволяет процессору эффективно выполнять инструкции. Это очень важно - понимать, какой регистр для чего используется, ибо это знание положит фундамент понимания того, как устроен отладчик. Давайте пройдемся по каждому регистру и его функциям. Мы закончим выполнением простым упражнением реверс-инженерии, для иллюстрации их использования.

Регистр EAX, так же называемый регистром аккумуляции (или аккумулятором), используется для выполнения расчетов, а так же для хранения значений возвращаемых вызванными функциями. Многие оптимизированные инструкции в наборе инструкций x86 разработаны для перемещения данных именно в регистр EAX и извлечения данных из него, а так же для выполнения расчетов с этими данными. Большинство простых операций, таких как сложение, вычитание и сравнение оптимизированы для использования регистра EAX. Кроме того, многие определенные операции, такие как умножение или деление, могут выполняться только в регистре EAX.

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

Регистр EDX это регистр данных(data register). Этот регистр в основном является дополнительным для регистра EAX, и он помогает хранить дополнительные данные для более сложных вычислений, таких как умножение и деление. Он так же может быть хранилищем данных общего назначения, но обычно он используется в расчетах, выполненных в сочетании с регистром EAX.

Регистр ECX так же называется регистром-счетчиком (count register), он используется в операциях цикла. Часто повторяющиеся операции стоит хранить в упорядоченной пронумерованной строке. Очень важно понимать, что счетчик регистра ECX уменьшает, а не увеличивает значение. Продемонстрируем это на примере отрывка, написанного на Python:

counter = 0

while counter < 10:
    print "Loop number: %d" % counter
    counter +=1

Если мы переведем этот код в язык ассемблера, то в первом цикле значение в ECX будет равно 10, 9 в следующем цикле и так далее. Вас может немного смутить, что это противоречит тому, что написано в листинге на Python, но просто запомните, что он уменьшается, и все будет хорошо.

В ассемблере x86 циклы, которые обрабатывают данные, полагаются на регистры ESI и EDI для продуктивной манипуляции данными. Регистр ESI это индекс источника (source index) данных операции, и хранит адрес входящего потока данных. Регистр EDI указывает на место, где находится результат обработанных данных, поэтому он называется индексом назначения(приемника) (destination index). Это легко запомнить таким образом: регистр ESI используется для чтения данных, а EDI для записи. Использование регистров индексов источника и приемника значительно увеличивает производительность выполняемой программы.

Регистры ESP и EBP соответственно указатель стека (stack pointer) и указатель базы(base pointer). Эти регистры используются для управления вызовами функций и операциями со стеком. Когда функция вызвана, аргументы функции перемещаются (проталкиваются) в стек и следуют по адресу возврата. Регистр ESP указывает на самый верх стека, поэтому он будет указывать на адрес возврата. Регистр EBP указывает на самый низ стека вызовов. В некоторых случаях компилятор может использовать оптимизацию для удаления регистра EBP как указателя кадра, в этих случаях регистр EBP освобождается и может использоваться точно так же, как любой другой регистр общего назначения.

Единственный регистр, который не был разработан для чего-то конкретного - это EBX. Он может использоваться, как дополнительное хранилище данных.

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

Отладчик должен уметь легко читать и изменять содержимое этих регистров. Каждая операционная система предоставляет интерфейс отладчикам для взаимодействия с процессором, возможности извлекать и модифицировать данные в регистрах. Мы рассмотрим отдельные интерфейсы в главах о конкретных операционных системах.

Стек.

Стек - это очень важная структура, которую надо знать при разработке отладчика. Стек хранит информацию о том, как вызывается функция, какие параметры она забирает, и что надо вернуть после выполнения функции. Структура стека представляет собой модель "Первый пришел, последний вышел" (FILO, First In, Last Out), когда аргументы проталкиваются в стек для вызова функции, и извлекаются из стека после того, как функция завершит свое выполнение. Регистр ESP используется для отслеживания самой вершины кадра стека, а регистр EBP используется для отслеживания самого низа стека. Стек "растет" от верхних адресов памяти к нижним адресам памяти. Давайте используем нашу предыдущую рассмотренную функцию my_socks(), как простейший пример того, как работает стек.

На языке C

int my_socks(color_one, color_two, color_three);

Вызов функции в ассемблере x86

push color_three
push color_two
push color_one
call my_socks

Как будет выглядеть стек изображено на Рисунке 2-1.

Stack.PNG

Рисунок 2-1: кадр стека для вызова функции my_socks()

Перевод обозначений: Stack growth direciton - направление роста стека. Base of stack frame - основание (база) кадра стека. Return adress - адрес возвращаемого значения.

Как вы можете видеть, это прямая структура данных и это основа для всех вызываемых функций в двоичном коде. Когда функция my_socks() завершает работу (и возвращает какое-либо значение), она выталкивает из стека все значения и переходит к адресу возврата для продолжения выполнения родительской функции, которая её вызвала. Рассмотрим понятие локальных переменных. Локальные переменные это часть памяти, которая доступна только для функции, которая выполняется в данный момент. Немного расширим функцию my_socks(), давайте предположим, что первое, что будет делать эта функция - это создавать массив символов, в который будет скопирован параметр color_one. Код будет выглядеть так:

int my_socks(color_one, color_two, color_three)
{
char stinky_sock_color_one[10];
...
}

Переменная stinky_sock_color_one будет находится в стеке так, что ее можно использовать в конкретном кадре стека. Как только произошло это распределение, кадр стека будет выглядеть как на Рисунке 2-2.

Stack2.PNG

Рисунок 2-2. Кадр стека после того, как была объявлена локальная переменная stinky_sock_color_one.

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

События отладчика.

Отладчик работает как бесконечный цикл который ждет события отладки. Когда событие для отладки случается, цикл прерывается, и вызывается соответствующий обработчик событий.

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

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

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

Точки останова.

Возможность остановить отлаживаемый процесс достигается установкой точек останова (breakpoints). Остановив процесс, у вас появляется возможность проверить переменные, аргументы стека, и что находится в памяти без изменения переменных процесса, прежде чем вы сможете записать их. Точки останова - это определенно самая частая вещь, которую вы можете использовать при отладке процесса, и рассмотрим их очень внимательно. Есть три основных вида точек останова: программные (soft breakponit), аппаратные (hardware breakpoint), и памяти (memory breakpoint). Они ведут себя очень похоже, но выполняются очень разными способами.

Программные точки останова

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

Инструкция ассемблера - это высокоуровневое представление команды на выполнение для процессора. Пример:

MOV EAX, EBX

Эта инструкция говорит процессору переместить значение, хранящееся в регистре EBX в регистр EAX. Очень просто, правда? Однако, процессор не знает, как интерпретировать эту инструкцию, ему нужно конвертировать её в что-то, называемое опкодом. Код операции (code operation) или опкод это команда машинного языка, которую выполняет процессор. Для иллюстрации, давайте переведем предыдущий пример инструкции в его "родной" опкод:

8BC3

Как вы можете видеть, он затемняет (обфусцирует) то, что реально происходит "за сценой", но это язык, на котором говорит процессор. Думать о инструкциях ассемблера, как DNS (прим. Сервер имён) для процесора. Инструкции упрощают возможность запомнить команды выполнения (имена сайтов, хостов), вместо запоминания всех индивидуальных опкодов (IP адреса). Вам очень редко понадобится использовать опкоды в вашей обыденной отладке, но они важны для понимания целей программных точек останова.

Если инструкция, которую мы рассматривали ранее, будет находится по адресу 0x44332211, общее представление будет выглядеть так:

0x44332211: 8BC3 MOV EAX, EBX

Это отображаются адрес, опкод, и высокоуровневая инструкция ассеблера. Для того, чтобы установить программный брейкпоинт по этому адресу и остановить процессор, мы заменим один байт из 2-байтного опкода 8BC3. Этот одиночный байт представляет собой инструкцию прерывания (interrupt) 3 (INT 3), которая "приказывает" процессору остановится. Инструкция INT3 конвертируется в однобайтный опкод 0xCC. Вот наш предыдущий пример до и после установки точки останова.

Опкод до установки точки останова

0x44332211: 8BC3 MOV EAX, EBX

Модифицированный опкод после установки точки останова

0x44332211: CCC3 MOV EAX, EBX

Вы можете видеть, что заменили байт 8B на байт CC. Когда процессор идет вприпрыжку (кроме шуток, comes skipping along) и натыкается на этот байт, он останавливается и запускает событие INT 3. Отладчики имеют встроенную возможность обрабатывать это событие, но прежде чем вы разработаете ваш собственный отладчик, надо хорошо понимать, как они делают это. Когда отладчик говорит установить точку останова в желаемый адрес, он сначала читает первый байт опкода по запрошенному адресу и сохраняет его. Затем отладчик записывает CC байт по этому адресу. Когда точка останова, или INT 3, срабатывает при интерпретации процессором опкода CC, отладчик перехватывает это. Затем отладчик проверяет указывает ли указатель инструкций (регистр EIP) на адрес, на который предварительно была установлена точка останова. Если адрес находится во внутреннем списке точек останова, он записывает обратно сохраненный байт по этому адресу, чтобы опкод мог выполниться правильно, после продолжения выполнения процесса. Рисунок 2-3 детально описывает этот процесс.

breakpoint.PNG

Рисунок 2-3. Процесс установки программной точки останова.

  1. Отладчик посылает инструкцию установить точку останова по адресу 0x44332211; он считывает и сохраняет первый байт.
  2. Переписывает первый байт на опкод 0xCC (INT 3).
  3. Когда процессор встречает точку останова, происходит внутренний поиск, и байт возвращается на место.

Breakpoint List - список точек останова.

Как видите, отладчик должен словно станцевать, чтобы обработать программную точку останова. Есть два типа точек останова, которые вы можете установить: одноразовые (one-shot) брейкпоинты и стойкие (persistent) точки остановы. Одноразовые точки останова подразумевают то, что точка останова встречается, она удаляется из внутреннего списка точек останова отладчика, они удобны для только одной установки. Стойкая точка останова восстанавливается после того, как процессор выполнит оригинальный опкод, поэтому запись в списке отладчика сохраняется.

Однако программные точки останова имеют одно ограничение, когда вы меняете байт исполняемого в памяти, вы изменяете контрольную сумму циклического избыточного кода (Cyclic redundancy code, CRC) выполняемого приложения. CRC - это тип функции, которая используется для определения изменения данных каким либо способом, и она может быть применена к файлам, памяти, тексту, сетевым пакетам, или чего-нибудь еще, за изменением данных которого вам надо наблюдать. CRC возьмет диапазон данных, в данном случае память выполняемого процесса, и получит хэш содержимого. Затем она сравнивает хеш с контрольной суммой для определения были ли изменены данные. Если контрольная сумма отличается от контрольной суммы, которая хранится для подтверждения, проверка CRC собьется. Важно заметить, как часто вредоносное ПО будет проверять свой исполняемый код в памяти для любых изменений CRC и убьёт себя, если обнаружится сбой. Это очень эффективная техника для замедления реверс-инженерии, таким образом предотвращается использование программных точек останова, ограничивая динамический анализ его поведения. Для того, чтобы обойти эти особенности используются аппаратные точки останова.

Аппаратные точки останова.

Аппаратные точки останова (hardware breakpoints) полезны, когда нужно установить небольшое число точек останова, и отлаживаемая программа не может быть модифицирована. Этот тип точек устанавливается на уровне процессора, в специальных регистрах, называемых регистрами отладки. Типичный процессор имеет 8 регистров отладки (по порядку с DR0 до DR7 соответственно), которые используются для установки и управлением аппаратных точек. Регистры отладки с DR0 до DR3 зарезервированы для адресов точек останова. Это означает, что вы можете использовать лишь 4 аппаратных точки одновременно. Регистры DR4 и DR5 зарезервированы, а регистр DR6 используется, как регистр статуса, который определяет тип события отладки, вызванного встречей точки останова. Регистр отладки DR7 по существу является выключателем (вкл/выкл) аппаратных точек останова, а так же хранит разные состояния точек останова. При установке специальных флагов в регистр DR7, вы можете создать точки останова в следующих состояниях:

Это очень полезно, если у вас есть возможность выставить до четырех точек останова в конкретные состояния без модифицирования выполняемого процесса. Рисунок 2-4 покажет вам как поля в DR7 связанны с поведением аппаратных точек останова, длиной и адресом.

Биты 0-7 по существу переключатели (вкл/выкл) для активации точек останова. Поля L и G в битах 0-7 стоят для глобального и локального масштаба. Я изобразил оба бита, как установленные. Тем не менее, установка только одного из них будет работать, и на моем опыте у меня не было каких либо проблем в отладке на уровне пользователя. Биты 8-15 в DR7 не используются для нормальных целей отладки, которые мы будем осуществлять. Обратитесь к мануалу по архитектуре Intel x86 для дополнительного разъяснения назначения этих битов. Биты 16-31 определяют тип и длину точки останова, которая была установлена для связанного регистра отладки.

DR7.PNG

Рисунок 2-4. Вы можете видеть, как установка флагов в регистре DR7 указывает тип используемой точки останова.

Перевод: Layout of DR7 register - вывод регистра DR7. Type - тип Len - сокр. длина. DR7 with 1-byte Execution Breakpoint Set at... - DR7 с точкой останова на исполнение 1 байта установленной по адресу... DR7 with Additional 2-byte Read/Write Breakpoint at... - DR7 с точкой останова на исполнение дополнительного 2-байтного чтения/записи по адресу... Breakpoint Flags - флаги точек останова Breakpoint Length Flags - фдаги длины точек останова Break on execution - останов, когда инструкция выполняется. Break on data writes - останов на запись данных. Break on read or write but not execution - останов на чтение или запись, но не выполнение.

В отличие от программных, которые используют событие INT3, аппаратные точки используют прерывание 1 (INT1). INT1 случается для аппаратных точек останова и одноступенчатых событий. Одноступенчатый означает проход по порядку по инструкциям, что позволяет вам очень близко исследовать критические участки кода во время изменения наблюдаемых данных.

Аппаратные точки останова обрабатываются таким же способом, как и программные, но их механизм находится на низком уровне. Прежде чем процессор попытается выполнить инструкцию, он сначала проверит, не установлена ли на адрес аппаратная точка. Он так же проверит операторов инструкции, не имеют ли они доступ к адресу, на который установлена аппаратная точка. Если адрес хранится в регистрах отладки DR0-DR3 и условия чтения, записи, или выполнения встречаются, прерывание INT1 приказывает процессору остановиться. Если адрес не хранится в регистрах отладки, процессор выполняет инструкцию и переходит к следующей, и эта проверка выполняется снова, и так далее.

Аппаратные точки очень полезны, но у них есть некоторые ограничения. Помимо того, что вы можете выставить только четыре ваших точки в одно время, вы так же можете установить точку только на данные длинной 2 байта. Это может сильно ограничить вас, если вы собираетесь получить доступ к большому участку памяти. Как правило, для обхода этих ограничений вы можете использовать точки останова памяти.

Точки останова памяти.

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

Выполнение страницы (page execution) Дает возможность выполнять, но выдает нарушение, если процесс попытается прочитать или записать данные на страницу.

Чтение страницы (Page read) Позволяет процессу только считывать со страницы; любая запись или попытка выполнения вызовут нарушение доступа.

Запись страницы (Page write) Позволяет процессу записывать на страницу.

Сторожевая страница (Guard page) Любой доступ к сторожевой странице возвращает единовременное исключение, и затем страница возвращается к своему первоначальному статусу.

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

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

premission.PNG

Рисунок 2-5. Поведение разных разрешений страниц памяти.

Перевод:

Read, Write, or Execute flags on a memory page allow data to be moved in and out or executed on - Флаги на чтение, запись или выполнение на странице памяти позволяют данным быть записанными и полученными или выполненным на странице.

Any type of data access on a guard page will result in an exception being raised. The original data operation will fail. - Любой тип доступа к данным на сторожевой странице приведут к возникновению исключения. Оригинальная операция с данными не выполнится.

GUARD PAGE EXCEPTION - исключение сторожевой страницы.

Наконец, мы рассмотрели некоторые простейшие аспекты того, как работает отладчик и как он взаимодействует с операционной системой, теперь пришло время написать наш первый легковесный отладчик на Python. Мы начнем с создание простенького отладчика в Windows, где накопленные знания о ctypes и внутренностях отладчика нам очень пригодятся. Давайте разогреем наши пальцы для кодинга.

Перевод: Чумичев Михаил

Книги/GrayHatPython/ОтладчикиИУстройствоОтладчика (последним исправлял пользователь alafin 2010-07-02 19:12:23)