CVE-2024-20693:Windows 缓存代码签名操纵

CVE-2024-20693:Windows 缓存代码签名操纵

Ots安全 2024-08-25 15:51

在 2024 年 4 月的补丁星期二更新中,Microsoft 发布了针对我们报告的漏洞 CVE-2024-20693 的修复程序。此漏洞允许操纵可执行文件或 DLL 的缓存签名签名级别。在这篇文章中,我们将描述我们如何发现此问题以及它对 Windows 11 可能产生的影响。

背景

去年,我们启动了一个项目,以提高我们对 Windows 内部机制的了解,特别是关于权限提升等本地漏洞。开始研究新目标的最佳方式是查看其他研究人员的最新出版物。这提供了最新的安全设计概述,允许寻找漏洞的变体,甚至可以绕过已实施的修复程序。

我们发现最有帮助的先前工作是 Google 的 Maddie Stone 和 James Forshaw 在 OffensiveCon 2023 上发表的演讲“
打印后台处理程序中不存在的打印后台处理程序错误
”。
https://youtu.be/H03b0UaogVs 该演讲介绍了在野外发现的 Windows 特权提升漏洞。

使用模拟设备映射和隔离感知 DLL 进行权限提升

如果您还没有看过这个演示,我们在这里总结一下:代表低特权进程处理请求的高特权 Windows 服务可以模拟请求进程,以便使高特权服务执行的所有操作都以低特权进程的权限和特权执行。这是一项很好的安全措施,因为这意味着高特权服务永远不会意外地做低特权进程自己无法做的事情。

需要注意的一点是,Windows 上的(低权限)进程可以更改其设备映射,这可用于将设备字母重定向C:到其他位置(例如特定子文件夹,如C:\fakeroot)。更改的设备映射是模拟中包含的方面之一。这非常危险:如果模拟服务在模拟设置了不同设备映射的另一个进程时尝试加载 DLL 会怎样?该问题已于2015 年由 James Forshaw报告并修复。

但是,如果涉及并行程序集 (WinSxS),确定要加载哪个文件的逻辑LoadLibrary可能会非常复杂。在 Windows 上,可以安装 DLL 的多个不同版本,并且可以使用清单文件来指定要为特定应用程序加载哪个版本。DLL 文件还可以包含嵌入式清单,以指定要加载其版本依赖项的哪个版本。这些被称为“隔离感知”DLL。

所利用漏洞的核心在于,当加载隔离感知 DLL 文件时,模拟设备映射将用于查找其依赖项的清单。通过将其与清单文件中的路径遍历相结合,可以让特权服务从较低特权进程指定的磁盘文件夹中加载 DLL。加载此恶意 DLL 会导致权限提升(加载恶意代码时,模拟设计不再提供任何安全性,因为它可以恢复模拟)。要使此攻击成功,模拟服务必须加载隔离感知 DLL,该 DLL 依赖于至少一个其他 DLL。

Microsoft 为解决 Maddie Stone 和 James Forshaw 演讲中提到的问题而采取的修复措施是应用一种新的缓解措施,以禁用使用模拟设备映射加载 WinSxS 清单,但仅限于已明确选择加入的进程。此标志仅针对已知易受攻击的少数服务设置。这意味着还有很多特权服务可以检查相同的问题。Maddie 和 James 非常有帮助地解释了如何在 Windows 上配置进程监视器以查找这些问题:

因此,我们开始着手查找此类问题。我们列出了至少依赖另一个库的隔离感知 DLL 列表,按照说明设置了进程监视器过滤器,并编写了一个简单的 PowerShell 脚本(使用James Forshaw 的NtObjectManager PowerShell 模块)来枚举所有 RPC 服务并尝试调用所有方法。然后,我们使用清单将模拟下加载的库与 DLL 列表进行交叉引用。

我们找到了一个匹配项:wscsvc.dll!

WSVC.dll

当在此服务上调用编号为 12 的 RPC 端点(不接受任何参数)时,它会间接加载gpedit.dll。这是一个隔离感知 DLL,它依赖于(其中包括)comctl32.dll。我们复制了野外漏洞的设置,在 处创建一个新目录C:\fakeroot,添加了所需的清单和 DLL 文件,重定向C:到C:\fakeroot然后发送此 COM 消息。

