SEEDLAB2.0-Meltdown

在这里插入图片描述

在下面的代码中,我们有一个规模为104096的数组,首先访问它的两个元素array[34096],array[74096],这样,有这两个元素的页就被cache了。然后从array[04096]到array[94096]的值,测量读取内存花费的时间。代码中,1在内存读之前读CPU的时间戳(TSC),2在内存读之后读计时器。它们的差就是花费在内存读上的时间(CPU周期)。
需要注意,在cache block层次,cache过程已经完成,但是在byte层次还没有。一个典型的cache block大小是64字节。
我们使用array[k
4096],所以程序中用到的元素没有两个会在同一个cache block中
在这里插入图片描述

使用下列命令编译并运行
在这里插入图片描述

从结果可以看到,array[34096]和array[74096]的访问速度远高于其他元素。

假设现在有个漏洞函数使用secret value作为索引从数组中加载特定的值,同时假设secret value不能从外面访问。我们的目标是使用侧信道来获得这个secret value,我们将会使用的技术是FLUSH+RELOAD.
在这里插入图片描述

整个技术包括三步:
1.从cache memory中FLUSH整个数组,确保数组没有被cached
2.调用victim function,它会基于secret value访问数组的一个元素。这个动作会导致相应的数组元素被缓存
3.RELOAD整个数组,测量每个元素reload时的时间,如果某个特定的元素load的时间很快,它很有可能就是已经在缓存中的元素了。这个元素一定就是victim function访问的元素。因此,我们可以找到secret value究竟是什么。

下面的程序使用FLUSH+RELOAD技术找到包含在变量secret中的一字节的secret value
由于1字节的secret有256种可能,我们需要把每个值映射到一个数组元素。最简单的方法就是定义一个有256个元素的数组(array[256])。不过这不行。caching是在block层次完成的,而不是byte层次。如果array[k]被访问了,那么包括这个元素在内的一个block的元素都被缓存了。因此,在array[k]的邻接元素也会被缓存,这样就很难推断secret是什么了。为了解决这个问题,我们创建一个2564096字节的数组。在RELOAD步骤用到的每个元素是array[k4096],因为4096比典型的cache block size要大(64字节),所以不会有两个不同的元素array[i4096]和array[j4096]在同一个cache block中
既然array[04096]可能会和邻接的变量在同一个cache block中。因为我们应该避免在FLUSH+RELOAD方法中使用array[04096].
在程序中我们对所有的k值使用array[k*4096+DELTA],其中DELTA被定义为常量1024

在这里插入图片描述在这里插入图片描述

编译并执行
在这里插入图片描述

成功了

内存隔离memory isolation是系统安全的基础,在大多数操作系统中,kernel memory并不是对use-space程序直接可访问的,这个隔离是通过处理器的一个supervisor bit实现的,它定义了kernel的一个memory page是否可以被访问。当cpu进入到kernel space时,这个bit会被置位,当其到user space时会被clear。通过这一特性,kernel memory可以被安全映射到每个进程的地址空间,所以当一个user-level的程序陷入kernel时,页表就不需要改变。然而,这种隔离特性被meltdown打破了,它允许非特权的user-level程序读任意kernel memory

为了简化攻击,我们在kernel space放一个secret data,我们将演示user-level program是如何找出secret data是什么的
我们使用kernel module存储secret data。
代码如下
在这里插入图片描述在这里插入图片描述

有两个条件需要满足,否则meltdown攻击很难成功。
1.我们需要知道secret data的地址。在2中,kernel module将该地址保存到了kernel message buffer,这是公开可访问的,我们从其中获取地址。在实际攻击中,攻击者不得不找到一个方法来获取地址,否则只能猜测
2.Secret data需要被cached,否则攻击者的成功率会下降。为了实现这一点,我们需要使用secret一次。我们创建一个data entry /proc/secret_data(3),这会为user-level program与kernel module交互提供一个window。
当一个user-level program从其中读取时,kernel module中的read_proc函数会被唤醒,在其中,secret 变量将被加载(1),因此也就被CPU cached了。需要注意,read_proc()不返回secret data到user space,所以不会泄露secret data。我们仍然需要通过meltdown攻击来获取secret。

Make,编译kernel module
在这里插入图片描述

使用insmod安装kernel module
在这里插入图片描述

安装后,使用dmesg命令从kernel message 缓冲区中找到secret data的地址
在这里插入图片描述

现在我们知道secret data的地址了,我们来看看是否可以直接根据地址获取secret。
在diamante第一行,将地址替换为实际的地址。然后编译并运算看看是否可以
在这里插入图片描述

可以看到从user-space是无法访问的

在meltdown攻击中,在访问kernel memory后我们需要做一些其他事情才不会让程序crash。访问禁止的memory location会报出SIGSEV 信号;如果程序自身不处理的话,操作系统将会处理并终止程序。这就是为什么我们上个程序crash了
在程序中定义我们自己的signal handler的一种方法是捕获由carastrophic events抛出的异常。
不像C++或其他高级语言,C没有提供对错误处理(或称之为异常处理)的直接支持,比如try/cache
然而我们使用sigsetjmp()和siglongjmp()模拟try/cache。
下面的代码可以看到,程序如何在关键异常(内存访问异常)出现时继续执行。
在这里插入图片描述

编译并执行结果如下
在这里插入图片描述

