CVE-2024-26229 Windows CSC 本地内核提权漏洞复现
前言
该漏洞利用Windows CSC驱动程序存在的问题,进行越界写0修改KTHREAD结构中的PreviousMode字段,使其为KernelMode。这样,后续的写操作不会进行权限检查,从而允许我们修改内核数据结构,实现任意地址写入。
通过任意地址写入操作,我们可以将当前进程的Token字段替换为系统进程的Token字段。这样当前进程就能够获得系统进程的权限,从而实现提权。
其流程图可简化如下:
漏洞分析
如何实现越界写0
CSC驱动
csc.sys驱动是一个处理客户端缓存(Client-Side Caching)和提供离线文件功能的系统驱动(windows默认启用)。csc.sys允许用户在断网的情况下继续访问和操作网络文件,当用户在没有网络连接的情况下对这些文件进行更改时,这些更改首先影响的是本地缓存的副本;一旦网络连接恢复,CSC.sys 会负责将这些本地更改同步回网络位置,确保网络上的数据与本地的副本保持一致。
NtFsControlFile
引发的写0漏洞
Windows在涉及与内核通信的时候,会使用一种叫做IRP(I/O Request Package)
的IO数据包,将用户态的必要数据带入到内核态,再有内核态进行处理后返回。这个IRP可以注册多种处理,包括常见的文件读写,创建等等。其中当为了能够直接与特定类型设备通信的时候,会在内核态注册一种叫做IRP_MJ_DEVICE_CONTROL
的调用函数,此时用户态可通过DeviceIOCoontrol
与其通信。类似的,当操作涉及文件系统的时候,通常会注册针对文件系统的IRP_MJ_FILE_SYSTEM_CONTROL
,此时与设备通信的时候就会用到NtFsControlFile
。
此函数的描述如下:
NtFsControlFile( |
其中介绍几个比较重要的参数:
- FileHandle:指向打开的设备句柄
- IoStatusBlock:指向IO操作结果的指针
- FSControlCode:用于描述访问结构的ControlCode,类似于IOCTL
- InputBuffer:用户输入数据的指针地址
- InputBufferLength:用户输入数据的长度
- OutputBuffer:用户输出数据的指针地址
- OutputBufferLength:用户输出数据的长度
实际上严格来说FSCTL与IOCTL非常相似,尤其是从数据传输角度来说,从官方文档来看,用户态对这两种过程使用过程应当是大差不差的
当进行这几种直接通信的过程时候,用户通常可以直接从用户态传入两段内存地址,用于存储输入和输出。
这个函数中有一个指针Type3InputBuffer
,他指向由用户态传入NtFsControlFile
的指针InputBuffer
,并且该指针完全不被内核解析处理。这样一来,指针指向的地址是否合法,以及指针内容的大小均不被检查,所以此处的指针可以写入任意地址中。总结一下,漏洞即为由于对指针使用检查不严谨,导致了一个可以往用户可控内存地址写入0的漏洞出现。
KernelMode
和UserMode
PreviousMode 通常是 KTHREAD 结构的一部分,在 Windows 内核的头文件中,PreviousMode 通常被定义为 MODE 枚举类型,数据结构如下图所示:
在 Windows 内核中,PreviousMode
用于标识线程最后一次执行的模式。这个字段有两个值:
- PreviousMode = UserMode (1)
- 当线程从用户模式调用系统服务(如通过
Nt
或Zw
前缀的函数)进入内核模式时,PreviousMode
被设置为UserMode
。这表明调用来源于用户空间,并且是在用户态下发起的系统调用。在用户模式下,应用程序受到更多的限制和安全检查。
- PreviousMode = KernelMode (0)
- 当线程已经在内核模式下执行,并且调用内核模式的例程或函数时,
PreviousMode
保持为KernelMode
。这表明操作是在内核空间内部进行的。 - 在内核模式下,代码具有完全的系统访问权限,也就是我们提权的关键。
修改进程字段
NtReadVirtualMemory
和NtWriteVirtualMemory
在2018年的Bluehat上,Kaspersky研究员提出了一种很有趣的利用技巧,对于NtReadVirtualMemory
和NtWriteVirtualMemory
这类函数,在PreviouseMode
为UserMode的时候,它会检查当前访问的地址空间是否为用户态可访问的空间,但当PreviouseMode
为KernelMode
的时候,并不会进行这类检查
__int64 __fastcall MiReadWriteVirtualMemory( |
也就是说,当我们能够想办法将当前线程的PreviouseMode
值为0的时候,我们即可绕过内存地址检查,直接调用NtReadVirtualMemory
或者NtWriteVirtualMemory
实现真正意义的任意地址写
最终的攻击
当我们能够实现任意地址写,即可配合这个github中提到的Windows常见泄露技巧,尝试泄露敏感进程(System进程)的Token,并且将该Token写入我们当前进程,即可实现提权。
这边结合公开的脚本分析一下利用流程
- 首先利用由
NtQuerySystemInformation
封装的函数GetObjPtr
泄露System进程的EPROCESS
以及当前线程的ETHREAD
:
GetObjPtr(&Sysproc, 4, 4); // 泄露System 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); |
- 此时,可以实现往内核地址的读写,将System进程Token地址拷贝到当前进程
Write64(Curproc + EPROCESS_TOKEN_OFFSET, Sysproc + EPROCESS_TOKEN_OFFSET, 0x8); |
- 恢复PreviousMode,此时该进程完成提权
// |
其流程可以大致表示为下图:
POC分析
/* |