并且它几乎成功了。进程监视器显示它打开并读取了我们的伪造 DLL 文件,但从未到达“加载图像”,这是实际代码执行开始的步骤。不知何故,它解析了我们的 DLL,但拒绝执行其代码。

然后我们发现与 wscsvc.dll 服务关联的进程,即“Windows 安全中心服务”,被归类为 PPL(受保护进程轻量级)。这意味着它对其加载的 DLL 文件的代码签名进行了限制。

受保护进程(轻量)

Windows 可识别多种不同的保护级别,以防止进程被较低保护级别的进程“修改”(例如终止进程、修改内存、添加新线程)。例如,这可以使禁用 AV 工具或重要的 Windows 服务变得更加困难。

从 Windows 11 开始,保护级别如下:

等级 价值
应用程序 8
系统软件 7
温控器 6
视窗 5
拉美人 4
反恶意软件 3
代码生成 2
验证码 1
没有任何 0

一个操作是否被允许,由一个称为 的表来决定RtlProtectedAccess。我们将其总结如下:

→Target ↓Requesting Authenti- code CodeGen Anti- malware Lsa Windows WinTcb WinSystem App
Authenticode
CodeGen
Antimalware
Lsa
Windows
WinTcb
WinSystem

大致可以总结如下:Windows、WinTcb 和 WinSystem 形成一个层次结构(Windows < WinTcb < WinSystem)。Authenticode、CodeGen、Antimalware 和 Lsa 是单独的组,仅允许从同一组或 Win-* 层次结构中的进程进行访问。我们不确定“App”是如何使用的,它是 Windows 10 中的新功能,并且没有很好的文档记录。

此外,受保护进程 (PP) 和受保护进程 Light (PPL) 之间存在差异:受保护进程可以修改受保护进程 Light(基于上表),但反之则不行。一些示例是用于第三方安全工具的反恶意软件 PPL 和用于关键 Windows 服务(如管理 DRM)的 WinTCB 或 Windows at PP。请记住,这些保护级别也是 Windows 中所有其他授权检查(如完整性级别)的补充。有关受保护进程的更多信息,请参阅https://itm4n.github.io/lsass-runasppl/。

请注意,Microsoft 并不认为这是受保护的安全边界:以管理员身份运行的进程可以加载可利用的内核驱动程序,该驱动程序可用于修改所有受保护的进程。由于 Microsoft 认为从管理员到内核不是安全边界,因此受保护的进程也不能成为管理员的安全边界。

除了被其他进程操纵的限制之外,受保护的进程在加载哪些 DLL 方面也受到限制。例如,反恶意软件服务只能加载使用相同代码签名证书或 Microsoft 本身签名的 DLL。来自保护反恶意软件服务:

DLL 签名要求

任何加载到受保护服务中的非 Windows DLL 都必须使用与签署反恶意软件服务相同的证书进行签署。

对于受保护的进程,通常只允许加载使用特定签名级别签名的 DLL 。签名级别是根据用于代码签名的证书及其颁发者分配给 DLL 的。何时哪个 PPL 级别可以加载具有特定签名级别的 DLL 的具体规则非常复杂(这些规则甚至可以使用安全启动策略进行自定义),我们在此不再赘述。但总结一下:只有某些 Windows 签名的 DLL 才被允许加载到我们的目标服务中。

此时我们有两个选择:找到具有相同 WinSxS 模拟漏洞的其他服务,或者尝试绕过 Windows DLL 文件的签名。最有可能产生结果的方法当然是寻找其他服务,但该项目的目标是更好地了解 Windows 内部结构,因此我们决定花一点时间来了解 DLL 文件的签名方式。

DLL 签名

PE 文件的签名过程称为Authenticode。与 TLS 一样,它基于 X.509 证书。Authenticode 签名是通过计算 PE 文件的哈希值(忽略签名后会发生变化的某些字段,例如校验和和签名本身的部分)生成的,然后对该哈希值进行签名并将其与证书链(以及可选的时间戳)一起附加到文件中。