上面代码中的异常处理机制比较复杂,所以进一步解释一下,
准备一个signal handler:我们在2注册了一个SIGSEGV signal handler,所以当SIGSEGV signal被抛出时,handler function catch_segv()就会被调用
准备检查点checkpoint:在signal handler处理好exception后,它需要让程序从特定checkpoint之后继续执行。因此我们首先需要定义checkPoint,这是通过3中的sigsetjmp()实现的。
Sigsetjmp(jbuf,1)保存栈的上下文/环境到jbuf,稍后被siglongjmp()使用,当checkpoint准备好后它会返回0
退到检查点:当siglongjmp(jbuf,1)被调用时,存在jbuf变量中的状态被复制回处理器,以及从sigsethjmp函数的返回点再次开始计算,但是sufsetjmp函数的返回值是siglongjmp函数的第二个参数,在我们的例子中是1.因此,在异常处理时,程序会从else分支开始继续执行
触发异常:4的代码将会触发一个SIGSEGV信号,由于内存访问隔离的原因

以下面的代码为例,我们知道第3行代码会抛出异常,因为该地址属于kernel
因此,执行将会在第3行中断,第4行就得不到执行,所以number变量将是0
在这里插入图片描述

但我们从CPU外部看的时候,这个分析是对的。
但是,当从CPU内部看的时候,这就不是完全对的。
我们来看看微架构层面的执行序列。我们会在第3行成功地得到内核数据,而第4行以及其后的序列指令也会得到执行。这是由于现代CPU采用的重要的优化技术。叫做乱序执行(out-of-order execution)
不像以原始的顺序严格执行指令,现代高性能处理器允许乱序执行以充分利用执行单元。一条接一条执行指令的话,性能比较差而且没有充分利用好资源,比如即使当前执行单元是空闲状态,当前的指令仍然要等上一条指令执行完成。有了乱序执行的特性,CPU在资源可用的情况下就会执行。
以上面的代码为例,在微架构层面,第3行涉及到两个操作。
加载数据(通常是到寄存器),以及检查数据访问是否被允许。
如果数据直接在CPU cache中,那么第一个操作是很快的,而第二个操作可能需要等一会儿。为了避免等待,CPU将会继续执行第4行以及后续的指令,同时会进行访问的检查。这就是乱序执行。
在访问检查结束前,执行结果不会被commit。
在我们的例子中,如果范文检查失败,那么执行结果就被丢弃,就像从来没有发生过一样。这就是为什么从外部看,我们看不到第4行被执行了。
在这里插入图片描述

但是intel以及一些CPU制造商在设计乱序执行时犯了一个严重的错误。
当执行行为不应该发生时,他们会把乱序执行对寄存器、主存的影响全部擦除,所以对外没有可见的影响。但是他们忘记对CPU cache的影响了。
在乱序执行中,被访问的内存不仅写到了寄存器,同时也被存到了cache中,如果乱序执行被抛弃,那么该行为造成的cache的影响也需要被清除。但是在大多数CPU中,并没有做到这一点。
所以通过前面提到的侧信道技术,我们可以观察到这种现象。Meltdown聪明地利用这种现象找出在kernel memory中的secret
下面的代码可以演示乱序执行的效果。
1会造成异常,所以2不会被执行。但是由于乱序执行的原因,2会被CPU执行,虽然结果被会丢弃。不过,array[7*4096+DELTA]被CPU cache了
我们看看是否可以观察到这一点。
在这里插入图片描述在这里插入图片描述

注意替换掉3中的地址为自己实际的地址

在这里插入图片描述

CPU在乱序执行中可以走多远依赖于访问检查有多慢,他们是并行进行的
这是一个典型的条件竞争问题
前面我们已经得到了array[74096+DELTA].事实上,使用FLUSH+RELOAD技术,我们检查array[i4096+DELTA]对于i=0,255的时间。如果我们发现只有array[k*4096+DELTA]在cache中,我们可以推断kernel_data是k。

乱序执行越快,我们就可以执行越多的指令,我们就越有可能观察到可以帮助我们获得secret的现象。
那么怎么让乱序执行更快呢?
我们代码中乱序执行的第一步是加载Kernel data到寄存器中,在同时,对这种访问是否合法进行检查。如果数据加载比安全检查慢,比如当安全检查完成时,kernel data仍然在从memory到register的路上,乱序执行就会立刻被中断并被丢弃,因为检查失败。我们的攻击自然也就失败了。
如果kernel data已经在cpu cache中,那么加载kernel data到寄存器就更快了,我们也许就能达到关键指令(比如在检查失败终止乱序执行之前,加载数组)实际上,如果kernel data没有被缓存,使用meltdown来窃取信息是困难的,但是也不是不行,只不过需要更高性能的CPU和DRAM
在这次实验中,我们在发动攻击前,先让kernel secret data缓存好。我们让user-level的程序调用kernel module内部的函数,这函数将会访问secret data,并且不会将其泄露给user-level 程序。这样就可以使得CPU cache中缓存secret data。

在内存访问前加些汇编指令来提升成功率。
下面的汇编
做一个400次的循环,在循环中,简单地加0x141到eax寄存器。这其实是在做无效的计算,但是根据之前的讨论,这些代码“give algorihtmic units something to chew while memory access is being speculated。
这是增加攻击成功率的一个有效的trick
在这里插入图片描述在这里插入图片描述

为了提供正确率,我们可以使用统计技术。创建一个score array,大小为256,一个元素对应一个可能的secret value。运行我们的攻击多次。每一次,如果程序说k是secret,那么就给scores[k]加1,在运行多次后,我们使用最高的k值作为我们对secret最后的估计。这比仅仅基于一次运行可以得到更可靠的结果
在这里插入图片描述在这里插入图片描述

编译并运行

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值