导语:最近在Windows 10的19H1(1903版)版本中发生了一件非常令人激动的事情,经过多年的讨论,Intel“控制流实施技术”(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检查。
本文翻译自:https://windows-internals.com/cet-on-windows/如若转载,请注明原文地址: