Windows 20H1中的CET内部机制(中)

  • A+
所属分类:安全文章

Windows 20H1中的CET内部机制(上)

CONTEXT_EX结构

不幸的是,尽管现在使用了所有内核和用户模式异常处理功能,但CONTEXT_EX结构基本上没有记录,除了意外释放Windows 7标头文件中的某些信息和英特尔参考代码:

//
// This structure specifies an offset (from the beginning of CONTEXT_EX
// structure) and size of a single chunk of an extended context structure.
//
// N.B. Offset may be negative.
//
typedef struct _CONTEXT_CHUNK
{
    LONG Offset;
    DWORD Length;
} CONTEXT_CHUNK, *PCONTEXT_CHUNK;

//
// CONTEXT_EX structure is an extension to CONTEXT structure. It defines
// a context record as a set of disjoint variable-sized buffers (chunks)
// each containing a portion of processor state. Currently there are only
// two buffers (chunks) are defined:
//
// - Legacy, that stores traditional CONTEXT structure;
// - XState, that stores XSAVE save area buffer starting from
// XSAVE_AREA_HEADER, i.e. without the first 512 bytes.
//
// There a few assumptions exists that simplify conversion of PCONTEXT
// pointer to PCONTEXT_EX pointer.
//
// 1. APIs that work with PCONTEXT pointers assume that CONTEXT_EX is
// stored right after the CONTEXT structure. It is also assumed that
// CONTEXT_EX is present if and only if corresponding CONTEXT_XXX
// flags are set in CONTEXT.ContextFlags.
//
// 2. CONTEXT_EX.Legacy is always present if CONTEXT_EX structure is
// present. All other chunks are optional.
//
// 3. CONTEXT.ContextFlags unambigiously define which chunks are
// present. I.e. if CONTEXT_XSTATE is set CONTEXT_EX.XState is valid.
//
typedef struct _CONTEXT_EX
{
    //
    // The total length of the structure starting from the chunk with
    // the smallest offset. N.B. that the offset may be negative.
    //
    CONTEXT_CHUNK All;

    //
    // Wrapper for the traditional CONTEXT structure. N.B. the size of
    // the chunk may be less than sizeof(CONTEXT) is some cases (when
    // CONTEXT_EXTENDED_REGISTERS is not set on x86 for instance).
    // CONTEXT_CHUNK Legacy;
    //

    // CONTEXT_XSTATE: Extended processor state chunk. The state is
    // stored in the same format XSAVE operation strores it with
    // exception of the first 512 bytes, i.e. staring from
    // XSAVE_AREA_HEADER. The lower two bits corresponding FP and
    // SSE state must be zero.
    // CONTEXT_CHUNK XState;
} CONTEXT_EX, *PCONTEXT_EX;

#define CONTEXT_EX_LENGTH ALIGN_UP_BY(sizeof(CONTEXT_EX), STACK_ALIGN)

//
// These macros make context chunks manupulations easier.
//

如图所示,CONTEXT_EX结构始终位于CONTEXT结构的末尾,并具有3个CONTEXT_CHUNK类型的字段,称为All,Legacy和XState。它们中的每一个都定义了与之关联的数据的偏移量和长度,并且存在各种RTL_宏来检索适当的数据指针。

Legacy字段指的是原始上下文结构的开始部分(如果不提供context_extended_register,则在x86上长度可能会更短)。All字段也引用原始上下文结构的开头,但是它的长度描述了所有数据的总体,包括CONTEXT_EX本身和XSAVE区域所需的填充/对齐空间。最后,XState字段引用XSAVE_AREA_HEADER结构(然后该结构定义启用状态位的状态掩码,从而显示其数据)和整个XSAVE区域的长度。

由于所运算很困难,因此Ntdll.dll导出各种API以简化构建、读取、复制以及以其他方式操作存储在CONTEXT_EX中的各种数据。反过来,KernelBase.dll导出了在内部使用这些功能的Win32函数。

初始化CONTEXT_EX

首先,调用者应弄清楚要分配多少内存才能存储CONTEXT_EX,这可以通过使用以下API来完成:

