您现在的位置是:首页 >学无止境 >本地提权漏洞分析【网络安全】网站首页学无止境

本地提权漏洞分析【网络安全】

不是程序媛呀 2024-06-14 17:20:17
简介本地提权漏洞分析【网络安全】

 0. 前言

CVE-2023-21752 是 2023 年开年微软第一个有 exploit 的漏洞,原本以为有利用代码会很好分析,但是结果花费了很长时间,难点主要了两个:漏洞点定位和漏洞利用代码分析,欢迎指正。

1. 漏洞简介

根据官方信息,该漏洞是 Windows Backup Service 中的权限提升漏洞,经过身份认证的攻击者可利用此漏洞提升至 SYSTEM 权限。成功利用此漏洞需要攻击者赢得竞争条件。

EXP 代码位于 Github,提供了两个版本,版本 1 可以实现任意文件删除,可稳定复现;版本 2 尝试利用任意删除实现本地提权,但是复现不稳定。

2. 漏洞点定位的曲折之路

这部分内容是一些失败的过程记录,防止自己之后犯同样的错误,只对漏洞分析感兴趣的可以略过 2.1 和 2.3 小节。

2.1 失败的过程

首先尝试复现,提权版本的利用程序在虚拟机上没有复现成功,任意文件删除版本的利用程序由于没有使用完整路径,也没有复现成功。

之后尝试进行补丁对比,但是想要进行补丁对比首先要确定漏洞位于哪个文件中,根据漏洞利用程序的文件命名SDRsvcEop,找到了文件 sdrsvc.dll,但是补丁对比后并没有发现差异。

搜索了关于这个漏洞的信息,但是除了漏洞通告和 GitHub 的 exp 代码外,没有找到其他内容。

这个时候已经开始对漏洞利用代码进行分析了,一方面通过微软的文档,了解代码中一些函数和参数的使用,一方面开始在 Windbg 上进行调试,并由此找到了 rpcrt4.dll、combase.dll 这些和漏洞无关的文件。

在调试过程中,花费了很多时间在DeviceIoControl这个函数上,因为之前看的很多漏洞最终定位的文件都是 sys 驱动文件,因此为 dll 文件留了一些位置,但是在方法选择上,仍旧趋向去寻找某个 sys 文件。

在这个时候才想起来要把利用程序参数的相对路径改成绝对路径,并且成功复现了任意文件删除。

之前学习病毒分析的时候,有一个算是标准的流程,就是要先执行病毒,看一下它的动态特征,以此方便后面的动态分析。之前看的很多漏洞分析文章,也都是要执行一下 poc 或者 exp,进行进程监控,虽然想到了这个方法,但是并没有十分重视。

继续分析漏洞利用代码,在此期间看了一些关于 DCOM 的资料,确定 sdrsvc.dll 是依赖 rpcss.dll 文件功能的(明明可以通过 process hacker 直接确定的……),通过补丁对比发现函数 CServerSet::RemoveObject 被修改,尝试在 Windbg 中在这个函数设置断点,但是利用程序没有执行到这里,所以漏洞点不在这个文件。

2.2 转入正轨

此时我仍旧没有使用 procmon 对利用程序进行监控,我选择在安装补丁前后的系统上执行利用程序,并检查输出(输出内容做了一些修改),得到以下结果(因为只是测试功能,并没有选择对高权限文件进行删除):

补丁修复前

