Подпишитесь на наш Telegram-канал, чтобы всегда быть в курсе важных обновлений! Перейти

Гайд Абьюз SMM для самых маленьких

  • Автор темы Автор темы Kodama
  • Дата начала Дата начала
Obstruct Omicronium
Пользователь
Пользователь
Статус
Оффлайн
Регистрация
28 Авг 2022
Сообщения
90
Реакции
110
В последнее время (эдак год-полтора) на чит-сцене появилось несколько публичных решений для SMM (System Management Mode). Эти решения как «боевые», так и чисто исследовательские. В основном тема эксплуатации SMM с точки зрения читов обсуждается на англоязычных форумах, но на СНГ сцене как-то по этому поводу тихо.

Я решил, что будет неплохой идеей немного осветить тему, как вообще вся эта шняга работает и как её можно использовать с точки зрения читов, ну или собственных исследований по безопасности платформы. Я в принципе занимаюсь этой темой весьма долго и мне есть что рассказать. Я по большей части буду делать отсылки к AMD, но при необходимости постараюсь впихнуть и Intel. С последним я работал намного меньше, чем с первым, отсюда и больший уклон в красных. Да и примерчики тоже на красных будут.

Также, стоит сказать; Я подразумеваю, что вы уже знакомы с устройством как минимум UEFI, работы процессора и некоторых других вещей. Совсем базовые концепции я детально разбирать не буду.

SMM как режим работы процессора
Для начала стоит просто понять, что из себя подразумевает SMM – это привилегированный режим работы процессора на базе архитектуры x86_64, управляющий аппаратным обеспечением ПК. Более детально, режим работает, например, с управлением энергопотребления платформы, ну или исполняет управляющий OEM-код (ref: AMD SP (AMD PSP), Intel ME), эмулирует какие-нибудь устаревшие вещи, обрабатывает ошибки чипсета или памяти.

Режим работает независимо от любых других режимов, и, по сути, является прозрачным для всех остальных (а также наиболее привилегированным). Часто можно увидеть упоминание «Ring -2» (в топологии колец защит), что и есть SMM.

image.png

Код режима SMM защищён от модификаций извне (любой другой режим, кроме RoT (Root-of-Trust: Intel ME, AMD SP (AMD PSP))). Очевидно, что SMM-код должен как-то защищаться, ну и соответственно сперва он должен быть инициализирован.

Инициализация режима и где обитает код для SMM
Очевидно, что SMM код должен инициализироваться где-то в начале работы платформы, когда уже инициализированы и сконфигурированы чипсет и память, была пройдена первичная валидация образов драйверов. Собственно, так оно и есть: платформа инициализирует SMM код ещё на стадии загрузки. DXE Dispatcher получает HOB (Hand-off Block) с SMM драйверами, где в первую очередь находится SMM IPL (SMM Initial Program Loader), который загружает остальные SMM драйвера. Этот же код будет доступен на протяжении всей работы платформы, до подачи сигнала на определённый пин (CPURST#) чипсета. Попроще изображено на иллюстрации ниже:

image.png

Отлично, теперь нам стало понятнее, когда загружается код режима SMM. Но тут есть как минимум ещё один вопрос: куда он загружается?

Вообще, ещё на этапе ранней инициализации, UEFI конфигурирует два MSR: SMM_ADDR и SMM_MASK для AMD, а для IntelSMMR_PHYS_BASE и SMMR_PHYS_MASK. Комбинация двух регистров устанавливает определённый сегмент DRAM, называемый TSEG, в который входит SMRAM – память под код SMM. Этот же сегмент памяти будет защищён от модификаций извне, а также «невидим» для других режимов. Сам сегмент может быть весьма большим, в зависимости от настроек платформы, например, 16 мегабайт и даже больше. Защита сегмента также настраивается отдельными конфигурационными битами. Например, у AMD это SMMLock в HWCR.

image.png


Что находится внутри SMRAM?
SMRAM имеет свою определённую «планировку» внутри, в зависимости от количества ядер процессора, мы можем увидеть следующее (на примере AMD):

image.png

Теперь, подробнее разберём, что вообще здесь творится:
  • SMM Base – это базовый адрес SMM для конкретного ядра процессора, на его основе считаются следующие две вещи;
  • SMM Entrypoint – входная точка для SMM, устанавливается для каждого ядра, на них же переходит управление при срабатывании SMI прерываний. Это весьма забавная цепочка из переходов: Real Mode -> Protected Mode -> Long Mode, а только потом начнёт выполняться код SMI обработчика;
  • SMM Save State – сохранённое состояние всех регистров процессора перед входом в SMM;
  • SMM Core – «пограничный» драйвер, обрабатывает прерывания (определённые), регистрирует и предоставляет таблицу сервисов SMM. Об этом драйвере речь зайдёт позже;
  • SMI Handlers – обработчики SMI прерываний, о прерываниях мы поговорим в следующей главе.
Стоит также сказать, что все SMI обработчики регистрируются SMM драйверами, есть несколько видов обработчиков, но о них поговорим далее.

SMM драйвера также работают изолировано в своём адресном пространстве, но, технически, так как SMRAM может достигать размера 4 гигабайт, то и сами драйвера могут тоже обращаться к адресам до 4 гигабайт, но, вообще, нынче вводятся некоторые изменения в эти особенности. Однако, дальше это может нам понадобиться.

Вход в SMM и выход из него

Мы уже коснулись этой темы, но теперь пройдёмся по ней глубже. Для входа в режим требуется срабатывание SMI (System Management Interrupt) прерывания. Есть несколько способов триггера прерывания, мы разберём все из них. Первым будут прерывания от чипсета или периферии на материнской плате. Это чисто аппаратные прерывания, которые триггерят отдельный пин на самом чипсете. Зачастую он назван весьма прямолинейно:

image.png

Такие прерывания можно триггерить через периферию, например, от xHCI. В принципе, варианты есть, надо только найти их.

Вторым типом прерываний будут те, для которых по адресу записи ввода/вывода на который платформой установлена необходимость триггера SMI (иногда можно услышать название IO Trap).

Третьим же будет прерывания, которые срабатывают при записи в определённый порт ввода/вывода, зачастую это 0xB2, но на всякий случай можно проверить поле SmiCmd в Fixed ACPI Description Table таблице ACPI.

Стоит также помнить, что SMI являются одними из самых привилегированных прерываний, что их нельзя «проигнорировать» (cli), и что процессор обработает их в первую очередь (например, если одновременно поступили SMI и NMI прерывания – обработается сначала первое). Выход из режима происходит только по достижению инструкции RSM (Resume From System Management Mode). Инструкция возвращает поток управления в процедуру, которая была прервана SMI, предварительно вернув состояние процессора до триггера SMI.

Разбираемся на примере
Допустим, пусть Core#0 выполняет какой-нибудь код. На пути выполнения пусть будет инструкция out 0xB2, 0x41:

image.png

Well, запись в порт 0xB2 сигнализирует чипсету, что поток данных и инструкций переходит в SMM, а значит, время проверить, какие адреса зарезервированы TSEG. Во время перехода, конечно же, сохраняется состояние регистров до триггера SMISMM Save State). Таким образом, поток данных и инструкций переходят в SMM:

image.png

По выполнению работы обработчика прерывания, ядро процессора восстановит своё состояние, прочитав из SMM Save State, поток управления достигнет инструкции RSM, и, ядро продолжит своё выполнение после инструкции out.

Тут может встать вопрос: «А что, если обращение к зарезервированным адресам TSEG произойдёт без SMI?». Ну, вообще на этот вопрос есть весьма простой ответ. Например, у AMD, если приглядеться на одну из картинок выше, в SMM_MASK MSR есть биты TE и AE (ASEG нам не особо интересен, поэтому, я и не упоминаю его). Если эти биты валидны (равны 1), и обращение происходит не в контексте прерывания, то, ядро процессора просто обратится к MMIO:

AvLitri.png

А значит, мы увидим следующую картину:

dJdFrPv.png

Это был пример для AMD, но и для Intel это вроде бы тоже справедливо (если я правильно помню), думаю, что пример равнозначен для двух производителей процессоров.

Надо сказать, что это только про софтварные прерывания (SW SMI), есть немного более сложный алгоритм срабатывания для ACPI SMI (мы их тоже затронем), например. Но, в конечном итоге всё действительно сводится к записи в порт, триггера SMI# пина на чипсете или запись в специальные адреса ввода/вывода. Мы немного познакомились с SMM и его работой, а значит, время для демагогий.

SMM как вектор атаки на игрушки
Чисто технически, SMM это весьма интересный вектор атаки с точки зрения защитных решений – режим, прозрачно работающий для ОС, который никто не может нормально проверить на работу каких-либо приколов в нём (на самом деле может, но об этом позже).