由于签名验证速度较慢且 Windows 上经常加载 DLL,因此实现了一种代码签名缓存方法。对于签名的 DLL 或 EXE 文件,证书验证的结果可以存储在名为 的 NTFS 扩展属性 (EA) 中
$KERNEL.PURGE.ESBCACHE。
$KERNEL此名称的部分表示仅 Windows 内核可以设置或更改此 EA。部分PURGE表示如果文件内容被修改,EA 将被自动删除。这意味着如果不删除 EA,就不可能从用户模式设置此 EA 或修改文件。这仅适用于日志记录的 NTFS 分区,因为功能PURGE取决于日志。请注意,此 EA 中的任何内容都不会将其绑定到文件:这些属性包含日志 ID,但不包含文件路径或 inode 编号之类的内容。

2017 年,James Forshaw 曾报告说,可以对这种 EA 的应用进行竞争:通过使文件引用目录,可以减慢验证速度,足以在签名验证和 EA 应用之间修改文件内容。由于这一问题很早就被发现了,因此这种做法不太可能奏效。

我们尝试将文件放在 SMB 共享上,并尝试在验证和图像加载之间重写内容,但这也不起作用(文件只被读取一次)。但查看我们的 Wireshark 捕获和 CI.DLL 中解析
$KERNEL.PURGE.ESBCACHE扩展属性的反编译代码,我们注意到一些突出的东西:

扩展属性
$KERNEL.PURGE.ESBCACHE应仅在本地卷上受信任,因为(例如)USB 驱动器或已安装的磁盘映像上的文件系统可能已被用户操纵。我们假设代码中有一个检查是为了检查这一点,并且仅允许使用该功能的本地启动磁盘
CipGetVolumeFlags。

__int64 __fastcall CipGetVolumeFlags(__int64 file, int *attributeInformation, _BYTE *containerState)
{
  int *v6; // x20
  BOOL shouldFree; // w21
  int ioctlResponse; // w9
  unsigned int err; // w19
  unsigned int v10; // w19
  __int64 buffer; // x0
  int outputBuffer; // [xsp+0h] [xbp-40h] BYREF
  int returnedOutputBufferLength; // [xsp+4h] [xbp-3Ch] BYREF
  int fsInformation[14]; // [xsp+8h] [xbp-38h] BYREF

  outputBuffer = 0;
  returnedOutputBufferLength = 0;
  memset(fsInformation, 0, 48);
  v6 = fsInformation;
  shouldFree = 0;
  // containerState will be set based on the response to the ioctl with ID 0x90390LL on the file
  if ( (int)FsRtlKernelFsControlFile(file, 0x90390LL, 0LL, 0LL, &outputBuffer, 4LL, &returnedOutputBufferLength) >= 0 )
    ioctlResponse = outputBuffer;
  else
    ioctlResponse = 0;
  outputBuffer = ioctlResponse;
  *containerState = ioctlResponse & 1;
  // attributeInformation will be set based on the IoQueryVolumeInformation for FileFsAttributeInformation (5)
  err = IoQueryVolumeInformation(file, 5LL, 48LL, fsInformation, &returnedOutputBufferLength);
  if ( err == 0x80000005 )
  {
    // Retry in case the buffer is too small
    v10 = fsInformation[2] + 8;
    buffer = ExAllocatePool2(258LL, (unsigned int)(fsInformation[2] + 8), 'csIC');
    v6 = (int *)buffer;
    if ( !buffer )
      return 0xC000009A;
    shouldFree = 1;
    err = IoQueryVolumeInformation(file, 5LL, v10, buffer, &returnedOutputBufferLength);
  }
  if ( (err & 0x80000000) == 0 )
    *attributeInformation = *v6;
  if ( shouldFree )
    ExFreePoolWithTag(v6, 'csIC');
  return err;
}

这是来自以下地址的调用
CipGetFileCache:

__int64 __fastcall CipGetFileCache(
        __int64 fileObject,
        unsigned __int8 a2,
        int a3,
        unsigned int *a4,
        _DWORD *a5,
        unsigned __int8 *a6,
        int *a7,
        __int64 a8,
        _DWORD *a9,
        _DWORD *a10,
        __int64 a11,
        __int64 a12,
        _QWORD *a13,
        __int64 *a14)
{
  __int64 eaBuffer_1; // x20
  unsigned __int64 v17; // x22
  unsigned int fileAttributes; // w25
  unsigned int attributeInformation_FileSystemAttributes; // w19
  unsigned int err; // w19
  unsigned int err_1; // w0
  __int64 v22; // x4
  __int64 v23; // x3
  __int64 v24; // x2
  __int64 v25; // x1
  int containerState_1; // w10
  unsigned int v28; // w8
  __int64 eaBuffer; // x0
  _DWORD *v30; // x23
  unsigned __int8 *v31; // x24
  int v32; // w8
  char v33; // w22
  const char *v34; // x10
  __int16 v35; // w9
  char v36; // w8
  unsigned int v37; // w25
  int v38; // w9
  int IsEnabled; // w0
  unsigned int v40; // w8
  unsigned int ContextForReplay; // w0
  __int64 v42; // x2
  _QWORD *v43; // x11
  int v44; // w10
  __int64 v45; // x9
  unsigned __int8 containerState; // [xsp+10h] [xbp-C0h] BYREF
  char v47[7]; // [xsp+11h] [xbp-BFh] BYREF
  _DWORD *v48; // [xsp+18h] [xbp-B8h]
  unsigned __int8 *v49; // [xsp+20h] [xbp-B0h]
  unsigned __int8 v50; // [xsp+28h] [xbp-A8h]
  unsigned __int64 v51; // [xsp+30h] [xbp-A0h] BYREF
  unsigned int v52; // [xsp+38h] [xbp-98h]
  int attributeInformation; // [xsp+3Ch] [xbp-94h] BYREF
  int v54; // [xsp+40h] [xbp-90h] BYREF
  int lengthReturned_1; // [xsp+44h] [xbp-8Ch] BYREF
  int lengthReturned; // [xsp+48h] [xbp-88h] BYREF
  int v57; // [xsp+4Ch] [xbp-84h]
  __int64 v58; // [xsp+50h] [xbp-80h]
  __int64 v59; // [xsp+58h] [xbp-78h]
  __int64 v60; // [xsp+60h] [xbp-70h]
  _QWORD *v61; // [xsp+68h] [xbp-68h]
  int *v62; // [xsp+70h] [xbp-60h]
  int eaList[8]; // [xsp+78h] [xbp-58h] BYREF
  char fileBasicInformation[40]; // [xsp+98h] [xbp-38h] BYREF

  [...]

  if ( (*(_DWORD *)(*(_QWORD *)(fileObject + 8) + 48LL) & 0x100) != 0 )
  {
    containerState_1 = 0;
  }
  else
  {
    lengthReturned_1 = 0;
    memset(fileBasicInformation, 0, sizeof(fileBasicInformation));
    err = IoQueryFileInformation(fileObject, 4LL, 40LL, fileBasicInformation, &lengthReturned_1);
    if ( (err & 0x80000000) != 0 )
    {
      [...]
      goto LABEL_8;
    }
    fileAttributes = *(_DWORD *)&fileBasicInformation[32];
    // Calling the function above
    err_1 = CipGetVolumeFlags(fileObject, &attributeInformation, &containerState);
    v17 = v51;
    err = err_1;
    if ( (err_1 & 0x80000000) != 0 )
    {
      *a4 = 27;
LABEL_7:
      v22 = *a4;
      goto LABEL_8;
    }
    attributeInformation_FileSystemAttributes = attributeInformation;
    containerState_1 = containerState;
  }
  // If the out variable containerState was non-zero, all of the checks don't matter and we go to LABEL_19 to read the EA.
  if ( (*(_DWORD *)(*(_QWORD *)(fileObject + 8) + 48LL) & 0x100) != 0 || containerState_1 )
    goto LABEL_19;
  if ( (g_CiOptions & 0x100) == 0 )
  {
    if ( (attributeInformation_FileSystemAttributes & 0x20000) == 0 || (fileAttributes & 0x4000) == 0 )
    {
      *a4 = 5;
      v17 = fileAttributes | ((unsigned __int64)attributeInformation_FileSystemAttributes << 32);
      err = 0xC00000BB;
      goto LABEL_7;
    }
    goto LABEL_23;
  }
  if ( (attributeInformation_FileSystemAttributes & 0x20000) != 0 && (fileAttributes & 0x4000) != 0 )
  {

  [...]

  }
LABEL_19:
  eaBuffer = ExAllocateFromPagedLookasideList(&g_CiEaCacheLookasideList);
  eaBuffer_1 = eaBuffer;
  if ( !eaBuffer )
  {
    v28 = 28;
    err = 0xC0000017;
    goto LABEL_12;
  }
  v33 = v50;
  eaList[0] = 0;
  LOBYTE(eaList[1]) = 22;
  if ( v50 )
  {
    v34 = "$Kernel.Purge.CIpCache";
    *(_OWORD *)((char *)&eaList[1] + 1) = *(_OWORD *)"$Kernel.Purge.CIpCache";
  }
  else
  {
    v34 = "$Kernel.Purge.ESBCache";
    *(_OWORD *)((char *)&eaList[1] + 1) = *(_OWORD *)"$Kernel.Purge.ESBCache";
  }
  v35 = *((_WORD *)v34 + 10);
  *(int *)((char *)&eaList[5] + 1) = *((_DWORD *)v34 + 4);
  v36 = v34[22];
  *(_WORD *)((char *)&eaList[6] + 1) = v35;
  HIBYTE(eaList[6]) = v36;
  err = FsRtlQueryKernelEaFile(fileObject, eaBuffer, 380LL, 0LL, eaList, 32LL, 0LL, 1LL, &lengthReturned);
  if ( (err & 0x80000000) != 0 )
  {
    *a4 = 2;
LABEL_34:
    v30 = v48;
    v31 = v49;
LABEL_35:
    ExFreeToPagedLookasideList(&g_CiEaCacheLookasideList, eaBuffer_1);
    v17 = v51;
    goto LABEL_36;
  }
  err = CipParseFileCache(eaBuffer_1, v33, (int *)a4, &v51, eaBuffer_1 + 488);
  if ( (err & 0x80000000) != 0 )
    goto LABEL_34;
  v37 = v57;
  err = CipVerifyFileCache((__int64 *)(eaBuffer_1 + 488), eaBuffer_1, fileObject, v57, v58, &v54, (int *)a4, &v51);

  [...]

  return err;
}

