以CVE-2024-26229为例分析Windows RDBSS机制

一、前言

CVE-2024-26229是今年4月微软修补的一个任意地址写零漏洞,该漏洞位于csc.sys驱动中。攻击者在Windows上获得较低权限的任意代码执行后,可以利用该漏洞将低权限提升至system权限。目前Exp由varwara公布在了github上。

二、csc.sys简介

csc.sys(Windows Client Side Caching Driver)是脱机文件(offline files)功能的一部分。脱机文件功能是默认在本机存一份共享文件的副本,这样做的优点有两点,一方面可以节省网络流量,另一方面是断网条件下,仍然可以使用共享文件夹,等到有网之后再进行同步。默认副本文件存储在C:\Windows\CSC文件夹中,普通用户没有权限访问该文件夹的内容。

下面以SMB协议为例,演示如何开启脱机文件功能。

首先准备两台虚拟机,一台为SMB server,一台为SMB client。

在SMB Server首先新建一个文件夹,然后共享此文件夹,并设置此权限为完全控制。

在SMB client打开控制面板进入同步中心->管理脱机文件->启动脱机文件。

然后访问SMB server,点击始终脱机可用。

之后就会出现一个新的文件夹,即为本机副本。

此时关闭share文件夹分享,仍然能操作share文件夹。

三、漏洞成因分析

通过README可以看出该漏洞产生的原因是在csc.sys中没有校验用户态以METHOD_NEITHER方式和驱动进行通信时用户态传入的缓冲区地址是否是合法地址,从而导致任意地址读或者任意地址写。

下面介绍一下windows用户态与驱动的通信方式来解释为什么以METHOD_NEITHER与驱动进行通信时需要对用户态传入的地址进行合法校验。

在windows中用户态与驱动的通信方式共有三种:

  • METHOD_BUFFERED
  • METHOD_IN_DIRECT
  • METHOD_OUT_DIRECT
  • METHOD_NEITHER

METHOD_BUFFERED方式对UserInputBuffer和UserOutputBuffer都进行缓冲,驱动程序无需对用户态传入的缓冲区地址进行校验。

METHOD_IN_DIRECT和METHOD_OUT_DIRECT只对UserInputBuffer进行缓冲,对于UserOutputBuffer采用的是将用户态地址锁定(即不让其换出内存),然后映射为内核地址。在驱动写入后,重新映射为用户态地址。

METHOD_NEITHER (也就是出现漏洞的这种通信方式) 驱动程序直接读写用户态的缓冲区,优点是读写更快,缺点是驱动程序在读写缓冲区之前需要使用ProbeForRead/ProbeForWrite函数去探测地址是否合法。就可能会出现漏洞,例如用户态的传入的UserInputBuffer和UserOutputBuffer均为内核态地址,驱动就会根据用户态传入地址读写,即可造成任意地址读写。

四、补丁分析

可以看到有两个函数有改动,既然本漏洞是关于I/O control的,那么优先看CscDevFcbXXXControlFile这个函数改动。

通过bindiff可以看到从00000001C006B243这个块开始有所不同,定位到对应的反编译的伪c代码进行比较。

在patch之后的代码加了一个if判断

  • 如果if 判断条件为true,则首先在使用前判断v12是否小于0x24
    • 如果小于0x24,则调用失败
    • 如果大于等于0x24,则会判断(a1+40)+64的是否不为0,这里推测如果为1的话,应该表示该请求来自用户态
      • 如果不为0,则会使用ProbeForWrite探测a1+536的地址是否合法
    • 然后对a1+536的位置写入一个8字节的0 (这里可能就是未patch的漏洞点了)
  • 如果if判断条件为false,则使用原来的漏洞代码

patch代码保留原来漏洞代码目的推测为微软可能认为新代码可能会影响正常的功能,一旦遇到问题可以只需要改一下某个标志位就可以回滚回原来的代码,等到新代码完全稳定后估计就会删除原来的漏洞代码。

五、漏洞触发流程分析

从目前的分析来看,这个漏洞模式比较简单,为什么之前没有被人扫描到或者分析出来,我认为有两点原因。

  • ida反编译显示错误,有的时候你看csc.sys驱动的DriverEntry函数,可能如下图所示:

如果你多次Undefine然后重新反编译DriverEntry,就会发现DriverEntry函数是很长的。

  • csc.sys是一个内核网络微型重定向器驱动程序(Kernel Network Mini-Redirector Driver)使用了重定向驱动器缓冲子系统 (Redirected Drive Buffering Subsystem) 使得处理DeviceIoControl调用栈过深,不易被发现。