Те же античиты, например, если и интересуются режимом SMM (а в последнее время они реально интересуются, по крайней мере, их разработчики), то делают это «слабо» на текущий момент. Например, можно в тупую просто мониторить счётчик прерываний (что автоматически подразумевает большое количество ложных срабатываний) или же пытаться вычислить «специфичные» участки памяти, используемые, как коммуникация между режимами. Их вычисление тоже не самая простая задача, да и также может генерировать огромное количество ложных срабатываний. На текущий момент остаётся только глядеть маппнутый образ SPI Flash в физической памяти, сопоставлять каждый GUID драйвера (если вы незнакомы с устройством прошивок, то каждый драйвер имеет свой GUID), в надежде, что появится какой-то неизвестный идентификатор. Это, может и действенно, но кто будет держать весь этот список говна? Да и что делать, если вендоры введут какой-то новый драйвер? Собственно, на текущий момент с детектом SMM драйверов всё немного сложно. Но это не значит, что защитные решения стоят на месте.

Единственная проблема, которая может быть – вендор. Intel и AMD уже давно ввели и вводят некоторые штуки, предназначенные для защиты прошивки и целостности платформы от сторонних драйверов. О них речь пойдёт ближе к концу статьи.

Пишем простой SMM драйвер
Наверное, стоит написать что-нибудь простенькое и забавное. Напишем драйвер, регистрирующий SMI обработчик. Пока это будет софтварный (SW SMI) обработчик, но позже мы познакомимся с ещё одним.

В качестве библиотеки я использую реализацию
Пожалуйста, авторизуйтесь для просмотра ссылки.
(есть ещё GNU-EFI, но это объективно кал). Так как я ещё и особенный, в качестве среды разработки у меня Visual Studio, я использую базу в виде
Пожалуйста, авторизуйтесь для просмотра ссылки.
(правда, модифицированную под свои нужды, но вам будет достаточно собрать ещё пару библиотек при желании). Собирать мы свой драйвер будем через тулчейн EDKII, так что, я дополнительно покажу ещё и настройку конфигурационных файлов.

Хорошо, что должен сделать наш драйвер? В первую очередь, он должен проверить, находится ли он в SMM (параноидально, но так принято, например, для комбинированных DXE-SMM драйверов), затем, получить таблицу SMM сервисов, а потом зарегистрировать SMI обработчик. Собственно, на картинке ниже это и представлено:

03OJgnx.png

В спецификации UEFI есть
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_BASE2_PROTOCOL, который, предоставляет сервис для проверки, запущены ли мы в SMM, а также предоставляет сервис для получения
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_SYSTEM_TABLE2 (извините за тавтологию). Получить указатель на протокол EFI_SMM_BASE2_PROTOCOL можно через
Пожалуйста, авторизуйтесь для просмотра ссылки.
LocateProtocol из
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_BOOT_SERVICES. Отлично! Теперь, напишем код:

SMM Driver EP:
Expand Collapse Copy
EFI_SMM_SYSTEM_TABLE2 *Smst = NULL;


EFI_STATUS
EFIAPI
SmmMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    EFI_SMM_BASE2_PROTOCOL *SmmBase2 = NULL;
    EFI_STATUS Status = SystemTable->BootServices->LocateProtocol(&gEfiSmmBase2ProtocolGuid, NULL, (VOID **)&SmmBase2);
    if(EFI_ERROR(Status))
        return Status;

    BOOLEAN InMmRam = FALSE;
    SmmBase2->InSmm(SmmBase2, &InMmRam);
    if(!EFI_ERROR(Status) && InMmRam) {
        Status = SmmBase2->GetSmstLocation(SmmBase2, Smst);
        if(EFI_ERROR(Status))
            return Status;
    } else {
        return EFI_DEVICE_ERROR;
    }

    return EFI_SUCCESS;
}

Окей, мы получили таблицу сервисов SMM, теперь же, мы зарегистрируем обработчик SW SMI. Делается это через
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_SW_DISPATCH2_PROTOCOL. Обработчику будет нужен собственный номер, пусть это будет 0x99. Для регистрации обработчика нужно передать адрес используемой функции в качестве этого обработчика:

Register SW SMI Handler:
Expand Collapse Copy
EFI_STATUS
EFIAPI
SwSmiHandler(
    IN EFI_HANDLE  DispatchHandle,
    IN CONST VOID *Context         OPTIONAL,
    IN OUT VOID   *CommBuffer      OPTIONAL,
    IN OUT UINTN  *CommBufferSize  OPTIONAL
) {

    return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
RegisterSwSmi(
    VOID
) {
    EFI_SMM_SW_DISPATCH2_PROTOCOL *SwDispatch2 = NULL;
    EFI_STATUS Status = gSmst->SmmLocateProtocol(&gEfiSmmSwDispatch2ProtocolGuid, NULL, (VOID **)&SwDispatch2);
    if(EFI_ERROR(Status))
        return Status;

    EFI_SMM_SW_REGISTER_CONTEXT SwSmiRegCtx = { 0 };
    SwSmiRegCtx.SwSmiInputValue = 0x99;

    EFI_HANDLE Handler = NULL;
    return SwDispatch2->Register(SwDispatch2, SwSmiHandler, &SwSmiRegCtx, &Handler);
}

Отлично! У нас есть зарегистрированный обработчик, который будет доступен по записи в порт 0xB2 значения 0x99. Теперь, можно что-нибудь придумать для своего обработчика. Моей жертвой станет ещё один SMM драйвер от AMD – AmdPspP2CmboxV2. Это драйвер для коммуникации между внешним миром и сопроцессором AMD PSP, расположенным на чипсете, вот тут видно:

ivzZozB.png

У меня есть дамп с SPI чипа своего старого ноутбука, мы ещё вернёмся к аппаратной составляющей, так что пока просто держим это в голове.

j9QteRL.png

Драйвер интересен тем, что регистрирует протокол с адресом буфера для коммуникации между PSP и внешним миром. Коммуницировать с PSP можно, конечно, по-разному, но AMD сделало это более удобным способом. Протокол существует исключительно в SMM, поэтому, почему бы не получить его адрес?

sQ0g7Tw.png

Обработчик должен получить адрес протокола PSP_MBOX_SMM_BUFFER_ADDRESS_PROTOCOL и отдать его нам обратно. Как обработчик его вернёт? Ну, для этого мы будем использовать SMM Save State, в который ядро процессора сохраняет своё состояние. Это делается через ещё один
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_CPU_SAVE_STATE_PROTOCOL. Кешируем последний, а первый получим прям в обработчике. В качестве регистра, в который мы положим результат, будет использоваться r15. Нам также нужно будет извлечь GUID нужного нам протокола.

Кстати, SW SMI обработчикам важно, какое ядро находится в SMM. Обработчики имеют одинаковый набор аргументов, но в случае с SW SMI всё немного интереснее. Если вы взгляните на код, то увидите аргумент CommBuffer. В случае триггера SW SMI, туда будет передана
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_SW_CONTEXT, в которой будет индекс ядра процессора, перешедшего в SMM. Это важно помнить.

Modified Handler:
Expand Collapse Copy
 typedef struct _PSP_MBOX_SMM_BUFFER_ADDRESS_PROTOCOL {
    EFI_PHYSICAL_ADDRESS PspMboxSmmBuffer;
    UINT64               PspMboxSmmFlagAddr;
} PSP_MBOX_SMM_BUFFER_ADDRESS_PROTOCOL;


EFI_STATUS
EFIAPI
SwSmiHandler(
    IN EFI_HANDLE  DispatchHandle,
    IN CONST VOID *Context         OPTIONAL,
    IN OUT VOID   *CommBuffer      OPTIONAL,
    IN OUT UINTN  *CommBufferSize  OPTIONAL
) {
    EFI_SMM_SW_CONTEXT *SwCtx = (EFI_SMM_SW_CONTEXT *)CommBuffer;
    UINTN CpuIdx = SwCtx->SwSmiCpuIndex;

    EFI_GUID PspCmboxProtocolGuid = { 0x579CB2CB, 0x3403, 0x4B26, { 0x84, 0xCD, 0x72, 0x89, 0xFC, 0x91, 0x4D, 0x35 } };
    PSP_MBOX_SMM_BUFFER_ADDRESS_PROTOCOL *PspMbox = NULL;
    EFI_STATUS Status = gSmst->SmmLocateProtocol(&PspCmboxProtocolGuid, NULL, (VOID **)&PspMbox);
    if(EFI_ERROR(Status))
        return Status;

    // ignore last status, no need to execute another handlers
    gSmmCpu->WriteSaveState(gSmmCpu, sizeof(EFI_PHYSICAL_ADDRESS), EFI_MM_SAVE_STATE_REGISTER_R15, CpuIdx, (VOID **)&PspMbox);

    return EFI_SUCCESS;
}

Ну, мы написали небольшой SMM драйвер. Теперь нам надо с ним как-то общаться, правда ведь? Напишем теперь DXE драйвер, который триггернёт прерывание и выплюнет нам адрес протокола.

Пишем DXE драйвер

Да собственно тут нечего особо писать. Нам пригодится немножечко ассемблер и прямые руки, но для начала, точка входа:

DXE Driver EP:
Expand Collapse Copy
#include <Uefi.h>
#include <Base.h>

#include <Library/BaseLib.h>
#include <Library/UefiLib.h>


EFI_STATUS
EFIAPI
DxeMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    Print(L"[ + ] Triggering 0xB2 port\n");

    return EFI_SUCCESS;
}

