Трассировка используется во многих видах ПО: в эмуляторах, динамических распаковщиках, фаззерах. Традиционные трейсеры работают по одному из четырех принципов: эмуляция набора инструкций (Bochs), бинарная трансляция (QEMU), патчинг бинарных файлы для изменения потока управления (Pin), либо работа через отладчик (PaiMei, основанный на IDA). Но сейчас речь пойдет о более интересных подходах.
WARNING
Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.Зачем отслеживать?
Задачи, которые решают с помощью трассировки можно условно разделить на три группы в зависимости от того, что именно отслеживается: выполнение программы (поток управления), поток данных или взаимодействие с ОС. Давай поговорим о каждом подробнее.
Поток управления
Отслеживание потока управления помогает понять, что делает бинарник во время исполнения. Это хороший способ работы с обфусцированным кодом. Также, если ты работаешь с фаззером, это поможет с анализом покрытия кода. Или возьмем, например, антивирусное ПО, где трассировщик проследит за исполнением бинарного файла, сформулирует некий паттерн его поведения, а также поможет с динамической распаковки исполняемого файла.
Трассировка может происходить на разных уровнях: отслеживание каждой инструкции, базовых блоков либо только определенных функций. Как правило, она осуществляется путем пред/постинструментации, то есть патчинга потока управления в наиболее «интересных» местах. Другой метод состоит в том, чтобы просто приаттачить отладчик к исследуемой программе и обрабатывать ловушки и точки останова. Однако есть еще один не очень распространенный способ — задействовать функции центрального процессора. Одна из интересных возможностей процессоров Intel — флаг MSR-BTF, который позволяет отслеживать выполнение программы на уровне базовых блоков — на ветвлениях (бранчах). Вот что говорится по поводу данного флага в документации:
«Когда ПО устанавливает флаг BTF в MSR-регистре MSR_DEBUGCTLA и устанавливает флаг TF в регистре EFLAGS, процессор будет генерировать отладочное прерывание только после встречи с ветвлением или исключением.»
Поток данных
В этом сценарии трассировка применяется для распаковки кода, а также для наблюдения за обработкой ценной информации — во время его можно обнаружить неправильное использование объектов, переполнения и прочие ошибки. Кроме того, оно также может использоваться для сохранения и восстановления контекста в процессе трассировки. Обычно это делается так: исследуемая библиотека полностью дизассемблируется, после этого в ней локализуются все инструкции чтения/записи, а затем в процессе выполнения кода происходит их парсинг и определяется адрес назначения. Есть и другой вариант — с помощью соответствующей API-функции устанавливается защита виртуальной памяти, после чего отслеживаются все нарушения доступа к ней. Реже используется метод, когда в памяти изменяется таблица страниц.
Взаимодействие с ОС
Мониторинг взаимодействия с ОС позволяет отфильтровывать попытки доступа к реестру, контролировать изменения файлов, отслеживать взаимодействие процесса с различными системными ресурсами, а также вызовы определенных API-функций. Как правило, это реализуется через перехват API-функций, путем вставки «трамплинов», inline-хуков, модификацию таблицы импорта, установку брейкпоинтов. Другой вариант — задействовать системный вызов SYSCALL. Ведь если вспомнить, то каждая API-функция, которая вносит какие-то изменения в ОС, на самом деле представляет собой не что иное, как простую обертку для определенного системного вызова.
Механизм SYSCALL представляет собой быстрый способ переключить CPL (Current Privilege Level) из режима пользователя в режим супервайзера, таким образом, приложение режима пользователя может вносить изменения в ОС (рис. 4).
Погружаемся в ядро
Для выполнения упомянутых функций необходимо опуститься на уровень ядра (ring 0). Однако в режиме супервайзера уже появляется доступ к некоторым функциям, предоставляемым самой операционной системой: LoadNotify
, ThreadNotify
,ProcessNotify
. Их использование помогает собрать информацию по загрузке и выгрузке для целевого процесса, такую как: список модулей, диапазоны адресов стека какого-либо потока, список дочерних процессов и прочее.
Вторая группа функций включает в себя дампер памяти, использующий MDL (memory descriptor list — список дескрипторов памяти), монитор памяти процессов, основанный на VAD (Virtual Address Descriptor), монитор взаимодействия с системой, который задействуетnt!KiSystemCall64
, перехват доступа к памяти и ловушкам через IDT (Interrupt Descriptor Table).
Монитор памяти использует для своей работы VAD-дерево, которое представляет собой AVL-дерево, используемое для хранения информации об адресном пространстве процесса. Оно же используется, когда необходимо инициализировать PTE (Page Table Entry) для конкретной страницы памяти.
Как я предложил выше, отслеживание доступа к памяти может осуществляться через механизм защиты памяти (такая вот тавтология), но его реализация в режиме пользователя с помощью API-функций может слишком сильно отразиться на производительности. Однако если принять во внимание, что защита памяти основана на механизме MMU — пейджинге, то есть более простой способ: изменять таблицу страниц в режиме ядра, после чего нарушение режима доступа к памяти будет обрабатываться через генерацию процессором исключения PageFault, а управление будет передаваться на обработчик IDT[PageFault]. Установка перехватчика на обработчик PageFault позволит быстро получить сигнал о запросе на доступ к выбранным страницам.
INFO
Дереву VAD посвящена очень интересная статья Брендана Долан-Гэвитта «The VAD tree: A process-eye view of physical memory5».
Все потому, что процесс может использовать только страницы памяти, помеченные как Valid (то есть выгруженные в память), в противном же случае будет возникать исключение PageFault, которое и будет перехватываться. Это означает, что если мы намеренно поставили Valid-флаг выбранной страницы памяти в значение invalid(0), то каждая попытка доступа к этой странице будет вызывать обработчик PageFault, что позволяет легко отфильтровать и обработать соответствующий запрос (вызывая callback к трейсеру и выставляя Valid-флаг для конкретного PTE).
Копаем глубже — идем в VMM!
В предыдущем разделе я предложил некоторые «грязные» методы для режима ядра. Вообще, установка хуков — это неправильный способ, и мне он не нравится, точно так же, как не нравится он и ребятам из Microsoft. Для борьбы с такими методами мелкомягкие и разработали PatchGuard. К счастью, есть и другой способ для отлова PageFaults, ловушек или SYSCALL’ов — это гипервизор. Правда, данный вариант имеет как свои плюсы, так и свои минусы.
Минусы:
- Виртуализировано не отдельное приложение, а вся система — на уровне ядра ЦП.
- Оператор
switch( VMMExit )
отбирает немного производительности, равно как и код гипервизора, выполняющийся для каждого из вариантов switch’а.
Плюсы:
- Более высокий уровень прав, чем уровень супервайзера, а также целый набор callback’ов, предоставляемый технологией виртуализации.
При этом сам VMM (Virtual Machine Monitor) может быть минималистичным (микроVMM) и реализовывать только необходимую обработку, занимая при этом минимальный объем кода (пример).
Помимо всего, в данном случае вместо того, чтобы ставить хуки на IDT, можно все обрабатывать напрямую с помощью дебаг-исключения в VMM. То же самое относится и к перехвату ошибок страниц с помощью исключения PageFault в VMM или через реализацию EPT (Extended Page Table).
Подводные камни VMM
Можно отметить некоторые основные особенности описанного подхода:
- целевой файл остается практически неизмененным:
- для отслеживания (как пошагового, так и на уровне ветвлений) внедряется флаг TRAP;
- адресные брейкпоинты через 0xCC или использование DRx;
- мониторинг памяти путем изменения таблицы страниц процесса;
- не нужно патчить бинарный файл;
- можно использовать как трассировочный модуль из другого приложения;
- можно отслеживать несколько приложений одновременно;
- можно отслеживать несколько потоков одного приложения;
- реализованы быстрые вызовы для переключения CPL.
Выделение трейсера из пространства целевого процесса в другой процесс дает несколько преимуществ: можно использовать его как отдельный модуль, можно сделать биндинги для Python, Ruby и других языков. Однако у этого решения есть и недостаток — очень большой удар по производительности (взаимодействие между процессами: чтение из памяти другого процесса, событийный механизм ожидания). Для ускорения трассировки необходимо перенести логику в адресное пространство целевого процесса, чтобы можно было быстро получать доступ к его ресурсам (памяти, стеку, содержимому регистров), а также опционально отказаться от VMM из-за негативного влияния обработки VMMExit на производительность и вернуться обратно к установке хуков для ловушек и обработчиков PageFault. Но с другой стороны, в будущих процессорах технологии виртуализации, наверное, станут более эффективными и не будут оказывать настолько большого влияния на производительность. К тому же возможности виртуализации для трассировки можно использовать гораздо шире, чем мы рассматриваем в рамках статьи, поэтому плюсы могут компенсировать снижение производительности.
Трейсер для ядра
Что касается трассировщика для ядра, то здесь действуют все те же принципы:
- отслеживание через ловушки (TRAP);
- мониторинг памяти через изменение таблицы страниц;
- callback’и трейсера передаются в приложения уровня пользователя;
- не нужно патчить бинарные файлы целевого приложения.
Главная особенность таких трейсеров в том, что не надо патчить бинарный файл, а также что трассировку (включая распаковку и фаззинг) можно осуществлять из уровня пользователя (например, из трейсера, написанного на Python), хотя с точки зрения производительности гораздо более эффективно делать это напрямую из режима ядра.
С другой стороны, за все эти возможности тоже приходится расплачиваться:
- адресное пространство драйвера принадлежит не ему;
- фаззинг в памяти — не такое уж простое дело;
- неверное значение RIP, регистров, памяти... манипулирование ими может очень плохо закончиться;
- необходимо четко представлять себе, что именно ты отслеживаешь или проверяешь;
- необходимо в течение всего процесса трассировки помнить о многочисленных IRQL;
- обработка исключений.
Отделение от целевого процесса, а также инкапсуляция в модуль дают нам высокую масштабируемость и возможность совместной работы с другими модулями для создания более сложного инструмента. Таким образом, в случае реализации трейсера, например, на Python, можно будет использовать IDA Python, привязки LLVM, Dbghelp для отладочных символов, дизассемблеры (движки capstone и bea) и многое другое. Чтобы показать, насколько легко и быстро можно реализовать трассировщик на Python, приведу пример, в котором контролируется более трех вариантов доступа (RWE) в заданную область памяти:
target = tracer.GetModule("codecoverme")
dis = CDisasm(tracer)
for i in range(0, 3):
print("next access")
tracer.SetMemoryBreakpoint(0x2340000, 0x400)
tracer.Go(tracer.GetIp())
inst = dis.Disasm(tracer.GetIp())
print(hex(inst.VirtualAddr), " : ", inst.CompleteInstr)
tracer.SingleStep(tracer.GetIp())
Как видишь, код очень лаконичен и понятен.
DbiFuzz-фреймворк
Все рассмотренные выше подходы к трассировке я воплотил в DbiFuzz-фреймворке, который демонстрирует, как можно отслеживать работу исполняемого файла альтернативными методами. Как мы уже отмечали, некоторые из известных методов используют инструментацию, которая дает быстрое решение, но при этом предполагает серьезное вмешательство в целевой процесс и не сохраняет целостности бинарного файла. В отличие от них, DbiFuzz оставляет бинарный файл практически нетронутым, изменяя только PTE, BTF и вставляя флаг TRAP. Другая сторона этого подхода состоит в том, что при интересующем событии включается прерывание: переход ring 3 —ring 0 — ring 3. Так как DbiFuzz подразумевает прямолинейное вмешательство в контекст и поток управления целевого процессора, то его можно использовать для написания собственных инструментов (даже на Python) для доступа к целевому бинарному файлу и его ресурсам.
INFO
Более подробно узнать про DbiFuzz-фреймворк ты можешь на моем сайте, на SlideShare и на портале ZeroNights
Show time
Для многих задач, решаемых с помощью трассировки, может оказаться полезной динамическая бинарная инструментация. Что касается DbiFuzz-фреймворка, то его можно использовать в следующих случаях:
- когда необходимо отслеживать код на лету;
- при распаковке бинарного файла, трассировке упаковщика вредоносной программы;
- для мониторинга обработки конфиденциальных данных;
- для фаззинга в памяти (легко отслеживать и изменять поток);
- при использовании в разных инструментах, не обязательно написанных на С.
Нет никаких проблем в запуске DbiFuzz на лету, просто установи ловушку или INT3-перехватчик. Поскольку мы не трогаем бинарный код целевого файла, то не будет никаких проблем с проверкой целостности, а флаг TRAP может быть заменен на MTF. Отслеживание ценных данных тоже не представляет никаких проблем, нужно просто установить соответствующий PTE — и твой монитор готов! Инструменты Python/Ruby/…? Просто создай нужные привязки (bindings) — и вперед!
Конечно, у этого фреймворка тоже есть свои недостатки, но в целом он обладает многими полезными возможностями. И ты всегда можешь поиграть с DbiFuzz, использовать входящие в него инструменты для своих нужд и отслеживать все, что пожелаешь.
Полезные ссылки
Блоги:
Intel:
Относительно VAD:
Виртуализация:
- Intel Virtualization Technology
- HDBG — hypervisor-based debugger
- HyperDbg
- Доклад Джоанны Рутковской на BH US 06
Модули Python (дизассемблеры):
To be continued
Как видишь, динамическая бинарная инструментация — не единственный метод трассировки. Альтернатив ей достаточно много, и большинство из них представлены в DbiFuzz-фреймворке. Уже сейчас некоторые возможности этого проекта могут помочь с работой в кодом на уровне ядра, а в дальнейшем я планирую перевести в это пространство весь трейсер. Кстати, уже сейчас ты можешь использовать исходники фреймворка, улучшать концепцию и экспериментировать с новыми идеями...