由DriverEntry可以看到用户态所有请求都由CscFsdDispatch处理,跟进CscFsdDispatch函数可以发现大部分的请求都是由导入函数RxFsdDispatch处理

RxFsdDispatch是由rdbss.sys中实现,下面介绍以下csc.sys与rdbss.sys交互为例介绍rdbss机制。

5.1 rdbss机制分析

csc.sys与rdbss.sys的交互分为三个状态,并且是顺序的。

5.1.1 csc.sys在初始化时调用RxRegisterMinirdr()初始化PRDBSS_DEVICE_OBJECT的部分成员

RxRegisterMinirdr函数声明如下:

NTSTATUS RxRegisterMinirdr(
  [out]     OUT PRDBSS_DEVICE_OBJECT *DeviceObject,
  [in, out] IN OUT PDRIVER_OBJECT    DriverObject,
  [in]      IN PMINIRDR_DISPATCH     MrdrDispatch,
  [in]      IN ULONG                 Controls,
  [in]      IN PUNICODE_STRING       DeviceName,
  [in]      IN ULONG                 DeviceExtensionSize,
  [in]      IN DEVICE_TYPE           DeviceType,
  [in]      IN ULONG                 DeviceCharacteristics
);

这里主要关注MrdrDispatch这个参数,这个参数指向网络微型重定向驱动的调度表的指针。此调度表包括网络微型重定向驱动的配置信息和指向由网络微型重定向器内核驱动程序实现的回调例程的指针表。RDBSS 通过此回调例程列表调用网络微型重定向器驱动程序。

在csc.sys由CscInitializeDispatchTable()初始化此表。可以看到漏洞函数也在此表中。

在RxRegisterMinirdr具体实现如下:

NTSTATUS __stdcall RxRegisterMinirdr(
        PRDBSS_DEVICE_OBJECT *PRdbssDeviceObject,
        PDRIVER_OBJECT DriverObject,
        _NEW_MINIRDR_DISPATCH *MrdrDispatch,
        ULONG Controls,
        PUNICODE_STRING DeviceName,
        ULONG DeviceExtensionSize,
        ULONG DeviceType,
        ULONG DeviceCharacteristics)
{

  v38 = PRdbssDeviceObject;
  // 省略部分代码
  ......
  // 创建新的device object
  result = IoCreateDevice(
             *(PDRIVER_OBJECT *)DriverObjecta,
             v16 + v19,
             v20,
             DeviceType,
             DeviceCharacteristics,
             0,
             &v);
  // 省略部分代码
  .....
  v21 = v38;
  // 省略部分代码
  .....
  v22 = DeviceObject;
  // 初始化PRdbssDeviceObject的DeviceObject成员
  *v21 = DeviceObject;
  // 将调度表放到新的device object 偏移0x160处
  v22->MrdrDispatch = (__int64)MrdrDispatch;
  // 初始化DeviceObject其他成员
  DeviceObject->Controls = Controls;
  DeviceObject->DeviceName = *DeviceName;
  LOBYTE(DeviceObject->field_180) = (Controls & 1) == 0;
  BYTE1(DeviceObject->field_180) = (Controls & 2) == 0;
  DeviceObject->DeviceName.MaximumLength = DeviceName->MaximumLength;
  DeviceObject->DeviceName.Length = 0;
  DeviceObject->DeviceName.Buffer = (wchar_t *)((char *)&DeviceObject[1].Controls + v16 + v37);
  RtlCopyUnicodeString(&DeviceObject->DeviceName, DeviceName);
  // 省略部分代码
  .....
}

通过上面代码分析实际上CSC_DISPATCH_TABLE最终放在了CscDeviceObject中,在后面的RxFsdDispatch函数处理就可以通过CscDeviceObject索引到CSC_DISPATCH_TABLE从而进行调用。

5.1.2 用户态进程打开csc.sys驱动设备的符号链接或者创建脱机文件、文件夹的操作

当用户态进程进行此操作时,会向csc.sys发起IRP_MJ_CREATE请求。此IRP请求首先由csc.sys驱动中CscFsdDispatch()处理。