Теперь, надо триггернуть порт 0xB2 с номером нашего обработчика. Я особенный, поэтому напишем небольшой код на MASM:

SW SMI Port Write:
Expand Collapse Copy
PUBLIC _swsmi_trigger

.code

;
; Prototype: VOID _swsmi_trigger(EFI_PHYSICAL_ADDRESS *Output)
;
_swsmi_trigger PROC PUBLIC
    push rcx
    xor rax, rax
    mov rax, 099h
    out 0B2h, ax
    pop rcx
    mov qword ptr[rcx], r15
    ret
_swsmi_trigger ENDP

END

Объявим прототип в отдельном заголовке, модифицируем входную точку DXE драйвера:

Modified DXE EP:
Expand Collapse Copy
EFI_STATUS
EFIAPI
DxeMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    Print(L"[ + ] Triggering 0xB2 port\n");

    EFI_PHYSICAL_ADDRESS PspMboxBuf = 0;
    _swsmi_trigger(&PspMboxBuf);

    Print(L"[ + ] PSP MBOX Communication buffer: 0x%llX\n", PspMboxBuf);

    return EFI_SUCCESS;
}

Теперь мы можем общаться с SMM драйвером. Переходя к этапу сборки, нам нужно будет написать пару конфигов, но можно воспользоваться существующими в EDKII, нащупать самостоятельно что нужно. Останется только отредактируовать конфиг в Conf/target.txt. В ACTIVE_PLATFORM прописать путь до исходника драйверов, в TARGET прописать условный RELEASE, а в TARGET_ARCH прописать X64. После этого можно билдить.

P.S: Вероятнее всего, ещё придётся пошаманить с Conf/tools_def.txt, в частности, вырубить параметр «предупреждения как ошибки» и немного побаловаться с MASM командами.

Модификация содержимого SPI чипа
Далее мы проведём тесты непосредственно на хардвари, так как драйвер весьма простой и ничего нам не сломает. Но я всё равно не несу ответственности за сохранность вашего оборудования. Мы позже поговорим об отладке SMM драйверов и почему это весьма больная тема. А пока, я выбрал своего подопытного – это мой старенький ноутбук, года эдак 2019, если не раньше. В первую очередь нам нужно нащупать SPI чип. На текущий момент на платах все флешки 8-пиновые и бывают двух форм-факторов: SOIC8/SOP8 и WSON8 (различия можно нагуглить). Зная, как они выглядят, можно начинать брут-форсить плату в поиске чипа. Желательно ещё обращать на маркировки чипов, чаще всего это будет какой-нибудь Winbond, Macronix или GigaDevice. С освещением у меня всё плохо, извиняйте:

QmCLJxI.png

Это чип от GigaDevice формата SOIC8/SOP8. В качестве программатора у меня xGecu T48, но можно использовать любой другой. В качестве адаптера для чипа я буду «прищепку», она как раз подходит для зажима пинов SOIC8/SOP8:

NMDVEq4.png

Здесь нужно не ошибиться с расположением пинов, да и с самим вендором чипа и его вольтажом. Благо, софтина от xGecu умеет в самостоятельное определение по айдишнику. У меня уже есть дамп образа прошивки с чипа, вам же сначала нужно будет его прочитать и сохранить куда-нибудь. Теперь переходим в
Пожалуйста, авторизуйтесь для просмотра ссылки.
(лучше всего версии 0.28.0). Он нам нужен на старом движке, так как в новом нет поддержки модификации образов.

После скачивания тулзы, можно приступить к поиску жертвы на замену. В моём чипе лежал дебажный SMM драйвер для SMBus, поэтому я могу смело заменить его целиком. Вам же, возможно, потребуется дальнейший поиск или модификация существующих SMM драйверов. Всё что нам остаётся сделать – заменить драйвер на наш:

LIWZb7M.png

После замены сохраним бинарь с каким-нибудь другим именем, а дальше прошьём наш чип:

aq7ifQd.png

Нам останется только найти любую USB флешку, форматнуть в FAT32, закинуть бинарь с EFI Shell (можно собрать или найти самому), закинуть наш DXE драйвер и идти смотреть, что вообще происходит. Как и ожидалось – SMM драйвер выплюнул нам адрес протокола (извиняюсь за классные фотки):

b4SgTrY.png

Можно убедиться, что адрес действительно лежит в SMRAM, для этого нам достаточно проверить SMM_ADDR и SMM_MASK MSR, из SMM_MASK мы вычислим размер SMRAM:

TUQNwOh.png

Круто, у нас есть свой первый рабочий драйвер! Но ведь это можно развить во что-то большее, ведь так? Да и как отлаживать всё это дело? Об этом мы и поговорим сейчас.

Отладка SMM драйверов

Несмотря на предоставление EDKII эмулятора (
Пожалуйста, авторизуйтесь для просмотра ссылки.
) – он всё же существует для отладки DXE драйверов, в то время как SMM драйвера требуют другого подхода.

В качестве системы для отладки можно использовать, например, QEMU. EDKII предоставляет
Пожалуйста, авторизуйтесь для просмотра ссылки.
– пакет с драйверами прошивки для QEMU. В случае выбора этого варианта, нужно будет просто закинуть наши драйвера в OvmfPkg, подредактировать конфиги и собрать пакет, затем явно указать QEMU, что он должен грузиться именно с него. Есть множество плюсов такого подхода, например:
  • Не нужно покупать дополнительное оборудование;
  • Можно отправлять отладочные сообщения в Serial I/O порты;
  • QEMU предоставляет GDB Stub;
  • С пинка можно добиться отладки на уровне сурсов.
По моему опыту, как раз таки QEMU это весьма удобная вещь. По специфике работы, я постоянно пользуюсь WinDbg. В связке GDB Stub + EXDI + WinDbg можно добиться отладки с дебаг-символами, что, собственно, весьма круто и удобно. Я не буду здесь рассказывать о том, как это правильно запустить, оставлю это на совесть читающего.

Вендоры используют такую штуку, как JTAG, аппаратный интерфейс для отладки. Иногда также используются специальные отладочные платы (по рассказам некоторых знакомых – такое бывает). Угарнее становится тогда, когда ты можешь нащупать JTAG-пины на «продуктовых» платах, но чаще всего это обрубки от некогда существовавшего интерфейса:

9IdGJv1.png

У Intel есть такая штука, как DCI, по сути, модифицировав прошивку (DCI нужно включать самостоятельно), и сделав самопальный DCI шнур из USB – уже можно пробовать что-то делать. По крайней мере, когда-то можно было, сейчас не знаю. Ещё Intel предоставлял вот такие прекрасные вещи:

s5imqUl.png

Yep, эта беспонтовая херня будет вам стоить дохера, а ещё вам придётся подписывать NDA.

Собственно, самым простым вариантом для самопальных исследований будет QEMU (я опущу процессорный БДСМ с замерами таймингами старта чипсета для определения возможных отладочных интерфейсов).

Развиваем концепцию во что-то большее
Коммуницировать посредством SMM Save State и аутично записывать в порт номер своего обработчика безусловно хорошо, но что, если мы хотим передавать относительно большие массивы данных? Окей, для начала познакомимся с ещё несколькими типами SMI:
  • Root SMI – это «корневые» прерывания, которые срабатывают при каждом SMI. То есть, если произошёл триггер по записи в порт – Root SMI тоже сработает;
  • ACPI SMI – из названия не сложно догадаться, для чего они сделаны. Они имеют весьма удобный метод для коммуникации с внешним миром;
  • Periodic SMI – это прерывания, срабатывающие по определённому тайм-ауту, заданным APIC. Они требуют большего контроля, так как при загрузке системы APIC перенастраивается.
Более подробно мы остановимся на первых двух, начнём, пожалуй, с ACPI SMI.

ACPI SMI как удобный метод коммуникации
Как и любые SMI обработчики – этот также регистрируется в SMM, только для него нужно указывать собственный GUID при регистрации. Собственно, вернёмся к нашему SMM драйверу и перепишем его:

SMM Driver with ACPI SMI Handler:
Expand Collapse Copy
EFI_STATUS
EFIAPI
AcpiSmiHandler(
    IN EFI_HANDLE  DispatchHandle,
    IN CONST VOID *Context         OPTIONAL,
    IN OUT VOID   *CommBuffer      OPTIONAL,
    IN OUT UINTN  *CommBufferSize  OPTIONAL
) {

    return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
RegisterAcpiSmi(
    VOID
) {
    EFI_HANDLE Handler = NULL;
    EFI_GUID AcpiSmiHandlerGuid = { 0xA3715FCB, 0x05F9, 0x4FD6, { 0xA3, 0x5B, 0x31, 0x2F, 0x29, 0xD5, 0x08, 0xA3 } };
    return gSmst->SmiHandlerRegister(AcpiSmiHandler, &AcpiSmiHandlerGuid, &Handler);
}

EFI_STATUS
EFIAPI
SmmMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    EFI_SMM_BASE2_PROTOCOL *SmmBase2 = NULL;
    EFI_STATUS Status = SystemTable->BootServices->LocateProtocol(&gEfiSmmBase2ProtocolGuid, NULL, (VOID **)&SmmBase2);
    if(EFI_ERROR(Status))
        return Status;

    BOOLEAN InMmRam = FALSE;
    SmmBase2->InSmm(SmmBase2, &InMmRam);
    if(!EFI_ERROR(Status) && InMmRam) {
        Status = SmmBase2->GetSmstLocation(SmmBase2, gSmst);
        if(EFI_ERROR(Status))
            return Status;

        // register ACPI SMI handler
        Status = RegisterAcpiSmi();
        if(EFI_ERROR(Status))
            return Status;
    } else {
        return EFI_DEVICE_ERROR;
    }

    return EFI_SUCCESS;
}

