- PVSM.RU - https://www.pvsm.ru -
Трассировка используется во многих видах ПО: в эмуляторах, динамических распаковщиках, фаззерах. Традиционные трейсеры работают по одному из четырех принципов: эмуляция набора инструкций (Bochs), бинарная трансляция (QEMU), патчинг бинарных файлы для изменения потока управления (Pin), либо работа через отладчик (PaiMei, основанный на IDA). Но сейчас речь пойдет о более интересных подходах.
Задачи, которые решают с помощью трассировки можно условно разделить на три группы в зависимости от того, что именно отслеживается: выполнение программы (поток управления), поток данных или взаимодействие с ОС. Давай поговорим о каждом подробнее...
Отслеживание потока управления помогает понять, что делает бинарник во время исполнения. Это хороший способ работы с обфусцированным кодом. Также, если ты работаешь с фаззером, это поможет с анализом покрытия кода. Или возьмем, например, антивирусное ПО, где трассировщик проследит за исполнением бинарного файла, сформулирует некий паттерн его поведения, а также поможет с динамической распаковки исполняемого файла.
Трассировка может происходить на разных уровнях: отслеживание каждой инструкции, базовых блоков либо только определенных функций. Как правило, она осуществляется путем пред/постинструментации, то есть патчинга потока управления в наиболее «интересных» местах. Другой метод состоит в том, чтобы просто приаттачить отладчик к исследуемой программе и обрабатывать ловушки и точки останова. Однако есть еще один не очень распространенный способ — задействовать функции центрального процессора. Одна из интересных возможностей процессоров 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-дерево [1], используемое для хранения информации об адресном пространстве процесса. Оно же используется, когда необходимо инициализировать PTE (Page Table Entry) для конкретной страницы памяти.
Как я предложил выше, отслеживание доступа к памяти может осуществляться через механизм защиты памяти (такая вот тавтология), но его реализация в режиме пользователя с помощью API-функций может слишком сильно отразиться на производительности. Однако если принять во внимание, что защита памяти основана на механизме MMU — пейджинге, то есть более простой способ: изменять таблицу страниц в режиме ядра, после чего нарушение режима доступа к памяти будет обрабатываться через генерацию процессором исключения PageFault, а управление будет передаваться на обработчик IDT[PageFault]. Установка перехватчика на обработчик PageFault позволит быстро получить сигнал о запросе на доступ к выбранным страницам.
Все потому, что процесс может использовать только страницы памяти, помеченные как Valid (то есть выгруженные в память), в противном же случае будет возникать исключение PageFault, которое и будет перехватываться. Это означает, что если мы намеренно поставили Valid-флаг выбранной страницы памяти в значение invalid(0), то каждая попытка доступа к этой странице будет вызывать обработчик PageFault, что позволяет легко отфильтровать и обработать соответствующий запрос (вызывая callback к трейсеру и выставляя Valid-флаг для конкретного PTE).
В предыдущем разделе я предложил некоторые «грязные» методы для режима ядра. Вообще, установка хуков — это неправильный способ, и мне он не нравится, точно так же, как не нравится он и ребятам из Microsoft. Для борьбы с такими методами мелкомягкие и разработали PatchGuard. К счастью, есть и другой способ для отлова PageFaults, ловушек или SYSCALL’ов — это гипервизор. Правда, данный вариант имеет как свои плюсы, так и свои минусы.
Минусы:
switch( VMMExit )
отбирает немного производительности, равно как и код гипервизора, выполняющийся для каждого из вариантов switch’а.Плюсы:
При этом сам VMM (Virtual Machine Monitor) может быть минималистичным (микроVMM) и реализовывать только необходимую обработку, занимая при этом минимальный объем кода (пример [2]).
Помимо всего, в данном случае вместо того, чтобы ставить хуки на IDT, можно все обрабатывать напрямую с помощью дебаг-исключения в VMM. То же самое относится и к перехвату ошибок страниц с помощью исключения PageFault в VMM или через реализацию EPT (Extended Page Table).
Можно отметить некоторые основные особенности описанного подхода:
Выделение трейсера из пространства целевого процесса в другой процесс дает несколько преимуществ: можно использовать его как отдельный модуль, можно сделать биндинги для Python, Ruby и других языков. Однако у этого решения есть и недостаток — очень большой удар по производительности (взаимодействие между процессами: чтение из памяти другого процесса, событийный механизм ожидания). Для ускорения трассировки необходимо перенести логику в адресное пространство целевого процесса, чтобы можно было быстро получать доступ к его ресурсам (памяти, стеку, содержимому регистров), а также опционально отказаться от VMM из-за негативного влияния обработки VMMExit на производительность и вернуться обратно к установке хуков для ловушек и обработчиков PageFault. Но с другой стороны, в будущих процессорах технологии виртуализации, наверное, станут более эффективными и не будут оказывать настолько большого влияния на производительность. К тому же возможности виртуализации для трассировки можно использовать гораздо шире, чем мы рассматриваем в рамках статьи, поэтому плюсы могут компенсировать снижение производительности.
Что касается трассировщика для ядра, то здесь действуют все те же принципы:
Главная особенность таких трейсеров в том, что не надо патчить бинарный файл, а также что трассировку (включая распаковку и фаззинг) можно осуществлять из уровня пользователя (например, из трейсера, написанного на Python), хотя с точки зрения производительности гораздо более эффективно делать это напрямую из режима ядра.
С другой стороны, за все эти возможности тоже приходится расплачиваться:
Отделение от целевого процесса, а также инкапсуляция в модуль дают нам высокую масштабируемость и возможность совместной работы с другими модулями для создания более сложного инструмента. Таким образом, в случае реализации трейсера, например, на 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-фреймворке [3], который демонстрирует, как можно отслеживать работу исполняемого файла альтернативными методами. Как мы уже отмечали, некоторые из известных методов используют инструментацию, которая дает быстрое решение, но при этом предполагает серьезное вмешательство в целевой процесс и не сохраняет целостности бинарного файла. В отличие от них, DbiFuzz оставляет бинарный файл практически нетронутым, изменяя только PTE, BTF и вставляя флаг TRAP. Другая сторона этого подхода состоит в том, что при интересующем событии включается прерывание: переход ring 3 —ring 0 — ring 3. Так как DbiFuzz подразумевает прямолинейное вмешательство в контекст и поток управления целевого процессора, то его можно использовать для написания собственных инструментов (даже на Python) для доступа к целевому бинарному файлу и его ресурсам.
WWW
Более подробно узнать про DbiFuzz-фреймворк ты можешь на моем сайте [4], на SlideShare [5] и на портале ZeroNights [6]
Дереву VAD посвящена очень интересная статья [7] Брендана Долан-Гэвитта «The VAD tree: A process-eye view of physical memory5».
Для многих задач, решаемых с помощью трассировки, может оказаться полезной динамическая бинарная инструментация. Что касается DbiFuzz-фреймворка, то его можно использовать в следующих случаях:
Нет никаких проблем в запуске DbiFuzz на лету, просто установи ловушку или INT3-перехватчик. Поскольку мы не трогаем бинарный код целевого файла, то не будет никаких проблем с проверкой целостности, а флаг TRAP может быть заменен на MTF. Отслеживание ценных данных тоже не представляет никаких проблем, нужно просто установить соответствующий PTE — и твой монитор готов! Инструменты Python/Ruby/…? Просто создай нужные привязки (bindings) — и вперед!
Конечно, у этого фреймворка тоже есть свои недостатки, но в целом он обладает многими полезными возможностями. И ты всегда можешь поиграть с DbiFuzz, использовать входящие в него инструменты для своих нужд и отслеживать все, что пожелаешь.
Как видишь, динамическая бинарная инструментация — не единственный метод трассировки. Альтернатив ей достаточно много, и большинство из них представлены в DbiFuzz-фреймворке. Уже сейчас некоторые возможности этого проекта могут помочь с работой в кодом на уровне ядра, а в дальнейшем я планирую перевести в это пространство весь трейсер. Кстати, уже сейчас ты можешь использовать исходники фреймворка, улучшать концепцию и экспериментировать с новыми идеями...
Полезные ссылки
Блоги:
Intel:
Относительно VAD:
Виртуализация:
- Intel Virtualization Technology [14]
- HDBG — hypervisor-based debugger [15]
- HyperDbg [16]
- Доклад Джоанны Рутковской на BH US 06 [17]
Модули Python (дизассемблеры):
- BeaEngine [18]
- Capstone [19]
- Python arsenal [20]
Впервые опубликовано в журнале «Хакер» от 02/2014.
Подпишись на «Хакер»
Автор: XakepRU
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/vzlom/66479
Ссылки в тексте:
[1] AVL-дерево: http://bit.ly/1e4BvWm
[2] пример: http://bit.ly/1md4Sdq
[3] DbiFuzz-фреймворке: http://bit.ly/1jmFWk0
[4] моем сайте: http://bit.ly/1eDINC1
[5] SlideShare: http://slidesha.re/1m7wxMV
[6] портале ZeroNights: http://bit.ly/1eDIWp2
[7] статья: http://bit.ly/1ajBvGt
[8] Трассировка ветвлений при помощи MSR-регистров: http://bit.ly/1cLtLHD
[9] ExcpHook Monitor: http://bit.ly/1e2YaFg
[10] Расширения для виртуальных машин: http://intel.ly/1h8pmDh
[11] Мануал для разработчиков ПО: http://intel.ly/KqEL7c
[12] Кратко про дескрипторы виртуальных адресов: http://bit.ly/1jhzhKC
[13] ReactOS: https://www.reactos.org
[14] Intel Virtualization Technology: http://bit.ly/1aDIrfm
[15] HDBG — hypervisor-based debugger: http://bit.ly/1jhzZHF
[16] HyperDbg: http://bit.ly/1hFxhIX
[17] Доклад Джоанны Рутковской на BH US 06: http://bit.ly/1e3hfat
[18] BeaEngine: http://bit.ly/1dtLl97
[19] Capstone: http://bit.ly/1hUIR5u
[20] Python arsenal: http://bit.ly/1eRdtQG
[21] Бумажный вариант: http://bit.ly/habr_subscribe_paper
[22] «Хакер» на iOS/iPad: http://j.mp/Xakep_ipad_xakep_ru
[23] «Хакер» на Android: http://j.mp/Xakep_android_xakep_ru
[24] Источник: http://habrahabr.ru/post/231813/
Нажмите здесь для печати.