我们假设 ioctl 会由 SMB 驱动程序处理(使用代码0x90390,该代码没有正式记录,但可能参考FSCTL_QUERY_VOLUME_CONTAINER_STATE,基于Microsoft 的 Rust 标头),结果却是通过 SMB 转发到服务器的 ioctl。(虽然我们称之为NTFS扩展属性,但这些扩展属性实际上也可以通过 SMB 工作。)

如果该 icotl 导致设置了最低位的值,则containerState/containerState_1将CipGetFileCache变为非零,并且代码将跳转到LABEL_19上面(跳过对文件类型、设备类型和g_CiOptions我们也不完全理解的全局的大量检查)。

换句话说:如果 SMB 服务器本身响应此 ioctl 表示应该信任,则$KERNEL.PURGE.ESBCACHESMB 共享上文件的扩展属性是受信任的!这当然是一个问题,因为默认情况下非管理员用户可以挂载新的网络共享。

我们首先使用 samba,然后对其进行修补,使其始终响应0x00000001此 ioctl(目前尚未实现),并实现了另外两个 ioctl:(0x900f4)FSCTL_QUERY_USN_JOURNAL用于读取日志信息和0x900ef(FSCTL_WRITE_USN_CLOSE_RECORD)用于刷新日志。我们将 Samba 配置为使用 ext3 扩展属性来存储用于 SMB 的 EA。

成功了!从运行 Samba 的 Linux 服务器,我们可以$KERNEL.PURGE.ESBCACHE对文件应用任何属性,Windows 会信任它。在 Linux 上,可以使用 来设置 Samba 使用的扩展属性setfattr。

setfattr -n 'user.$KERNEL.PURGE.ESBCACHE' -v '0skwAAAAMAAg4AAAAAAAAAAIC1e18kqdkBQgAAAHUAJwEMgAAAIGliE1R8dXRmTogdh511MDKXHu0gQC2E1gewfvL5KmZ+JwAMgAAAIGOIg7QdUUiX461yis071EIc4IyH1TDa1WkxRY/PW8thJwQMgAAAIDSDabKZZ2jBOK8AdcS2gu8F0miSEm+H/RilbYQrLrbj' "$1"