Для его регистрации отдельный протокол не используется, используется только
Пожалуйста, авторизуйтесь для просмотра ссылки.
SmiHandlerRegister из таблицы сервисов SMM.

Как же нам коммуницировать с этим обработчиком? Теперь вернёмся к DXE драйверу. Спецификация предоставляет три версии протокола для коммуникации, мы воспользуемся
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_COMMUNICATION_PROTOCOL. Также, нам нужно будет выделить пул памяти, так как именно в нём будут «конвоироваться» данные из внешнего мира в SMM. Тут возникнет вопрос: «А как ACPI SMI обработчик узнает, что обращаются именно к нему, а не к другому?». Спецификация UEFI предоставляет решение в виде
Пожалуйста, авторизуйтесь для просмотра ссылки.
это структуры с GUID обработчика, которые помещаются в начало буфера для коммуникации. Это будет проще увидеть на коде:

DXE Driver:
Expand Collapse Copy
EFI_STATUS
EFIAPI
DxeMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    EFI_SMM_COMMUNICATION_PROTOCOL *SmmCommunication = NULL;
    EFI_STATUS Status = SystemTable->BootServices->LocateProtocol(&gEfiSmmCommunicationProtocolGuid, NULL, (VOID **)&SmmCommunication);
    if(EFI_ERROR(Status)) {
        Print(L"[ ! ] LocateProtocol returned 0x%llX (%r)\n", Status, Status);
        return Status;
    }

    // allocate communication buffer
    UINTN Size = 0x40;
    VOID *CommPool = NULL;
    Status = SystemTable->BootServices->AllocatePool(EfiRuntimeServicesData, Size, &CommPool);
    if(EFI_ERROR(Status)) {
        Print(L"[ ! ] LocateProtocol returned 0x%llX (%r)\n", Status, Status);
        return Status;
    }

    // craft header
    EFI_SMM_COMMUNICATE_HEADER *Hdr = (EFI_SMM_COMMUNICATE_HEADER *)CommPool;

    EFI_GUID AcpiSmiHandlerGuid = { 0xA3715FCB, 0x05F9, 0x4FD6, { 0xA3, 0x5B, 0x31, 0x2F, 0x29, 0xD5, 0x08, 0xA3 } };
    CopyGuid(&Hdr->HeaderGuid, &AcpiSmiHandlerGuid);
    Hdr->MessageLength = Size;

    // place here your data
    // Hdr->Data = [ your data here ]

    // fire SMI
    Status = SmmCommunication->Communicate(SmmCommunication, CommPool, &Size);

    // process output

    return Status;
}

Таким образом мы можем свободно коммуницировать с SMM посредством ACPI SMI обработчика! Теперь разберёмся с тем, как это работает. Помните SMM Core, который присутствует на карте SMRAM выше? Так вооооооот…

Протокол для коммуникации на самом деле отправит нас
Пожалуйста, авторизуйтесь для просмотра ссылки.
. В этом драйвере, по заголовку, будет определён тип обработчика – если GUID нет в заголовке, то драйвер делать с этим
Пожалуйста, авторизуйтесь для просмотра ссылки.
, а просто отправит его дальше. Если же в заголовке буфера GUID есть, то, SMM Core
Пожалуйста, авторизуйтесь для просмотра ссылки.
какой обработчик был зарегистрирован с переданным ему GUID. Если такого нет – он вернёт определённый статус, а если есть –
Пожалуйста, авторизуйтесь для просмотра ссылки.
в порт 0xB2 ноль (любая запись в этот порт триггерит SMI), а там платформа уже
Пожалуйста, авторизуйтесь для просмотра ссылки.
, что к чему. То есть, мы видим следующую картину:

RPtJw4z.png

Наверное, это всё, что касается ACPI SMI..

Работа с памятью в SMM

SMM «в оригинале» работает в реальном режиме, а значит, для адресации ему доступны только первые 4 гигабайта физической памяти. Это большая проблема для x64 систем, в особенности для адресов расположенных выше 4 гигабайт. У нас есть два решения:
  • Самостоятельно создавать собственную страничную иерархию выходить в длинный режим. Платформа умеет это делать, но только в случаях выхода из режима S3 и если прилетает капсульный апдейт;
  • Модифицировать страницы SMRAM и ремаппать каждую «внешнюю» таблицу памяти к себе в SMRAM.
На мой взгляд, из двух вариантов я бы выбрал последний, так как мы можем узнать CR3 ядра процессора, а также уже имеем, допустим, виртуальный адрес, который собираемся «переместить» к себе, нам также понадобится какая-нибудь свободная страница памяти в SMRAM, но её можно спокойно выделить самому. На реализацию можно посмотреть вот
Пожалуйста, авторизуйтесь для просмотра ссылки.
.

SMM и вендоры
Не самый нужный пункт статьи, но стоит понимать, что вендоры всё же заботятся о целостности прошивки, в особенности об SMM. Например, у Intel и AMD есть «загрузочные» решения - Intel Boot Guard и AMD Platform Secure Boot. Они срабатывают ещё до передачи управления SEC Core (ещё одна стадия, которая в разговоре не упоминалась), то есть, код для проверки подлинности находится уже в микрокоде процессора. На примере Intel будет схожая цепочка:

0OnCkPX.png

Не вдаваясь глубоко в суть вопроса (это действительно отдельная большая тема) – первый сперва находит в SPI чипе отдельный модуль, который верифицирует стартовую часть прошивки, где находится основной модуль, который проверифицирует основную часть прошивки. Вот такая цепь доверенной загрузки.

AMD PSB работает схожим образом, но немного интереснее. Если IBG в случае «правильной» конфигурации, записанной в FPF, просто не загрузит платформу, то PSB, судя по всему, не загрузит модифицированный (или заменённый) драйвер (судя по моим тестам).

Также, уже на новых чипсетах Intel есть SMM Isolation, который ломает приколы с адресацией в SMM, вот
Пожалуйста, авторизуйтесь для просмотра ссылки.
хорошо написано.

Послесловие

Ну, вот и краткое описание того, что представляет из себя SMM, как под него писать, куда можно ориентироваться при разработке смешных штук. Возможно, это кому-то пригодится или хотя бы заинтересует.
В идеале, если нацеливаться на игоры – смотреть на SMI# пин чипсета, как он триггерится и при каких событиях, я упоминал тот же xHCI, с чего, в принципе, можно и начать. Меньше оставляешь следов – живёшь дольше.


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

- Спецификации
Пожалуйста, авторизуйтесь для просмотра ссылки.
и
Пожалуйста, авторизуйтесь для просмотра ссылки.

- Спецификации
Пожалуйста, авторизуйтесь для просмотра ссылки.
и
Пожалуйста, авторизуйтесь для просмотра ссылки.
 
Последнее редактирование:
вот откуда ты так знаешь ассемблер каааак
и защита там вовсе не говно, кастомые регистры, вехи, cpuid, crc, обфа импортов, то что ты так легко это крякаешь это невероятно...тут два варианта либо ты как культ супер богатый москвич и заказываешь код, либо вам с вильямом эти знания даны свыше...
 
В последнее время (эдак год-полтора) на чит-сцене появилось несколько публичных решений для SMM (System Management Mode). Эти решения как «боевые», так и чисто исследовательские. В основном тема эксплуатации SMM с точки зрения читов обсуждается на англоязычных форумах, но на СНГ сцене как-то по этому поводу тихо.

Я решил, что будет неплохой идеей немного осветить тему, как вообще вся эта шняга работает и как её можно использовать с точки зрения читов, ну или собственных исследований по безопасности платформы. Я в принципе занимаюсь этой темой весьма долго и мне есть что рассказать. Я по большей части буду делать отсылки к AMD, но при необходимости постараюсь впихнуть и Intel. С последним я работал намного меньше, чем с первым, отсюда и больший уклон в красных. Да и примерчики тоже на красных будут.

Также, стоит сказать; Я подразумеваю, что вы уже знакомы с устройством как минимум UEFI, работы процессора и некоторых других вещей. Совсем базовые концепции я детально разбирать не буду.