NTSYSAPI
ULONG
NTAPI
RtlGetExtendedContextLength (
    _In_ ULONG ContextFlags,
    _Out_ PULONG ContextLength
);

调用者应提供适当的CONTEXT_XXX标志,以指定要保存的寄存器即CONTEXT_XSTATE。然后,此API读取SharedUserData.XState.EnabledFeatures和SharedUserData.XState.EnabledUserVisibleSupervisorFeatures,并将所有位的并集传递给如下所示的扩展功能(也已导出)。

NTSYSAPI
ULONG
NTAPI
RtlGetExtendedContextLength2 (
    _In_ ULONG ContextFlags,
    _Out_ PULONG ContextLength,
    _In_ ULONG64 XStateCompactionMask
);

请注意,此更新的API如何允许手动指定要实际保存的XState状态,而不是从共享用户数据中的XState配置中获取所有启用的功能。这会导致CONTEXT_EX结构更小,并且不会为所有可能的XState状态数据提供足够的空间,因此将来使用这个CONTEXT_EX时应该确保永远不要在指定的掩码之外利用XState状态位。

接下来,调用者将为CONTEXT_EX分配内存,大多数情况下,Windows将使用alloca()以避免异常路径中的内存耗尽故障,并使用以下一种一个API:

NTSYSAPI
ULONG
NTAPI
RtlInitializeExtendedContext (
    _Out_ PVOID Context,
    _In_ ULONG ContextFlags,
    _Out_ PCONTEXT_EX* ContextEx
);

NTSYSAPI
ULONG
NTAPI
RtlInitializeExtendedContext2 (
    _Out_ PVOID Context,
    _In_ ULONG ContextFlags,
    _Out_ PCONTEXT_EX* ContextEx,
    _In_ ULONG64 XStateCompactionMask
);

与以前一样,较新的API允许手动指定要以压缩形式保存的XState状态,否则所有可用的功能(基于SharedUserData)都将被假定为可用。显然,期望调用者指定与调用RtlGetExtendedContextLength(2)时相同的ContextFlags,以确保上下文结构的大小与分配的大小相同。作为回应,调用者现在会收到一个指向CONTEXT_EX结构的指针,该指针应进入输入的CONTEXT缓冲区。

一旦CONTEXT_EX存在,调用者可能首先对从它那里获得旧的上下文结构感兴趣(不需要对大小做任何假设),这可以通过下一个API实现:

NTSYSAPI
PCONTEXT
NTAPI
RtlLocateLegacyContext (
    _In_ PCONTEXT_EX ContextEx,
    _Out_opt_ PULONG Length,
);

但是,如上所述,这些是Windows NT层公开的未公开文档和内部API,合法的Win32应用程序将改为使用以下函数来简化其与XState兼容的CONTEXT结构的使用:

WINBASEAPI
BOOL
WINAPI
InitializeContext (
    _Out_writes_bytes_opt_(*ContextLength) PVOID Context,
    _In_ DWORD ContextFlags,
    _Out_ PCONTEXT_EX Context,
    _Inout_ PDWORD ContextFlags
);

WINBASEAPI
BOOL
WINAPI
InitializeContext2 (
    _Out_writes_bytes_opt_(*ContextLength) PVOID Context,
    _In_ DWORD ContextFlags,
    _Out_ PCONTEXT_EX Context,
    _Inout_ PDWORD ContextFlags,
    _In_ ULONG64 XStateCompactionMask
);

这两个API的行为类似于使用未公开的API的组合:当调用者首次将NULL作为缓冲区和上下文参数传递时,该函数将在ContextLength中返回所需的长度,调用者应从内存中分配该长度。在第二次尝试时,调用者在缓冲区中传递分配的指针,并在不了解基础CONTEXT_EX结构的情况下在上下文中接收指向CONTEXT结构的指针。

在CONTEXT_EX中控制XState功能掩码

为了访问深入嵌入CONTEXT_EX的XSAVE_AREA_HEADER的Mask字段中的XSTATE_BV(扩展功能掩码),系统导出两个API,以轻松检查CONTEXT_EX中启用了哪些XState功能,并使用相应的API修改XState掩码。

