利用极长中断攻击系统管理模式
Exploiting System Management Mode with a very long interrupt

原始链接: https://github.com/xoreaxeaxeax/smiiiiiiiiiiiiiiii

研究人员发现了一种绕过系统管理模式(SMM)安全机制的方法,该模式是 x86 架构中权限极高的执行环境。SMM 依赖于“集合”(rendezvous)原则,即所有 CPU 核心必须同时进入 SMM,以确保在执行安全操作时没有代码在外部运行。 该漏洞通过在其中一个核心上执行一条“极其漫长”的机器指令来打破这种同步,指令耗时超过一秒——这正是系统等待所有核心加入的超时阈值。通过强制一个核心执行高延迟的内存映射 I/O(MMIO)读取并使其超时,攻击者可以让该核心在其他核心进入 SMM 时仍保持在外部运行。 这种异步破坏了 SMM 的核心安全保障。它允许 CPU 核心在 SMM 活跃时执行软件攻击,将以往被认为需要物理硬件访问才能实现的“检查时机与使用时机”(TOCTOU)漏洞转化为可行的软件攻击。Christopher Domas 的研究表明,仅需一条经过精心调整的指令,就能提供破坏这一被视为独立环境所需的窗口。

开发者 xoreaxeaxeax 最近发布了一篇文章,讨论了针对 x86 架构中系统管理模式(SMM)的一种漏洞利用方法。该技术的核心在于触发一条“超长”指令,从而使核心执行不同步,进而允许拥有 root 权限的用户绕过 SMM 的保护机制。 此次讨论凸显了 SMM 的争议性——这是一种运行在操作系统之下的、具有极高权限且不透明的执行模式。尽管厂商最初设计 SMM 是为了实现电源管理和硬件仿真等良性目的,但批评者认为,由于其对用户不可见且不可控,它已成为一个“对用户不友好”的黑箱。一些评论者将其比作现代的平台锁定机制,指出曾经作为实现向后兼容的便利工具,如今已演变为一种剥夺用户硬件所有权的机制。 技术层面上,关于 SMM 固件中“1秒超时”机制的争论也随之出现。一些人认为此类超时对系统稳定性至关重要,而另一些人则批评这是一种危险的“软故障”处理方式,容易导致安全漏洞。归根结底,这一讨论提醒人们,硬件层面的维护需求与对透明、可验证计算的追求之间存在着持续的冲突。
相关文章

原文

Exploiting System Management Mode with a very very very very very very very long interrupt.

It turns out that you can break SMM — the secure, ultra privileged execution environment running invisibly in the background of every x86 CPU — with nothing more than an obscenely long-running machine instruction.

SMM requires that all cores are either in SMM or out of SMM at the same time. Its security model doesn't work without this - when one thread enters SMM, it makes all the others enter too.

To break this, all we need is someone too busy to notice they're supposed to join SMM.

It works something like this:

core 0 - start a long instruction
       |
       |
       |
core 1 - invite core 0 to smm
       |
       |
       |
core 1 - enter smm
       |
       |
       |
core 1 - wait for core 0
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
core 1 - wait for core 0
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
core 1 - wait for core 0
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
core 1 - wait for core 0
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
core 1 - wait for core 0
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
core 1 - wait for core 0
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
core 1 - wait for core 0
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
core 1 - wait for core 0
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
       |
core 1 - give up
core 1 - do secret smm stuff
core 1 - finish smm
       |
       |
       |
core 0 - join smm

At this point, core 1 is out of SMM while core 0 is in, letting core 1 attack core 0. Here's the catch: for this to work we need a very, very, verrrrry long instruction — longer than any instruction was ever supposed to take. Most machine instructions on a modern CPU are fast: add takes 1 cycle. To get core 1 to give up waiting on core 0, we need an instruction on core 0 that takes around 4,000,000,000 cycles — over 1 second of wall-clock time.

x86 firmware runs the following code when a CPU core enters SMM:

  for (Timer = StartSyncTimer ();
       !IsSyncTimerTimeout (Timer, mTimeoutTicker) && SyncNeeded;
       )
  {
    mSmmMpSyncData->AllApArrivedWithException = AllCpusInSmmExceptBlockedDisabled ();
    if (mSmmMpSyncData->AllApArrivedWithException) {
      break;
    }

    CpuPause ();
  }

The code waits for all cores to enter SMM, or for up to 1 second, whichever occurs first. To get a core to execute SMM code while another core stays executing outside SMM, we need that outside core to stay uninterruptible for the entire second — an SMI is taken at an instruction boundary, so any gap between two instructions is a door the pending SMI walks through to drag the core in. The delay therefore has to be a single instruction: one uninterruptible op that outlasts the one-second rendezvous.