PS C:UsersexpDesktop> C:UsersexpDesktopSDRsvcEop.exe C:UsersexpDesktop	est.txt
[wmain] Directory: C:usersexpappdatalocal	emp23980418-9164-497e-8ce7-930949d1af55
[Trigger] Path: \127.0.0.1c$UsersexpAppDataLocalTemp23980418-9164-497e-8ce7-930949d1af55
[FindFile] Catch FILE_ACTION_ADDED of C:usersexpappdatalocal	emp23980418-9164-497e-8ce7-930949d1af55SDT2C35.tmp
[FindFile] Start to CreateLock...
[cb] Oplock!
[CreateJunction] Junction \?C:UsersexpAppDataLocalTemp23980418-9164-497e-8ce7-930949d1af55 -> RPC Control created!
[DosDeviceSymLink] Symlink GlobalGLOBALROOTRPC ControlSDT2C35.tmp -> ??C:UsersexpDesktop	est.txt created!
[Trigger] Finish sdc->proc7
[wmain] Exploit successful!
[DeleteJunction] Junction \?C:UsersexpAppDataLocalTemp23980418-9164-497e-8ce7-930949d1af55 deleted!
[DelDosDeviceSymLink] Symlink GlobalGLOBALROOTRPC ControlSDT2C35.tmp -> ??C:UsersexpDesktop	est.txt deleted!

补丁修复后

PS C:UsersexpDesktop> C:UsersexpDesktopSDRsvcEop.exe C:UsersexpDesktop	est.txt
[wmain] Directory: C:usersexpappdatalocal	emp183c772e-f444-4aec-a489-7d9f734ee719
[Trigger] Path: \127.0.0.1c$UsersexpAppDataLocalTemp183c772e-f444-4aec-a489-7d9f734ee719
[FindFile] Catch FILE_ACTION_ADDED of C:usersexpappdatalocal	emp183c772e-f444-4aec-a489-7d9f734ee719SDT1F8A.tmp
[Trigger] Finish sdc->proc7
_

由此可知修复后利用程序无法再获取一个 tmp 文件的句柄,我猜测应该是补丁修复之前,漏洞文件创建了这个 tmp 文件,并且创建的权限有问题(这个猜测不一定准确),但是这个猜测目前没什么用,还是没办法定位漏洞文件。

终于想起来要用 procmon 了。

根据上面利用程序输出结果的对比,确定漏洞修复的位置和创建的 tmp 文件有关,因此格外注意 procmon 中该文件的创建操作:

并在 Stack 选项卡中,定位到 sdrsvc.dll 调用的功能位于 sdengin2.dll 中:

根据SdCheck + 0x490c2,在 IDA 中定位到函数 CSdCommonImpl::QueryStorageDevice,该地址为这个函数调用 QueryStorageDevice 的位置。

经过补丁对比,发现了函数 IsWritable,这个函数进行了修改,并且被 QueryStorageDevice 所调用。

2.3 反思

这次漏洞分析遇到了几个障碍:

  1. 无法通过漏洞名称直接确认漏洞文件,导致无法使用常用的补丁对比的分析方法;

  2. exp 一开始未成功复现,这种情况对我来说很常见,但是由于不清楚原因,我以为是对备份服务的功能以及 exp 代码不熟悉导致;

  3. 由于不熟悉利用代码:

  4. 花费很多时间查找相关资料;

  5. 需要对辅助功能代码和直接漏洞利用代码进行区分。

除此之外,之前极少分析带有 exp 且 exp 可以正常复现的漏洞,习惯从静态分析入手,再使用 windbg 动态辅助分析。一般遇到可以使用的 poc,我也是直接触发崩溃,然后使用 windbg 从崩溃开始进行调试分析,从没有使用过 procmon 进行动态监控,并且这样的方法也都成功对漏洞进行了分析,因此轻视了 procmon 动态监控方法的有效性。

不过 procmon 也不是万能的,目前看来,这个方法在漏洞点定位上十分有效,但是如果通过其他信息已经能够对漏洞点进行定位,那么 procmon 提供的帮助就不那么显著了,而且通过其他方法也能够完成漏洞分析。

3. 漏洞原理

3.1 补丁对比

漏洞修复前:

__int64 __fastcall IsWritable(unsigned __int16 *a1, int a2, int *a3)
{
  ...
  v7 = -1;
  if ( a2 == 7 )
  {
    if ( !GetTempFileNameW(a1, L"SDT", 0, TempFileName) )// 如果获取 temp 文件名失败,进入 if 语句
    {
      rtnValue = v17;
LABEL_28:
      *a3 = v6;
toend2:
      if ( v7 != -1 )
      {
        CloseHandle(v7);
        rtnValue = v17;
      }
      goto end;
    }
    rtnValue = SxDeleteFile(TempFileName);      // 删除之前可能存在的 tmp 文件
    v17 = rtnValue;
    v8 = 0x148;
    if ( rtnValue >= 0 )                        // 删除成功
    {
      v18 = 0x148;
LABEL_27:
      v6 = 1;
      goto LABEL_28;
    }
toend:
    v19 = v8;
    goto end;
  }
  ...
}

上述代码中的 a2 和传入的路径类型有关,由于利用程序传入的是 UNC 路径,因此最终程序执行流程到达此处。

根据 GetTempFileNameW 函数的文档说明,当其第三个参数为数值 0 时,该函数会尝试使用系统时间生成一个唯一数字文件名,如果该文件已存在,数字递增直至文件名唯一,在这种情况下,该函数会创建一个该文件名的空文件并释放其句柄。因此在漏洞修复之前,系统通过 GetTempFileNameW 创建临时文件是否成功的方式检查传入的 unc 路径是否可以写入,如果可以写入,再删除创建的这个临时文件。

漏洞修复后:

