CVE-2024-6769:毒害激活缓存,将完整性从中提升至高

CVE-2024-6769:毒害激活缓存,将完整性从中提升至高

Ots安全 2024-10-05 14:25

本博客是关于两个连锁漏洞:第一阶段是由于 ROOT 驱动器重新映射导致的 DLL 劫持漏洞,第二阶段是由 CSRSS 服务器管理的激活缓存中毒漏洞。

第一阶段在 Ekoparty 2023 上 由 BlueFrost Security的 Nicolás Economou 在名为“I’m High”的演讲中详细介绍。他解释了如何利用当时 Microsoft 尚未修补的漏洞。这允许 MEDIUM INTEGRITY 用户提升为具有有限的 HIGH 特权,但没有完全访问权限成为完全管理员。

第二阶段并未在该会议上提出,但提出了一些开始研究的步骤。

首先,我们将回顾第一阶段以提供介绍性背景。然后,我们将深入研究我对第二阶段的研究,详细介绍如何从有限的高完整性升级到完全管理员。这包括适用于所有 Windows 版本的两个阶段的完整工作 PoC,已在 Windows 10、Windows 11、Windows Server 2022 和 Windows Server 2019 中成功测试并应用了所有更新。

第一阶段回顾

此阶段的唯一要求是初始过程从中等完整性级别开始,并且用户属于管理员组。

第一阶段的攻击可以概括如下步骤:

1.使用NtCreateSymbolicLinkObject 函数重新映射 ROOT 驱动器。

例如:从以下位置重新映射磁盘:

“ C:\ ” 更改为 “ C:\users\public ”

这也将重新映射“ system32 ”文件夹:

“ C:\windows\system32 ” 更改为 “ C:\users\public\windows\system32 ”

2.重新映射后,一些服务会受到影响,并将尝试从新的、虚假的用户控制的系统32 加载库。

受影响的程序之一是CTFMON,它以高完整性级别运行,但没有管理员权限。

通常,它会尝试从真实的 system32 文件夹加载名为MsCtfMonitor.dll的模块,但由于 ROOT 驱动器已被重新映射,它会在我们伪造的 system32 中查找MsCtfMonitor.dll,我们可以在其中创建并放置一个具有相同名称的精心设计的 DLL。 

3.创建 MsCtfMonitor.dll

此时,通过将我们的MsCtfMonitor.dll版本放在伪造的 system32 文件夹中,将调用其DoMsCtfMonitor函数并以高完整性级别执行我们的代码。

4.在DoMsCtfMonitor函数上放置一个MessageBoxA,当MsCtfMonitor.dll被加载时,它会显示MessageBoxA “TRIGGER”。

5.验证 DLL 是否已加载到以高完整性级别运行的 CTFMON 进程中:

同时,我们可以证实,尽管该进程处于高完整性级别,但没有管理员权限:

第二阶段的利用步骤

在他的 Ekoparty 演示中,Nicolas 建议按照以下步骤完成漏洞利用:

虽然这看起来很简单,但需要大量时间进行逆向和调试。

深入研究这个攻击媒介故事后,很明显,激活上下文缓存的毒害已被用于一些漏洞利用。因此,有必要了解以前如何进行这种利用,以提供更多背景和见解。有关这种利用的详细信息,请参阅 Zero Day Initiative 的文章《 激活上下文缓存毒害:利用 CSRSS 进行权限提升》。

什么是激活缓存?

当程序要加载需要特定版本的库时,就会使用激活缓存。

例如,如果应用程序要加载C:\Windows\System32\comctl32.dll,则无法保证该位置的comctl32.dll是应用程序所需的版本。这是激活上下文缓存的基本用例。程序可以向 CSRSS 服务器发送请求,以处理要输入到缓存中的新激活上下文条目,因此该程序可以加载所需的特定库版本。

为此,需要使用 XML 格式的清单。它通常作为资源嵌入到 EXE 或 DLL 文件中。或者,Windows 将在程序可执行文件所在的同一文件夹中搜索清单文件。

上面提到的 URL 中包含一些旧漏洞利用的 Manifest 文件示例,例如,欺骗系统从攻击者通过 PATH TRAVERSAL 技术到达的受控目录中加载库 advapi32.dll。

当然,一些被利用的攻击媒介被修补了,也发现了一些新的技术。此外,在 2022 年 10 月针对 Windows 11 22H2 的补丁中,还添加了一项新检查。

实施此补丁后,仅当在缓存中添加新条目的进程具有与将使用该条目的进程相同或更高的 RID 时,才可以绕过注册激活上下文 (ACTX)时的检查。

在winnt.h中我们可以看到RID值:

绕过此检查的建议是从运行精心设计的 DLL 的CTFMON进程创建一个具有激活上下文 的请求。此精心设计的 DLL 具有RID=0x3000,在将条目添加到缓存后,具有RID=0x3000的TCMSETUP将加载tapi32.dll。

在尝试执行这些步骤的过程中,我尝试了所有可能的组合来使用 CreateActCtx注册ACTX。但事实证明这是不可能的,因为总有一个检查可以避免这种情况。 

需要注意的是,该函数位于用户空间,由 kernel32.dll 导出。可以通过修补内存中的 DLL 来避免检查,虽然不太优雅,但可行,而且应该可行。

Nicolas 的演示幻灯片建议使用 LOW LEVEL。但是,从眨眼的表情可以看出,在没有补丁的情况下利用此漏洞显然使用CreateActCtx并不是更好的选择。

使用 ALPC 攻击向量毒害激活缓存

高级本地过程调用 (ALPC)是一种跨进程通信机制,用于在 Windows 操作系统内高速发送消息。与标准 Windows API 不同,ALPC不能直接供应用程序使用。相反,它是一种内部机制,只能由 Windows 操作系统的组件访问。(还有我们 😉。)

经过进一步研究,我发现一些旧的缓存中毒漏洞利用ALPC直接与服务器通信。可以在 Philip Tsukerman 的文章《 激活上下文——一个爱情故事》中看到一个例子。

CsrClientCallServer函数实现Win32进程和CSRSS进程之间的ALPC接口。

因此,应该使用CsrClientCallServer尝试调用充当服务器的CSRSS进程。

在寻找旧漏洞利用中的示例时,我在 Packet Storm 上找到了一个 有关堆缓冲区溢出问题的页面。

当使用正确的包调用CSRSS服务器时,它会在属于模块sxssrv.dll的BaseSrvSxsCreateActivationContextFromMessage函数中收到。

该函数只有一个参数:指向接收数据包的指针。为了反转它,我创建了一个自定义的TotalMessage结构。

TotalMessage结构包的前 0x40 字节为HEADER ,后跟嵌入的激活上下文消息,其结构为_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG。

TotalMessage结构如下所示:

以下是_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG结构:

在这个结构中有六个UNICODE_STRINGS对应于语言或CultureFallbacks、AssemblyDirectory、TextualAssemblyIdentity、AssemblyName,以及两个_BASE_MSG_SXS_STREAM结构,每个结构内部包含一个UNICODE_STRING。

以下是_BASE_MSG_SXS_STREAM结构:

鉴于创建服务器接受的有效数据包的难度,值得详细说明如何做到这一点。

_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG中的 Flags 字段值非常重要,因为有多种组合。如果没有正确的标志值,则无法利用该漏洞。

以我的MsCtfMonitor.dll代码为例。经过多次尝试,我得出结论,此漏洞利用的一个正确标志值是0x41:

不同值的组合可能会导致错误的路径标志值:

相同的TotalMessage结构将具有大小为0x40字节的标头。剩余的0x1f8字节保留给_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG结构:

struct TotalMessage{
    signed __int64& pad[8];
    _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG message;
}

分配的大小为0x40+0x1f8:

然后,我把这些字符串组合起来,并对tapi32.dll执行了激活缓存上下文。这是一个很少使用的DLL ,由名为TCMSETUP 的进程加载。它具有高权限完整性级别( RID=0x3000 ),具有与管理员相同的权限。

在我的 DLL 代码中,调用了CaptureUnicodestring 函数。最终调用了CsrCaptureMessageString:

NTSTATUS CaptureUnicodeString(LPVOID CaptureBuffer, PSTR OutputString,
    PCWSTR String, ULONG Length = 0) 
    if (Length == 0) {
        Length = lstrlenW(String);
    }
    return CsrCaptureMessageString(CaptureBuffer, (PCSTR)String, Length * 2,
        Length * 2 + 2, OutputString);
}

此步骤对于正确准备包是必要的,它允许系统将我的数据包的字符串复制到进程CSRSS。这可以保持字符串有效,并将我的指针替换为其上下文中的有效指针。

我还添加了一个嵌入式 XML 清单,其中包含“ Tasks ”语言,这是一种不存在的语言,但它将成为利用的关键(为此感谢 Nico):

我的代码中另一个重要的细节是CaptureBuffer 的创建时间。函数CsrAllocateCaptureBuffer有一个参数,它定义了它应该管理并复制到服务器的 UNICODE_STRINGS数量。

在我的例子中,我使用了“4”个字符串:

值为“4”的参数如下所示:

为了到达激活服务器,CsrClientCallServer函数将从我的MsCtfMonitor.dll发送我的数据包,其 ApiNumber 为0x1001001E,与上面提到的旧漏洞利用程序相同。

Geoff Chappell 的博客提供了有关 CsrClientCallServer 的更多详细信息:

以下是对CsrClientCallServer的调用:

这是要发送的包,用我的 DLL 构建的:

Manifest.Offset值指向我嵌入的 XML Manifest:

记录激活过程的一个有趣命令是sxstrace,它在目标内的管理员控制台中使用。

此命令启用跟踪并将日志结果保存在sxstrace.etl中。(按 ENTER 完成跟踪。)

sxstrace trace -logfile:sxstrace.etl

然后可以将原始 sxstrace.etl 文件转换为可读格式:

>sxstrace parse -logfile:sxstrace.etl -outfile:sxstrace.txt>

系统是否接受我们的激活上下文?

如果包正确,应该到达csrss进程的sxssrv模块中的BaseSrvSxsCreateActivationContextFromMessage函数。所以,在调试远程内核时,需要将上下文切换到此进程。然后,需要重新加载用户模式符号以在其上放置断点:

我使用 IDA PRO 和 Windbg 插件调试内核:

一旦在BaseSrvSxsCreateActivationContextFromMessage处停止,RCX 将指向TotalMessage结构:

在初始的 0x40 HEADER字节之后(由系统填充一些值,如客户端进程的 PID 等…),可以看到属于_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG结构的激活消息:

请注意,指向字符串的指针的值与我发送它们时的值不同:

但他们正确地指出了字符串:

当数据包从客户端发送到服务器时,系统将字符串从我的进程复制到进程CSRSS,并将我的数据包中的指针更改为在其上下文中有效。

之后,函数BaseSrvSxsCreateActivationContextFromMessage检查字符串是否有效。

在循环中,它检查了六个字符串,但完美地通过了检查。在我的例子中,我只通过了四个字符串,其他两个是零。

经过其他一些小检查后,它会调用BaseSrvSxsCreateActivationContextFromStructEx,这是激活过程中最重要的函数:

如何毒害激活缓存?

一旦到达BaseSrvSxsCreateActivationContextFromStructEx,r8将指向_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG,即激活消息:

它评估标志的值。在我的例子中,该值为0x41,而0xD为:

可以使用对应于验证处理器架构 (1) 的标志选项来绕过测试函数。

之后,它会获取调用进程的 RID 并存储起来以供进一步比较。在本例中,由于CTFMON具有高完整性级别,因此 RID 为0x3000 。

该函数最重要的部分是对BaseSrvActivationContextCacheLookupEntry的调用:

它搜索激活上下文缓存 来确定是否有tapi32.dll的条目。

它调用一个名为BaseSrvActivationContextCacheCompareEntries的函数,该函数将激活消息条目的某些部分与缓存中的所有现有条目进行比较:

它将我的数据包中发送的LastWriteTime值与所有条目中的相同值进行比较。

我之前使用tapi32.dll中的GetFileTime 计算了该值,并将其发送到我的激活包中:

由于没有tapi32.dll的条目,因此比较不匹配。正如预期的那样,它返回错误0xC0000225。之后它将检查我的ACTX以查看它是否适合添加到缓存中:

