解析一种SCA(侧通道攻击)的工作原理

文章目录

  • 一、 侧通道攻击的概念
    • 1、概念解释
    • 2、实际例子
  • 二、基于处理器数据缓存的侧通道攻击方法解析
    • 1、代码呈现
    • 2、代码结构概述
      • 2.1 结构体定义
      • 2.2 数组指针定义
    • 3、代码执行流程
      • 3.1 数据读取与索引计算
      • 3.2 利用缓存特性提取位信息
    • 4、结论
  • 三、 投机执行(Speculative Execution)困扰:为什么处理器会执行里面的分支?
    • 1. CPU流水线与多级处理
    • 2. 分支预测与投机执行
    • 3. 示例代码中的投机执行
    • 4. 条件为假时的处理
    • 5. 为什么缓存不回滚?
    • 6. 安全影响与防范措施
    • 7. 总结

一、 侧通道攻击的概念

1、概念解释

**侧通道攻击(Side-Channel Attacks, SCAs)**是一种通过分析系统在执行过程中产生的副产品信息,如时间消耗、电力使用、电磁辐射等,来推断内部敏感数据或状态的攻击方式。与直接破解加密算法不同,SCA利用了系统物理实现中的漏洞,因此具有较强的隐蔽性和挑战性。

2、实际例子

  1. 基于时间的侧通道攻击

    • 应用场景:加密算法的时间消耗可能与输入明文或密钥相关。
    • 攻击过程:测量加密函数在不同输入下的执行时间,通过分析时间差异推断出密钥信息。
  2. 缓存侧通道攻击

    • 应用场景:现代处理器中的缓存机制可能导致某些数据访问模式被泄漏。
    • 攻击过程:通过监测缓存访问的时间延迟,推断出加密算法中使用的密钥。
  3. 电源消耗分析

    • 应用场景:设备在执行加密操作时的电力消耗可能与密钥相关。
    • 攻击过程:使用示波器等工具测量设备的电流波动,分析数据以获取密钥信息。

二、基于处理器数据缓存的侧通道攻击方法解析

本章将围绕一段代码,详细解析如何基于处理器的数据缓存机制来提取特定数据位。

1、代码呈现

struct array {
    unsigned long length;
    unsigned char data[];
};
struct array *arr1 = ...; /* small array */
struct array *arr2 = ...; /* array of size 0x400 */
unsigned long untrusted_offset = network_read(...);
unsigned char value = arr1->data[untrusted_offset];
unsigned long index2 = ((value&1)*0x100)+0x200;
unsigned char value2 = arr2->data[index2];

这段代码构建了一个用于后续数据处理和位提取的基础结构。下面将对代码的各个部分进行详细解读。

2、代码结构概述

2.1 结构体定义

代码首先定义了结构体 array,其代码如下:

struct array {
    unsigned long length;
    unsigned char data[];
};

该结构体包含两个成员:一个无符号长整型的 length,用于表示数组的长度;另一个是无符号字符型的可变长数组 data,用于存储实际的数据。

2.2 数组指针定义

struct array *arr1 = ...; /* small array */
struct array *arr2 = ...; /* array of size 0x400 */

代码中定义了两个指向 array 结构体的指针 arr1arr2,其中 arr1 指向一个小数组,而 arr2 指向一个大小为 0x400 的数组。

3、代码执行流程

3.1 数据读取与索引计算

unsigned long untrusted_offset = network_read(...);
unsigned char value = arr1->data[untrusted_offset];
unsigned long index2 = ((value&1)*0x100)+0x200;
unsigned char value2 = arr2->data[index2];

从网络读取一个无符号长整型值 untrusted_offset,并将其作为索引访问 arr1->data 数组,获取一个字节的值 value。接着,通过对 value 进行位运算 value&1,提取其最低位。根据这个最低位的值,计算 arr2 数组的索引 index2。具体来说,如果 value 的最低位是 0,则 index20x200;如果是 1,则 index20x300。最后,通过 index2 访问 arr2->data 数组获取另一个字节的值 value2

3.2 利用缓存特性提取位信息

关键的步骤在于后续操作,即比较读取 arr2->data[0x200]arr2->data[0x300] 的时间来推断提取的位是 0 还是 1。这一步正是利用了处理器的数据缓存机制。

现代处理器的数据缓存是以缓存行为单位进行数据存储和加载的,通常一个缓存行大小为 64 字节。当程序首次访问某个内存地址时,如果该地址的数据已经在缓存中(缓存命中),那么读取速度会非常快;如果数据不在缓存中(缓存未命中),则需要从内存中读取,这会花费更长的时间。

在这个例子中,选择较大的数组 arr2 以及距离较远的索引 0x2000x300 是有原因的。如果使用小数组或者距离较近的索引,这两个地址很可能位于同一个缓存行内,无论 value 的最低位是 0 还是 1,访问这两个地址时都很可能是缓存命中,无法通过时间差来判断 value 的最低位。而使用大数组和距离较远的索引,可以确保这两个地址位于不同的缓存行,从而增大缓存未命中的概率,使时间差更明显。