__int64 __fastcall IsWritable(unsigned __int16 *a1, int a2, int *a3)
{
  ...
  v7 = -1;
  if ( a2 == 7 )
  {
    if ( CheckDevicePathIsWritable(a1) < 0 )
    {
LABEL_10:
      rtnValue = v16;
      *a3 = v6;
toend2:
      if ( v7 != -1 )
      {
        CloseHandle(v7);
        rtnValue = v16;
      }
      goto end;
    }
LABEL_9:
    v6 = 1;
    goto LABEL_10;
  }

漏洞修复后,原本 GetTempFileNameW 函数的位置变成了 CheckDevicePathIsWritableGetTempFileNameW 函数的实现位于 kernelbase.dll 文件中,如果你仔细对比,会发现这两个函数中的大部分代码相同,只有一处差异点需要注意,就是在创建临时文件的时候,两者的代码如下:

// GetTempFileNameW
v24 = CreateFileW(lpTempFileName, GENERIC_READ, 0, 0i64, 1u, 0x80u, 0i64);

// CheckDevicePathIsWritable
v22 = CreateFileW(FileName, GENERIC_READ, 0, 0i64, 1u, 0x4000080u, 0i64);

可以看到在 CheckDevicePathIsWritable 函数中,CreateFileW 函数的第六个参数 dwFlagsAndAttributes 数值由 0x80 变成了 0x4000080,即从 FILE_ATTRIBUTE_NORMAL 变成了 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE

根据文档说明,FILE_FLAG_DELETE_ON_CLOSE 表示文件会在所有句柄关闭时直接删除,并且之后打开该文件的请求必须包含 FILE_SHARE_DELETE 共享模式,否则会失败。

简单来说,修复后的代码将临时文件的创建和删除操作整合成为了一个元操作。

3.2 漏洞原理分析

上面补丁对比的结果可以确定这是一个条件竞争漏洞,由于临时文件的创建操作和删除操作接次发生,并且在两个操作之间没有对文件进行限制,这就导致攻击者可以创建另一线程,在临时文件创建之后,删除之前,获取文件句柄并创建机会锁阻止其他线程操作,同时将文件删除,并设置原文件路径指向其他文件,当机会锁释放后,指向的其他文件就会被删除。

4. 漏洞利用

4.1 文件删除漏洞利用代码流程总结

  1. 在临时文件夹下,使用 FULL_SHARING 模式创建目录 dir ,作为上述临时文件的保存位置;

  2. 创建线程 FindFile,监控 dir 目录下的文件创建操作:

  3. 获取创建文件句柄并创建机会锁;

  4. 将创建的文件移动到其他目录下;

  5. 创建符号链接,将原文件路径指向要删除的目标文件;

  6. 释放机会锁;

  7. 主线程将目录 dir 的路径转换为 unc 格式,并通过 CoCreateInstance 的方式调用 sdrsvc 服务的 CSdCommonImpl::QueryStorageDevice 接口;

  8. sdrsvc 服务在 unc 格式目录下创建临时文件,之后删除文件。

如下图所示:

4.2 提权漏洞利用

4.2.1 原理分析

这部分内容基本上看 ZDI 的文章就可以,这里做一下介绍。

简单来说,从 任意文件删除 到本地提权,需要与 MSI installer 文件运行过程进行条件竞争。

Windows Installer 服务负责应用程序的安装,而 msi 文件定义了安装过程中会发生的变化,例如创建了哪些文件夹、复制了哪些文件、修改了哪些注册表等等。因为程序安装过程中会对系统进行修改,为了避免安装出错导致系统无法恢复,msi 会在运行时创建文件夹 C:/Config.msi,将安装过程中做的所有更改记录到 .rbs 后缀的文件中,同时将被替换的系统文件存储为 .rbf 格式放入该文件夹。所以如果可以替换其中的 rbf 文件,就能将系统文件替换为任意恶意文件。正是因为有文件替换的风险,所以 C:/Config.msi 及其中的文件默认具有强 DACL。

但是如果攻击者能做到 任意目录删除,就可以将 C:/Config.msi 删除,重新创建一个弱 DACL 的 C:/Config.msi 目录,并在 msi 程序创建完 rbs 和 rbf 文件之后,对其进行替换,使用恶意 rbf 文件实现提权。

具体来看,msi 在运行时经历了 创建->删除->再创建 的过程,之后才会开始创建 rbs 文件,因此 任意目录删除 需要在 再创建 之后,rbs 文件创建之前删除 C:/Config.msi 目录,并监控 rbs 文件的产生,对文件进行替换,条件竞争就发生在这里。

上面提到的漏洞是 任意目录删除,如果发现的是 任意文件删除,可以删除 C:/Config.msi::$INDEX_ALLOCATION 数据流,同样可以实现目录的删除。

利用任意文件删除漏洞实现提权的流程如下图所示:

4.2.2 失败原因分析

上面介绍的流程把 Config.msi 的删除 当作一个元操作,但在 CVE-2023-21752 这个漏洞中,文件删除同样需要条件竞争才能实现,具有相对繁琐的步骤。也就是说想要利用这个漏洞实现本地提权,需要同时实现赢得两个条件竞争,这也是一开始复现总是失败的原因。

为了方便了解利用代码的执行流程,对代码中的注释进行了添加和修改,得到了如下的执行结果:

PS C:UsersexpDesktop> C:UsersexpDesktopSDRsvcEop.exe
[wmain] Config.msi directory created!
[wmain] Directory: C:usersexpappdatalocal	emp3bbbd2cf-7baf-42b7-98ea-242f703b08f8
[wmain] Got handle of uuid directory
[wmain] Finish create oplock for config.msi

[Trigger] Path: \127.0.0.1c$UsersexpAppDataLocalTemp3bbbd2cf-7baf-42b7-98ea-242f703b08f8
[FindFile] Found added file C:usersexpappdatalocal	emp3bbbd2cf-7baf-42b7-98ea-242f703b08f8SDT73B.tmp
[FindFile] Got handle of C:usersexpappdatalocal	emp3bbbd2cf-7baf-42b7-98ea-242f703b08f8SDT73B.tmp
[cb] Oplock!
[Move] Finish moving to ??C:windows	empc5b82788-8133-4971-b351-38f58233ced1
[CreateJunction] Junction \?C:UsersexpAppDataLocalTemp3bbbd2cf-7baf-42b7-98ea-242f703b08f8 -> RPC Control created!
[DosDeviceSymLink] Symlink GlobalGLOBALROOTRPC ControlSDT73B.tmp -> ??C:Config.msi::$INDEX_ALLOCATION created!
[FindFile] End

[Move] Finish moving to ??C:windows	empf1161f2-a8c5-4798-a71d-f32ebba87125
[install] MSI file: C:windows	empMSI72F.tmp
[install] Start ACTION=INSTALL
[cb1] Detect first create
[cb1] Detect first delete
[install] Start REMOVE=ALL
[install] Start delete msi file

[Fail] Race condtion failed!
[DeleteJunction] Junction \?C:UsersexpAppDataLocalTemp3bbbd2cf-7baf-42b7-98ea-242f703b08f8 deleted!
[DelDosDeviceSymLink] Symlink GlobalGLOBALROOTRPC ControlSDT73B.tmp -> ??C:Config.msi::$INDEX_ALLOCATION deleted!

在不同流程的结果之间添加了回车,方便观察,可以看到在 删除文件 流程中,程序监控到了临时文件的生成,也创建了符号链接,但是由于符号链接将临时文件链接到了 C:/Config.msi::$INDEX_ALLOCATION 上,而 C:/Config.msi 的机会锁又掌握在 msi 线程上,因此删除操作停滞了。

于此同时 msi 线程上只监测到了 C:\Config.msi 的第一次创建和删除,并没有监测到第二次创建,因为这里使用了循环对创建行为进行检测,因此该线程也陷入了无限循环。

与 msi 的条件竞争失败,导致两个线程发生死锁,漏洞利用失败。

4.2.3 问题解决

首先,将利用代码的整个流程画成了如下的流程图:

红框部分就是条件竞争失败的地点。

尝试增加虚拟机的 CPU 数量,对监控 Config.msi 创建的代码进行优化,但是都没有成功。同时也单独使用 procmon 监控了 msi 文件的运行过程,确定 Config.msi 目录确实发生了二次创建。

所以结论只有一个,Config.msi 目录的二次创建发生的太快了。但是既然 Config.msi 目录的二次创建是确实发生的,同时利用代码已经监测到了第一次删除的行为,那么如果这个时候就释放机会锁 2,又会如何呢?

如果不对 Config.msi 目录的二次创建进行监控,直接释放机会锁 2,因为 Config.msi 目录的二次创建时间间隔非常短,等待良久的 sdrsvc 就有机会成功删除 Config.msi。此时漏洞利用流程可以继续进行下去,并成功实现漏洞利用!

3.2 符号链接的问题

之前对利用代码中如何链接向待删除文件存在疑问,实际上这种利用手法来自 James Forshaw,参考链接 5 和 6 对其进行了介绍。

重分析点/Junction 是一种 NTFS 文件系统中文件夹的属性,NTFS 驱动在打开文件夹的时候会对它进行读取。可以使用它对目录之间进行链接,假设要建立目录 A 向目录 B 的重分析点,只要普通用户对目录 A 具有可写权限就能够进行,而对目录 B 的权限没有任何要求。

在 Windows 系统中,我们常提到的 C 盘目录并不是一个真的文件夹,它实际上是一个指向设备物理地址的符号链接对象,你可以使用 WinObj 在 GLOBAL?? 中看到 C: 项是一个 SymbolicLink。当我们访问 C 盘中的某个文件时,系统会对访问路径进行转换,转换成真正的设备物理地址。普通用户也可以在对象管理器中添加或删除符号链接,但是该行为只能在有限的目录下进行,例如 RPC Control.

在上面利用代码执行结果中,有下面两行输出:

[CreateJunction] Junction \?C:UsersexpAppDataLocalTemp3bbbd2cf-7baf-42b7-98ea-242f703b08f8 -> RPC Control created!
[DosDeviceSymLink] Symlink GlobalGLOBALROOTRPC ControlSDT73B.tmp -> ??C:Config.msi::$INDEX_ALLOCATION created!

 首先创建了攻击者可控的目录 3bbbd2cf-7baf-42b7-98ea-242f703b08f8 指向 RPC Control 的重分析点,这样访问 RPC Control 就相当于访问这个可控目录;之后在 RPC Control 下面创建了一个由 SDT73B.tmp 指向待删除文件的符号链接,这一步就相当于将可控目录下的 SDT73B.tmp 指向了待删除文件,删除 SDT73B.tmp 就相当于删除了目标文件

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。