Хабрахабр

Использование Intel Processor Trace для трассировки кода System Management Mode

Работа была выполнена в рамках Summer Of Hack 2019. Эта статья посвящена тестированию возможности использования технологии Intel Processor Trace (Intel PT) для записи трассы в System Management Mode (SMM) режиме. Автор работы: @sysenter_eip.

Результат представляет собой лишь объединение имеющихся инструментов с целью получения трассы исполнения кода в режиме SMM для одной конкретной материнской платы. Большинство использованных инструментов написаны другими людьми (в частности @d_olex, @aionescu). Однако, материал может быть интересен для тех, кто захочет повторить это для своей платформы или просто интересуется работой SMM.

System Management Mode

Он предназначен для низкоуровневого взаимодействия с железом, управления питанием, эмуляции легаси устройств, перехода в режим сна (S3), обращения к TPM и прочего. SMM – особый, привилегированный режим процессора архитектуры x86, который доступен во время работы операционной системы, но прозрачен для нее. На время исполнения SMM работа ОС полностью останавливается. Работает полностью изолировано от ОС. Программный код, который исполняется в этом режиме, хранится в SPI-Flash памяти материнской платы и входит в состав прошивки UEFI BIOS.

Один из вариантов этого прерывания доступен для использования в нулевом кольце (т.е. Переход в режим SMM осуществляется при помощи специальных прерываний SMI (System Management Interrupt). Далее речь пойдет именно об этих прерываниях. из ядра ОС) – SMI-прерывание уровня приложений (Software SMI).

Компрометация SMM приводит к серьезным нарушениям целостности и конфиденциальности всей системы, и в большинстве случаев позволяет внедрить не удаляемый и не обнаруживаемый средствами операционной системы вредоносный код в прошивку UEFI BIOS. Ввиду своей высокой привилегированности, SMM представляет особый интерес для исследования безопасности.

Intel Processor Trace

Их можно сократить с помощью решения с аппаратной поддержкой. Одним из подводных камней процесса отладки различных высоконагруженных приложений является оверхед – издержки инструментов отладки.

Чем же она полезна? Пятое поколение процессоров от Intel (Broadwell) подарило миру такую технологию, как Intel Processor Trace. При этом она поддерживает многопоточность и может помочь в установлении ошибок типа "состояние гонки" (race condition) благодаря отметкам времени при записи трассы приложения. Intel PT позволяет получить полный поток исполнения (Control Flow) отлаживаемого приложения с минимальным оверхедом (<5%). Несомненно, технология Intel PT открывает большие возможности для написания инструментов поиска уязвимостей в приложениях.

Примеры инструментов можно посмотреть на сайте Intel. Сегодня эта технология используется в различных инструментах трассировки, отладки, и оценки покрытия кода – как в пользовательских, так и в приложениях уровня ядра. Из свежих проектов стоит обратить внимание на iptanalyzer. Вариант AFL-фаззера, использующего преимущества Intel PT, доступен в репозитории PTfuzzer.

Поскольку ничто не мешает использовать Intel PT в этом контексте, мы решили выяснить, можно ли с ее помощью произвести трассировку кода System Management Mode. Тем не менее, мы не встречали ни одной работы, посвященной использованию Intel PT в режиме SMM.

Подготовка к работе

Если она была активна в момент срабатывания SMI, то процессор отключит ее до передачи управления на точку входа обработчика SMI. Из Intel Developer Manual следует, что невозможно штатными средствами активировать трассировку Intel PT в SMM извне. Единственный способ активации – добровольное включение самим кодом обработчика SMI.

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

Так как этот регион RAM защищен, мы не можем получить к нему доступ из операционной системы (даже по DMA этого сделать не получится). В первую очередь, необходимо получить доступ к SMRAM (область оперативной памяти, в которой располагается код, исполняемый в режиме SMM). Есть несколько вариантов развития событий:

  1. эксплуатировать известную уязвимость в SMM и получить R/W примитив из нее. Это может быть как программная ошибка (уязвимость в самом SMI обработчике; как правило, в SMM достаточно кода, который был добавлен OEM-производителем, поэтому уязвимости не редкость), так и уязвимая конфигурация платформы (разблокировка/перемещение SMRAM);
  2. пропатчить образ UEFI таким образом, что у нас появится интерфейс для чтения и записи по произвольным адресам – бэкдор. Для реализации этого варианта необходимо найти материнскую плату, на которой отключен Intel Boot Guard или присутствуют уязвимости, позволяющие его обойти.

Внедрение своего кода в прошивку

