Эта статья посвящена тестированию возможности использования технологии Intel Processor Trace (Intel PT) для записи трассы в System Management Mode (SMM) режиме. Работа была выполнена в рамках Summer Of Hack 2019. Автор работы: @sysenter_eip.
Большинство использованных инструментов написаны другими людьми (в частности @d_olex, @aionescu). Результат представляет собой лишь объединение имеющихся инструментов с целью получения трассы исполнения кода в режиме SMM для одной конкретной материнской платы. Однако, материал может быть интересен для тех, кто захочет повторить это для своей платформы или просто интересуется работой SMM.
System Management Mode
SMM – особый, привилегированный режим процессора архитектуры x86, который доступен во время работы операционной системы, но прозрачен для нее. Он предназначен для низкоуровневого взаимодействия с железом, управления питанием, эмуляции легаси устройств, перехода в режим сна (S3), обращения к TPM и прочего. Работает полностью изолировано от ОС. На время исполнения SMM работа ОС полностью останавливается. Программный код, который исполняется в этом режиме, хранится в SPI-Flash памяти материнской платы и входит в состав прошивки UEFI BIOS.
Переход в режим SMM осуществляется при помощи специальных прерываний SMI (System Management Interrupt). Один из вариантов этого прерывания доступен для использования в нулевом кольце (т.е. из ядра ОС) – SMI-прерывание уровня приложений (Software SMI). Далее речь пойдет именно об этих прерываниях.
Ввиду своей высокой привилегированности, SMM представляет особый интерес для исследования безопасности. Компрометация SMM приводит к серьезным нарушениям целостности и конфиденциальности всей системы, и в большинстве случаев позволяет внедрить не удаляемый и не обнаруживаемый средствами операционной системы вредоносный код в прошивку UEFI BIOS.
Intel Processor Trace
Одним из подводных камней процесса отладки различных высоконагруженных приложений является оверхед – издержки инструментов отладки. Их можно сократить с помощью решения с аппаратной поддержкой.
Пятое поколение процессоров от Intel (Broadwell) подарило миру такую технологию, как Intel Processor Trace. Чем же она полезна? Intel PT позволяет получить полный поток исполнения (Control Flow) отлаживаемого приложения с минимальным оверхедом (<5%). При этом она поддерживает многопоточность и может помочь в установлении ошибок типа "состояние гонки" (race condition) благодаря отметкам времени при записи трассы приложения. Несомненно, технология Intel PT открывает большие возможности для написания инструментов поиска уязвимостей в приложениях.
Сегодня эта технология используется в различных инструментах трассировки, отладки, и оценки покрытия кода – как в пользовательских, так и в приложениях уровня ядра. Примеры инструментов можно посмотреть на сайте Intel. Вариант AFL-фаззера, использующего преимущества Intel PT, доступен в репозитории PTfuzzer. Из свежих проектов стоит обратить внимание на iptanalyzer.
Тем не менее, мы не встречали ни одной работы, посвященной использованию Intel PT в режиме SMM. Поскольку ничто не мешает использовать Intel PT в этом контексте, мы решили выяснить, можно ли с ее помощью произвести трассировку кода System Management Mode.
Подготовка к работе
Из Intel Developer Manual следует, что невозможно штатными средствами активировать трассировку Intel PT в SMM извне. Если она была активна в момент срабатывания SMI, то процессор отключит ее до передачи управления на точку входа обработчика SMI. Единственный способ активации – добровольное включение самим кодом обработчика SMI.
Даже если изначально обработчик не предоставляет такой возможности, мы можем его перехватить и активировать Intel PT вручную. Однако нужно каким-то образом определить, что система готова записывать трассу (адрес буфера для вывода установлен), а также выключить трассировку в конце исполнения обработчика (исполнение инструкции RSM). В противном случае процессор завершит работу всей системы.
В первую очередь, необходимо получить доступ к SMRAM (область оперативной памяти, в которой располагается код, исполняемый в режиме SMM). Так как этот регион RAM защищен, мы не можем получить к нему доступ из операционной системы (даже по DMA этого сделать не получится). Есть несколько вариантов развития событий:
- эксплуатировать известную уязвимость в SMM и получить R/W примитив из нее. Это может быть как программная ошибка (уязвимость в самом SMI обработчике; как правило, в SMM достаточно кода, который был добавлен OEM-производителем, поэтому уязвимости не редкость), так и уязвимая конфигурация платформы (разблокировка/перемещение SMRAM);
- пропатчить образ UEFI таким образом, что у нас появится интерфейс для чтения и записи по произвольным адресам – бэкдор. Для реализации этого варианта необходимо найти материнскую плату, на которой отключен Intel Boot Guard или присутствуют уязвимости, позволяющие его обойти.
Внедрение своего кода в прошивку
Несмотря на то, что уязвимости SMM в коде различных производителей находят время от времени, будет лучше, если мы не будем полагаться на них. Для нас интереснее выполнить трассировку кода на новых прошивках и, соответственно, попытаться найти уязвимости в них. У нас уже была в распоряжении материнская плата GIGABYTE GA-Q270M-D3H с отключенным Intel Boot Guard, поэтому нужно было только добавить бэкдор в SMM.
Рисунок 1. Тестовый стенд
Уже существует фреймворк для "заражения" SMM и работы с бэкдором. Он состоит из трех компонентов: UEFI-драйвер на C, "инфектор" и клиентский скрипт на Python. Для его работы нужно извлечь произвольный DXE драйвер (можно сделать это с помощью UEFITool) и обработать его инфектором. Оригинальный модуль был заменен на "улучшенный", и прошивка была залита в память SPI (для удобства перепрошивки SPI-флешка была выпаяна с платы).
Рисунок 2. Чип SPI-Flash памяти
Система успешно запустилась, и теперь у нас есть полный доступ к памяти SMRAM из Python (вместе с бэкдором идет пример использования). Так как клиентский скрипт для бэкдора основан на CHIPSEC, необходимо предоставить ему доступ в режим ядра (мы использовали драйвер RWEverything; кому-то будет удобно использовать собственный драйвер CHIPSEC'а с выключенной проверкой подписей в системе).
Проверить работу бэкдора можно запросив дамп SMRAM.
$ python SmmBackdoor.py -d
После выполнения этой команды будет создан файл SMRAM_dump_cb000000_cb7fffff.bin, содержащий текущее состояние SMRAM. Значения cb000000 и cb7fffff – это, соответственно, физические адреса начала и конца SMRAM.
Работа с дампом SMRAM
Дамп SMRAM можно загрузить в дизассемблер или передать для анализа скрипту smram_parse.py, который извлечет для нас много полезной информации. Самыми важными для нас будут адреса точек входа SMI. Это адреса функций, на которые будет передано управлении при срабатывании SMI. У каждого CPU своя точка входа.
Рисунок 3. Вывод работы скрипта smram_parse
Посмотрим на их код. Так как 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.
Рисунок 5. Схема расположения SMBASE
Определяем состояние Intel PT
В SMM Save State должно сохраняться значение MSR-регистра IA32_RTIT_CTL, который отвечает за управление трассировкой Intel PT. К сожалению, Intel Manual не указывает, куда процессор сохраняет состояние бита IA32_RTIT_CTL.TraceEn в момент перехода в SMM (включена ли трассировка, нулевой бит). Однако мы можем определить это самостоятельно, сделав дамп SMM Save State дважды: с включенной трассировкой и без.
Мы использовали инструмент WinIPT для активации трассировки на процессе интерпретатора Python (pid 1337), выделяя при этом 2^12 (4096) байт на буфер трассировки, а затем исполняли внутри интерпретатора скрипт SmmBackdoor.py (аргумент 0 – это флаги, для нас они не важны, так как в SMM все равно придется форсировать свои настройки трассировки).
$ ipttool.exe --start 1337 12 0
Сравнив снимки SMRAM, мы определили расположение регистра IA32_RTIT_CTL в структуре SMM Save State. Он хранится на смещении SMBASE + 0xFE3C. Состояние бита IA32_RTIT_CTL.TraceEn – это главное условие переактивации Intel PT внутри SMM. Поле по этому смещению помечено в Intel Developer Manual как Reserved.
Рисунок 6. Пометка о том, что поля зарезервированы
Пишем shellcode
Нам не хотелось самостоятельно настраивать Intel PT внутри SMM, так как это бы усложнило наш shellcode (например, находясь в SMM, было бы сложно выделить большой кусок оперативной памяти так, чтобы он не был задействован самой операционной системой). Поэтому мы решили воспользоваться уже настроенным трейсером и просто "пропустить" его внутрь SMM, тем более у него уже есть функция сохранения трассы в файл.
Так как мы использовали для этих целей WinIPT, который на тот момент не поддерживал трассировку ядерного кода (CPL == 0), было очевидно, что даже при включении трассы в SMM в логе ничего не появится, так как код SMM исполняется при CPL=0. Нам необходимо модифицировать некоторые фильтры, чтобы трейсер мог работать на протяжении всего времени нахождения в SMM. Перечислим все, что необходимо проверить и установить:
- Трассировка при CPL=0 должна быть разрешена.
- Трассировка при CPL>0 должна быть разрешена (необязательно).
- Диапазоны IP допустимых для записи событий нужно отключить.
- IA32_RTIT_STATUS.PacketByteCnt необходимо сбросить.
- Фильтрацию по CR3 необходимо отключить.
Следует сказать несколько слов о PacketByteCnt. Этот счетчик определяет, в какой момент нужно вставить синхронизационные пакеты (последовательность из нескольких PSB-команд) внутрь трассы. Нам необходимо сбросить этот счетчик, иначе во время обработки трассы будет пропущен момент входа в 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
Этот код должен быть размещен в SMRAM, а переход на диспетчер SMI должен быть пропатчен для перехода на наш код. Все это делается при помощи SmmBackdoor.
Работа с трассой
Перехватчик диспетчера SMI позволил нам записать первую трассу кода из SMM. Следующей командой можно попросить WinIPT сохранить трассу в файл:
$ 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
Пример вывода:
Рисунок 7. Трасса первых инструкций SMM
Мы можем видеть некоторые адреса, однако использовать эту информацию крайне тяжело, так как она очень низкоуровневая.
Для получения более читаемого вида есть утилита ptxed (из libipt), которая сконвертирует трассу в лог исполненных ассемблерных инструкций. Конечно же, нам придется предоставить утилите дамп памяти SMRAM, так как IPT-лог не содержит информации о значениях ячеек памяти или о том, какие инструкции исполнялись; в нем есть только информация о том, какие изменения происходили в потоке управления.
$ ptxed.exe --pt tracesmm_12 --raw SMRAM_dump_cb000000_cb7fffff.bin:0xcb000000 > tracesmm_12_ptasm
Рисунок 8. Ассемблерный листинг, соответствующий логу IPT
Это выглядит уже намного лучше, однако если код содержит цикл, вывод будет забит одними и теми же инструкциями.
Определяем покрытие кода при помощи трассы
Чтобы получить визуализацию покрытия, мы выбрали плагин Lighthouse для IDA Pro, который использует формат drcov.
Готовых инструментов найдено не было, поэтому мы модифицировали ptxed так, чтобы он генерировал и файл покрытия в процессе своей работы. Пропатченый ptxed доступен в репозитории. Взгляните на историю коммитов, чтобы определить, что конкретно было добавлено.
После завершения исполнения ptxed появится файл SMRAM_dump_cb000000_cb7fffff.bin.log, который будет содержать информацию о покрытии в формате drcov.
Примечание: Существует небольшая проблема, связанная с синхронизацией дизассемблера по первому PSB. По не совсем понятной причине, если PSB генерируется до PGE (счетчик обнуляется до повторной активации трассировки), то ptxed не может синхронизоваться по нему. Для обхода этой проблемы мы сделали небольшой патч. Не ясно, является ли это проблемой самого ptxed, или мы делаем что-то не верно, сбрасывая IA32_RTIT_STATUS.PacketByteCnt.
Рисунок 9. Патч, который позволяет использовать PSB-блок, расположенный прямо перед PGE
Сгенерированные файлы покрытия можно загрузить в IDA Pro и получить красивую подсветку, а также статистику процента покрытия по каждой функции.
Рисунок 10. Плагин IDA Pro Lighthouse с информацией о покрытии кода
Примечание: Плагин Lighthouse немного странно работает на не до конца проанализированных базах (исполняемый код не размечен, функции не созданы). Мы проследили эту "проблему" до функции get_instructions_slice в файле lighthousemetadata.py, где она возвращает 0 инструкций даже для адреса, на котором была вручную создана функция. Кажется, плагин использует кэш и игнорирует новый определенный код. Это можно обойти, вызвав 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 PT для изучения и поиска уязвимостей в коде SMM. Адаптация нашей работы к другим материнским платам не должна вызвать затруднений (не забудьте про Intel Boot Guard). Главное – полностью разобраться в том, как это устроено. Самое сложное – определить способ перехвата SMI-диспетчера и написать шеллкод для перехватчика. В нашем варианте использовались "вшитые" адреса, так что следует внимательно переносить шеллкод на другую систему.
Все использованные инструменты и скрипты доступны в репозитории на GitHub.
Автор: backtrace