但是请注意,Windows从未在XSAVE区域中存储x87 FPU(0)和SSE(1)状态,而是使用FXSAVE指令,这意味着XSAVE区域将永远不包含旧版区域,并立即以XSAVE_AREA_HEADER开始。因此,Get API总是将下面的2位屏蔽掉。此外,Set API还应确保XState Configuration的EnabledFeatures中存在指定的功能。

请记住,如果在InitializeContext2或内部本机API中指定了硬编码的压缩掩码,则不应使用Set API来淘汰现有的状态位,因为添加新位将意味着额外的,未初始化的输出位。

NTSYSAPI
ULONG64
NTAPI
RtlGetExtendedFeaturesMask (
    _In_ PCONTEXT_EX ContextEx
);

NTSYSAPI
ULONG64
NTAPI
RtlSetExtendedFeaturesMask (
    _In_ PCONTEXT_EX ContextEx,
    _In_ ULONG64 FeatureMask
);

这些API的文档形式如下:

WINBASEAPI
BOOL
WINAPI
GetXStateFeaturesMask (
    _In_ PCONTEXT Context
    _Out_ PDWORD64 FeatureMask
);

NTSYSAPI
ULONG64
NTAPI
SetXStateFeaturesMask (
    _In_ PCONTEXT Context,
    _In_ DWORD64 FeatureMask
);

在CONTEXT_EX中定位XState功能

由于CONTEXT_EX结构的复杂性,以及XState功能可能以压缩形式或非压缩形式存在的事实,并且它们的存在还取决于先前描述的各种状态掩码,调用者需要一个库函数,以便快速轻松地获取指向CONTEXT_EX中XSAVE区域中相关状态数据的指针。

当前存在两个这样的函数,如下所示,RtlLocateExtendedFeature只是RtlLocateExtendedFeat的封装结构,后者为它提供指向SharedUserData.XState的指针作为Configuration参数。当两者都导出时,调用者也可以选择在后者的API中手动指定他们自己的自定义XState配置。

NTSYSAPI
PVOID
NTAPI
RtlLocateExtendedFeature (
    _In_ CONTEXT_EX ContextEx,
    _In_ ULONG FeatureId,
    _Out_opt_ PULONG Length
);

NTSYSAPI
PVOID
NTAPI
RtlLocateExtendedFeature2 (
    _In_ CONTEXT_EX ContextEx,
    _In_ ULONG FeatureId,
    _In_ PXSTATE_CONFIGURATION Configuration,
    _Out_opt_ PULONG Length
);

这两个函数都接收一个CONTEXT_EX结构和一个用于所请求功能的ID,并解析XState配置数据,以返回用于将功能存储在XSAVE区域中的指针。请注意,它们不会验证或返回指定功能的任何实际值,这具体取决于调用者。

为了找到指针,RtlLocateExtendedFeature2执行以下操作:

1. 确保功能部件ID大于2(因为x87 FPU和SSE状态从不通过Windows的XSAVE保存)和小于64(尽可能高的XState功能位);

2. 从CONTEXT_EX + CONTEXT_EX.XState.Offset获取XSAVE_AREA_HEADER;

3. 读取Configuration-> ControlFlags.CompactionEnabled标志以了解是否使用压缩;

如果使用非压缩格式:

读取Configuration-> Features [n] .Offset和.Size,以了解XSAVE区域中所请求功能的偏移量和大小。

如果使用压缩格式:

1. 从XSAVE_AREA_HEADER(对应于XCOMP_BV)中读取CompactionMask,并检查其是否包含请求的功能;

2. 读取Configuration-> AllFeatures以了解其状态位在请求的功能ID之前的所有启用状态的大小,并基于这些大小的总和来计算请求格式的偏移量,将每个先前状态区域的开头对齐为64字节,不过前提是在Configuration-> AlignedFeatures中设置了相应的位,然后最终根据需要将指定的功能ID的区域的开头对齐。

从Configuration.AllFeatures [n]中读取请求的功能的大小

根据上面计算的偏移量在XSAVE区域中定位功能,并返回指向该功能的指针,还可以在输出长度变量中分别显示该功能的大小。