服务器需要读取我的XML 嵌入清单,以及指向它的Manifest.Offset地址。但是,在这个新上下文中,它还不是一个有效的指针。值得在这个值中放置一个断点,以查看如何以及何时使用此值读取我的XML 嵌入清单。

如何读取我的嵌入式 XML 清单?

为了验证CSRSS在哪里读取我在ACTX请求中发送的嵌入式 XML 清单,应在Manifest.Offset中放置断点。此外,如果它复制到另一个地址,则每次停止时都应添加断点。

它在读取Manifest.Offset值地址时停在断点处。

它将使用该地址通过NtReadVirtualMemory从CTFMON进程中读取我嵌入的 XML 清单,因为放置在Manifest.Offset字段中的地址属于该上下文:

我嵌入的 XML 清单被读取并复制到目标缓冲区:

切换到CTFMON进程上下文,验证我嵌入的 XML Manifest是否位于我之前发送的Manifest.Offset地址中。在我的例子中,它是0x7ff93a261470。

嵌入的 XML 清单读取由SxSGenerateActivationContext调用。由于在缓存中没有找到任何有效条目,因此它尝试使用嵌入的清单“生成”它:

从那里,它开始解析我嵌入的 XML 清单。

嵌入式 XML 清单如何解析?

查看最后的调用堆栈,我决定在对RtlReadOutOfProcessMemoryStream 的调用中放置一个断点,以便在缓冲区完全填满时停止。

现在可以在访问字符串“ Tasks ”时放置一个断点,以便在服务器读取或处理它时停止。

以下是嵌入 XML 清单中的任务字符串:

它多次停止读取和复制:

当它将字符串“ tasks ”转换为宽字符时,它在CharEncoder::wideCharFromUtf8 中停止:

然后它在XML 解析器中停止:

正如函数名parseAttributes 所暗示的那样,它继续解析 XML 属性。

然后,它在从ValidateElementAttributes调用的memcpy中停止:

可以在复制的位置放置另一个断点:

它验证语言属性,正如函数名SxspValidateLanguageAttribute所暗示的那样:

它再次在 memcpy 中停止,但是从SxspCreateAssemblyIdentityfromIdentityElement调用:

它再次在 memcpy 中停止,这次从SxsInsertAssemblyIdentityAttribute +0xc48 调用:

然后它在SxsInsertAssemblyIdentityAttribute中停止:

它最后一次调用 memcpy,在这个例子中是从BufferedStream::prepairForInput调用的:

然后它在此处读取字符串任务:

然后,它从这里读取:

从这里继续读下去:

这些函数的名称引起了我的注意。在名称ProbingCandidate中,它包含与SXS txt 日志文件中使用的相同的单词(probing manifests)。 

它又在这里停了下来:

接下来,它使用GetFileAttributesExW检查SXS txt 日志中提到的第一个文件是否存在。由于不存在,因此它返回零。 

文件检查的顺序可以在日志文件中看到:

第二个文件不存在,因为它是任务文件夹中 tapi32.dll 的路径:

从那里开始,它似乎正在任务中“探测” tapi32.manifest:

然后到达CProbedAssemblyInformation::ProbeManifestExistence:

它检查我的清单文件是否存在于任务文件夹中。由于它确实存在,因此它不会返回任何错误:

好了,在“ tasks ”文件夹中找到了tapi32.manifest 。

我嵌入的 XML 清单强制服务器在system32的“ tasks ”子文件夹中搜索清单文件,其中包含“ tasks ”语言值:

如果继续放置断点来查看它使用路径的位置,它会在读取tapi32.manifest文件内容的EncodingStream::Read中停止。

接下来,它将解析TAPI32.manifest 文件内容。如果有错误,它将在SXS TRACE日志中显示该错误,这样可以更轻松地纠正错误。

如果我的TAPI32.manifest文件解析正确,它将返回BaseSrvSxsCreateActivationContextFromStructEx而不会出现错误。这样可以避免打印带有字符串FAILED 的消息。

就我而言,使用我的TAPI32.manifest 文件,激活上下文生成成功。

然后我到达了要将我的条目插入到缓存中的呼叫。

