Windows 20H1中的CET内部机制(上)
2020-07-07 11:40:00 Author: www.4hou.com(查看原文) 阅读量:211 收藏

导语:最近在Windows 10的19H1(1903版)版本中发生了一件非常令人激动的事情,经过多年的讨论,Intel“控制流实施技术”(CET)的终于可以实现了。

最近在Windows 10的19H1(1903版)版本中发生了一件非常令人激动的事情,经过多年的讨论,Intel“控制流实施技术”(CET)的终于可以实现了。以后在每个Windows版本中都增加了更多这样的实现,今年的发行版20H1(2004版)完全支持CET的用户模式影子堆栈功能,该功能将在Intel Tiger Lake CPU中发布。

需要提醒的是,Intel CET是一个基于硬件的缓解机制,它解决了两种通常被利用的控制流完整性违规:前边缘违规(间接调用和JMP指令)和后边缘违规(RET指令)。

前边缘实现不那么有趣,因为它本质上是clang-cfi的一种弱化形式,类似于Microsoft的控制流保护,后边缘实现依赖于ISA中的一个基本变化,即引入一个新的 “影子堆栈”,现在复制进入栈上的返回地址调用指令,而RET指令现在同时验证堆栈和影子堆栈的值,并在其中生成INT#21(控制流保护故障)不匹配的情况。

由于操作系统和编译器有时必须支持除CALL/RET之外的控制流序列(例如异常展开和longjmp),因此有时必须在系统级别操纵“影子堆栈指针”(SSP)以匹配所需的行为,并且反过来,经过验证可避免这种操作本身成为潜在的绕过漏洞。在本文中,我们将介绍Windows如何实现这一目标。

在深入探讨Windows如何为线程操作和验证影子堆栈的方法之前,必须首先了解其实现的两个过程。第一个是SSP的实际位置和权限,第二个是在线程之间进行上下文切换时用于存储/恢复SSP的机制,以及在需要时(例如在异常展开期间)如何对SSP进行修改。

为了解释这些机制,我们必须深入研究Intel最初引入的CPU功能,该功能是为了支持“高级向量扩展”(AVX)指令,并且在Windows 7中首先得到了Microsoft的支持。该功能需要将CONTEXT结构大规模重组为未记录的CONTEXT_EX结构,并添加已记录的本机API和用于操作它的本机API,所以我们也必须讨论它的内部机制!

最后,我们甚至必须通过一些编译器和PE文件格式的内部机制,以及新的进程信息类,以涵盖对Windows上的CET功能的其他细微之处和需求。CET内部机制包含以下许多部分:

· XState内部

· XSAVE区

· XState配置

· XState政策

· CET XSAVE区域格式

· CONTEXT_EX内部

· CONTEXT_EX结构

· 初始化CONTEXT_EX

· 在CONTEXT_EX中控制XState功能掩码

· 在CONTEXT_EX中定位XState功能

· 用法示例和输出

· CONTEXT_EX验证

· NtContinueEx和KCONTINUE_ARGUMENT

· 影子堆栈指针(SSP)验证

· 指令指针(RIP)验证

· 异常解除和longjmp验证

· PE元数据表

· 用户倒排函数

· 动态异常处理程序的持续目标

· 目标验证

XState内部

x86-x64架构类处理器最初是以大多数安全研究人员都熟悉的简单寄存器开始的:通用寄存器(RAX、RCX)、控制寄存器(例如RIP/RSP)、浮点寄存器(XMM、YMM、ZMM),以及一些控制、调试和测试寄存器。但是,随着更多处理器功能的添加,必须定义新的寄存器,以及与这些功能相关的特定处理器状态。由于这些功能中有许多是线程本地的,因此必须在上下文切换期间保存和恢复它们。

作为回应,Intel定义了“扩展状态”(XState)规范,该规范将各种处理器状态与“状态掩码”中的位相关联,并引入诸如XSAVE和XRSTOR之类的指令来从“XSAVE区域”读取和写入请求的状态。由于此区域现在是每个线程的CET寄存器存储的关键部分,并且由于人们最初将重点放在浮点,AVX和“内存保护扩展”(MPX)功能上,因此大多数人都在很大程度上忽略了XSAVE支持。

XSAVE区