我们现在可以创建伪造的 EA,并指定我们想要的任何代码签名级别。我们如何才能滥用此功能?

结合 DLL 加载和签名绕过

现在我们面临下一个挑战:如何将这两个漏洞结合起来?我们可以wscsvc.dll使用路径遍历加载我们自己的 DLL,但无法从C:SMB 共享中进行路径遍历。符号链接可以工作,但默认情况下,Windows 上的非管理员用户不允许创建这些链接。Windows 支持的目录连接和其他类似符号链接的构造不能指向 SMB 共享。

如果用户插入带有指向 SMB 共享的符号链接的 NTFS 格式的 USB 设备,我们就可以执行攻击。然后,用户可以从C:设备映射中的新挂载点到 USB 磁盘创建目录连接。

C:\fakeroot --(directory junction)--> E:\ --(symlink)--> \\sambaserver\mount

但这需要物理访问机器。我们更喜欢可以远程操作的东西。

因此我们需要第三个漏洞:以非管理员用户身份创建符号链接。

我们尝试了各种各样的方法,比如挂载磁盘映像或解压带有符号链接的 zip 文件,但在我们找到方法之前,微软在 2023 年 8 月推出了一个更广泛的修复程序,以解决模拟设备地图下 WinSxS 清单加载的问题(CVE-2023-35359):设备地图不再是进程的选择,而是在读取清单时始终被忽略。

这意味着我们的 DLL 加载漏洞wscsvc.dll不再起作用,但我们仍然可以绕过签名。那么,下一个问题是:在 Windows 上,我们能用缓存签名级别操作做什么?

应用签名绕过

使用.theme 文件将权限提升至 SYSTEM

在上一篇文章“在 Windows 上成功获取 SYSTEM 权限”中,我们展示了如何通过竞争 Windows.theme文件中包含的 DLL 的签名检查来提升 Windows 权限。在那篇文章中,我们使用了竞争条件,但我们最初是通过
$KERNEL.PURGE.ESBCACHE在
*.msstyles_vrf.dll文件上设置可操纵的属性来找到它的。其工作原理基本相同:我们设置一个新主题,该主题引用专门制作的
.msstyles文件。在文件旁边
.msstyles,我们放置一个
.msstyles_vrf.dll文件。当用户登录(或将 DPI 缩放比例设置为 >100%)时,WinLogon.exe(以 SYSTEM 身份运行)将检查此 DLL 文件的签名级别,如果它至少在级别 6(“存储”)上签名,它将加载它,从而提升我们的权限。

*.msstyles_vrf.dll
随着微软针对 CVE-2023-38146 彻底移除了主题文件的加载,该问题也得到了修复。

绕过 WDAC

缓存签名用于可执行文件的一个地方是 Windows Defender 应用程序控制 (WDAC),这是 Windows 上可执行文件的允许列表技术。此功能可用于(通常在企业环境中)限制用户可以运行哪些应用程序以及可以加载哪些 DLL。可以根据文件路径、文件哈希以及代码签名者的身份将二进制文件列入允许列表。WDAC 策略非常强大且细化,因此使用它的每个公司可能都有自己的策略,但默认模板允许运行 Microsoft 自己签名的所有软件。

假设 WDAC 策略允许所有由 Microsoft 签名的软件,我们可以向任何可执行文件添加表明 Microsoft 为签名者的 EA 并运行它。

将代码注入受保护的进程

管理员还可以使用签名绕过将代码注入受保护的进程(无论级别如何)。例如,通过将 system32 中的 DLL 替换为指向 SMB 共享的符号链接,然后启动作为受保护进程运行的服务。

请记住,Microsoft 不将此视为安全边界,这也意味着滥用此边界的已知技术不会得到修复。因此,为了进行演示,我们将它与ANGRYORCHARD使用的方法相结合,将我们的线程标记为内核模式线程,然后将设备的物理内存映射到我们的进程中。