它顺利通过,返回零。这是正确的值,并且已成功插入带有精心设计的TAPI32.manifest 的 条目。

我的条目包含在激活缓存中,并且服务器对来自CTFMON的 DLL 的调用返回 OK 响应。

日志txt文件显示了完整的过程。

它读取嵌入的 XML 清单。由于其语言为“ Tasks ”,因此它会在system32的“ Tasks ”子文件夹中搜索新的清单文件,就像如果语言设置为“ en-us ”,它会在system32的子文件夹中搜索名为“ en-us ”的清单一样。

SXS 日志 txt 文件显示消息“激活上下文生成成功”!

我的伪造的 imm32.dll 是如何被加载的?

将我的ACTX条目添加到缓存后,如果 运行tcmsetup.exe ,它将加载tapi32.dll,并且它应该使用我的清单文件来加载imm32.dll。 

然而,事情并没有那么简单,因为它无法加载imm32.dll,因为有一些检查可以阻止它加载。

检查是在对同一个BaseSrvSxsCreateActivationContextFromStructEx函数的后续调用上完成的,因此请删除所有断点并只留下一个。 

从那里我们可以从控制台运行TCMSETUP.EXE ,尽管我的 PoC在激活缓存中毒完成后从MsCtfMonitor.dll执行 TCMSETUP:

它会多次在断点处停止。每次停止时,都会查看 r8 指向的结构,看它是否对应于与tapi32.dll相关的请求。

在其他模块多次停止后,出现对 TCMSETUP.exe 的请求:

我们在调用堆栈中看到它来自进程创建的那一刻。它被调用来检查激活缓存中是否有TCMSETUP的任何条目。 

保持运行直到调用TAPI32.dll。在此之前,将多次调用TCMSETUP。

最后,到达的数据包必须与我之前在缓存中插入条目时从我的 DLL 生成的数据包非常相似。但是,现在当TCMSETUP尝试加载TAPI32.dll时,它会停止。

此时我注意到这个包中的一些重要价值。

从_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG结构开头向上扩展0x40字节,分配TotalMessage结构。请求TAPI32.dll的进程的 PID是TCMSETUP,因为它要加载 DLL。

将上下文更改为TCMSETUP过程,可以看到Manifest.Offset值指向某些嵌入的 XML 清单。

在记事本中打开 tapi32.dll,可以看到收到的 XML 嵌入清单与文件中包含的清单相同。 

TCMSETUP先前读取文件资源来读取清单并将其作为嵌入的 XML 清单放入包中。 

之后,由BaseSrvActivationContextCacheCompareEntries函数再次进行比较,该函数由BaseSrvSxsCreateActivationContextFromStructEx调用。现在 tapi32.dll 的条目也在缓存中。

BaseSrvActivationContextCacheCompareEntries在一个循环内被调用,以将实际请求与激活上下文缓存的每个条目(就像我的一样)进行比较。

首先,它比较两个LastWriteTime 值,因为它们相等,所以它继续比较更多值。

这个LastWriteTime 值至关重要。如果值不同,它将丢弃我的缓存条目,并且我的imm32.dll将不会被加载。

它继续前进并在下一次检查时停止。

接下来,它检查ResourceName值,该值在两者中都必须是0x7c。

然后它将实际ACTX数据包的语言(即“ en-us ”)与我的缓存条目的语言进行比较。我的缓存条目的语言也是“ en-us ”。

我的包具有相同的语言值:

然后,它比较处理器架构,在本例中两种情况下都是 9:

然后,它比较两个Manifest.path。

我使用系统目录值构建了相同的路径,无需硬编码:

然后它比较AssemblyDirectory,它也是一样的:

如果所有比较都正确,则返回零。这意味着它在激活缓存中找到了我的条目,并将使用它。

请记住,当我第一次发送添加条目的请求时,比较返回了错误,因为缓存中没有TAPI32.dll的条目。由于我的条目之前已添加,因此现在返回零。

之后,它将TCMSETUP的 RID与CTFMON进行比较,由于两者都有RID = 0x3000,因此过程继续。

有关 RID 补丁的完整说明,请参阅 Zero Day Initiative 博客。