XSAVE区域最初用于存储一些新的浮点功能,如Intel已添加到处理器的AVX,以及合并先前通过FXSTOR和FXRSTR指令存储的现有x87 FPU和SSE状态。前两个传统状态被定义为“旧版XSAVE区域”的一部分,并且任何其他处理器寄存器(例如AVX)都被添加到“扩展XSAVE区域”。在这两者之间,使用“XSAVE Area Header”来描述哪些扩展功能是通过一个名为XSTATE_BV的状态掩码呈现的。

同时,添加了一个新的“扩展控制寄存器”(XCR0),它定义了作为XSAVE功能一部分的操作系统支持哪些状态,并添加了XGETBV和XSETBV指令来配置XCR0以及未来可能的XCRs。例如,操作系统可以选择对XCR0进行编程,使其不包含x87 FPU和SSE的功能状态位,这意味着它们将使用旧版FXSTOR指令手动保存此信息,并且仅将扩展功能状态存储在其XSAVE区域中。

随着先进的注册数量的设置和功能的数量增加,例如添加了“保护密钥寄存器用户状态”(PKRU)的“内存保护密钥”(MPK),较新的处理器引入了“超级用户状态”与“超级用户状态”之间的区别。可以通过CPL0代码使用XSAVES和XRSRTORS以及“压缩”和“优化”版本(XSAVEC / XSAVEOPT)进行修改,从而以英特尔典型的方式使问题复杂化。添加了一个名为IA32_XSS的新“特定于模型的寄存器”(MSR),以定义哪些状态仅属于管理员。

1.png

“优化的XSAVE”机制的存在是为了确保只有在最后一次上下文切换(如果有的话)之后,被另一个线程修改过的处理器状态真正写入XSAVE区域,内部处理器寄存器XINUSE用于跟踪此信息。当使用XSAVEOPT时,XSTATE_BV掩码现在仅包括与实际保存的状态相对应的位,而不仅仅是所有请求的状态。

另一方面,“压缩XSAVE”机制修复了XState设计中的一个浪费性缺陷:随着越来越多的扩展功能,例如AVX512和“英特尔处理器跟踪”(IPT)的加入,这意味着即使是线程不使用这些功能,也需要足够大的XSAVE区域分配,并由处理器将其写入(完全为零)。尽管优化的XSAVE可以避免这些写操作,但这仍然意味着,在尚未使用的状态下进行扩展的功能与基本XSAVE区域缓冲区之间的偏移量较大。

使用XSAVEC,仅通过使用空间来保存当前线程实际启用的XState功能,然后依次将每个保存的状态布置在内存中,这样就解决了两者之间存在偏移量的问题。现在,前面显示的XSAVE区域标头使用称为XCOMP_BV的第二个状态掩码进行了扩展,该状态掩码指示被请求的哪个请求状态位可能出现在组合区域中。请注意,与XSTATE_BV不同,此掩码不会忽略不属于XINUSE的状态位,它包括所有可能已经压缩的位。不过你必须仍然检查XSTATE_BV以确定实际存在的状态区。最后,当使用压缩指令时,总是在XCOMP_BV中将位63设置为XSAVE区域的格式指示。

因此,使用压缩和非压缩格式决定了XSAVE区域的内部布局和大小。压缩格式将仅在XSAVE区域中为线程使用的处理器功能分配内存,而非压缩格式将为处理器支持的所有处理器功能分配内存,但仅填充线程使用的功能。下图显示了一个示例,,展示了XSAVE区域对于相同的线程是如何使用的。

2.png

总而言之,使用的XSAVE*/XRSTOR*系列指令是以下指令的组合:

1. 操作系统使用XSETBV指令设置声明在XCR0中支持的状态位;

2. 使用XSAVE指令时,调用者将哪些状态位存储在EDX:EAX中;

3. 如果使用非特权指令,则IA32_XSS中未设置哪些状态位;

4. 在支持“优化的XSAVE”的处理器上,这些状态位是在XINUSE中设置的,XINUSE是一个内部寄存器,它跟踪当前线程自上次转换以来使用的与xstate相关的实际寄存器;

5. 一旦这些位元一起被掩蔽,XSAVE指令就会将最后一组产生的状态位元写入XSAVE区域的一个名为XSTATE_BV的字段的头中。在使用“压缩的XSAVE”的情况下,会将省略项目符号4 (XINUSE)的结果状态位写入XCOMP_BV字段的XSAVE区域的标题中。

