Pull to refresh

Альтернативные методы трассировки приложений

Reading time 9 min
Views 14K
image

Трассировка используется во многих видах ПО: в эмуляторах, динамических распаковщиках, фаззерах. Традиционные трейсеры работают по одному из четырех принципов: эмуляция набора инструкций (Bochs), бинарная трансляция (QEMU), патчинг бинарных файлы для изменения потока управления (Pin), либо работа через отладчик (PaiMei, основанный на IDA). Но сейчас речь пойдет о более интересных подходах.

Зачем отслеживать?


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


Поток управления


Отслеживание потока управления помогает понять, что делает бинарник во время исполнения. Это хороший способ работы с обфусцированным кодом. Также, если ты работаешь с фаззером, это поможет с анализом покрытия кода. Или возьмем, например, антивирусное ПО, где трассировщик проследит за исполнением бинарного файла, сформулирует некий паттерн его поведения, а также поможет с динамической распаковки исполняемого файла.
Трассировка может происходить на разных уровнях: отслеживание каждой инструкции, базовых блоков либо только определенных функций. Как правило, она осуществляется путем пред/постинструментации, то есть патчинга потока управления в наиболее «интересных» местах. Другой метод состоит в том, чтобы просто приаттачить отладчик к исследуемой программе и обрабатывать ловушки и точки останова. Однако есть еще один не очень распространенный способ — задействовать функции центрального процессора. Одна из интересных возможностей процессоров Intel — флаг MSR-BTF, который позволяет отслеживать выполнение программы на уровне базовых блоков — на ветвлениях (бранчах). Вот что говорится по поводу данного флага в документации:
«Когда ПО устанавливает флаг BTF в MSR-регистре MSR_DEBUGCTLA и устанавливает флаг TF в регистре EFLAGS, процессор будет генерировать отладочное прерывание только после встречи с ветвлением или исключением.»

Поток данных


В этом сценарии трассировка применяется для распаковки кода, а также для наблюдения за обработкой ценной информации — во время его можно обнаружить неправильное использование объектов, переполнения и прочие ошибки. Кроме того, оно также может использоваться для сохранения и восстановления контекста в процессе трассировки. Обычно это делается так: исследуемая библиотека полностью дизассемблируется, после этого в ней локализуются все инструкции чтения/записи, а затем в процессе выполнения кода происходит их парсинг и определяется адрес назначения. Есть и другой вариант — с помощью соответствующей API-функции устанавливается защита виртуальной памяти, после чего отслеживаются все нарушения доступа к ней. Реже используется метод, когда в памяти изменяется таблица страниц.

Рис. 1. Трансляция виртуальных адресов в физические


Взаимодействие с ОС


Мониторинг взаимодействия с ОС позволяет отфильтровывать попытки доступа к реестру, контролировать изменения файлов, отслеживать взаимодействие процесса с различными системными ресурсами, а также вызовы определенных API-функций. Как правило, это реализуется через перехват API-функций, путем вставки «трамплинов», inline-хуков, модификацию таблицы импорта, установку брейкпоинтов. Другой вариант — задействовать системный вызов SYSCALL. Ведь если вспомнить, то каждая API-функция, которая вносит какие-то изменения в ОС, на самом деле представляет собой не что иное, как простую обертку для определенного системного вызова.

Рис. 2. Нумерация идентификаторов (ID) SYSCALL в Windows 8


Механизм SYSCALL представляет собой быстрый способ переключить CPL (Current Privilege Level) из режима пользователя в режим супервайзера, таким образом, приложение режима пользователя может вносить изменения в ОС (рис. 4).

Рис. 4. Обработка операций SYSCALL (по учебнику Intel)


Погружаемся в ядро


Для выполнения упомянутых функций необходимо опуститься на уровень ядра (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) для конкретной страницы памяти.

Рис. 3. Пример VAD-дерева


Как я предложил выше, отслеживание доступа к памяти может осуществляться через механизм защиты памяти (такая вот тавтология), но его реализация в режиме пользователя с помощью API-функций может слишком сильно отразиться на производительности. Однако если принять во внимание, что защита памяти основана на механизме MMU — пейджинге, то есть более простой способ: изменять таблицу страниц в режиме ядра, после чего нарушение режима доступа к памяти будет обрабатываться через генерацию процессором исключения PageFault, а управление будет передаваться на обработчик IDT[PageFault]. Установка перехватчика на обработчик PageFault позволит быстро получить сигнал о запросе на доступ к выбранным страницам.
Все потому, что процесс может использовать только страницы памяти, помеченные как Valid (то есть выгруженные в память), в противном же случае будет возникать исключение PageFault, которое и будет перехватываться. Это означает, что если мы намеренно поставили Valid-флаг выбранной страницы памяти в значение invalid(0), то каждая попытка доступа к этой странице будет вызывать обработчик PageFault, что позволяет легко отфильтровать и обработать соответствующий запрос (вызывая callback к трейсеру и выставляя Valid-флаг для конкретного PTE).

Рис. 5. Флаги PTE


Копаем глубже — идем в VMM!