Для нас интереснее выполнить трассировку кода на новых прошивках и, соответственно, попытаться найти уязвимости в них. Несмотря на то, что уязвимости SMM в коде различных производителей находят время от времени, будет лучше, если мы не будем полагаться на них. У нас уже была в распоряжении материнская плата GIGABYTE GA-Q270M-D3H с отключенным Intel Boot Guard, поэтому нужно было только добавить бэкдор в SMM.

Тестовый стенд
Рисунок 1.

Он состоит из трех компонентов: UEFI-драйвер на C, "инфектор" и клиентский скрипт на Python. Уже существует фреймворк для "заражения" SMM и работы с бэкдором. Оригинальный модуль был заменен на "улучшенный", и прошивка была залита в память SPI (для удобства перепрошивки SPI-флешка была выпаяна с платы). Для его работы нужно извлечь произвольный DXE драйвер (можно сделать это с помощью UEFITool) и обработать его инфектором.

Чип SPI-Flash памяти
Рисунок 2.

Так как клиентский скрипт для бэкдора основан на CHIPSEC, необходимо предоставить ему доступ в режим ядра (мы использовали драйвер RWEverything; кому-то будет удобно использовать собственный драйвер CHIPSEC'а с выключенной проверкой подписей в системе). Система успешно запустилась, и теперь у нас есть полный доступ к памяти SMRAM из Python (вместе с бэкдором идет пример использования).

Проверить работу бэкдора можно запросив дамп SMRAM.

$ python SmmBackdoor.py -d

Значения cb000000 и cb7fffff – это, соответственно, физические адреса начала и конца SMRAM. После выполнения этой команды будет создан файл SMRAM_dump_cb000000_cb7fffff.bin, содержащий текущее состояние SMRAM.

Работа с дампом SMRAM

Самыми важными для нас будут адреса точек входа SMI. Дамп SMRAM можно загрузить в дизассемблер или передать для анализа скрипту smram_parse.py, который извлечет для нас много полезной информации. У каждого CPU своя точка входа. Это адреса функций, на которые будет передано управлении при срабатывании SMI.

Вывод работы скрипта smram_parse
Рисунок 3.

Так как SMM начинает свое исполнение в 16-битном Real Mode (при этом первые 4 Гб RAM отражаются на виртуальное пространство), первое, что делает код – это переключение в 64-битный режим. Посмотрим на их код. При этом весь SMRAM доступен с правами на запись и исполнение, так как был создан только один сегмент (существуют ли вендоры, которые делают по-другому?).

Нам бы не хотелось писать 16-битный код или подготавливать все необходимое для переключения в 64-битный режим самостоятельно, так что мы разместим наш перехватчик прямо перед вызовом функции диспетчера SMI (эта функция определяет, какому SMM модулю нужно передать исполнение в зависимости от того, какой сервис был вызван или какое событие произошло).

Место для размещения хука
Рисунок 4.

У всех точек входа одинаковый код, так что патч нужно повторить для каждой. Самый простой способ перехватить управление – подменить адрес диспетчера на наш.

Так как структура SMRAM нам до конца не известна, мы выбрали случайный кусок зануленной памяти рядом с одной из точек входа, где и разместили код перехватчика. Примечание: Относительно места размещения кода перехватчика. Лучшим вариантом было бы добавить в прошивку свой SMM модуль, который UEFI бы легально разместил в SMRAM, чтобы не беспокоиться, что нашим кодом будет перезаписано что-то важное.

Реализация перехватчика диспетчера SMI

Сперва нам необходимо определить, был ли включен Intel PT до перехода в SMM. Обозначим, что конкретно мы ходим сделать внутри нашего перехватчика. Из документации Intel известно, что у каждого процессора есть своя база SMBASE (MSR 0x9E) и свое пространство для хранения состояния процессора (SMM Save State area) в момент перехода в SMM.

Схема расположения SMBASE
Рисунок 5.

Определяем состояние Intel PT

К сожалению, Intel Manual не указывает, куда процессор сохраняет состояние бита IA32_RTIT_CTL. В SMM Save State должно сохраняться значение MSR-регистра IA32_RTIT_CTL, который отвечает за управление трассировкой Intel PT. Однако мы можем определить это самостоятельно, сделав дамп SMM Save State дважды: с включенной трассировкой и без. TraceEn в момент перехода в SMM (включена ли трассировка, нулевой бит).

Мы использовали инструмент WinIPT для активации трассировки на процессе интерпретатора Python (pid 1337), выделяя при этом 2^12 (4096) байт на буфер трассировки, а затем исполняли внутри интерпретатора скрипт SmmBackdoor.py (аргумент 0 – это флаги, для нас они не важны, так как в SMM все равно придется форсировать свои настройки трассировки).

$ ipttool.exe --start 1337 12 0

Он хранится на смещении SMBASE + 0xFE3C. Сравнив снимки SMRAM, мы определили расположение регистра IA32_RTIT_CTL в структуре SMM Save State. TraceEn – это главное условие переактивации Intel PT внутри SMM. Состояние бита IA32_RTIT_CTL. Поле по этому смещению помечено в Intel Developer Manual как Reserved.

Пометка о том, что поля зарезервированы
Рисунок 6.

Пишем shellcode

Поэтому мы решили воспользоваться уже настроенным трейсером и просто "пропустить" его внутрь SMM, тем более у него уже есть функция сохранения трассы в файл. Нам не хотелось самостоятельно настраивать Intel PT внутри SMM, так как это бы усложнило наш shellcode (например, находясь в SMM, было бы сложно выделить большой кусок оперативной памяти так, чтобы он не был задействован самой операционной системой).

Нам необходимо модифицировать некоторые фильтры, чтобы трейсер мог работать на протяжении всего времени нахождения в SMM. Так как мы использовали для этих целей WinIPT, который на тот момент не поддерживал трассировку ядерного кода (CPL == 0), было очевидно, что даже при включении трассы в SMM в логе ничего не появится, так как код SMM исполняется при CPL=0. Перечислим все, что необходимо проверить и установить:

  1. Трассировка при CPL=0 должна быть разрешена.
  2. Трассировка при CPL>0 должна быть разрешена (необязательно).
  3. Диапазоны IP допустимых для записи событий нужно отключить.
  4. IA32_RTIT_STATUS.PacketByteCnt необходимо сбросить.
  5. Фильтрацию по CR3 необходимо отключить.

Этот счетчик определяет, в какой момент нужно вставить синхронизационные пакеты (последовательность из нескольких PSB-команд) внутрь трассы. Следует сказать несколько слов о PacketByteCnt. Нам необходимо сбросить этот счетчик, иначе во время обработки трассы будет пропущен момент входа в SMM, и трасса начнется со случайного места, когда PSB будет сгенерирован естественным образом.

Ниже приведен использованный нами shellcode:

sub rsp, 0x18 ; this will align stack at 16 byte boundary (in case SMM ; code uses align dependent instructions) mov qword ptr ss:[rsp+0x10], rcx ; need to save rcx for SMI_Dispatcher mov ecx, 0x9E ; MSR_IA32_SMBASE rdmsr test byte ptr ds:[rax+0xFE3C], 0x1 ; Save State area contains saved ; IA32_RTIT_CTL.TraceEn je short @NoTrace call @Trace_Enable mov rcx, qword ptr ss:[rsp+0x10] ; SMI_Dispatcher is __fastcall ; (first argument in rcx) mov eax, 0xCB7DDAA4 ; original SMI_Dispatcher !!!!!!!!!!!!!!!!!!!!! call rax call @Trace_Disable add rsp, 0x18 ret @NoTrace: mov rcx, qword ptr ss:[rsp+0x10] ; SMI_Dispatcher is __fastcall mov eax, 0xCB7DDAA4 ; original SMI_Dispatcher !!!!!!!!!!!!!!!!!!!!! call rax add rsp, 0x18 ret @Trace_Disable: mov ecx, 0x570 ; IA32_RTIT_CTL rdmsr mov rax, qword ptr ss:[rsp+0x10] ; restore IA32_RTIT_STATUS wrmsr mov ecx, 0x571 ; IA32_RTIT_STATUS rdmsr mov rax, qword ptr ss:[rsp+0x8] ; restore IA32_RTIT_CTL wrmsr ret @Trace_Enable: mov ecx, 0x571 ; IA32_RTIT_STATUS rdmsr mov qword ptr ss:[rsp+0x8], rax ; save IA32_RTIT_STATUS and edx, 0xFFFF0000 ; IA32_RTIT_STATUS.PacketByteCnt = 0 wrmsr mov ecx, 0x570 ; IA32_RTIT_CTL rdmsr mov qword ptr ss:[rsp+0x10], rax ; save IA32_RTIT_CTL and eax, 0xFFFFFFBF ; IA32_RTIT_CTL.CR3Filter = 0 or eax, 0x5 ; IA32_RTIT_CTL.OS = 1; IA32_RTIT_CTL.User = 1; and edx, 0xFFFF0000 ; IA32_RTIT_CTL.ADDRx_CFG = 0 wrmsr ret

Все это делается при помощи SmmBackdoor. Этот код должен быть размещен в SMRAM, а переход на диспетчер SMI должен быть пропатчен для перехода на наш код.

Работа с трассой

Следующей командой можно попросить WinIPT сохранить трассу в файл: Перехватчик диспетчера SMI позволил нам записать первую трассу кода из SMM.

$ ipttool.exe --trace 1337 trace_file_name

Отключение трассировки на процессе:

$ ipttool.exe --stop 1337

Можно попробовать дизассемблировать трассу с помощью утилиты dumppt из libipt.

$ ptdump.exe --no-pad ./examples/trace_smm_handler_33 > ./examples/trace_smm_handler_33_pt_dump.txt

Пример вывода:

Трасса первых инструкций SMM
Рисунок 7.

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

Конечно же, нам придется предоставить утилите дамп памяти SMRAM, так как IPT-лог не содержит информации о значениях ячеек памяти или о том, какие инструкции исполнялись; в нем есть только информация о том, какие изменения происходили в потоке управления. Для получения более читаемого вида есть утилита ptxed (из libipt), которая сконвертирует трассу в лог исполненных ассемблерных инструкций.

$ ptxed.exe --pt tracesmm_12 --raw SMRAM_dump_cb000000_cb7fffff.bin:0xcb000000 > tracesmm_12_ptasm

Ассемблерный листинг, соответствующий логу IPT
Рисунок 8.

Это выглядит уже намного лучше, однако если код содержит цикл, вывод будет забит одними и теми же инструкциями.

Определяем покрытие кода при помощи трассы

Чтобы получить визуализацию покрытия, мы выбрали плагин Lighthouse для IDA Pro, который использует формат drcov.

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

После завершения исполнения ptxed появится файл SMRAM_dump_cb000000_cb7fffff.bin.log, который будет содержать информацию о покрытии в формате drcov.

По не совсем понятной причине, если PSB генерируется до PGE (счетчик обнуляется до повторной активации трассировки), то ptxed не может синхронизоваться по нему. Примечание: Существует небольшая проблема, связанная с синхронизацией дизассемблера по первому PSB. Не ясно, является ли это проблемой самого ptxed, или мы делаем что-то не верно, сбрасывая IA32_RTIT_STATUS. Для обхода этой проблемы мы сделали небольшой патч. PacketByteCnt.

Патч, который позволяет использовать PSB-блок, расположенный прямо перед PGE
Рисунок 9.

Сгенерированные файлы покрытия можно загрузить в IDA Pro и получить красивую подсветку, а также статистику процента покрытия по каждой функции.

Плагин IDA Pro Lighthouse с информацией о покрытии кода
Рисунок 10.

Мы проследили эту "проблему" до функции get_instructions_slice в файле \lighthouse\metadata.py, где она возвращает 0 инструкций даже для адреса, на котором была вручную создана функция. Примечание: Плагин Lighthouse немного странно работает на не до конца проанализированных базах (исполняемый код не размечен, функции не созданы). Это можно обойти, вызвав Reanalyze на программе и переоткрыв IDB. Кажется, плагин использует кэш и игнорирует новый определенный код. Так как эта проблема очень мешает в случае SMRAM дампа (который при первой загрузке почти полностью состоит из неопределенного кода), мы внесли одно небольшое изменение в код Lighthouse, чтобы можно было определять новый код вручную быстрее. Только после этого плагин сможет увидеть новый код и начать учитывать его.

Добавленное лог-сообщение, помогающее в определении нового кода
Рисунок 11.

Поддержка Linux

Так как все наши тесты проводились на Windows 10 x64 (нам был нужен ipt.sys, который появился в Windows October Creators Update 2018), скажем несколько слов о возможности реализации подобного в Linux.

  • Существует модуль perf ядра Linux, который может сделать аналогичные WinIPT (ipt.sys) действия, включая возможность трассировки кода в режиме ядра.
  • Так как интерфейс SMM бэкдора основан на кроссплатформенном фреймворке CHIPSEC, наш патч будет работать на системе с Linux без каких-либо модификаций.

Вывод

Аналогичного результата можно было добиться при помощи дорогостоящего оборудования и ПО, которое продается не каждому. Мы успешно справились с задачей получения трассы кода, исполняющегося в SMM, при помощи технологии Intel Processor Trace. Скорость снятия трассы действительно впечатляющая, а к точности результата нет никаких претензий. Нам же достаточно было иметь на руках одну материнскую плату и SPI-программатор.

Адаптация нашей работы к другим материнским платам не должна вызвать затруднений (не забудьте про Intel Boot Guard). Мы надеемся, что эта статья поможет другим воспользоваться технологией Intel PT для изучения и поиска уязвимостей в коде SMM. Самое сложное – определить способ перехвата SMI-диспетчера и написать шеллкод для перехватчика. Главное – полностью разобраться в том, как это устроено. В нашем варианте использовались "вшитые" адреса, так что следует внимательно переносить шеллкод на другую систему.

Все использованные инструменты и скрипты доступны в репозитории на GitHub.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»