There are many ways to reach the forbidden 1-second instruction, and the exact approach will vary platform-to-platform. But, roughly: find a high-latency MMIO address, and then convince the CPU to read from it as slowly as possible — abuse an undocumented region that answers reads at a crawl, then use the widest load the ISA will give you to haul a ridiculous pile of bytes across it in a single instruction, ideally while the other cores are elbowing onto the same bus so the whole thing wheezes through under contention. One read, one instruction, and the CPU is stuck holding it for the better part of a second.

The provided proof-of-concept is tuned for a Zen 3 Ryzen 7 5800H, where a wide xmm load from slow MMIO at 0xfcc68860 stalls long enough to break the all-cores rendezvous:

mov     $0xfcc68860, %rsi   ; the target MMIO address
vmovdqu (%rsi), %xmm0       ; the very, very long load

The PoC exploits this by pitting two cores against each other. One core is held outside SMM by the long instruction — a tight loop on the very slow load:

/* the victim core: spin on the ~1-second load, too busy to answer the SMI */
for (;;)
    asm volatile ("vmovdqu (%0), %%xmm0" :: "r"(mmio) : "xmm0");

Meanwhile another core arms the per-core SMI counters:

#define MSR_PERF_CTL0  0xc0010200        /* AMD core perf event-select MSR */
#define MSR_PERF_CTR0  0xc0010201        /* the paired 48-bit counter      */

for (int cpu = 0; cpu < ACTIVE_CPUS; cpu++) {
    msr_write(cpu, MSR_PERF_CTL0, 0x43002b);  /* EN | OS | USR | event 0x2b */
    msr_write(cpu, MSR_PERF_CTR0, 0);         /* zero the count            */
}

Then fires a storm of SMIs:

asm volatile ("outb %%al, $0xb2" :: "a"(0));  /* kick port 0xb2 -> #SMI    */

And reads every core's tally back:

/* ...fire the storm, then read every core's tally back... */
uint64_t delta = smi_max - smi_min;
if (delta)
    puts("!!! a core ran outside SMM");

The counters tell the story. If they diverge, a core kept running outside SMM while the others were pulled in — it lived through SMIs the rest of them serviced without it.

SMI counter divergence in action

And that's the entire result: SMM's one promise, that nothing else runs while it does, falls apart in the face of one absurdly long instruction.

SMM's security relies on a simple assumption: while it runs, nothing else does.

There are 100+ SMM TOCTOU CVEs out there: an SMM handler checks a value in shared memory, then uses it. All you need for exploitation is to rewrite that value in between the check and use, and you're inside SMM. But these issues sit dormant and largely unpatched in the wild, because of one assumption: exploitation requires something to modify the shared memory while SMM executes, and because of the SMM rendezvous no CPU cores are outside SMM to launch an attack. The only way in — or so we thought — was a DMA-capable peripheral writing behind the CPU's back — physical access, a malicious device — so the whole class is written off as a hardware problem.

SMI desynchronization removes the prerequisite: an outside core, no physical access or hardware required, can now run while SMM executes — and suddenly the dormant CVEs become exploitable from software.

In this project, we've shown only that the window opens; but that window was the whole reason these bugs were thought safe.

The default vmovdqu at 0xfcc68860 in the proof-of-concept is a slow spot on this box — a Zen 3 Ryzen 7 5800H — and likely nowhere else. To break the rendezvous on your box, you will need to retune the long instruction so the stall outlasts your SMM timeout. Some tips on how to do this:

  1. Aim at your MMIO. Find a slow MMIO region on your platform with mmiotic.
  2. Widen the read. Step -r xmm → ymm → zmm until the stall crosses the rendezvous timeout.
  3. Swap the instruction. If no single MMIO read is slow enough, you need a different pathologically long instruction; the asm-hall-of-shame shows how to find them.
make          # builds smiiiiiiiiiiiiiiii

The defaults are tuned to one machine. On anything but a Zen 3 Ryzen 7 5800H, expect no divergence until you retune the long instruction — see Porting to your platform.

Run the tool to repeatedly fire the very-very-long instruction while watching each core's SMI counter for a divergence:

sudo ./smiiiiiiiiiiiiiiii          # default: -r xmm at 0xfcc68860

Flags:

Flag Default Description
-r xmm|ymm|zmm xmm Vector register width for the timed MMIO read (16/32/64 bytes). If no SMI count delta is observed, the tool advises stepping up to the next size.
-a <phys-addr> 0xfcc68860 Target physical address for the MMIO timing loop (hex 0x... or decimal).
-h, --help — Print usage and exit.

smiiiiiiiiiiiiiiii is a research effort from Christopher Domas (@xoreaxeaxeax).

联系我们 contact @ memedata.com