В предыдущем разделе я предложил некоторые «грязные» методы для режима ядра. Вообще, установка хуков — это неправильный способ, и мне он не нравится, точно так же, как не нравится он и ребятам из Microsoft. Для борьбы с такими методами мелкомягкие и разработали PatchGuard. К счастью, есть и другой способ для отлова PageFaults, ловушек или SYSCALL’ов — это гипервизор. Правда, данный вариант имеет как свои плюсы, так и свои минусы.
Минусы:
  • Виртуализировано не отдельное приложение, а вся система — на уровне ядра ЦП.
  • Оператор switch( VMMExit ) отбирает немного производительности, равно как и код гипервизора, выполняющийся для каждого из вариантов switch’а.

Плюсы:
  • Более высокий уровень прав, чем уровень супервайзера, а также целый набор callback’ов, предоставляемый технологией виртуализации.

При этом сам VMM (Virtual Machine Monitor) может быть минималистичным (микроVMM) и реализовывать только необходимую обработку, занимая при этом минимальный объем кода (пример).

Рис. 6. Некоторые callback’и, предоставляемые Intel VTx

Помимо всего, в данном случае вместо того, чтобы ставить хуки на IDT, можно все обрабатывать напрямую с помощью дебаг-исключения в VMM. То же самое относится и к перехвату ошибок страниц с помощью исключения PageFault в VMM или через реализацию EPT (Extended Page Table).

Рис. 7. Включаем вывод VMX для ловушек и сбоев

Подводные камни 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())


А следующий участок кода демонстрирует трассировку приложения на уровне ветвлений, при этом пропуская их обработку вне основного модуля:

for i in range(0, 0xffffffff):
    
  if (target.Begin > tracer.GetIp() or target.Begin + target.Size < tracer.GetIp()):    
    ret = tracer.ReadPrt(tracer.GetRsp())
    tracer.SetAddressBreadkpoint(ret)
    tracer.Go(tracer.GetIp())
    print("out-of-module-hook")   
  isnt = dis.Disasm(tracer.GetPrevIp())
  print(hex(inst.VirtualAddr), " : ", inst.CompleteInstr)
  tracer.BranchStep(tracer.GetIp())


Как видишь, код очень лаконичен и понятен.

DbiFuzz-фреймворк


Все рассмотренные выше подходы к трассировке я воплотил в DbiFuzz-фреймворке, который демонстрирует, как можно отслеживать работу исполняемого файла альтернативными методами. Как мы уже отмечали, некоторые из известных методов используют инструментацию, которая дает быстрое решение, но при этом предполагает серьезное вмешательство в целевой процесс и не сохраняет целостности бинарного файла. В отличие от них, DbiFuzz оставляет бинарный файл практически нетронутым, изменяя только PTE, BTF и вставляя флаг TRAP. Другая сторона этого подхода состоит в том, что при интересующем событии включается прерывание: переход ring 3 —ring 0 — ring 3. Так как DbiFuzz подразумевает прямолинейное вмешательство в контекст и поток управления целевого процессора, то его можно использовать для написания собственных инструментов (даже на Python) для доступа к целевому бинарному файлу и его ресурсам.

WWW

Более подробно узнать про DbiFuzz-фреймворк ты можешь на моем сайте, на SlideShare и на портале ZeroNights
Дереву VAD посвящена очень интересная статья Брендана Долан-Гэвитта «The VAD tree: A process-eye view of physical memory5».


Show time


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

Нет никаких проблем в запуске DbiFuzz на лету, просто установи ловушку или INT3-перехватчик. Поскольку мы не трогаем бинарный код целевого файла, то не будет никаких проблем с проверкой целостности, а флаг TRAP может быть заменен на MTF. Отслеживание ценных данных тоже не представляет никаких проблем, нужно просто установить соответствующий PTE — и твой монитор готов! Инструменты Python/Ruby/…? Просто создай нужные привязки (bindings) — и вперед!
Конечно, у этого фреймворка тоже есть свои недостатки, но в целом он обладает многими полезными возможностями. И ты всегда можешь поиграть с DbiFuzz, использовать входящие в него инструменты для своих нужд и отслеживать все, что пожелаешь.

To be continued


Как видишь, динамическая бинарная инструментация — не единственный метод трассировки. Альтернатив ей достаточно много, и большинство из них представлены в DbiFuzz-фреймворке. Уже сейчас некоторые возможности этого проекта могут помочь с работой в кодом на уровне ядра, а в дальнейшем я планирую перевести в это пространство весь трейсер. Кстати, уже сейчас ты можешь использовать исходники фреймворка, улучшать концепцию и экспериментировать с новыми идеями...

Полезные ссылки

Блоги:

Intel:

Относительно VAD:

Виртуализация:

Модули Python (дизассемблеры):



Впервые опубликовано в журнале «Хакер» от 02/2014.

Подпишись на «Хакер»



Tags:
Hubs:
+20
Comments 5
Comments Comments 5

Articles

Information

Website
xakep.ru
Registered
Founded
Employees
51–100 employees
Location
Россия