XState配置

因为每个处理器都有其自己的一组启用XState的功能,所以Intel通过操作系统在处理XState时应该查询的各种CPUID类来公开所有这些信息。 Windows在启动时执行这些查询,并将信息存储在XSTATE_CONFIGURATION结构中,如下所示(在Winnt.h中记录)。

typedef struct _XSTATE_CONFIGURATION
{
    ULONG64 EnabledFeatures;
    ULONG64 EnabledVolatileFeatures;
    ULONG Size;
    union
    {
        ULONG ControlFlags;
        struct
        {
            ULONG OptimizedSave:1;
            ULONG CompactionEnabled:1;
        };
    };
    XSTATE_FEATURE Features[MAXIMUM_XSTATE_FEATURES];
    ULONG64 EnabledSupervisorFeatures;
    ULONG64 AlignedFeatures;
    ULONG AllFeatureSize;
    ULONG AllFeatures[MAXIMUM_XSTATE_FEATURES];
    ULONG64 EnabledUserVisibleSupervisorFeatures;
} XSTATE_CONFIGURATION, *PXSTATE_CONFIGURATION;

在整理出这些数据之后,内核将这些信息保存在KUSER_SHARED_DATA结构中,可以通过SharedUserData变量访问这个结构,它位于所有Windows平台上的0x7FFE0000。

例如,以下是我们测试的19H1系统的输出,它支持优化和压缩的XSAVE格式,并启用了x87 FPU(0)、SSE(1)、AVX(2)和MPX(3,4)功能位。

dx ((nt!_KUSER_SHARED_DATA*)0x7ffe0000)->XState
    [+0x000] EnabledFeatures  : 0x1f [Type: unsigned __int64]
    [+0x008] EnabledVolatileFeatures : 0xf [Type: unsigned __int64]
    [+0x010] Size             : 0x3c0 [Type: unsigned long]
    [+0x014] ControlFlags     : 0x3 [Type: unsigned long]
    [+0x014 ( 0: 0)] OptimizedSave    : 0x1 [Type: unsigned long]
    [+0x014 ( 1: 1)] CompactionEnabled : 0x1 [Type: unsigned long]
    [+0x018] Features         [Type: _XSTATE_FEATURE [64]]
    [+0x218] EnabledSupervisorFeatures : 0x0 [Type: unsigned __int64]
    [+0x220] AlignedFeatures  : 0x0 [Type: unsigned __int64]
    [+0x228] AllFeatureSize   : 0x3c0 [Type: unsigned long]
    [+0x22c] AllFeatures      [Type: unsigned long [64]]
    [+0x330] EnabledUserVisibleSupervisorFeatures : 0x0 [Type: unsigned __int64]

在“功能”数组中,可以找到以下五个功能中每个功能的大小和偏移量:

dx -r2 (((nt!_KUSER_SHARED_DATA*)0x7ffe0000)->XState)->Features.Take(5)
    [0]              [Type: _XSTATE_FEATURE]
        [+0x000] Offset           : 0x0 [Type: unsigned long]
        [+0x004] Size             : 0xa0 [Type: unsigned long]
    [1]              [Type: _XSTATE_FEATURE]
        [+0x000] Offset           : 0xa0 [Type: unsigned long]
        [+0x004] Size             : 0x100 [Type: unsigned long]
    [2]              [Type: _XSTATE_FEATURE]
        [+0x000] Offset           : 0x240 [Type: unsigned long]
        [+0x004] Size             : 0x100 [Type: unsigned long]
    [3]              [Type: _XSTATE_FEATURE]
        [+0x000] Offset           : 0x340 [Type: unsigned long]
        [+0x004] Size             : 0x40 [Type: unsigned long]
    [4]              [Type: _XSTATE_FEATURE]
        [+0x000] Offset           : 0x380 [Type: unsigned long]
        [+0x004] Size             : 0x40 [Type: unsigned long]

将这些大小相加得出0x3C0,就是上面在FeatureSize字段中看到的值。但是请注意,由于该系统有压缩XSAVE功能,因此此处显示的偏移量无关紧要,只有AllFeatures字段对内核有用,该字段包含每个功能的大小,但不包含其偏移量(因为这将是根据XCOMP_BV中使用的压缩掩码确定)。

