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

    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.

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



    • +20
    • 13.1k
    • 5
    Журнал Хакер
    66.16
    Company
    Share post

    Comments 5

      0
      Задам вопрос почти в тему — а бывают ли отладчики, особенно для JVM, позволяющие одновременно запустить один и тот же проект два раза с разными данными, и чтоб отладчик остановился в первом месте, где ветвление в этой паре процессов расходится?
      Давайте для простоты не думать про многопоточность.
        0
        Сделать трасы и сравнить?!
          0
          Решается достаточно тривиальным модулем для DBI фреймворков типа PIN или DynamoRIO.
          0
          Статья интересная, но как я понимаю в топике автора не будет?
            +3
            Есть еще static binary instrumentation который в статье почему-то не упомянут:

            + Высокая производительность недостигаемая любыми другими способами трассировки кода;
            + Может работать на любом уровне привилегий, с любым IRQL, итд.;

            — Требует отключения механизмов проверки цифровой подписи исполняемого кода при наличии таковых;
            — По этой теме есть научные работы, но нет вменяемых публичных инструментов, т.ч. нужно писать самому;

            Only users with full accounts can post comments. Log in, please.