结合所有步骤
1. 我们在文件上使用修改后的 EA
.msstyles_vrf.dll来绕过 Winlogon.exe 中的签名验证,从而将权限提升到
SYSTEM。

  1. 我们将 中的 DLL 文件替换为
    system32指向 SMB 共享上带有操纵缓存签名的文件的符号链接。然后,我们启动一个在级别
    WindowsTCB(我们选择
    services.exe)上运行的受保护进程。

  2. 我们使用运行的代码
    services.exe来注入代码
    CSRSS.exe并应用ANGRYORCHARD的技术来获取物理内存的读写。

结合carrot_c4k3发现的
.themepack 文件的 Mark-of-the-Web 绕过方法,只要用户打开下载的文件,即可触发此攻击。根据 WDAC 策略,我们也可以绕过这一点。

使固定

那么,微软是如何解决这个问题的呢?

我们曾希望他们能够完全禁用从 SMB 读取$KERNEL.扩展属性的功能。然而,他们并没有采取这种做法。相反,他们修复了我们利用的漏洞:
1. CVE-2023-38146 的修复已经完全禁用文件加载
.msstyles_vrf.dll,从而解决了权限提升问题。

  1. 当启用 WDAC 时,检索文件的缓存签名级别的功能现在总是返回错误(即使对于本地文件!)。

  2. 将 DLL 加载到受保护的进程中时,不再使用缓存的签名级别。(尽管微软不认为这是受保护的安全边界,但这个问题已经得到修复。)

时间线
– 2023 年 8 月 25 日:问题报告给 MSRC。

  • 2023 年 9 月 12 日:CVE-2023-38146 的修复程序发布,打破了我们的权限提升漏洞。

  • 2023 年 9 月 20 日:MSRC 表示他们已经重现了该问题,并计划于 2024 年 1 月修复。

  • 2023 年 12 月 11 日:MSRC 通知我们发现了回归问题,并要求将修复时间重新安排到 2024 年 4 月。

  • 2024 年 4 月 9 日:针对 WDAC 和 PPL 绕过的修复程序发布为 CVE-2024-20693。

  • 2024 年 4 月 25 日:MSRC 向 Microsoft Bounty Team 请求更新,并抄送给我们。

  • 2024 年 4 月 26 日:微软赏金团队发回一份样板答复,称该案件正在审查中。

  • 2024 年 5 月 17 日:MSRC 要求 Microsoft Bounty Team 提供更新信息,并再次抄送我们。

  • 2024 年 5 月 22 日:微软赏金团队回复称,该漏洞超出了赏金范围,并声称该漏洞无法在正确的 WIP 版本上重现。

  • 2024 年 5 月 23 日:通知 Microsoft Bounty Team,我们相信该漏洞确实在最新的 WIP 版本中重现,并且 WDAC 绕过并不是一个已知问题。

  • 2024 年 5 月 29 日:Microsoft Bounty Team 重新评估了该问题并分配了赏金。

减轻

此攻击依赖于受害者连接到恶意 SMB 服务器。因此,阻止向互联网发送 SMB 将使此攻击变得更加困难(除非攻击者已经在本地网络上站稳脚跟)。阻止用户安装新的 SMB 共享也可以作为一种缓解措施,但可能会产生更多意想不到的后果。

0x90390通过查找对 ioctl 的响应或对 EA 的响应,也可以检查 SMB 流量以利用此问题
$KERNEL.PURGE.ESBCACHE。

结论

我们打算通过使用 WinSxS 将 DLL 加载研究应用到模拟服务中,以加深对 Windows 内部机制的了解,但我们偏离了主题,开始研究用于 DLL 文件的代码签名方法,并找到了一种绕过它的方法。虽然我们无法在开始的场景中应用它,但我们确实找到了其他地方可以使用它来提升权限、绕过 WDAC 并将代码注入受保护的进程。就像我们之前关于 macOS .pkg 文件签名绕过的研究“坏事成大器:macOS 上的 .pkg 签名验证绕过”一样,我们在这里看到加密操作中的漏洞通常可以以多种方式应用,从而允许绕过不同的安全措施。就像那个例子一样,这个漏洞可能从用户打开下载的文件发展到整个系统被攻陷。

感谢您抽出

.

.

来阅读本文

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