这是该补丁的代码:

R15具有调用者 TCMSETUP 的RID = 0x3000,并且缓冲区存储了CTFMON进程的RID=0x3000。

如前所述,微软在 2022 年 10 月添加了此RID 检查补丁。

实施该补丁后,如果您想尝试使用来自MEDUIM 完整性级别进程 (0x2000)的相同MsCtfMonitor.dll将tapi32.dll条目添加到缓存中,该条目将被添加到缓存中,但会失败。这是因为存储了调用方进程0x2000的 RID ,当您尝试执行TCMSETUP并使用RID=0x3000加载 imm32 时,将比较RID并删除该条目。

在该假设情况下,R15将具有请求加载tapi32.dll的TCMSETUP进程的RID=0x3000,“缓冲区”变量将存储将条目添加到具有中等完整性级别的缓存的进程的RID=0x2000。

在最新版本的 Windows 上,如果请求添加条目的进程低于执行程序,并且条目被删除,则缓存中毒将不起作用。在此修补程序之前发布的先前版本将能够正常工作,而不会出现任何 RID 问题。

回到这个案例,RID 检查通过,两个进程具有相同的RID=0x3000。因此,该条目不会被删除,并且会继续运行而不会出现任何错误。

服务器将响应返回给 TCMSETUP。当它加载tapi32.dll时,它将使用tapi32.manifest文件中的我的条目,该文件将从任务文件夹加载imm32.dll。

这是从LoadLibrary到TCMSETUP向激活缓存发出请求的完整过程

加载tapi32.dll时。

BasepCreateActCtx 是向CSRSS服务器发出请求的程序。需要尝试查看它何时最终加载IMM32.dll模块。

查看kernel32.dll,它调用CsrBasepCreateActCtxCommon。里面有一个类似于从我的DLL发出的服务器调用,用于插入我的缓存条目。

它使用与我的相同的ApiNumber。

执行TCMSETUP时,在我的tapi32.manifest文件被接受后,当它从服务器返回时可以在那里放置一个断点。

这是在CsrBasepCreateActCtxCommon中产生对服务器的调用之前的整个调用堆栈。

断点被放置在调用堆栈的某些函数的返回处。

当它停止时,可以观察到imm32.dll从“ tasks ”文件夹加载:

可以使用PROCESS MONITOR来验证TCMSETUP是否从“ tasks ”文件夹加载IMM32.dll。

刚刚执行的CMD进程具有高权限。 

此外,它具有与管理员相同的权限。

有了这些权限,我们现在可以安装任何需要提升为管理员权限的程序,并写入任何文件夹。例如,写入 SYSTEM32 或任何程序安装文件夹,如下面的视频演示所示。

以下是利用之前的权限(完整性级别中等,而非管理员):

现在,以下是利用后的权限(完整性级别高 FULL 管理员):

此时,这是一个轻松提升到 SYSTEM 权限的好机会,可以将一些精心设计的 DLL 放入系统文件夹中。

视频演示和 PoC

完整的视频演示和功能概念证明可在 Fortra 的 github 上找到。 
– https://github.com/fortra/CVE-2024-6769

  • https://github.com/fortra/CVE-2024-6769/blob/main/CVE-2024-6769.mp4

TL; DR: 漏洞利用步骤的简要描述
– 我向CSRSS服务器发送了一条精心设计的ACTX消息。

  • 此ACTX消息有一个嵌入的 XML 清单,并有一个指向它的偏移量。

  • 当服务器收到它时,它使用该偏移量从CTFMON进程上下文中读取嵌入的 XML 清单。

  • 已解析嵌入的 XML 清单。如果被接受,它将尝试从外部文件夹加载第二个外部清单。

  • 要读取的文件夹取决于我控制的嵌入式 XML 清单中的语言字段。

  • 在我的例子中,嵌入的 XML 清单以“ tasks ”作为其语言。因此,它在 system32 的“ tasks ”子目录中搜索外部清单并找到了它。

  • 它解析了我创建的tapi32.manifest文件并接受它,允许它从同一个“任务”文件夹加载外部IMM32.dll。

感谢您抽出

.

.

来阅读本文

点它,分享点赞在看都在这里