4、结论

通过巧妙地利用处理器的数据缓存机制,这段代码实现了一种基于数组访问时间差来提取特定数据位的方法。这种技术在某些特定的应用场景中,如数据隐藏、安全验证等方面可能具有潜在的应用价值。但同时也需要注意,这种方法依赖于处理器的缓存特性,不同的处理器可能会有不同的缓存行为,因此在实际应用中需要进行充分的测试和验证。

三、 投机执行(Speculative Execution)困扰:为什么处理器会执行里面的分支?

上面程序为了避免程序能够访问任意地址,导致信息泄露,是不是可以通过增加边界判断来进行防御性编程。

  if (untrusted_offset < arr1->length) {
    unsigned char value = arr1->data[untrusted_offset];
    unsigned long index2 = ((value & 1) * 0x100) + 0x200;
    if (index2 < arr2->length) {
        unsigned char value2 = arr2->data[index2];
    }
}

答案是不一定可以。cpu可能绕过if (untrusted_offset < arr1->length)边界判断执行后面的分支。为什么会出现这种情况?

现代CPU为了提高性能,采用了多种优化技术,其中之一便是投机执行(Speculative Execution)。这一机制允许CPU在不确定条件的情况下,提前执行可能的后续指令,从而减少等待时间并提升效率。

1. CPU流水线与多级处理

CPU采用流水线技术将一条指令分解为多个阶段进行处理:

  • 取指 (Fetch):从内存中读取指令。
  • 解码 (Decode):解析指令的含义。
  • 执行 (Execute):完成计算或数据操作。
  • 写回 (Write Back):将结果保存到寄存器或内存。

通过同时处理不同阶段的指令,CPU能够显著提高吞吐量。然而,条件分支(如if语句)会打破这种流水线的优势,因为后续指令的执行依赖于当前条件的结果。

2. 分支预测与投机执行

为了解决这个问题,CPU采用了分支预测技术,试图预测条件分支的走向:

  • 静态预测:基于固定的规则(如“总是认为分支会被执行”)。
  • 动态预测:根据历史执行情况来预测未来的分支行为。

一旦预测完成,CPU就会开始投机性地执行被预测到的后续指令。如果预测正确,执行效率将显著提升;如果错误,CPU会回滚到正确的路径,并丢弃之前错误的执行结果。

3. 示例代码中的投机执行

回到用户提供的代码:

struct array {
    unsigned long length;
    unsigned char data[];
};

struct array *arr1 = ...;  /* small array */
struct array *arr2 = ...;  /* array of size 0x400 */
unsigned long untrusted_offset = network_read(...);

if (untrusted_offset < arr1->length) {
    unsigned char value = arr1->data[untrusted_offset];
    unsigned long index2 = ((value & 1) * 0x100) + 0x200;
    if (index2 < arr2->length) {
        unsigned char value2 = arr2->data[index2];
    }
}
  • 假设 arr1->length 不在缓存中,需要从内存加载,这可能需要约100个时钟周期。
  • 在等待arr1->length的这段时间内,CPU可能会基于预测(例如认为untrusted_offset < arr1->length为真),开始执行if内部的指令。

4. 条件为假时的处理

如果实际的条件判断结果为false,即untrusted_offset >= arr1->length

  • CPU会回滚到正确的执行路径,并丢弃之前错误执行的结果。
  • 然而,缓存状态不会完全回滚。例如,访问arr1->data[untrusted_offset]可能已经导致数据被加载到缓存中。

5. 为什么缓存不回滚?

CPU的投机执行虽然会丢弃错误的结果,但对缓存的影响通常是不可逆的:

  • 缓存加载:一旦从内存加载数据到缓存,即使后续判断为假并回滚执行路径,这些数据仍然存在于缓存中。
  • 性能考虑:完全回滚缓存状态会增加额外的开销,影响整体性能。

6. 安全影响与防范措施

投机执行可能导致侧信道攻击(如Meltdown、Spectre漏洞),攻击者可以利用缓存状态的变化推断敏感信息。例如:

  • 即使条件为假,访问特定内存地址的行为仍然可能被检测到。

防范措施:

  • 软件层面:使用缓解技术,如分支预测器的硬化(Branch Prediction Hardening)和不可推测执行的屏障(Indirect Branch Restricted Instructions, IBRS)。
  • 硬件层面:现代处理器增加了新的指令集扩展,以防止投机执行导致的安全漏洞。

7. 总结

CPU为了提高性能,采用了投机执行技术,在条件分支的情况下提前执行可能的后续指令。即使条件为假,内部的分支也会被执行,并可能导致缓存状态的变化。这种行为虽然提升了性能,但也带来了潜在的安全风险,需要通过软硬件结合的方式来加以防范。

你可能感兴趣的:(c++,漏洞,攻击,安全)