SMM как режим работы процессора
Для начала стоит просто понять, что из себя подразумевает SMM – это привилегированный режим работы процессора на базе архитектуры x86_64, управляющий аппаратным обеспечением ПК. Более детально, режим работает, например, с управлением энергопотребления платформы, ну или исполняет управляющий OEM-код (ref: AMD SP (AMD PSP), Intel ME), эмулирует какие-нибудь устаревшие вещи, обрабатывает ошибки чипсета или памяти.

Режим работает независимо от любых других режимов, и, по сути, является прозрачным для всех остальных (а также наиболее привилегированным). Часто можно увидеть упоминание «Ring -2» (в топологии колец защит), что и есть SMM.

image.png

Код режима SMM защищён от модификаций извне (любой другой режим, кроме RoT (Root-of-Trust: Intel ME, AMD SP (AMD PSP))). Очевидно, что SMM-код должен как-то защищаться, ну и соответственно сперва он должен быть инициализирован.

Инициализация режима и где обитает код для SMM
Очевидно, что SMM код должен инициализироваться где-то в начале работы платформы, когда уже инициализированы и сконфигурированы чипсет и память, была пройдена первичная валидация образов драйверов. Собственно, так оно и есть: платформа инициализирует SMM код ещё на стадии загрузки. DXE Dispatcher получает HOB (Hand-off Block) с SMM драйверами, где в первую очередь находится SMM IPL (SMM Initial Program Loader), который загружает остальные SMM драйвера. Этот же код будет доступен на протяжении всей работы платформы, до подачи сигнала на определённый пин (CPURST#) чипсета. Попроще изображено на иллюстрации ниже:

image.png

Отлично, теперь нам стало понятнее, когда загружается код режима SMM. Но тут есть как минимум ещё один вопрос: куда он загружается?

Вообще, ещё на этапе ранней инициализации, UEFI конфигурирует два MSR: SMM_ADDR и SMM_MASK для AMD, а для IntelSMMR_PHYS_BASE и SMMR_PHYS_MASK. Комбинация двух регистров устанавливает определённый сегмент DRAM, называемый TSEG, в который входит SMRAM – память под код SMM. Этот же сегмент памяти будет защищён от модификаций извне, а также «невидим» для других режимов. Сам сегмент может быть весьма большим, в зависимости от настроек платформы, например, 16 мегабайт и даже больше. Защита сегмента также настраивается отдельными конфигурационными битами. Например, у AMD это SMMLock в HWCR.

image.png


Что находится внутри SMRAM?
SMRAM имеет свою определённую «планировку» внутри, в зависимости от количества ядер процессора, мы можем увидеть следующее (на примере AMD):

image.png

Теперь, подробнее разберём, что вообще здесь творится:
  • SMM Base – это базовый адрес SMM для конкретного ядра процессора, на его основе считаются следующие две вещи;
  • SMM Entrypoint – входная точка для SMM, устанавливается для каждого ядра, на них же переходит управление при срабатывании SMI прерываний. Это весьма забавная цепочка из переходов: Real Mode -> Protected Mode -> Long Mode, а только потом начнёт выполняться код SMI обработчика;
  • SMM Save State – сохранённое состояние всех регистров процессора перед входом в SMM;
  • SMM Core – «пограничный» драйвер, обрабатывает прерывания (определённые), регистрирует и предоставляет таблицу сервисов SMM. Об этом драйвере речь зайдёт позже;
  • SMI Handlers – обработчики SMI прерываний, о прерываниях мы поговорим в следующей главе.
Стоит также сказать, что все SMI обработчики регистрируются SMM драйверами, есть несколько видов обработчиков, но о них поговорим далее.

SMM драйвера также работают изолировано в своём адресном пространстве, но, технически, так как SMRAM может достигать размера 4 гигабайт, то и сами драйвера могут тоже обращаться к адресам до 4 гигабайт, но, вообще, нынче вводятся некоторые изменения в эти особенности. Однако, дальше это может нам понадобиться.

Вход в SMM и выход из него

Мы уже коснулись этой темы, но теперь пройдёмся по ней глубже. Для входа в режим требуется срабатывание SMI (System Management Interrupt) прерывания. Есть несколько способов триггера прерывания, мы разберём все из них. Первым будут прерывания от чипсета или периферии на материнской плате. Это чисто аппаратные прерывания, которые триггерят отдельный пин на самом чипсете. Зачастую он назван весьма прямолинейно:

image.png

Такие прерывания можно триггерить через периферию, например, от xHCI. В принципе, варианты есть, надо только найти их.

Вторым типом прерываний будут те, для которых по адресу записи ввода/вывода на который платформой установлена необходимость триггера SMI (иногда можно услышать название IO Trap).

Третьим же будет прерывания, которые срабатывают при записи в определённый порт ввода/вывода, зачастую это 0xB2, но на всякий случай можно проверить поле SmiCmd в Fixed ACPI Description Table таблице ACPI.

Стоит также помнить, что SMI являются одними из самых привилегированных прерываний, что их нельзя «проигнорировать» (cli), и что процессор обработает их в первую очередь (например, если одновременно поступили SMI и NMI прерывания – обработается сначала первое). Выход из режима происходит только по достижению инструкции RSM (Resume From System Management Mode). Инструкция возвращает поток управления в процедуру, которая была прервана SMI, предварительно вернув состояние процессора до триггера SMI.

Разбираемся на примере
Допустим, пусть Core#0 выполняет какой-нибудь код. На пути выполнения пусть будет инструкция out 0xB2, 0x41:

image.png

Well, запись в порт 0xB2 сигнализирует чипсету, что поток данных и инструкций переходит в SMM, а значит, время проверить, какие адреса зарезервированы TSEG. Во время перехода, конечно же, сохраняется состояние регистров до триггера SMISMM Save State). Таким образом, поток данных и инструкций переходят в SMM:

image.png

По выполнению работы обработчика прерывания, ядро процессора восстановит своё состояние, прочитав из SMM Save State, поток управления достигнет инструкции RSM, и, ядро продолжит своё выполнение после инструкции out.

Тут может встать вопрос: «А что, если обращение к зарезервированным адресам TSEG произойдёт без SMI?». Ну, вообще на этот вопрос есть весьма простой ответ. Например, у AMD, если приглядеться на одну из картинок выше, в SMM_MASK MSR есть биты TE и AE (ASEG нам не особо интересен, поэтому, я и не упоминаю его). Если эти биты валидны (равны 1), и обращение происходит не в контексте прерывания, то, ядро процессора просто обратится к MMIO:

AvLitri.png

А значит, мы увидим следующую картину:

dJdFrPv.png

Это был пример для AMD, но и для Intel это вроде бы тоже справедливо (если я правильно помню), думаю, что пример равнозначен для двух производителей процессоров.

Надо сказать, что это только про софтварные прерывания (SW SMI), есть немного более сложный алгоритм срабатывания для ACPI SMI (мы их тоже затронем), например. Но, в конечном итоге всё действительно сводится к записи в порт, триггера SMI# пина на чипсете или запись в специальные адреса ввода/вывода. Мы немного познакомились с SMM и его работой, а значит, время для демагогий.

SMM как вектор атаки на игрушки
Чисто технически, SMM это весьма интересный вектор атаки с точки зрения защитных решений – режим, прозрачно работающий для ОС, который никто не может нормально проверить на работу каких-либо приколов в нём (на самом деле может, но об этом позже).

Те же античиты, например, если и интересуются режимом SMM (а в последнее время они реально интересуются, по крайней мере, их разработчики), то делают это «слабо» на текущий момент. Например, можно в тупую просто мониторить счётчик прерываний (что автоматически подразумевает большое количество ложных срабатываний) или же пытаться вычислить «специфичные» участки памяти, используемые, как коммуникация между режимами. Их вычисление тоже не самая простая задача, да и также может генерировать огромное количество ложных срабатываний. На текущий момент остаётся только глядеть маппнутый образ SPI Flash в физической памяти, сопоставлять каждый GUID драйвера (если вы незнакомы с устройством прошивок, то каждый драйвер имеет свой GUID), в надежде, что появится какой-то неизвестный идентификатор. Это, может и действенно, но кто будет держать весь этот список говна? Да и что делать, если вендоры введут какой-то новый драйвер? Собственно, на текущий момент с детектом SMM драйверов всё немного сложно. Но это не значит, что защитные решения стоят на месте.

Единственная проблема, которая может быть – вендор. Intel и AMD уже давно ввели и вводят некоторые штуки, предназначенные для защиты прошивки и целостности платформы от сторонних драйверов. О них речь пойдёт ближе к концу статьи.

Пишем простой SMM драйвер
Наверное, стоит написать что-нибудь простенькое и забавное. Напишем драйвер, регистрирующий SMI обработчик. Пока это будет софтварный (SW SMI) обработчик, но позже мы познакомимся с ещё одним.

В качестве библиотеки я использую реализацию
Пожалуйста, авторизуйтесь для просмотра ссылки.
(есть ещё GNU-EFI, но это объективно кал). Так как я ещё и особенный, в качестве среды разработки у меня Visual Studio, я использую базу в виде
Пожалуйста, авторизуйтесь для просмотра ссылки.
(правда, модифицированную под свои нужды, но вам будет достаточно собрать ещё пару библиотек при желании). Собирать мы свой драйвер будем через тулчейн EDKII, так что, я дополнительно покажу ещё и настройку конфигурационных файлов.

