最近在Windows10的19H1(版本1903)版本中发生了一件非常激动人心的事情-经过多年的讨论,英特尔“控制流实施技术”(CET)的部分实现终于开始了。在每个Windows版本中都添加了更多此实施,今年的版本20H1(2004版)完成了对CET用户模式影子堆栈功能的支持,该功能将在英特尔老虎湖CPU中发布。
需要提醒的是,英特尔CET是一种基于硬件的缓解措施,可解决漏洞利用常用的两种类型的控制流完整性违规问题:前缘违规(间接调用与JMP指令)和后缘违规(RET指令)。
虽然前缘实现不那么有趣(因为它本质上是CLANG-CFI的较弱形式,类似于Microsoft的Control Flow Guard),但后缘实现依赖于ISA中的一个根本变化:引入了一个称为“影子堆栈”的新堆栈,它现在复制由CALL指令压入堆栈的返回地址,RET指令现在验证堆栈和影子堆栈值,并在不匹配的情况下生成INT#21(控制流保护错误)。
因为操作系统和编译器有时必须支持调用/RET以外的控制流序列(例如异常展开和long_mp),所以有时必须在系统级别操作“影子堆栈指针”(SSP),以匹配所需的行为-并反过来进行验证,以避免此操作本身成为潜在的旁路。在这篇文章中,我们将介绍Windows是如何实现这一点的。
在深入研究Windows如何操作和验证线程的影子堆栈之前,必须首先了解其实现的两个部分,第一部分是SSP的实际位置和权限,第二部分是在线程之间进行上下文切换时用于存储/恢复SSP的机制,以及在需要时(例如在异常展开期间)如何对SSP进行修改。
要解释这些机制,我们必须深入研究最初由英特尔引入的英特尔CPU功能,该功能最初由英特尔引入以支持“高级向量扩展”(AVX)指令,并首先由Microsoft在Windows 7中支持。由于添加对此功能的支持需要将上下文结构大规模重组为未记录的CONTEXT_EX结构(以及添加文档和原生API来操作它),因此我们还必须讨论其内部结构!
最后,我们甚至必须了解一些编译器和PE文件格式的内部结构,以及新的进程信息类,以涵盖Windows上CET功能的其他细微之处和要求。我们希望下面的目录能帮助您全面了解这些功能。此外,如果相关,可以根据我们关联的GitHub存储库,通过单击函数名称获得各种新引入的函数的带注释的源代码。
X86-x64体系结构类处理器最初只有一组大多数安全研究人员都熟悉的简单寄存器-通用寄存器(RAX、RCX)、控制寄存器(例如RIP/RSP)、浮点寄存器(XMM、YMM、ZMM)以及一些控制、调试和测试寄存器。然而,随着更多处理器功能的添加,必须定义新的寄存器,以及与这些功能相关联的特定处理器状态。由于这些功能中的许多都是线程的本地功能,因此它们必须在上下文切换期间保存和恢复。
作为回应,英特尔定义了“扩展状态”(XState)规范,该规范将各种处理器状态与“状态掩码”中的位相关联,并引入诸如XSAVE和XRSTOR之类的指令来从“XSAVE区域”读取和写入所请求的状态。由于这一区域现在是每个线程的CET寄存器存储的关键部分,而且由于XSAVE最初关注的是浮点、AVX和“内存保护扩展”(MPX)特性,因此大多数人在很大程度上忽略了XSAVE支持,因此我们认为概述其功能和内存布局会对读者有所帮助。
如前所述,XSAVE区域最初用于存储英特尔添加到处理器中的一些新浮点功能(如AVX),并合并以前通过FXSTOR和FXRSTR指令存储的现有x87 FPU和SSE状态。前两个遗留状态被定义为“遗留XSAVE区域”的一部分,并且任何进一步的处理器寄存器(例如AVX)都被添加到“扩展XSAVE区域”。在这两者之间,“XSAVE Area Header”用于通过称为XSTATE_BV的状态掩码描述存在哪些扩展功能。
同时,添加了一个新的“扩展控制寄存器”(XCR0),它定义了操作系统支持哪些状态作为XSAVE功能的一部分,并添加了XGETBV和XSETBV指令来配置XCR0(以及未来的XCR)。例如,操作系统可以选择将XCR0编程为不包含x87 FPU和SSE的功能状态位,这意味着它们将使用传统FXSTOR指令手动保存此信息,并且仅在其XSAVE区域中存储扩展功能状态。
随着高级寄存器集和功能(如“内存保护密钥”(MPK),增加了“保护密钥寄存器用户状态”(PKRU))的数量增长,较新的处理器引入了“Supervisor State”(只能由使用XSAVES和XRSRTORS的CPL0代码修改)以及“压缩”和“优化”版本(XSAVEC/XSAVEOPT),从而以英特尔典型的方式使问题复杂化。添加了一个新的“型号特定寄存器”(MSR),称为IA32_XSS,用于定义哪些状态仅限主控引擎使用。
“优化的XSAVE”机制的存在是为了确保只有自上次上下文切换(如果有的话)以来被另一个线程实际修改的处理器状态实际上将被写入XSAVE区域。内部处理器寄存器XINUSE用于跟踪此信息。使用XSAVEOPT时,XSTATE_BV掩码现在只包括与实际保存的状态相对应的位,而不仅仅是请求的所有状态的位。
另一方面,“压缩XSAVE”机制修复了XState设计中一个浪费的缺陷:随着越来越多的扩展功能(如AVX512和“Intel Processor Trace”(IPT))的添加,这意味着即使对于不使用这些功能的线程,也需要分配足够大的XSAVE区域,并由处理器写入(全为零)。虽然优化的XSAVE可以避免这些写入,但这仍然意味着,在大的但未使用的状态之后的任何扩展功能都将偏离基本XSAVE区域缓冲区很大的偏移量。
使用XSAVEC,这个问题可以通过以下方式得到解决:只使用空间保存当前线程实际启用(和使用中,因为压缩意味着优化)的XState特性,并在内存中顺序布局每个保存的状态,其间没有间隙(但可能使用固定的64字节对齐,这是通过CPUID作为“对齐掩码”的一部分提供的)。现在,前面显示的XSAVE区域标头使用名为XCOMP_BV的第二个状态掩码进行了扩展,该掩码指示哪些请求的状态位可能存在于计算区域中。请注意,与XSTATE_BV不同,此掩码不会省略不属于XINUSE的状态位-它包括所有可能已压缩的位-仍必须检查XSTATE_BV以确定实际存在哪些状态区域。最后,当使用压缩指令时,XCOMP_BV中的位63始终被设置,作为XSAVE区域具有哪种格式的指示符。
因此,使用压缩格式与非压缩格式确定XSAVE区域的内部布局和大小。压缩格式将只为线程使用的处理器功能分配XSAVE区域中的内存,而非压缩格式将为处理器支持的所有处理器功能分配内存,但只填充线程使用的功能。下图显示了同一线程的XSAVE区域在使用一种格式与另一种格式时的外观示例。
总而言之,XSAVE*/XRSTOR*指令系列将使用的是以下哪一种组合。
操作系统声称其在XCR0中支持的状态位(使用XSETBV指令设置)。
调用方在使用XSAVE指令时在edX:EAX中存储的状态位(英特尔将其称为“指令掩码”)。
在支持“优化XSAVE”的处理器上,在XINUSE中设置哪些状态位,这是一个内部寄存器,用于跟踪自上次转换以来当前线程使用的与XState相关的实际寄存器。
一旦这些位一起被屏蔽,最终的一组结果状态位将由XSAVE指令写入称为XSTATE_BV的字段中的XSAVE区域的标题中。在使用“压缩的XSAVE”的情况下,省略项目符号4(XINUSE)的结果状态位被写入XCOMP_BV字段中的XSAVE区域的报头。下图显示了生成的遮罩。
由于每个处理器都有自己的一组支持XState的功能、潜在大小、功能和机制,因此英特尔通过操作系统在处理XState时应查询的各种CPUID类公开所有这些信息。Windows在引导时执行这些查询,并将信息存储在XSTATE_CONFIGURATION结构中,如下所示(记录在Winnt.h中)。
Tyecif struct_XSTATE_CONFIGURATION{**ULONG64 EnabledVolatileFeature;*ULONG64 EnabledVolatileFeature;*ULong大小;*工会成员{**ULong ControlFlags;**Struct Ulong_Flag:{*ULong OptimizedSave:1;*ULong ComactionEnabled:1;*};**XSTATE_};*XSTATE_E_Enabled:1;*XSTATE_Enabled:1;*ULONG};*ULong ComactionEnabled:1;*XSTATE_};*XSTATE_。*Ulong AllFeatureSize;**Ulong AllFeature[MAXIMUM_XSTATE_FEATURES];*ULONG64 EnabledUserVisibleSupervisorFeature;}XSTATE_CONFIGURATION,*PXSTATE_CONFIGURATION;
整理此数据后,内核将此信息保存在KUSER_SHARED_DATA结构中,该结构可通过SharedUserData变量访问,在所有Windows平台上位于0x7FFE0000。
例如,下面是我们的测试19H1系统的输出,它既支持优化形式的XSAVE,也支持压缩形式的XSAVE,并且使能了x87 FPU(0)、SSE(1)、AVX(2)和MPX(3,4)特征位。
Dx((NT!_KUSER_SHARED_DATA*)0x7ffe0000)->;XState[+0x000]已启用功能:0x1f[类型:无符号__int64][+0x008]已启用卷功能:0xf[类型:无符号__int64][+0x010]大小:0x3c0[类型:无符号长整型][+0x014]控制标志:0x3[类型:无符号长整型][+0x014(0:0)]优化保存:0x1[类型。类型:UNSIGNED__int64][+0x220]AlignedFeature:0x0[Type:UNSIGNED__int64][+0x228]AllFeatureSize:0x3c0[Type:Unsign Long][+0x22c]All Feature[Type:Unsign Long[64]][+0x330]EnabledUserVisibleSupervisor功能:0x0[Type:UNSIGNED_INT64]。
在要素数组中,可以找到这五个要素中每个要素的大小和偏移:
DX-R2(((nt!_KUSER_SHARED_DATA*)0x7ffe0000)->;XState)->;要素。Take(5)[0][Type:_XSTATE_FEATURE][+0x000]偏移量:0x0[Type:UNSIGNED LONG][+0x004]大小:0xa0[Type:UNSIGNED LONG][1][Type:_XSTATE_FEATURE][+0x000]偏移量:0xa0[Type:UNSIGNED LONG][+0x004]大小:0x100[Type:UNSIGNED LONG][2][类型:+0x004]大小:0x100[类型:无符号长整型][3][类型:_XSTATE_FEATURE][+0x000]偏移量:0x340[类型:无符号长整型][+0x004]大小:0x40[类型:无符号长整型][4][类型:_XSTATE_FEATURE][+0x000]偏移量:0x380[类型:无符号长整型][+0x004]大小:0x40[类型:无符号长整型。
将这些大小相加得到0x3C0,这是上面在FeatureSize字段中看到的值。但是,请注意,由于该系统支持压缩的XSAVE功能,因此此处显示的偏移量并不相关,并且只有AllFeature字段对内核有用,它包含每个特性的大小,但不包含其偏移量(因为这将基于XCOMP_BV中使用的压缩掩码来确定)。
不幸的是,即使处理器可能声称支持给定的XState特性,但由于各种硬件勘误表的原因,某些特定的处理器最终可能并不完全或正确地支持该特性。为了处理这种情况,Windows使用XState策略,它是存储在硬件策略驱动程序的资源部分中的信息,通常称为HwPolicy.sys。
由于英特尔x86体系结构是多个处理器供应商的组合,所有供应商都在竞争彼此的功能集变体,因此内核必须解析XState策略,并比较当前处理器的供应商字符串和微码版本及其签名、功能和扩展功能(即CPUID 01h查询中的RAX、RDX和RCX),以便在策略中查找匹配项。
这项工作由KiInitializeXSave调用的KiIntersectFeaturesWithPolicy函数在引导时完成,该函数调用KiLoadPolicyFromImage来加载适当的XState策略,调用KiGetProcessorInformation来获取前面提到的CPU数据,然后通过调用KiIsXSaveFeatureAllowed来验证XState配置中当前启用的每个功能位。
这些函数使用HwPolicy.sys驱动程序中的资源101,该驱动程序以以下数据结构开始:
Tyecif struct_XSAVE_POLICY{*ULONG版本;*ULONG大小;*ULONG FLAGS;*ULONGLONG MaxSaveAreaLength;*ULONGLONG FeatureBitask;*ULONGLONG NumberOfFeature;*XSAVE_Feature Feature[1];}XSAVE_POLICY,*PXSAVE_POLICY;
例如,在我们的19H1系统上,内容(我们使用Resource Hacker提取)如下:
Dx@$POLICY=(_XSAVE_POLICY*)0x253d0e90000[+0x000]版本长度:0x3[类型:无符号长整型][+0x004]大小:长整型:0x2fd8[类型:无符号长整型][+0x008]标志长度:0x9[类型:无符号长整型][+0x00c]MaxSaveAreaLength:0x2000[类型:无符号长整型。
对于每个XSAVE_FEATURE,都会找到XSAVE_VADVIES结构的偏移量,该结构包含一组XSAVE_VADVER结构,每个结构都有一个CPU Vendor字符串(目前,每个字符串似乎是“GenuineIntel”、“AuthenticAMD”或“CentaurHauls”),以及一个到XSAVE_CPU_ERRATA结构的偏移量。例如,我们的19H1测试系统具有以下有关功能0的信息:
DX-R4@$VENDOR=(XSAVE_VANDILES*)((Int)@$POLICY->;Feature[0].Vendors+0x253d0e90000)[+0x000]NumberOfVendors:0x3[Type:UNSIGNED LONG][+0x008]Vendor:[Type:_XSAVE_VADVER[1]][0]Vendor[Type:_XSAVE_VADVER][+0x000]VendorID:[Type:UNSIGNED LONG[3]][0]*Vendor ID:0x75.。类型:0x6c65746e[类型:无符号长整型][+0x010]支持的Cpu[类型:_XSAVE_SUPPORTED_CPU][+0x000]CpuInfo错误类型:[类型:XSAVE_CPU_INFO][+0x020]CpuErrata属性:0x4c0[类型:XSAVE_CPU_ERRATA*][+0x020]未使用的错误数:0x4c。
最后,每个XSAVE_CPU_ERRATA结构都包含匹配的处理器信息数据,该数据对应于阻止指定的XState特性被支持的已知勘误表。例如,在我们的测试系统中,上述偏移量的第一个勘误表是:
DX-R3@$勘误表=(XSAVE_CPU_ERRATA*)((Int)@$VENDOR->;供应商[0].支持的Cpu.CpuErrata+0x253d0e90000)[+0x000]NumberOfErrata:0x1[Type:Unsign Long][+0x008]Errata[Type:XSAVE_CPU_INFO[1]][0][Type:XSAVE_CPU_INFO][+0x000]处理器:0x0[Type:Unsign Charr][+0x002]系列:0x002。类型:UNSIGNED SHORT][+0x00c]ExtendedFamily:0x0[Type:UNSIGNED LONG][+0x010]微码版本:0x0[Type:UNSIGNED__INT64][+0x018]保留:0x0[类型:UNSIGNED LONG]
我们的GitHub上提供了一个工具,可以为所有XState功能转储系统的硬件策略。目前,整个政策中只出现一个勘误表(如上图所示)。
最后,以下可选的加载器命令行选项(以及各自的BCD设置)可用于进一步自定义XState功能:
XSAVEPOLICY=n LOAD选项通过xsavepolicy bcd选项设置,该选项设置KeXSavePolicyId,指示要加载哪些XState策略。
XSAVEREMOVEFEATURE=n LOAD选项,通过设置KeTestRemovedFeatureMask的xsaveremoveFeatureBCD选项设置。这将在稍后由KiInitializeXSave解析,并从支持中删除指定的状态位。请注意,不能以这种方式删除状态0(X87 FPU)和状态1(SSE)。
通过xsaveDisable BCD选项设置的XSAVEDISABLE加载选项设置KeTestDisableXsave,并使KiInitializeXSave将所有与XState相关的配置数据设置为0,从而完全禁用整个XState功能。
作为CET实施的一部分,英特尔在XState标准中定义了两个新位,分别称为XSTATE_CET_U(11)和XSTATE_CET_S(12),分别对应于用户和主管状态。第一种状态是16字节数据结构,MSDN将其记录为XSAVE_CET_U_FORMAT,其中包含IA32_U_CET MSR(其中配置了“影子堆栈启用”标志)和IA32_PL3_SSP MSR(其中存储了“特权级别3 SSP”)。第二个还没有MSDN定义,包括IA32_PL0/1/2_SSP MSR。
Tyfinf struct_XSAVE_CET_U_Format{*ULONG64 Ia32CetUMsr;*ULONG64 Ia32Pl3SspMsr;}XSAVE_CET_U_Format,*PXSAVE_CET_U_Format;tyfinf Struct_XSAVE_CET_S_Format{*ULONG64 Ia32Pl0SspMsr;*ULONG64 Ia32Pl0SspMsr;
顾名思义,与CET相关的“寄存器”实际上是存储在各个MSR中的值,通常只能通过Ring 0中的RDMSR和WRMSR特权指令来访问。然而,与大多数存储处理器全局数据的MSR不同,CET可以在每个线程的基础上启用,并且影子堆栈指针显然也是每个线程的。出于这些原因,必须将与CET相关的数据作为XState功能的一部分,以便操作系统可以正确处理线程切换。
由于CET寄存器基本上是MSR,通常只能由内核代码修改,因此不能通过CPL3 XSAVE/XRSTOR指令访问它们,并且它们各自的状态位在IA32_XSS MSR中始终设置为1。然而,使事情变得更加困难的是,操作系统不能完全阻止用户模式代码修改SSP。作为异常处理、展开、setjmp/long jmp或特定功能(如Windows的“纤程”机制)的一部分,用户模式代码可能合法地需要更新SSP。
因此,操作系统需要为线程提供一种通过系统调用修改XState中的CET状态的方法,就像Windows提供SetThreadContex一样。
.