XState政策

不幸的是,尽管处理器支持给定的XState功能,但事实往往是由于各种硬件错误,某些特定的处理器可能无法完全实现这些功能。为了处理这种可能性,Windows使用XState策略,它是存储在硬件策略驱动程序的资源部分中的信息,通常称为HwPolicy.sys。

由于英特尔x86架构是多个处理器供应商的组合,所有这些供应商都在相互竞争各自功能集,因此内核必须解析XState策略,并比较当前处理器的供应商字符串和微码版本及其签名、功能和扩展功能(即来自CPUID 01h查询的RAX,RDX和RCX),以在策略中寻找匹配项。

这项工作是在启动时由KiInitializeXSave调用的KiIntersectFeaturesWithPolicy函数完成的,该函数调用KiLoadPolicyFromImage加载适当的XState策略,调用KiGetProcessorInformation以获取前面提到的CPU数据。

这些函数与HwPolicy.sys驱动程序中的资源101一起使用,该驱动程序以以下数据结构开头:

typedef struct _XSAVE_POLICY
{
    ULONG Version;
    ULONG Size;
    ULONG Flags;
    ULONG MaxSaveAreaLength;
    ULONGLONG FeatureBitmask;
    ULONG NumberOfFeatures;
    XSAVE_FEATURE Features[1];
} XSAVE_POLICY, *PXSAVE_POLICY;

例如,在我们的19H1系统上,我们使用Resource Hacker提取的内容如下:

dx @$policy = (_XSAVE_POLICY*)0x253d0e90000
[+0x000] Version       : 0x3 [Type: unsigned long]
[+0x004] Size          : 0x2fd8 [Type: unsigned long]
[+0x008] Flags         : 0x9 [Type: unsigned long]
[+0x00c] MaxSaveAreaLength : 0x2000 [Type: unsigned long]
[+0x010] FeatureBitmask   : 0x7fffffffffffffff [Type: unsigned __int64]
[+0x018] NumberOfFeatures : 0x3f [Type: unsigned long]
[+0x020] Features      [Type: _XSAVE_FEATURE [1]]

对于每个XSAVE_FEATURE,找到一个XSAVE_VENDORS结构的偏移量,该偏移量包含XSAVE_VENDOR结构的数组,每个结构都有一个CPU供应商字符串(目前看来,每个似乎都是“ GenuineIntel”,“ AuthenticAMD”或“ CentaurHauls”),以及XSAVE_CPU_ERRATA结构的偏移量。例如,我们的19H1测试系统发现了Feature 0有以下信息:

dx -r4 @$vendor = (XSAVE_VENDORS*)((int)@$policy->Features[0].Vendors + 0x253d0e90000)
[+0x000] NumberOfVendors  : 0x3 [Type: unsigned long]
[+0x008] Vendor        [Type: _XSAVE_VENDOR [1]]
    [0]           [Type: _XSAVE_VENDOR]
        [+0x000] VendorId      [Type: unsigned long [3]]
            [0]           : 0x756e6547 [Type: unsigned long]
            [1]           : 0x49656e69 [Type: unsigned long]
            [2]           : 0x6c65746e [Type: unsigned long]
[+0x010] SupportedCpu  [Type: _XSAVE_SUPPORTED_CPU]
[+0x000] CpuInfo       [Type: XSAVE_CPU_INFO]
[+0x020] CpuErrata     : 0x4c0 [Type: XSAVE_CPU_ERRATA *]
[+0x020] Unused        : 0x4c0 [Type: unsigned __int64]

最后,每个XSAVE_CPU_ERRATA结构都包含匹配的处理器信息数据,这些数据对应于一个已知的勘误表,该勘误表阻止支持指定的XState功能。例如,在我们的测试系统中,来自上面偏移量的第一个勘误表如下所示:

dx -r3 @$errata = (XSAVE_CPU_ERRATA*)((int)@$vendor->Vendor[0].SupportedCpu.CpuErrata + 0x253d0e90000)
    [+0x000] NumberOfErrata   : 0x1 [Type: unsigned long]
    [+0x008] Errata           [Type: XSAVE_CPU_INFO [1]]
        [0]              [Type: XSAVE_CPU_INFO]
            [+0x000] Processor        : 0x0 [Type: unsigned char]
            [+0x002] Family           : 0x6 [Type: unsigned short]
            [+0x004] Model            : 0xf [Type: unsigned short]
            [+0x006] Stepping         : 0xb [Type: unsigned short]
            [+0x008] ExtendedModel    : 0x0 [Type: unsigned short]
            [+0x00c] ExtendedFamily   : 0x0 [Type: unsigned long]
            [+0x010] MicrocodeVersion : 0x0 [Type: unsigned __int64]
            [+0x018] Reserved         : 0x0 [Type: unsigned long]

可以在我们的GitHub上找到一个工具,该工具可以为所有XState功能转储系统的硬件策略。目前,整个策略中仅显示一个勘误(如上所示)。

最后,以下可选的加载程序命令行选项(以及相应的BCD设置)可用于进一步自定义XState功能:

1. 通过xsavepolicy BCD选项设置XSAVEPOLICY = n加载选项,该选项设置KeXSavePolicyId,指示要加载的XState策略。

2. 通过xsaveremovefeature BCD选项设置XSAVEREMOVEFEATURE = n加载选项,该选项设置KeTestRemovedFeatureMask。这将稍后由KiInitializeXSave解析,并从支持中删除指定的状态位。请注意,无法以这种方式删除状态0(x87 FPU)和状态1(SSE)。

3. XsAVEDISABLE加载选项通过xsavedisable BCD选项设置,该选项设置KeTestDisableXsave,并使KiInitializeXSave将所有XState相关的配置数据设置为0,从而完全禁用整个XState功能。

CET XSAVE区域格式

作为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。

typedef struct _XSAVE_CET_U_FORMAT
{
    ULONG64 Ia32CetUMsr;
    ULONG64 Ia32Pl3SspMsr;
} XSAVE_CET_U_FORMAT, *PXSAVE_CET_U_FORMAT;

typedef struct _XSAVE_CET_S_FORMAT
{
    ULONG64 Ia32Pl0SspMsr;
    ULONG64 Ia32Pl1SspMsr;
    ULONG64 Ia32Pl2SspMsr;
} XSAVE_CET_S_FORMAT, *PXSAVE_CET_S_FORMAT;

就像字段名称所暗示的那样,与CET相关的“寄存器”实际上是存储在各个MSR中的值,通常只能通过0环中的RDMSR和WRMSR特权指令访问这些值。但是,与大多数存储处理器全局数据的MSR不同,CET可以启用每个线程,并且影子堆栈指针显然也是每个线程。由于这些原因,必须将与CET相关的数据作为XState功能的一部分,以便操作系统可以正确处理线程切换。

由于CET寄存器基本上是MSR,通常只能通过内核代码进行修改,因此无法通过CPL3 XSAVE / XRSTOR指令对其进行访问,并且IA32_XSS MSR中的相应状态位始终设置为1。但是,使事情变得更困难的是,操作系统无法完全阻止用户模式代码修改SSP。用户模式代码可能需要作为异常处理、解卷、setjmp/longjmp或特定功能(如Windows的“Fiber”机制)的一部分更新SSP。

因此,操作系统需要为线程提供一种通过系统调用来修改XState中的CET状态的方法,就像Windows提供SetThreadContext作为一种机制来更新某些受保护的CPU寄存器(例如CS和DR7)一样,只要满足某些规则即可。 因此,在下一节中,我们将看到CONTEXT结构如何在更现代的Windows版本上演变为CONTEXT_EX结构,以支持与XState相关的信息,以及如何为合法的与异常相关的场景添加特定于CET的处理,同时还避免了通过损坏的上下文造成的恶意控制流攻击。

CONTEXT_EX内部

为了支持必须在每个上下文开关上保存的越来越多的寄存器,除了旧的CONTEXT结构之外,新版本的Windows还具有CONTEXT_EX结构。之所以需要这样做,是因为CONTEXT是固定大小的结构,而XSAVE引入了对动态大小的处理器状态数据的依赖,该数据取决于线程、处理器甚至设备配置策略。

本文翻译自:https://windows-internals.com/cet-on-windows/如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/53Lq
如有侵权请联系:admin#unsafe.sh