Хорошо, что должен сделать наш драйвер? В первую очередь, он должен проверить, находится ли он в SMM (параноидально, но так принято, например, для комбинированных DXE-SMM драйверов), затем, получить таблицу SMM сервисов, а потом зарегистрировать SMI обработчик. Собственно, на картинке ниже это и представлено:

03OJgnx.png

В спецификации UEFI есть
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_BASE2_PROTOCOL, который, предоставляет сервис для проверки, запущены ли мы в SMM, а также предоставляет сервис для получения
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_SYSTEM_TABLE2 (извините за тавтологию). Получить указатель на протокол EFI_SMM_BASE2_PROTOCOL можно через
Пожалуйста, авторизуйтесь для просмотра ссылки.
LocateProtocol из
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_BOOT_SERVICES. Отлично! Теперь, напишем код:

SMM Driver EP:
Expand Collapse Copy
EFI_SMM_SYSTEM_TABLE2 *Smst = NULL;


EFI_STATUS
EFIAPI
SmmMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    EFI_SMM_BASE2_PROTOCOL *SmmBase2 = NULL;
    EFI_STATUS Status = SystemTable->BootServices->LocateProtocol(&gEfiSmmBase2ProtocolGuid, NULL, (VOID **)&SmmBase2);
    if(EFI_ERROR(Status))
        return Status;

    BOOLEAN InMmRam = FALSE;
    SmmBase2->InSmm(SmmBase2, &InMmRam);
    if(!EFI_ERROR(Status) && InMmRam) {
        Status = SmmBase2->GetSmstLocation(SmmBase2, Smst);
        if(EFI_ERROR(Status))
            return Status;
    } else {
        return EFI_DEVICE_ERROR;
    }

    return EFI_SUCCESS;
}

Окей, мы получили таблицу сервисов SMM, теперь же, мы зарегистрируем обработчик SW SMI. Делается это через
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_SW_DISPATCH2_PROTOCOL. Обработчику будет нужен собственный номер, пусть это будет 0x99. Для регистрации обработчика нужно передать адрес используемой функции в качестве этого обработчика:

Register SW SMI Handler:
Expand Collapse Copy
EFI_STATUS
EFIAPI
SwSmiHandler(
    IN EFI_HANDLE  DispatchHandle,
    IN CONST VOID *Context         OPTIONAL,
    IN OUT VOID   *CommBuffer      OPTIONAL,
    IN OUT UINTN  *CommBufferSize  OPTIONAL
) {

    return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
RegisterSwSmi(
    VOID
) {
    EFI_SMM_SW_DISPATCH2_PROTOCOL *SwDispatch2 = NULL;
    EFI_STATUS Status = gSmst->SmmLocateProtocol(&gEfiSmmSwDispatch2ProtocolGuid, NULL, (VOID **)&SwDispatch2);
    if(EFI_ERROR(Status))
        return Status;

    EFI_SMM_SW_REGISTER_CONTEXT SwSmiRegCtx = { 0 };
    SwSmiRegCtx.SwSmiInputValue = 0x99;

    EFI_HANDLE Handler = NULL;
    return SwDispatch2->Register(SwDispatch2, SwSmiHandler, &SwSmiRegCtx, &Handler);
}

Отлично! У нас есть зарегистрированный обработчик, который будет доступен по записи в порт 0xB2 значения 0x99. Теперь, можно что-нибудь придумать для своего обработчика. Моей жертвой станет ещё один SMM драйвер от AMD – AmdPspP2CmboxV2. Это драйвер для коммуникации между внешним миром и сопроцессором AMD PSP, расположенным на чипсете, вот тут видно:

ivzZozB.png

У меня есть дамп с SPI чипа своего старого ноутбука, мы ещё вернёмся к аппаратной составляющей, так что пока просто держим это в голове.

j9QteRL.png

Драйвер интересен тем, что регистрирует протокол с адресом буфера для коммуникации между PSP и внешним миром. Коммуницировать с PSP можно, конечно, по-разному, но AMD сделало это более удобным способом. Протокол существует исключительно в SMM, поэтому, почему бы не получить его адрес?

sQ0g7Tw.png

Обработчик должен получить адрес протокола PSP_MBOX_SMM_BUFFER_ADDRESS_PROTOCOL и отдать его нам обратно. Как обработчик его вернёт? Ну, для этого мы будем использовать SMM Save State, в который ядро процессора сохраняет своё состояние. Это делается через ещё один
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_CPU_SAVE_STATE_PROTOCOL. Кешируем последний, а первый получим прям в обработчике. В качестве регистра, в который мы положим результат, будет использоваться r15. Нам также нужно будет извлечь GUID нужного нам протокола.

Кстати, SW SMI обработчикам важно, какое ядро находится в SMM. Обработчики имеют одинаковый набор аргументов, но в случае с SW SMI всё немного интереснее. Если вы взгляните на код, то увидите аргумент CommBuffer. В случае триггера SW SMI, туда будет передана
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_SW_CONTEXT, в которой будет индекс ядра процессора, перешедшего в SMM. Это важно помнить.

Modified Handler:
Expand Collapse Copy
 typedef struct _PSP_MBOX_SMM_BUFFER_ADDRESS_PROTOCOL {
    EFI_PHYSICAL_ADDRESS PspMboxSmmBuffer;
    UINT64               PspMboxSmmFlagAddr;
} PSP_MBOX_SMM_BUFFER_ADDRESS_PROTOCOL;


EFI_STATUS
EFIAPI
SwSmiHandler(
    IN EFI_HANDLE  DispatchHandle,
    IN CONST VOID *Context         OPTIONAL,
    IN OUT VOID   *CommBuffer      OPTIONAL,
    IN OUT UINTN  *CommBufferSize  OPTIONAL
) {
    EFI_SMM_SW_CONTEXT *SwCtx = (EFI_SMM_SW_CONTEXT *)CommBuffer;
    UINTN CpuIdx = SwCtx->SwSmiCpuIndex;

    EFI_GUID PspCmboxProtocolGuid = { 0x579CB2CB, 0x3403, 0x4B26, { 0x84, 0xCD, 0x72, 0x89, 0xFC, 0x91, 0x4D, 0x35 } };
    PSP_MBOX_SMM_BUFFER_ADDRESS_PROTOCOL *PspMbox = NULL;
    EFI_STATUS Status = gSmst->SmmLocateProtocol(&PspCmboxProtocolGuid, NULL, (VOID **)&PspMbox);
    if(EFI_ERROR(Status))
        return Status;

    // ignore last status, no need to execute another handlers
    gSmmCpu->WriteSaveState(gSmmCpu, sizeof(EFI_PHYSICAL_ADDRESS), EFI_MM_SAVE_STATE_REGISTER_R15, CpuIdx, (VOID **)&PspMbox);

    return EFI_SUCCESS;
}

Ну, мы написали небольшой SMM драйвер. Теперь нам надо с ним как-то общаться, правда ведь? Напишем теперь DXE драйвер, который триггернёт прерывание и выплюнет нам адрес протокола.

Пишем DXE драйвер

Да собственно тут нечего особо писать. Нам пригодится немножечко ассемблер и прямые руки, но для начала, точка входа:

DXE Driver EP:
Expand Collapse Copy
#include <Uefi.h>
#include <Base.h>

#include <Library/BaseLib.h>
#include <Library/UefiLib.h>


EFI_STATUS
EFIAPI
DxeMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    Print(L"[ + ] Triggering 0xB2 port\n");

    return EFI_SUCCESS;
}

Теперь, надо триггернуть порт 0xB2 с номером нашего обработчика. Я особенный, поэтому напишем небольшой код на MASM:

SW SMI Port Write:
Expand Collapse Copy
PUBLIC _swsmi_trigger

.code

;
; Prototype: VOID _swsmi_trigger(EFI_PHYSICAL_ADDRESS *Output)
;
_swsmi_trigger PROC PUBLIC
    push rcx
    xor rax, rax
    mov rax, 099h
    out 0B2h, ax
    pop rcx
    mov qword ptr[rcx], r15
    ret
_swsmi_trigger ENDP

END

Объявим прототип в отдельном заголовке, модифицируем входную точку DXE драйвера:

Modified DXE EP:
Expand Collapse Copy
EFI_STATUS
EFIAPI
DxeMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    Print(L"[ + ] Triggering 0xB2 port\n");

    EFI_PHYSICAL_ADDRESS PspMboxBuf = 0;
    _swsmi_trigger(&PspMboxBuf);

    Print(L"[ + ] PSP MBOX Communication buffer: 0x%llX\n", PspMboxBuf);

    return EFI_SUCCESS;
}

Теперь мы можем общаться с SMM драйвером. Переходя к этапу сборки, нам нужно будет написать пару конфигов, но можно воспользоваться существующими в EDKII, нащупать самостоятельно что нужно. Останется только отредактируовать конфиг в Conf/target.txt. В ACTIVE_PLATFORM прописать путь до исходника драйверов, в TARGET прописать условный RELEASE, а в TARGET_ARCH прописать X64. После этого можно билдить.