这意味着要使用非压缩格式查找某个功能的地址,只需检查SharedUserData处理器支持哪些功能就足够了。然而,在压缩格式中,不可能依赖于SharedUserData中的偏移量,这就需要检查线程上启用了哪些功能,并根据前面所有功能的大小计算功能的正确偏移量。

在合法的Win32应用程序中,将使用不同的API,该API内部调用上述本地API,但需要进行一些预处理。由于状态位0和1永远不会保存为CONTEXT_EX中的XSAVE区域的一部分,因此Win32 API会通过从相应的Legacy CONTEXT字段中抓取它们来处理这两个功能位,即FltSave(用于XSTATE_LEGACY_FLOATING_POINT)和Xmm0(用于XSTATE_LEGACY_SSE)来处理这两个功能位。

WINBASEAPI
PVOID
WINAPI
LocateXStateFeature (
    _In_ CONTEXT_EX Context,
    _In_ DWORD FeatureId,
    _Out_opt_ PDWORD Length
);

用法示例和输出

为了使XState Internals更加合理,尤其是与CONTEXT_EX数据结构结合使用时,我们编写了一个简单的测试程序,可在GitHub上获得。该实用程序演示了一些API用法以及涉及的各种偏移量、大小和行为。下面是程序在AVX、MPX和Intel PT系统上的输出:

除其他事项外,请注意传统的语境如何按预期方式处于负偏移量,以及即使系统支持x87 FPU状态(1)和GSSE状态(2),XSAVEBV也不按原样包含这些位保存在“旧文本”区域中,因此请注意其相关状态数据的负偏移量。

CONTEXT_EX验证

由于用户模式API可以构造一个CONTEXT_EX并最终由内核处理并修改XSAVE区域的特权部分(即CET状态数据),因此Windows必须防止通过接受CONTEXT_EX的API进行的不良修改,如:

1. NtContinue,用于在异常发生后恢复,处理longjmp CRT功能,以及执行堆栈退卷;

2. NtRaiseException,用于将异常注入到现有线程中;

3. NtQueueUserApc,用于劫持现有线程的执行流;

4. NtSetContextThread,用于修改现有线程的处理器寄存器/状态。

由于这些系统调用中的任何一个都可能导致内核修改IA32_PL3_SSP或IA32_CET_U MSR,以及直接将RIP修改为意外目标,因此Windows必须验证传入的CONTEXT_EX是否违反CET保证。

我们将很快介绍如何在19H1验证SSP,以及如何在20H1验证RIP。但是首先,必须进行少量重构以减少滥用NtContinue的可能性:引入NtContinueEx函数。

NtContinueEx和KCONTINUE_ARGUMENT

如上所述,NtContinue的功能在许多情况下都可以使用,并且为了使CET在允许对处理器状态进行任意更改的API面前具有弹性,必须在接口上添加更精细的控制。这是通过创建一个称为KCONTINUE_TYPE的新枚举完成的,该枚举存在于KCONTINUE_ARGUMENT数据结构中,现在必须将其传递给增强版的NtContinue - NtContinueEx。

此数据结构还包含一个新的ContinueFlags字段,该字段用标志CONTINUE_FLAG_RAISE_ALERT(0x1)替换了NtContinue的原始TestAlert参数,同时还引入了新的CONTINUE_FLAG_BYPASS_CONTEXT_COPY(0x2)标志,该标志直接通过新的TrapFrame传递了APC。这是一种优化,以前是通过检查CONTEXT记录指针是否在用户堆栈中的特定位置来实现的,从而使该函数假定它已被用作用户模式APC传递的一部分。现在,需要此行为的调用者必须在ContinueFlags中显式设置该标志。

请注意,尽管由于某些原因旧的接口仍在使用,但它在内部调用NtContinueEx,它将输入参数识别为BOOLEAN TestAlert参数,而不是KCONTINUE_ARGUMENT。出于新接口的目的,这种情况被视为KCONTINUE_UNWIND。

作为此重构的一部分,存在以下四种可能的类型:

1. KCONTINUE_UNWIND,以前的NtContinue调用程序使用它,例如RtlRestoreContext和LdrInitializeThunk,这是在从异常中unwind时使用的。

