**侧通道攻击(Side-Channel Attacks, SCAs)**是一种通过分析系统在执行过程中产生的副产品信息,如时间消耗、电力使用、电磁辐射等,来推断内部敏感数据或状态的攻击方式。与直接破解加密算法不同,SCA利用了系统物理实现中的漏洞,因此具有较强的隐蔽性和挑战性。
基于时间的侧通道攻击:
缓存侧通道攻击:
电源消耗分析:
本章将围绕一段代码,详细解析如何基于处理器的数据缓存机制来提取特定数据位。
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];
这段代码构建了一个用于后续数据处理和位提取的基础结构。下面将对代码的各个部分进行详细解读。
代码首先定义了结构体 array
,其代码如下:
struct array {
unsigned long length;
unsigned char data[];
};
该结构体包含两个成员:一个无符号长整型的 length
,用于表示数组的长度;另一个是无符号字符型的可变长数组 data
,用于存储实际的数据。
struct array *arr1 = ...; /* small array */
struct array *arr2 = ...; /* array of size 0x400 */
代码中定义了两个指向 array
结构体的指针 arr1
和 arr2
,其中 arr1
指向一个小数组,而 arr2
指向一个大小为 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];
从网络读取一个无符号长整型值 untrusted_offset
,并将其作为索引访问 arr1->data
数组,获取一个字节的值 value
。接着,通过对 value
进行位运算 value&1
,提取其最低位。根据这个最低位的值,计算 arr2
数组的索引 index2
。具体来说,如果 value
的最低位是 0,则 index2
为 0x200
;如果是 1,则 index2
为 0x300
。最后,通过 index2
访问 arr2->data
数组获取另一个字节的值 value2
。
关键的步骤在于后续操作,即比较读取 arr2->data[0x200]
和 arr2->data[0x300]
的时间来推断提取的位是 0 还是 1。这一步正是利用了处理器的数据缓存机制。
现代处理器的数据缓存是以缓存行为单位进行数据存储和加载的,通常一个缓存行大小为 64 字节。当程序首次访问某个内存地址时,如果该地址的数据已经在缓存中(缓存命中),那么读取速度会非常快;如果数据不在缓存中(缓存未命中),则需要从内存中读取,这会花费更长的时间。
在这个例子中,选择较大的数组 arr2
以及距离较远的索引 0x200
和 0x300
是有原因的。如果使用小数组或者距离较近的索引,这两个地址很可能位于同一个缓存行内,无论 value
的最低位是 0 还是 1,访问这两个地址时都很可能是缓存命中,无法通过时间差来判断 value
的最低位。而使用大数组和距离较远的索引,可以确保这两个地址位于不同的缓存行,从而增大缓存未命中的概率,使时间差更明显。
通过巧妙地利用处理器的数据缓存机制,这段代码实现了一种基于数组访问时间差来提取特定数据位的方法。这种技术在某些特定的应用场景中,如数据隐藏、安全验证等方面可能具有潜在的应用价值。但同时也需要注意,这种方法依赖于处理器的缓存特性,不同的处理器可能会有不同的缓存行为,因此在实际应用中需要进行充分的测试和验证。
上面程序为了避免程序能够访问任意地址,导致信息泄露,是不是可以通过增加边界判断来进行防御性编程。
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在不确定条件的情况下,提前执行可能的后续指令,从而减少等待时间并提升效率。
CPU采用流水线技术将一条指令分解为多个阶段进行处理:
通过同时处理不同阶段的指令,CPU能够显著提高吞吐量。然而,条件分支(如if
语句)会打破这种流水线的优势,因为后续指令的执行依赖于当前条件的结果。
为了解决这个问题,CPU采用了分支预测技术,试图预测条件分支的走向:
一旦预测完成,CPU就会开始投机性地执行被预测到的后续指令。如果预测正确,执行效率将显著提升;如果错误,CPU会回滚到正确的路径,并丢弃之前错误的执行结果。
回到用户提供的代码:
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
内部的指令。如果实际的条件判断结果为false
,即untrusted_offset >= arr1->length
:
arr1->data[untrusted_offset]
可能已经导致数据被加载到缓存中。CPU的投机执行虽然会丢弃错误的结果,但对缓存的影响通常是不可逆的:
投机执行可能导致侧信道攻击(如Meltdown、Spectre漏洞),攻击者可以利用缓存状态的变化推断敏感信息。例如:
防范措施:
CPU为了提高性能,采用了投机执行技术,在条件分支的情况下提前执行可能的后续指令。即使条件为假,内部的分支也会被执行,并可能导致缓存状态的变化。这种行为虽然提升了性能,但也带来了潜在的安全风险,需要通过软硬件结合的方式来加以防范。