P.S: Вероятнее всего, ещё придётся пошаманить с Conf/tools_def.txt, в частности, вырубить параметр «предупреждения как ошибки» и немного побаловаться с MASM командами.

Модификация содержимого SPI чипа
Далее мы проведём тесты непосредственно на хардвари, так как драйвер весьма простой и ничего нам не сломает. Но я всё равно не несу ответственности за сохранность вашего оборудования. Мы позже поговорим об отладке SMM драйверов и почему это весьма больная тема. А пока, я выбрал своего подопытного – это мой старенький ноутбук, года эдак 2019, если не раньше. В первую очередь нам нужно нащупать SPI чип. На текущий момент на платах все флешки 8-пиновые и бывают двух форм-факторов: SOIC8/SOP8 и WSON8 (различия можно нагуглить). Зная, как они выглядят, можно начинать брут-форсить плату в поиске чипа. Желательно ещё обращать на маркировки чипов, чаще всего это будет какой-нибудь Winbond, Macronix или GigaDevice. С освещением у меня всё плохо, извиняйте:

QmCLJxI.png

Это чип от GigaDevice формата SOIC8/SOP8. В качестве программатора у меня xGecu T48, но можно использовать любой другой. В качестве адаптера для чипа я буду «прищепку», она как раз подходит для зажима пинов SOIC8/SOP8:

NMDVEq4.png

Здесь нужно не ошибиться с расположением пинов, да и с самим вендором чипа и его вольтажом. Благо, софтина от xGecu умеет в самостоятельное определение по айдишнику. У меня уже есть дамп образа прошивки с чипа, вам же сначала нужно будет его прочитать и сохранить куда-нибудь. Теперь переходим в
Пожалуйста, авторизуйтесь для просмотра ссылки.
(лучше всего версии 0.28.0). Он нам нужен на старом движке, так как в новом нет поддержки модификации образов.

После скачивания тулзы, можно приступить к поиску жертвы на замену. В моём чипе лежал дебажный SMM драйвер для SMBus, поэтому я могу смело заменить его целиком. Вам же, возможно, потребуется дальнейший поиск или модификация существующих SMM драйверов. Всё что нам остаётся сделать – заменить драйвер на наш:

LIWZb7M.png

После замены сохраним бинарь с каким-нибудь другим именем, а дальше прошьём наш чип:

aq7ifQd.png

Нам останется только найти любую USB флешку, форматнуть в FAT32, закинуть бинарь с EFI Shell (можно собрать или найти самому), закинуть наш DXE драйвер и идти смотреть, что вообще происходит. Как и ожидалось – SMM драйвер выплюнул нам адрес протокола (извиняюсь за классные фотки):

b4SgTrY.png

Можно убедиться, что адрес действительно лежит в SMRAM, для этого нам достаточно проверить SMM_ADDR и SMM_MASK MSR, из SMM_MASK мы вычислим размер SMRAM:

TUQNwOh.png

Круто, у нас есть свой первый рабочий драйвер! Но ведь это можно развить во что-то большее, ведь так? Да и как отлаживать всё это дело? Об этом мы и поговорим сейчас.

Отладка SMM драйверов

Несмотря на предоставление EDKII эмулятора (
Пожалуйста, авторизуйтесь для просмотра ссылки.
) – он всё же существует для отладки DXE драйверов, в то время как SMM драйвера требуют другого подхода.

В качестве системы для отладки можно использовать, например, QEMU. EDKII предоставляет
Пожалуйста, авторизуйтесь для просмотра ссылки.
– пакет с драйверами прошивки для QEMU. В случае выбора этого варианта, нужно будет просто закинуть наши драйвера в OvmfPkg, подредактировать конфиги и собрать пакет, затем явно указать QEMU, что он должен грузиться именно с него. Есть множество плюсов такого подхода, например:
  • Не нужно покупать дополнительное оборудование;
  • Можно отправлять отладочные сообщения в Serial I/O порты;
  • QEMU предоставляет GDB Stub;
  • С пинка можно добиться отладки на уровне сурсов.
По моему опыту, как раз таки QEMU это весьма удобная вещь. По специфике работы, я постоянно пользуюсь WinDbg. В связке GDB Stub + EXDI + WinDbg можно добиться отладки с дебаг-символами, что, собственно, весьма круто и удобно. Я не буду здесь рассказывать о том, как это правильно запустить, оставлю это на совесть читающего.

Вендоры используют такую штуку, как JTAG, аппаратный интерфейс для отладки. Иногда также используются специальные отладочные платы (по рассказам некоторых знакомых – такое бывает). Угарнее становится тогда, когда ты можешь нащупать JTAG-пины на «продуктовых» платах, но чаще всего это обрубки от некогда существовавшего интерфейса:

9IdGJv1.png

У Intel есть такая штука, как DCI, по сути, модифицировав прошивку (DCI нужно включать самостоятельно), и сделав самопальный DCI шнур из USB – уже можно пробовать что-то делать. По крайней мере, когда-то можно было, сейчас не знаю. Ещё Intel предоставлял вот такие прекрасные вещи:

s5imqUl.png

Yep, эта беспонтовая херня будет вам стоить дохера, а ещё вам придётся подписывать NDA.

Собственно, самым простым вариантом для самопальных исследований будет QEMU (я опущу процессорный БДСМ с замерами таймингами старта чипсета для определения возможных отладочных интерфейсов).

Развиваем концепцию во что-то большее
Коммуницировать посредством SMM Save State и аутично записывать в порт номер своего обработчика безусловно хорошо, но что, если мы хотим передавать относительно большие массивы данных? Окей, для начала познакомимся с ещё несколькими типами SMI:
  • Root SMI – это «корневые» прерывания, которые срабатывают при каждом SMI. То есть, если произошёл триггер по записи в порт – Root SMI тоже сработает;
  • ACPI SMI – из названия не сложно догадаться, для чего они сделаны. Они имеют весьма удобный метод для коммуникации с внешним миром;
  • Periodic SMI – это прерывания, срабатывающие по определённому тайм-ауту, заданным APIC. Они требуют большего контроля, так как при загрузке системы APIC перенастраивается.
Более подробно мы остановимся на первых двух, начнём, пожалуй, с ACPI SMI.

ACPI SMI как удобный метод коммуникации
Как и любые SMI обработчики – этот также регистрируется в SMM, только для него нужно указывать собственный GUID при регистрации. Собственно, вернёмся к нашему SMM драйверу и перепишем его:

SMM Driver with ACPI SMI Handler:
Expand Collapse Copy
EFI_STATUS
EFIAPI
AcpiSmiHandler(
    IN EFI_HANDLE  DispatchHandle,
    IN CONST VOID *Context         OPTIONAL,
    IN OUT VOID   *CommBuffer      OPTIONAL,
    IN OUT UINTN  *CommBufferSize  OPTIONAL
) {

    return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
RegisterAcpiSmi(
    VOID
) {
    EFI_HANDLE Handler = NULL;
    EFI_GUID AcpiSmiHandlerGuid = { 0xA3715FCB, 0x05F9, 0x4FD6, { 0xA3, 0x5B, 0x31, 0x2F, 0x29, 0xD5, 0x08, 0xA3 } };
    return gSmst->SmiHandlerRegister(AcpiSmiHandler, &AcpiSmiHandlerGuid, &Handler);
}

EFI_STATUS
EFIAPI
SmmMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    EFI_SMM_BASE2_PROTOCOL *SmmBase2 = NULL;
    EFI_STATUS Status = SystemTable->BootServices->LocateProtocol(&gEfiSmmBase2ProtocolGuid, NULL, (VOID **)&SmmBase2);
    if(EFI_ERROR(Status))
        return Status;

    BOOLEAN InMmRam = FALSE;
    SmmBase2->InSmm(SmmBase2, &InMmRam);
    if(!EFI_ERROR(Status) && InMmRam) {
        Status = SmmBase2->GetSmstLocation(SmmBase2, gSmst);
        if(EFI_ERROR(Status))
            return Status;

        // register ACPI SMI handler
        Status = RegisterAcpiSmi();
        if(EFI_ERROR(Status))
            return Status;
    } else {
        return EFI_DEVICE_ERROR;
    }

    return EFI_SUCCESS;
}

Для его регистрации отдельный протокол не используется, используется только
Пожалуйста, авторизуйтесь для просмотра ссылки.
SmiHandlerRegister из таблицы сервисов SMM.

Как же нам коммуницировать с этим обработчиком? Теперь вернёмся к DXE драйверу. Спецификация предоставляет три версии протокола для коммуникации, мы воспользуемся
Пожалуйста, авторизуйтесь для просмотра ссылки.
EFI_SMM_COMMUNICATION_PROTOCOL. Также, нам нужно будет выделить пул памяти, так как именно в нём будут «конвоироваться» данные из внешнего мира в SMM. Тут возникнет вопрос: «А как ACPI SMI обработчик узнает, что обращаются именно к нему, а не к другому?». Спецификация UEFI предоставляет решение в виде
Пожалуйста, авторизуйтесь для просмотра ссылки.
это структуры с GUID обработчика, которые помещаются в начало буфера для коммуникации. Это будет проще увидеть на коде:

DXE Driver:
Expand Collapse Copy
EFI_STATUS
EFIAPI
DxeMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    EFI_SMM_COMMUNICATION_PROTOCOL *SmmCommunication = NULL;
    EFI_STATUS Status = SystemTable->BootServices->LocateProtocol(&gEfiSmmCommunicationProtocolGuid, NULL, (VOID **)&SmmCommunication);
    if(EFI_ERROR(Status)) {
        Print(L"[ ! ] LocateProtocol returned 0x%llX (%r)\n", Status, Status);
        return Status;
    }

    // allocate communication buffer
    UINTN Size = 0x40;
    VOID *CommPool = NULL;
    Status = SystemTable->BootServices->AllocatePool(EfiRuntimeServicesData, Size, &CommPool);
    if(EFI_ERROR(Status)) {
        Print(L"[ ! ] LocateProtocol returned 0x%llX (%r)\n", Status, Status);
        return Status;
    }

    // craft header
    EFI_SMM_COMMUNICATE_HEADER *Hdr = (EFI_SMM_COMMUNICATE_HEADER *)CommPool;

    EFI_GUID AcpiSmiHandlerGuid = { 0xA3715FCB, 0x05F9, 0x4FD6, { 0xA3, 0x5B, 0x31, 0x2F, 0x29, 0xD5, 0x08, 0xA3 } };
    CopyGuid(&Hdr->HeaderGuid, &AcpiSmiHandlerGuid);
    Hdr->MessageLength = Size;

    // place here your data
    // Hdr->Data = [ your data here ]

    // fire SMI
    Status = SmmCommunication->Communicate(SmmCommunication, CommPool, &Size);

    // process output

    return Status;
}

Таким образом мы можем свободно коммуницировать с SMM посредством ACPI SMI обработчика! Теперь разберёмся с тем, как это работает. Помните SMM Core, который присутствует на карте SMRAM выше? Так вооооооот…

Протокол для коммуникации на самом деле отправит нас
Пожалуйста, авторизуйтесь для просмотра ссылки.
. В этом драйвере, по заголовку, будет определён тип обработчика – если GUID нет в заголовке, то драйвер делать с этим
Пожалуйста, авторизуйтесь для просмотра ссылки.
, а просто отправит его дальше. Если же в заголовке буфера GUID есть, то, SMM Core
Пожалуйста, авторизуйтесь для просмотра ссылки.
какой обработчик был зарегистрирован с переданным ему GUID. Если такого нет – он вернёт определённый статус, а если есть –
Пожалуйста, авторизуйтесь для просмотра ссылки.
в порт 0xB2 ноль (любая запись в этот порт триггерит SMI), а там платформа уже
Пожалуйста, авторизуйтесь для просмотра ссылки.
, что к чему. То есть, мы видим следующую картину:

RPtJw4z.png

Наверное, это всё, что касается ACPI SMI..

Работа с памятью в SMM

SMM «в оригинале» работает в реальном режиме, а значит, для адресации ему доступны только первые 4 гигабайта физической памяти. Это большая проблема для x64 систем, в особенности для адресов расположенных выше 4 гигабайт. У нас есть два решения:
  • Самостоятельно создавать собственную страничную иерархию выходить в длинный режим. Платформа умеет это делать, но только в случаях выхода из режима S3 и если прилетает капсульный апдейт;
  • Модифицировать страницы SMRAM и ремаппать каждую «внешнюю» таблицу памяти к себе в SMRAM.
На мой взгляд, из двух вариантов я бы выбрал последний, так как мы можем узнать CR3 ядра процессора, а также уже имеем, допустим, виртуальный адрес, который собираемся «переместить» к себе, нам также понадобится какая-нибудь свободная страница памяти в SMRAM, но её можно спокойно выделить самому. На реализацию можно посмотреть вот
Пожалуйста, авторизуйтесь для просмотра ссылки.
.

SMM и вендоры
Не самый нужный пункт статьи, но стоит понимать, что вендоры всё же заботятся о целостности прошивки, в особенности об SMM. Например, у Intel и AMD есть «загрузочные» решения - Intel Boot Guard и AMD Platform Secure Boot. Они срабатывают ещё до передачи управления SEC Core (ещё одна стадия, которая в разговоре не упоминалась), то есть, код для проверки подлинности находится уже в микрокоде процессора. На примере Intel будет схожая цепочка:

0OnCkPX.png

Не вдаваясь глубоко в суть вопроса (это действительно отдельная большая тема) – первый сперва находит в SPI чипе отдельный модуль, который верифицирует стартовую часть прошивки, где находится основной модуль, который проверифицирует основную часть прошивки. Вот такая цепь доверенной загрузки.

AMD PSB работает схожим образом, но немного интереснее. Если IBG в случае «правильной» конфигурации, записанной в FPF, просто не загрузит платформу, то PSB, судя по всему, не загрузит модифицированный (или заменённый) драйвер (судя по моим тестам).

Также, уже на новых чипсетах Intel есть SMM Isolation, который ломает приколы с адресацией в SMM, вот
Пожалуйста, авторизуйтесь для просмотра ссылки.
хорошо написано.

Послесловие

Ну, вот и краткое описание того, что представляет из себя SMM, как под него писать, куда можно ориентироваться при разработке смешных штук. Возможно, это кому-то пригодится или хотя бы заинтересует.
В идеале, если нацеливаться на игоры – смотреть на SMI# пин чипсета, как он триггерится и при каких событиях, я упоминал тот же xHCI, с чего, в принципе, можно и начать. Меньше оставляешь следов – живёшь дольше.


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

- Спецификации
Пожалуйста, авторизуйтесь для просмотра ссылки.
и
Пожалуйста, авторизуйтесь для просмотра ссылки.

- Спецификации
Пожалуйста, авторизуйтесь для просмотра ссылки.
и
Пожалуйста, авторизуйтесь для просмотра ссылки.
Истинно Computer Science: BDSM Edition
 
может я что-то не понимаю но как античит который работает на уровне ядра может спекторить то что происходит на уровне smm?
эт же архитектурно невозможно
это как пытаться из vtl0 посмотреть что происходит в vtl1(как пример)

если вопрос звучит туповато сорян
спрашиваю только из интереса
 
Последнее редактирование:
может я что-то не понимаю но как античит который работает на уровне ядра может спекторить то что происходит на уровне smm?
Да никто и не говорит, что он может глядеть в SMRAM.
Есть пара очень кривых векторов для детекта:
1. Аномально большое количество прерываний;
2. Мониторинг маппнутого образа прошивки с SPI;
3. Поиск буферов для коммуникации, если таковы имеются.

Все векторы они работаю вне контекста выполнения SMM.

В SMRAM, опять же, смотреть никто не будет, это требует, как минимум, Arbitrary R/W примитивов и выполнение кода в этом же сегменте. А это, в свою очередь, уже дырка в безопасности.
 
Да никто и не говорит, что он может глядеть в SMRAM.
Есть пара очень кривых векторов для детекта:
1. Аномально большое количество прерываний;
2. Мониторинг маппнутого образа прошивки с SPI;
3. Поиск буферов для коммуникации, если таковы имеются.

Все векторы они работаю вне контекста выполнения SMM.

В SMRAM, опять же, смотреть никто не будет, это требует, как минимум, Arbitrary R/W примитивов и выполнение кода в этом же сегменте. А это, в свою очередь, уже дырка в безопасности.
а у антивирусов тоже идентичные вектора детекта получается?
в целом, прикольно как и статья

я думал что для комьюнити геймхакинга обсуждение smm это что-то невероятное, т.к чаще всего этим уровнем интересуются те кто разрабатывает буткиты
 
Последнее редактирование:
а у антивирусов тоже идентичные вектора детекта получается?
Антивирусы вряд ли такое в ближайшем будущем детектить, на поприще киберсека такие решения ещё не эксплуатировались.
Но, если так рассуждать - да, могут использовать такие же.
 
Антивирусы вряд ли такое в ближайшем будущем детектить, на поприще киберсека такие решения ещё не эксплуатировались.
Но, если так рассуждать - да, могут использовать такие же.
Понял.

А будут еще статьи про SMM? С большей практикой, с большей информацией про безопасность/возможные уязвимости

Информации в ру сегменте мало, так что я думаю будет полезно людям которые интересуются низкоуровневым программированием прочитать.
 
А будут еще статьи про SMM?
Сложно сказать. Конкретно эта была написана в маниакальной фазе за два дня, плюсом, она укорочена (движок форума не позволил всё впихнуть). Если и будут - то про какие-то изощрённые механизмы или более углублённое изучение технологических аспектов.
Про уязвимости точно нет, эта тема не для форумов, на мой взгляд. Такие вещи весьма сложновато эксплуатировать, знаю по себе. Шанс что кто-то что-то да поймёт - около нулевой.
 
Назад
Сверху Снизу