这篇文章解释了如何使用Meltdown漏洞泄漏某些特定的内核数据并破坏最新的Windows版本(包括“Windows 10” 20H1)中的内核地址空间布局随机化(KASLR),尽管引入了KVA Shadow机制来缓解Meltdown漏洞,但效果似乎并不是很好。
在2018年初,出现了历史上最著名的漏洞之一:Meltdown漏洞。尽管这是一个CPU漏洞,但大多数操作系统(包括Windows)都为该攻击增加了缓解措施。尽管Windows缓解措施有效地防止了Meltdown漏洞,但它也引入了“设计”漏洞,即使在今天,该漏洞也仍然存在。
请注意,我们已就此漏洞与Microsoft联系,他们的立场是KVA Shadow缓解措施并不是要保护KASLR。他们还特别对此进行了说明:
请注意,这种设计选择的一个含义是,KVAshadow不能防止使用投机边信道攻击内核ASLR。鉴于KVAshadow的设计复杂性,所涉及的时间表以及其他边信道漏洞的现实情况,这是一个有意的决定。值得注意的是,容易受到恶意数据缓存载荷影响的处理器通常也容易受到对其BTB(分支目标缓冲区)和其他微体系结构资源的其他攻击,这些攻击可能允许内核地址空间布局泄露给正在执行的本地攻击者任意本机代码。
该文章将说明如何可以使用Meltdown漏洞泄漏某些特定的内核数据并破坏最新Windows版本(包括“Windows 10” 20H1)中的Windows KASLR。
攻击导致的崩溃
Meltdown漏洞攻击包括从任意内核地址中转储数据,这些数据可用于泄漏敏感数据(例如凭据)或内核漏洞利用,从而使漏洞利用更加轻松可靠。分配给Meltdown漏洞的CVE是CVE-2017-5754,它被显示为“恶意数据缓存载荷”漏洞。
与此同时,同一个研究小组发现了第二个漏洞:Spectre。此外,还发现AMD cpu没有受到Meltdown的影响,而是受到Spectre的影响。
需要澄清的是,Meltdown漏洞可以通过CPU微代码更新来修复,或者仅使用基于“Cascade Lake”微体系结构的最新Intel CPU模型来修复Meltdown漏洞。
崩溃详细过程(第一部分)
由于投机执行机制中的内存访问和特权检查之间存在竞争条件,因此有可能发生这种攻击,从而允许从在用户模式下运行的代码读取内核数据。由于用户和内核内存之间的权限,通常这是不可能的。但是,Meltdown漏洞表明,通过滥用投机执行的副作用,这仍然是可能的。
在非常高的级别上,当CPU不知道某些操作(例如内存访问)的结果时,将执行投机执行。 CPU无需等待操作完成,而是“投机”结果并根据此投机继续执行。当结果已知时,投机的代码将被提交(即使其结果可见)或被简单地丢弃。在最好的情况下,CPU预先完成了工作,而在最坏的情况下,它浪费了一些计算能力。
在理解这种攻击之前,我(像大多数人一样)假定投机性执行的影响对于正在运行的代码是不可见的。然而,实际证明这与事实相去甚远。
为了理解我们的意思,让我们先来分析一个类似于原论文的例子,但是用C代替assembly:
leaker_function ( unsigned __int64 pos ) { if ( pos < array_size ) { temp_value = array1 [ array2 [ pos ] ]; } }
现在,让我们假设:
pos被攻击者完全控制;
array_size是1;
array1是一个无符号字符数组,包含256个元素;
array2是一个无符号字符数组,包含256个元素;
temp_value只是读取array1的临时变量;
通过快速分析,达到条件的唯一方法是pos = 0,在该位置读取array2的位置0,并将结果用作读取array1的索引。然后将输出分配给temp_value。
换句话说,任何大于0的pos值都不会到达if条件内的代码。
现在,让我们假设一下该函数是按投机方式执行的,其中假设条件对于任意pos值都是成立的:
leaker_function ( unsigned __int64 pos ) { if ( pos < array_size ) // 1, array_size = 1, but branch predictor assumes branch is taken { temp_value = array1 [ array2 [ pos ] ]; // if pos > 255, out-of-bounds access } }
在本文的示例中,可以读取任何array2位置,然后将加载的字节用作索引来读取array1的一个元素。如果pos值足够大,可以用来读取相对于array2基址的内核内存,例如:
array2 [ 0xffff8000'12345678 ]
或者更好的方法是,按如下所示在任意地址读取一个字节:
array2 [ 0xffff8000'12345678 - &array2 ]
这里的漏洞是该代码由CPU内部执行触发的,然后由于投机不正确而被丢弃。因此,我们无法真正通过array2 [pos]看到加载的字节的内容。
崩溃详细过程(第二部分)
本节是Meltdown漏洞攻击的第二部分,其中第一部分(“流氓数据缓存加载”漏洞)与缓存侧通道攻击相结合。我们说过,不可能看到array2 [pos]的结果,但是它的内容被立即用作array1 [unknown]中的索引。因此,这里的技巧是先确定访问array1的哪个元素。
尽管乍一看似乎不太直观,但是可以通过代码本身来测量特定存储区域(如变量或数组元素)的访问时间。由于CPU使用缓存来存储最近访问的内存的内容,并且缓存比常规内存快得多,因此我们有机会通过观察对不同缓存行的访问时间来推断访问了哪个元素。
推断访问哪个高速缓存行的典型方法是,首先刷新所有高速缓存行,然后触发受害者内存访问,然后定时对每个高速缓存行的访问进行计时,受害者访问所访问的线路将产生最低的时间。此技术通常称为“刷新+重新加载”。
我们可以使用_mm_clflush内在函数来刷新测量数组所有元素的缓存行:
for ( i = 0 ; i < 256 ; i ++ ) { // Invalidating cache _mm_clflush ( &array1 [ i ] ); }
此后,我们通过激活投机执行来执行上述函数(leaker_function)。在最后一步中,有必要知道访问数组的哪个元素。为此,我们只需读取array1的每个元素并使用__rdtscp内部函数来测量访问时间。
让我们来看一个示例,其中pos = 0xffff8000'12345678-&array2,这意味着我们将读取0xffff8000'12345678内核地址:
temp_value = array1 [ array2 [ 0xffff8000'12345678 - &array2 ] ];
假设array2 [0xffff8000'12345678 - &array2]的内容为0x41,则进行如下访问:
temp_value = array1 [ 0x41 ];
因此,以投机方式执行代码时,将访问array1的位置0x41,CPU将继续缓存array1的位置0x41。在逐个数组检查元素时,此位置的访问时间应短于所有其他位置的访问时间。如果发生这种情况,我们可以假定加载的字节值为0x41,这意味着0xffff8000'12345678内核地址的内容为0x41。
概括而言,实施Meltdown漏洞攻击的步骤为:
刷新阵列中每个元素的CPU缓存;
执行“leaker_function”,以便在投机执行期间访问超出范围的内存;
检查数组中每个元素的访问时间并获得最佳的访问时间;
当然,这是关于攻击如何真正起作用的有限解释,必须考虑其他因素,但这足以理解一般原理。
崩溃详细过程(第3部分)
现在的问题是,我们如何启用投机性执行?一种方法是通过创建代码,它执行一些“混淆”的计算,然后把这个放到循环条件中。当CPU检测到此重复代码时,将激活“乱序”执行,该尝试通过查找所有可能的执行路径(有效值或“可能”有效值)来优化执行。根据采用的执行路径,CPU可以预先读取内存。
当执行流最终达到读取该内存的条件时,访问时间应该更快,因为它之前是由CPU缓存的。借助此处的PoC作为启发,我们可以看到一种触发投机者并泄漏内核内存的方法,如下所示:
// Kernel address to be read malicious_pos = kernel_address - &array1; // Looping 33 times for ( i = 0 ; i > 16 ) ); pos = pos & malicious_pos; // Leaking data when branch predictor is working leaker_function ( pos ); }
崩溃详细过程(注意事项)
非常重要的一点是,要想使用这种攻击从用户模式泄漏内核数据,数据必须由CPU缓存,否则就无法泄漏任何东西。由于这个原因,攻击者必须找到一种方法来缓存目标数据,可以通过调用内核API来缓存,也可以通过抛出异常来缓存,但是要确保CPU一直在使用这些数据。
说的另一个重要的事情是,CPU通常使用64字节的高速缓存线路(64字节缓存块),这意味着,如果我们能够迫使CPU缓存的内容内核地址X,则整个缓存行将被加载,这将使我们有机会泄漏此64字节范围的任何字节,而不会将泄漏限制为单个字节。
英特尔分页表
当AMD发行基于Intel x86指令集的64位处理器时,可寻址虚拟内存从2 ^ 32(4 GB)增加到2 ^ 48(256TB)。尽管CPU在64位模式下工作,但只有48位用于寻址虚拟内存(前17位全等于0或1的规范地址)。为此,CPU通过将虚拟内存分为两部分并在中间(非规范地址)放置一个大洞来跳过虚拟地址范围的16位。
虚拟内存范围是:
· 0x00000000'00000000〜0x00007FFF'FFFFFFFF(规范)
· 0x00008000'00000000〜0xFFFF7FFF'FFFFFFFF(非规范)
· 0xFFFF8000'00000000〜0xFFFFFFFF'FFFFFFFF(规范)
为了能够映射48位虚拟内存,CPU将页面调度级别从2增加到了4。
从最高级别到最低级别,分页表的名称为:
· PML4
· PDPT(页面目录指针表)
· PD(页面目录)
· PT(页表)
在这四个分页级别中,每个表都有512个条目(0x200),其中每个条目都是8个字节。因此,每个表的大小为0x200 * 8 = 0x1000(4 KB)。
从最低到最高的顺序:
每个页表项(PTE)可以寻址4KB(0x1000字节);
每个页面目录条目(PDE)可以寻址2MB(0x200 * 4KB = 0x200000字节);
每个页面目录指针表条目(PDPTE)可以寻址1GB(0x200 * 2MB = 0x40000000字节);
每个PML4条目均可寻址512GB(0x200 * 1GB = 0x8000000000字节);
最后,如果将PML4的条目乘以512GB,则可以获得完整的可寻址虚拟内存:
0x200 * 512GB = 0x10000'00000000 (256 terabytes)
在下图中,我们可以看到64位分页级别模型:
快速浏览一下,我们可以看到每个分页级别中的每个条目都具有类似的格式:分为两部分,即权限和条目所指向的物理地址(PFN)。
在最重要的保护位中,我们可以提及:
XD:执行禁用(第63位),通常称为NX位(无执行);
PS:页面大小(第7位),仅存在于PDPTE和PDE中;
U:用户/管理器(第2位);
R:读/写(第1位)
P:存在(0位)
其中每一个位的含义如下:
XD:启用后,内存不可执行,否则可执行
PS:这是一个大页面,否则是一个正常页面
U:可从用户模式访问内存,否则为内核页面(管理器);
R:内存是只读的,否则它也是可写的;
P:内存已映射,否则未映射;
如果启用PS位,则会忽略较低的分页级别,并且根据启用它的级别,页面大小可以为2MB或1GB。
注意:在64位Intel CPU之前,32位CPU使用3级分页系统,以便能够通过使用PAE支持(物理地址扩展)来寻址多达64GB的物理内存,其中PDPT表位于最高级别。
Windows Meltdown漏洞修复
2018年1月3日,Microsoft发布了Meltdown漏洞修复程序。这种Meltdown漏洞缓解措施称为KVA Shadow,它基于Linux实现的KPTI(内核页面表隔离)解决方案。基本上,此缓解措施包括在以用户模式执行时取消对大多数内核内存页面的映射,以减少攻击面。这样,可以缓解Meltdown漏洞攻击,基本上是因为即使CPU容易受到攻击,也不会泄漏任何内容。
缓解措施基于使用两个不同的分页表:一个用于内核模式执行,另一个用于用户模式执行。用于内核模式的PML4已满,因此用户和内核内存已完全映射。相反,用于用户模式的PML4是所谓的Shadow PML4,它映射所有用户内存和内核内存的一小部分。
在下面的屏幕截图中,我们可以看到完整的PML4和不存在许多条目的Shadow PML4。
完整PML4如下:
Shadow PML4
当用户进程调用syscall或仅产生异常(例如,除数为零,无效的访问内存等)时,Shadow和完整分页表之间的转换是在内核模式下完成的。
注意:以“高完整性级别”运行的代码,例如从“以管理员身份运行”选项启动的应用程序,不使用Shadow PML4。
Windows SMEP软件
在Intel Ivy Bridge微体系结构的基础上,引入了基于硬件的监控模式执行预防(SMEP)功能。基本上,添加此功能是为了避免在内核模式下执行用户区代码,这是内核漏洞在过去经常使用的方式。如今,大多数现代计算机都具有此功能。
尽管具有这种硬件保护,并且由于使用两种不同的PML4来实现Meltdown漏洞缓解,所以Microsoft决定利用此功能通过软件实现SMEP来增加安全性。
在下图中,我们可以看到完整PML4中的用户条目:
查看红色标记,我们可以看到PML4的用户条目设置有XD位(请点此),这意味着所有用户域代码在内核模式下都是不可执行的。这样,具有旧CPU模型的计算机就可以防止内核被利用,就像存在SMEP特性一样。简而言之,任何试图执行用户域代码的内核利用都会崩溃。
重要的是要澄清,在返回导向编程时,可以使用"Windows SMEP bypass: U=S"演示文稿中描述的技术直接旁路此SMEP硬件保护,或仅通过从CR4寄存器(第20位)禁用此功能即可。对于基于软件的SMEP实现,可以使用相同的技术,但是有必要通过禁用与目标虚拟地址相关的PML4条目中的XD位来添加额外的步骤。
本文翻译自:https://labs.bluefrostsecurity.de/blog/2020/06/30/meltdown-reloaded-breaking-windows-kaslr/如若转载,请注明原文地址