2. KCONTINUE_RESUME,KiInitializeUserApc在用户模式栈上构建KCONTINUE_ARGUMENT结构时使用这个函数,KiUserApcDispatcher将在这个用户模式栈上运行,然后再次调用NtContinueEx。

3. KCONTINUE_LONGJUMP,如果异常记录中的异常代码为STATUS_LONGJUMP,则由RtlRestoreContext调用的RtlContinueLongJump使用。

4. KCONTINUE_SET,它不会直接传递到NtContinueEx,而是在响应NtSetContextThread API通过PspGetSetContextInternal调用KeVerifyContextIpForUserCet时使用。

影子堆栈指针(SSP)验证

在某些情况下,用户模式代码需要更改影子堆栈指针,例如异常展开、APC、longjmp等。但是,操作系统必须依次验证为SSP请求的新值,以防止CET绕过。在19H1,这是通过新的KeVerifyContextXStateCetU函数实现的。该函数接收正在修改其上下文的线程以及该线程的新上下文,并执行以下操作:

如果CONTEXT_EX不包含任何XState数据,或者XState数据不包含CET寄存器(通过使用XSTATE_CET_U状态位调用RtlLocateExtendedFeature2进行检查),则无需验证。

如果在目标线程上启用了CET:

1. 通过从XSAVEBV屏蔽XSTATE_MASK_CET_U来验证调用者没有试图在这个线程上禁用CET,如果发生这种情况,该函数将重新启用状态位,在Ia32CetUMsr中设置MSR_IA32_CET_SHSTK_EN(这是启用CET影子堆栈功能的标志),并将当前影子堆栈设置为Ia32Pl3SspMsr。

2. 否则,请调用KiVerifyContextXStateCetUEnabled,以验证是否启用了CET影子堆栈(已启用MSR_IA32_CET_SHSTK_EN),新的SSP是8字节对齐的,并且介于当前SSP值和影子堆栈区域的VAD末尾之间。请注意,由于堆栈向后增长,因此该区域的“末端”实际上是堆栈的开始。因此,在为线程设置新的上下文时,任何SSP值均有效,只要它位于到目前为止线程所使用的影子堆栈部分中。

3. 如果在目标线程上禁用了CET,而调用者试图通过在CONTEXT_EX的XSAVEBV中包含XSTATE_CET_U掩码来启用它,那么只允许将两个MSR值都设置为0(没有影子堆栈,也没有SSP)。

所述验证中的任何失败将返回STATUS_SET_CONTEXT_DENIED,而在其他情况下则返回STATUS_SUCCESS。

启用CET也会隐式启用检查堆栈范围,该功能最初在Windows 8.1中与CFG一起实现,这可以通过KPROCESS的ProcessFlags字段中的CheckStackExtents位看到。这意味着每当验证目标SSP时,还将调用KeVerifyContextRecord,并将验证目标RSP作为当前线程的TEB的用户堆栈限制的一部分(如果这是一个WOW64进程,则是TEB32的用户堆栈限制的一部分)。

指令指针(RIP)验证

在19030年,出现了另一个使用Intel CET的功能,验证调用者试图为该进程设置的新RIP是有效的。与SSP验证一样,仅在为线程启用cet的情况下才能启用此缓解措施。但是,RIP验证默认情况下未启用,必须为该过程启用,这是由EPROCESS的MitigationFlags2Values字段中的usercetsetcontext验证位表示的。

就是说,对于当前版本,似乎在调用CreateProcess并使用PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY属性时,如果PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_ON标志已启用,则将设置该选项。请注意,使用ProcessUserShadowStackPolicy值调用SetProcessMitgationPolicy API是无效的,因为只能在创建流程时启用CET。

然而,有趣的是,在缓解映射中添加了一个新的缓解选项,PS_MITIGATION_OPTION_USER_CET_SET_CONTEXT_IP_VALIDATION(32)。切换这个(未文档化的)缓解选项的效果是在MitigationFlags2Values字段中启用auditusercetsetcontext验证位,稍后将对此进行描述。此外,由于这是第32个缓解选项(每个选项占用4位的DEFERRED / OFF / ON / RESERVED),因此现在需要132个缓解位,并且PS_MITIGATION_OPTIONS_MAP已扩展为3个64位数组元素Map字。

