STATEMENT
声明
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
NO.1 SpectreRSB:
基于RSB机制的瞬态执行漏洞
在本文中,我们将为读者介绍基于返回堆栈缓冲区(RSB)推测机制的瞬态执行漏洞的相关概念和原理,并深入分析相关的示例代码。
NO.2
SpectreRSB漏洞简介
SpectreRSB漏洞是一种基于返回堆栈缓冲区(RSB)的瞬态执行漏洞变种。而所谓返回堆栈缓冲区(RSB),其实就是一个硬件堆栈,用于跟踪先前call指令的返回地址,当遇到ret指令时,由于软件栈的访问速度较慢,处理器就会先把RSB的顶部的地址用作返回地址,进行推测地执行。当软件栈中的返回地址取回时,如果与RSB的顶部的地址一致,则接受推测执行的结果;如果不一致,则撤销推测执行的结果,并根据软件栈给出的返回地址继续执行。
由于在瞬态执行过程中,可以越界访问内存,并且在完成回滚后,访问的内存数据还会留在缓存中,因此,攻击者就可以设法让RSB与软件栈的返回地址不一致(如篡改软件栈),触发瞬态执行,越界访问机密信息并进行编码,然后,通过缓存侧信道来解码瞬态执行过程中访问的数据。
也就是说,这里的关键是理解函数过程中软件栈和RSB的变化情况,以及如何篡改软件栈,从而触发瞬态执行;至于机密信息的编码和缓存侧信道,与前面介绍的方法是一样的,这里就不再赘述。
NO.3
函数调用过程中软件调用栈和RSB的变化
RSB是处理器中的一个硬件堆栈缓冲区,每当执行call指令的时候,相应的返回地址都会压入做个内部的硬件堆栈中(通常情况下,它可以保存16个条目),以便将来预测返回地址。当遇到ret指令时,处理器就使用RSB的栈顶元素来预测返回地址,通常情况下,它的预测准确度是非常高的。
下面,我们举例说明函数调用过程中,RSB和软件栈的变化过程,具体如下图所示。
图1 软件调用栈与RSB在函数调用过程中的变化
当main函数调用函数F1时,会将相关参数压入栈中,然后,执行call指令。众所周知,call指令将执行两个动作:首先,将返回值(这里为0x00006210)压入栈中,同时,也将这个返回地址压入RSB中;然后,将函数F1的地址送入EIP寄存器中,这样,就会在下一个时钟周期来临时,跳转到F1中。进入函数F1后,会首先保存ERP寄存器的值,然后,将指向栈顶的ESP的值,送入ERP,这时,两个寄存器都指向保存原来的ERP值的内存地址,然后,将ESP的值减去一定数目(栈是向低地址方向生长的),也就是为当前函数F1的局部变量分配内存空间。之后,继续调用函数F2,并重复上述过程,最后,软件调用栈和RSB就变成了上面图1的样子。
需要注意的是,如果没有参数的话,就不用压入栈了;局部变量也是如此。不过,根据我的观察,有时候在最外层的函数中,并不会保存ERP寄存器。这可能会视具体的编译器和优化选项而定。
函数调用之后,通常是要返回的,那么,我们就来看看返回过程中,软件调用栈与RSB的变化情况。
NO.4
函数返回过程中软件调用栈和RSB的变化
函数返回时,其顺序与调用顺序正好相反,也就是最后调用的那个函数首先返回。就本例来说,F2会首先返回。那么,在返回过程中,它会做些什么呢?
首先,让指向栈顶的ESP寄存器的值等于ERP寄存器的值,这时,它们都指向F调用栈中保存EBP原值的内存地址,然后,弹出栈顶元素,保存至ERP寄存器。这时,ERP寄存器指向F1调用栈中保存ERP原值的内存位置,而EIP寄存器正好指向保存函数F2的返回地址的内存位置。
当执行ret指令时,在没有RSB的情况下,会弹出栈顶元素(就是返回地址),并将其送至EIP寄存器,这样就完成了F2函数返回的动作。我们知道,这里说的是软件栈,从软件栈将返回地址载入处理器是很费时间的,为了提高处理器的运算速度,后来引入了硬件实现的RSB,所以,遇到ret指令时,直接弹出RSB栈顶的元素(实际上,就是猜的返回地址),并把它送入EIP寄存器,先执行从这个猜测地址开始的代码。同时,还会从软件调用栈中,也就是从内存中读取返回地址,等将其载入处理器时,可能已经经过了几百甚至更多个时钟周期了。好了,现在,处理器会比较这两个返回地址,如果一致,就采纳提前执行的结果,并继续往下执行;如果不一致,则撤销提前执行的运算结果,并返回到从软件调用栈取回的内存地址处,重新开始运行。
之后,继续从F1返回,过程与上面所说类似。
图2 软件调用栈与RSB在函数返回过程中的变化
大部分情况下,由于软件调用栈和RSB遇到call指令就同步压入返回地址,遇到ret指令就同步弹出返回地址,所以,RSB的准确率还是非常高的。但是,某些情况下,比如,由于RSB通常只能存放16个或更少的返回地址,所以,如果嵌套调用16个以上的函数,两者就会发生不匹配的情况。或者,如果攻击者直接修改软件调用栈的话,两者还是会发生不匹配的情况。
一旦软件调用栈和RSB给出的返回地址不一致,那么,根据RSB提前执行的指令的结果就需要撤销,这就是我们前面文章所说的瞬态执行。而在瞬态执行的情况下,有时候是能够绕过许多安全限制的,比如越界读取等。
那么,如何才能修改软件调用栈呢?
NO.5
如何修改软件调用栈
下面,我们先来看一个示例代码:
示例代码清单1:
int a=0;
void f2() {}
void f1()
{
f2();
a=a+1;
}
int main()
{
f1();
a=a+2;
printf("a = %d",a);
}
很明显,上面的代码的输出结果为3。那么,如何才能在函数f2中不对变量a进行操作,而使得输出结果为2呢?为此,可以修改软件调用栈,使得函数f2返回时,直接回到f1的返回地址,这样,就能满足上述要求了。
为此,我们先来看看f2的汇编代码:
图3 函数f2的反汇编结果
我们看到,这里没有对EBP寄存器进行保存,所以,它现在还是指向函数f1调用栈中保存EBP寄存器值的内存。这时,软件调用栈的布局大致如下所示:
图4 调用函数f2后,堆栈布局示意图
为了跳过f1中其余的代码,我们需要从f2返回时,直接跳转到main函数中,也就是跳转到f1的返回地址。为此,我可以将EBP当前的值赋给ESP寄存器,相当于跳过f2的返回地址,然后,弹出栈顶元素,送入EBP寄存器,这是该寄存器的值指向main调用栈中保存EBP原值的内存,而ESP寄存器指向f1的返回地址,所以,f2函数返回时,使用的返回地址实际上是f1的返回地址。为了完成上面所说的操作,只需在f1函数中添加如下所示两行汇编代码即可:
示例代码清单2:
void f2()
{
asm volatile(
"mov %rbp, %rsp n"
"pop %rbp n"
);
}
图5 修改前后的程序输出
如图5所示,调用f2后,直接返回到了main函数中,而非f1函数中!
NO.6
SpectreRSB漏洞利用代码PoC
下面给出一个利用SpectreRSB漏洞的PoC示意代码。首先,通过篡改软件调用栈,使得其中的返回地址与RSB中的返回地址不符。这时,由于从RSB中取返回地址非常快,所以,程序会先根据RSB给出的返回地址(该地址处存放的正好是攻击者设计好的瞬态指令)推测性地执行,这样,程序就可以不紧不慢地继续从软件调用栈中取返回地址了。由于推测执行能够绕过某些安全限制,所以,瞬态指令能够越界读取一个字节的内存,这时这个内存地址中的内容将会读入缓存,即使瞬态执行结果被撤销,它仍会留在缓存中。大概几百个,或者更多个时钟周期过后,软件调用栈中的返回地址已经取回,发现RSB给出的返回地址是错误的,所以,立即撤销推测执行的结果,并重新从正确的返回地址(该地址处存放的正好是实现缓存侧信道的指令)处开始执行。这样,就能够越界读取内存信息了。
下面,是一个简单的示例代码。
图6 SpectreRSB漏洞PoC示意代码
为了帮助读者理解,我们来过一遍代码。上述代码执行时,系统首先会调用main函数。这时,会压入main函数的参数和它的返回地址,即0x00000080。注意,这个返回地址将同时压入软件调用栈和硬件实现的RSB,具体如图所示:
图7 调用main函数,在软件调用栈和RSB中压入其返回地址
进入main函数后,需要保存EBP寄存器的值,并在栈中为局部变量分配一段内存空间。
图8 进入main函数后,在软件调用栈中保存EBP的值,并为局部变量分配内存空间
完成上述工作后,开始调用执行瞬态指令的speculative函数。在跳转到这个函数的代码之前,需要将其参数和返回地址压入软件调用栈中,同时,也会将其返回地址压入RSB中,具体如图所示:
图9 调用speculative函数时,在软件调用栈中压入其参数和返回地址,同时将返回地址压入RSB中
进入speculative函数后,保存EBP寄存器的值,并在栈中为局部变量分配一段内存空间,具体如下图所示:
图10 进入speculative函数后,在软件调用栈中保存EBP寄存器的值,并为局部变量分配内存空间
然后,开始调用破坏软件调用栈的pollute函数。为此,需要先将其参数和返回地址压入软件调用栈,同时,还会将返回地址压入RSB中,具体如图所示:
图11 调用pollute函数时,先在软件调用栈中压入其参数和返回地址,同时将其返回地址压入RSB中
进入后,首先保存EBP寄存器的值,这时,软件调用栈和RSB的内容如下所示:
图12 进入pollute函数后,在软件调用栈中保存EBP寄存器的值
保存EBP寄存器的值之后,让该寄存器保存ESP寄存器的值,也就是说,现在这两个寄存器都指向栈顶:
图13 在pollute函数中,让寄存器EBP和ESP都指向栈顶
然后,执行一条pop指令,也就是从栈顶弹出一个元素,或者说,让栈顶指针往后退一步:
图14 在pollute函数中,从软件调用栈顶弹出一个元素
继续执行后面的4条pop指令,从栈顶弹出4个元素(其中包括pollute函数的返回地址)。这时,EBP寄存器指向main调用栈中保存EBP原值的元素,而ESP寄存器指向speculative函数的返回地址:
图15 在pollute函数中,连续从软件调用栈顶弹出元素,直到露出speculative函数的返回地址为止
这时,利用clflush指令将栈顶的元素,也就是ESP寄存器所指向的内存,也就是speculative函数的返回地址从缓存中逐出,这样的话,只能从内存读取读取该地址。为什么要这么做?因为这么做的话,会更加耗时,从而为瞬态执行争取更多的时间。
如您所见,执行返回指令时,软件调用栈顶部的返回地址,已经与RSB顶部的返回地址不一致了。
图16 在pollute函数返回时,软件调用栈顶部的返回地址已经与RSB顶部的返回地址不一致了
当pollute函数返回时,由于RSB的动作较快,所以,处理器会先跳转到0x00000022处,推测执行瞬态指令:读取一字节的机密数据值,并将其作为数组的下标,访问相应的数组元素。
图17 利用RSB返回的预测地址,瞬态执行读取机密数据的指令,并将读取的数据编码为数组的下标
等到处理器取到内存调用栈顶的返回地址时,发现预测的返回地址是错误的,所以,撤销之前的执行结果,重新从0x00000032处开始执行。我们从图中可以看出,这里的代码是在测量数组元素的读取时间。也就是说,这里的代码是根据缓存计时侧信道恢复出瞬态指令读取的机密数据。
NO.7 缓解措施
对瞬态执行攻击的防御方法,主要围绕以下四个方面进行:
- 限制瞬态指令的执行,或减小瞬态执行的时间窗口;
- 限制瞬态指令越权访问数据;
- 使微架构状态不受瞬态执行的影响,从而令隐蔽信道的发送端无效(无法编码);
- 降低隐蔽信道的精度,相当于降低隐蔽信道接收端的能力(无法解码)。
其中,前面两条用于防止触发瞬态执行,后面两条用于阻止侧信道攻击。
NO.8 小结
在本文中,我们为读者详细介绍了基于返回堆栈缓冲区(RSB)推测机制的瞬态执行漏洞的相关概念和原理,并深入分析相关的示例代码,希望对大家理解这个漏洞的原理和防御有所帮助。
RECRUITMENT
招聘启事
END
长按识别二维码关注我们
本文始发于微信公众号(雷神众测):瞬态执行漏洞之SpectreRSB篇
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论