__int64 __fastcall CscFsdDispatch(PDEVICE_OBJECT a1, IRP *a2, LARGE_INTEGER a3, struct _RDBSS_DEVICE_OBJECT *a4)
{
 
  CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation;
  v18 = 0i64;
  FileObject = CurrentStackLocation->FileObject;
  v19 = 0i64;
  v17[0] = 0i64;
  // 省略部分代码
  .......
  MajorFunction = CurrentStackLocation->MajorFunction;
  // 处理IRP_MJ_SYSTEM_CONTROL请求
  if ( CurrentStackLocation->MajorFunction == IRP_MJ_SYSTEM_CONTROL )
  {
    v13 = CscProcessSystemControlIrp((__int64)CscDeviceObject, a2);
    goto LABEL_23;
  }
  // 处理IRP_MJ_SHUTDOWN请求
  if ( MajorFunction == IRP_MJ_SHUTDOWN )
  {
    KeEnterCriticalRegion();
    LOBYTE(v14) = 1;
    v9 = CscStoreFlushState(v14);
    KeLeaveCriticalRegion();
    goto LABEL_14;
  }
  // 处理IRP_MJ_CREATE请求
  if ( !MajorFunction )
  {  
    // 省略部分代码
    .......

    FsContext2 = FileObject->FsContext2;
    FsContext = (char *)FileObject->FsContext;
    // 如果是IRP_MJ_CREATE请求,大多都进不了这个if,实际上还是会由RxFsdDispatch()函数处理
    if ( !FsContext2 || *(_WORD *)FsContext2 != 0xEB0F || FsContext2[1] != 0xC5C00C5C )
    {
     
      if ( FileObject->FsContext && *((_WORD *)FsContext + 20) > *((_WORD *)FsContext + 28) )
      {
             // 省略部分代码
               .......
      }
      goto LABEL_13;
    }
    goto LABEL_22;
  }
  if ( MajorFunction != IRP_MJ_DEVICE_CONTROL
    || ((CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode - 0x14018F) & 0xFFFFFFFB) != 0 )
  {
LABEL_22:
    v13 = RxFsdDispatch((__int64 (__fastcall **)())CscDeviceObject, a2, a3, a4);
LABEL_23:
    v9 = v13;
    goto LABEL_15;
  }
   // 省略部分代码
   .......
}

RxFsdDispatch()函数实现如下:

NTSTATUS __stdcall RxFsdDispatch(PRDBSS_DEVICE_OBJECT RxDeviceObject, PIRP Irp)
{
   // 省略部分代码
   .......
  CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation;
  isCreate = 0;
  MajorFunction = CurrentStackLocation->MajorFunction;
  FileObject = CurrentStackLocation->FileObject;
  switch ( (_BYTE)MajorFunction )
  {
    case IRP_MJ_SYSTEM_CONTROL:
      return RxSystemControl(RxDeviceObject, Irp, MajorFunction, RxDeviceObject);
    case IRP_MJ_CREATE_MAILSLOT:
    case IRP_MJ_CREATE_NAMED_PIPE:
      v11 = 0xC0000033;
      break;
    // 如果是IRP_MJ_CREATE请求,则使用默认的DispatchVector即RxFsdDispatchVector 然后调用RxFsdCommonDispatch()处理
    case IRP_MJ_CREATE:
      isCreate = 1;
      goto LABEL_9;
    default:
      // 默认则是取FsContext中的PrivateDispatchVector成员作为DispatchVector,其中FsContext为FCB(file control block)结构
      if ( FileObject )
      {
        v8 = FileObject->FsContext;
        if ( v8 )
        {
          if ( (v8->field_0 & 0xFFF0) == 0xEC20 )
          {
            PrivateDispatchVector = v8->PrivateDispatchVector;
            // 如果PrivateDispatchVector不为null,则使用PrivateDispatchVector,否则使用RxFsdDispatchVector进行调用
            if ( PrivateDispatchVector )
            {
LABEL_10:
              result = RxFsdCommonDispatch(
                         PrivateDispatchVector,
                         Irp,
                         (LARGE_INTEGER)FileObject,
                         (__int64)RxDeviceObject);
              if ( !isCreate )
                return result;
              if ( result == 0x8000002D )
                return 260;
              return result;
            }
          }
LABEL_9:
          PrivateDispatchVector = &RxFsdDispatchVector;
          goto LABEL_10;
        }
      }
      v11 = 0xC0000010;
      break;
  }
   // 省略部分代码
   .......
}

其中RxFsdDispatchVector结构体如下:

其中回调函数类型为:

NTSTATUS (__stdcall *CommonRoutine)(PRX_CONTEXT, PIRP)

接下来看RxFsdCommonDispatch的实现:

__int64 __fastcall RxFsdCommonDispatch(_RX_FSD_DISPATCH_VECTOR *a1, PIRP a2, LARGE_INTEGER a3, __int64 a4)
{
​
  // 省略部分代码
  .......
  RxDeviceObject = (PRDBSS_DEVICE_OBJECT)a4;
  Interval = a3;
  v82 = a1;
  v7 = 0;
  CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation;
  v90 = CurrentStackLocation;
  MajorFunction = CurrentStackLocation->MajorFunction;
  // 省略部分代码
  .......
  memset(v23, 0, sizeof(NEW_PRX_CONTEXT));
  v23->IrpTypeFlag = v25;
  v23->CurrentNodeNumber = CurrentNodeNumber;
  v23->field_1AA = LockArray_high;
  v26 = RxDeviceObject;
  // 初始化PRX_CONTEXT对象
  RxInitializeContext(a2, RxDeviceObject, InitialContextFlags, v23);
   // 省略部分代码
  .......
  v41 = CurrentStackLocation->MajorFunction;
  // 根据MajorFunction获得调度函数 由于IRP_MJ_CREATE是0,所以选用的调度函数应该是RxCommonCreate
  CommonRoutine = v82[CurrentStackLocation->MajorFunction].CommonRoutine;
   // 省略部分代码
  .......
  MinorFunction = CurrentStackLocation->MinorFunction;
   // 省略部分代码
  .......
  // 根据MinorFunction来确定单次调用还是多次调用函数
  if ( (MinorFunction & IRP_MN_QUERY_SINGLE_INSTANCE) != 0 )
  {
   // 省略部分代码
  .......
    v70 = 1;
LABEL_56:
    v23->processFunc = (__int64)CommonRoutine;
    if ( CommonRoutine )
    {
      v23->field_22 = 1;
      if ( v70 )
      {
      // 单次调用调度函数
        v7 = RxFsdPostRequest((PRX_CONTEXT)v23);
      }
      else
      {
        // 多次调用调度函数
        while ( 1 )
        {
          LODWORD(v23->field_B0) = 0;
          // 调用单次函数
          v7 = ((__int64 (__fastcall *)(NEW_PRX_CONTEXT *, PIRP))CommonRoutine)(v23, a2);
          if ( v7 == (unsigned int)STATUS_RDBSS_RESTART_OPERATION )
          {
             // 省略部分代码
            .......
          }
          if ( v7 == STATUS_PENDING )
            break;
          if ( v7 != (unsigned int)STATUS_RDBSS_RESTART_OPERATION )
          {
            irp = v23->irp;
            if ( !irp
              || irp != a2
              || v23->CurrentStackLocation != CurrentStackLocation
              || v23->MajorFunction != CurrentStackLocation->MajorFunction
              || v23->MinorFunction != CurrentStackLocation->MinorFunction )
            {
               // 省略部分代码
               .......
            }
            v23->field_22 = 0;
            RxCompleteRequestEx((PRX_CONTEXT)v23, v23->irp);
            goto LABEL_214;
          }
        }
      }
    }
    else
    {
      v7 = -1073741822;
    }
    goto LABEL_214;
  }
   // 省略部分代码
  .......

根据以上分析该函数会调用RxCommonCreate进行处理,这里主要分析FsContext的赋值,下面看RxCommonCreate的实现。

__int64 __fastcall RxCommonCreate(NEW_PRX_CONTEXT *Context, IRP *a2)
{
 
  CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation;
  FileObject = CurrentStackLocation->FileObject;
  v25 = 0i64;
  v29 = FileObject;
  p_FileName = &FileObject->FileName;
  // FileName为空RelatedFileObject为null并且则是使用RxDeviceFCB
  if ( !FileObject->FileName.Length && !FileObject->RelatedFileObject )
  {
    FileObject->FsContext2 = 0i64;
    FileObject->FsContext = &RxDeviceFCB;
    ++LODWORD(RxDeviceFCB.field_B8);
    ++LODWORD(RxDeviceFCB.field_B0);
    a2->IoStatus.Information = 1i64;
    // 省略部分代码
    .......
    return 0i64;
  }
   // 省略部分代码
  .......
  result = RxpInitializeCreateContext(Context);
  if ( (int)result < 0 )
    return result;
  // 这里判断Options是否包含FILE_CREATE_TREE_CONNECTION标志位,在RxCreateTreeConnect中会赋值 FileObject->FsContext = &RxDeviceFCB;
  if ( (CurrentStackLocation->Parameters.Create.Options & FILE_CREATE_TREE_CONNECTION) != 0 )
  {
    TreeConnect = RxCreateTreeConnect((PRX_CONTEXT)Context);
    goto LABEL_22;
  }
  // 省略部分代码
  .......
  while ( 1 )
  {
    if ( p_FileName->Length && FileObject->FileName.Buffer[((unsigned __int64)p_FileName->Length >> 1) - 1] == '\\' )
    {
      // 不能包含FILE_NON_DIRECTORY_FILE会出错
      if ( (CurrentStackLocation->Parameters.Create.Options & FILE_NON_DIRECTORY_FILE会出错) != 0 )
      {
        // 省略部分代码
        .......
        TreeConnect = 0xC0000033;
        goto LABEL_20;
      }
      p_FileName->Length -= 2;
      WORD1(Context->field_2D8) |= 2u;
    }
    TreeConnect = RxCanonicalizeNameAndObtainNetRoot(Context, a2, &p_FileName->Length, &v25);
    if ( TreeConnect != 0xC0000016 )
      goto LABEL_20;
    // 省略部分代码
    .......
    // 会在RxCreateFromNetRoot->RxSetupNetFileObject设置FsContext 经过调试其PrivateDispatchVector一般为null
    TreeConnect = RxCreateFromNetRoot(Context, a2, (PIRP)&v25);

RxDeviceFCB在RxInitializeDispatchVectors()函数中初始化PrivateDispatchVector成员

可以看到默认的RxDeviceFCBVector支持的调用并不是很多

5.1.3 用户态进程对打开的文件进程操作,例如DeviceIoControl、读写等。

这个状态和上一个状态的调用流程差不多,只是调度函数有所不同。以fsDeviceIoControl会调用RxCommonDevFCBIoCtl。

RxCommonDevFCBIoCtl调用RxXXXControlFileCallthru从而调用csc.sys的调度表。

在RxXXXControlFileCallthru会调用RxLowIoPopulateFsctlInf()会根据IoControlCode的标志位选用不同buffer。

下面以Exp的流程为例,回顾以下整个流程:

最后看一下逆向后在csc.sys的patch代码:

五、Exp分析

利用就比较简单:

  • 首先使用NtQuerySystemInformation获得当前进程的eprocess和系统进程的eprocess进程的地址;
  • 然后使用任意写0当前线程的_KTHREAD的结构的PreviouseMode改为0即为kernel mode;
  • 此时使用NtReadVirtualMemory和NtWriteVirtualMemory由于_KTHREAD的PreviouseMode为0(kernel mode),即当前线程认为是内核线程,则内核不再检查读写的地址是否为用户态,从而达到任意地址写读;
  • 最后是将系统token替换当前token达到提权。
	RtlInitUnicodeString(&objectName, L"\\Device\\Mup\\;Csc\\.\\.");
	InitializeObjectAttributes(&objectAttr, &objectName, 0, NULL, NULL);
	
	status = NtCreateFile(&handle, SYNCHRONIZE, &objectAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, FILE_CREATE_TREE_CONNECTION, NULL, 0);
	if (!NT_SUCCESS(status))
	{
		printf("[-] NtCreateFile failed with status = %x\n", status);
		return status;
	}

Exp进行fsIoControl文件的路径\Device\Mup\;Csc\.\. 这个路径实际上是通用命名约定 (Universal Naming Convention, UNC)的写法,UNC 名称必须遵循 \SERVERNAME\SHARENAME 语法,在这里

  • SERVERNAME为Csc
  • SHARENAME 为.\.

通过winobj可以看到csc驱动的符号链接为\Device\Mup\;Csc,至于为什么后面加上.\.,这里还没有研究清楚,即mup.sys和csc.sys关系没有研究清楚。

六、总结

本文首先介绍了漏洞所在驱动的作用,然后通过漏洞成因以及补丁分析确定漏洞点。在确定漏洞点后,主要分析漏洞触发的整个过程,并简要分析了该漏洞exp的原理。

总体来说,这个漏洞的漏洞模式比较简单,不易被发现的原因是触发流程过长而且还涉及到多个驱动之间的间接调用,如果不是专门研究这个驱动,很难会分析到漏洞所在的函数。希望通过本文的介绍可以让读者熟悉rdbss驱动的相关机制从而更容易挖掘使用rdbss机制相关驱动的漏洞。

七、参考链接