每当要更改线程上下文时,都会调用新的KeVerifyContextIpForUserCet函数。它将检查是否为该线程启用了CET和RIP缓解,还检查了是否在context参数中设置了CONTEXT_CONTROL标志,这意味着RIP将被此新上下文更改。如果所有这些检查均通过,它将调用内部KiVerifyContextIpForUserCet函数。该函数的目的是验证目标RIP是一个有效值,而不是被漏洞利用程序用来运行任意代码的值。

首先,它检查目标RIP地址不是内核地址,也不是低0x10000字节中不应该映射的地址。然后,它检索该基础陷阱框架,并检查目标RIP是否是该陷阱框架的RIP。这意味着允许的情况下,目标RIP是以前的地址在用户模式。这种情况通常发生在这个线程第一次调用NtSetThreadContext时,并且RIP被设置为线程的初始起始地址,但是也可能发生在其他不太常见的情况下。

该函数接收KCONTINUE_TYPE,并根据其值以不同方式处理目标RIP。在大多数情况下,它将在影子堆栈上进行迭代并搜索目标RIP。如果找不到,它将一直运行,直到遇到异常并进入异常处理程序。异常处理程序将检查提供的KCONTINUE_TYPE是否为KCONTINUE_UNWIND,以及是否使用KCONTINUE_UNWIND标志调用RtlVerifyUserUnwindTarget。此函数将尝试再次验证RIP,这一次使用更复杂的检查。

在任何其他情况下,如果在EPROCESS中设置了AuditUserCetSetContextIpValidation标志,它将返回STATUS_SET_CONTEXT_DENIED,这将使KeVerifyContextIpForUserCet调用KiLogUserCetSetContextIpValidationAudit函数的审核失败。这种“审核”非常有趣,因为它不是通过通常的缓解过程ETW通道完成的,而是通过直接通过Windows错误报告(WER)服务,即发送一个0xC000409异常,信息设置为FAST_FAIL_SET_CONTEXT_DENIED。为了避免向WER发送垃圾邮件,使用了另一个EPROCESS位AuditUserCetSetContextIpValidationLogged。

该函数将在找到目标RIP之前停止在影子堆栈上进行迭代,如果线程正在终止并且当前影子堆栈地址是页面对齐的。这意味着对于终止线程,该函数将尝试“尽最大努力”仅在影子堆栈的当前页面中验证目标RIP,。如果在该页面中找不到目标RIP,它将返回STATUS_THREAD_IS_TERMINATING。

此函数的另一种情况是KCONTINUE_TYPE为KCONTINUE_LONGJUMP,不过不会针对影子堆栈验证目标RIP,但将使用KCONTINUE_LONGJUMP标志来调用RtlVerifyUserUnwindTarget,以验证PE映像载荷配置目录的longjmp表中的RIP。我们将在下一部分中描述此表和这些检查。

KeVerifyContextIpForUserCet由以下函数调用:

PspGetSetContextInternal:在响应NtSetContextThread API时调用。

KiVerifyContextRecord:响应NtContinueEx、NtRaiseException和某些情况下NtSetContextThread API调用。在调用KeVerifyContextIpForUserCet之前(仅当接收到的ContinueArgument不为NULL时),此函数检查调用方是否尝试修改CS寄存器,以及新值是否有效,仅允许非WOW64进程将CS设置为KGDT64_R3_CODE,除非它们是微微pico进程。在本文示例中,可以将CS设置为KGDT64_R3_CODE或KGDT64_R3_CMCODE。其他任何值都将使KiVerifyContextRecord强制将新的CS值更改为KGDT64_R3_CODE。 KiVerifyContextRecord由KiContinuePreviousModeUser或KeVerifyContextRecord调用。在第二种情况下,该函数验证RSP是否在一个进程堆栈中(native或wow64),并且64位进程只会将CS设置为KGDT64_R3_CODE。

所有调用KeVerifyContextIpForUserCet来验证目标RIP的路径都首先调用KeVerifyContextXStateCetU来验证目标SSP,并且仅在确定SSP有效时才执行RIP检查。

21.jpg

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

本文来源于互联网:Windows 20H1中的CET内部机制(中)

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: