前言-分析:
此漏洞是Microsoft Streaming Service Proxy服务产生的,此服务的文件路径如下
根据官方文档也可以知道,此服务的核心代理组件在mskssrv.sys中。而且这是一个内核的组件
漏洞触发的话,需要与驱动程序进行通信,以触发其漏洞。就像之前的 CVE-2023-21768 Windows AFD 一样,都是需要DeviceIoControl函数去触发的。
这就是设计到和驱动程序通信了,windows上驱动通信首先是需要创建设备对象,然后驱动程序在注册的过程中和I/O管理器通信,这时候用户模式下应用程序一般是通过调用 CreateFile 这类函数去打开设备,获取设备的句柄,然后通过文件句柄进行I/O 请求,通常使用DeviceIoControl 函数向设备发送IOCTL(I/O Control Code)
请求,这时I/O请求通过系统调用传递到内核模式下,然后驱动程序通过传递过来的IRP实现相应的IRP处理例程,根据请求的类型进行处理。像IOCTL 就使用 IRP_MJ_DEVICE_CONTROL,驱动程序在内核模式下处理IRP请求,执行对应的操作,并且与其他系统组件或者硬件进行交互。
通过DeviceIoControl函数触发想要的功能,这时候需要了解一下DeviceIoControl函数
DeviceIoControl(h, IOCTL_FRAMESERVER_INIT_CONTEXT, &buf, sizeof(buf), &buf, sizeof(buf), &bytesReturned, 0);
BOOL DeviceIoControl(
[in] HANDLE hDevice,
[in] DWORD dwIoControlCode,
[in, optional] LPVOID lpInBuffer,
[in] DWORD nInBufferSize,
[out, optional] LPVOID lpOutBuffer,
[in] DWORD nOutBufferSize,
[out, optional] LPDWORD lpBytesReturned,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
首先,从用户模式下的应用程序去访问驱动程序,我们需要DeviceIoControl函数而且还需要向驱动程序发送IOCTL来去访问它,那么前提上述介绍中也提到了首先还是用通过CreateFile来获得驱动程序设备。在mskssrv. sys中的函数PnpAddDevice可以知道,这是一个pnp设备,所以访问这类设备,通常是需要设备的接口路径。
然后PnpAddDevice函数中又调用IoCreateDevice函数了.
NTSTATUS IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN OPT PUNICODE_STRING DeviceName,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
IoCreateDevice 用于创建设备对象。这个函数通常在驱动程序的 DriverEntry 入口点中被调用,用于初始化设备对象,根据参数我们可以知道在PnpAddDevice函数中DeviceName是0。(如果DeviceName为 NULL,操作系统将为设备生成一个默认名称)
在mskssrv.sys中当设备创建时,调用驱动程序的PnpAddDevice,然后我们就可以发送ioctl来与将在驱动程序的调度控制函数中执行的设备进行通信。在此之前,我们还需要获取到设备接口,微软文档提供了枚举已经安装的设备接口的方法了。也可以通过下述代码来获取,总之这一点并不费劲。方法很多
#include <Windows.h>
#include <stdio.h>
#include <cfgmgr32.h>
int main(int argc, char** argv)
{
GUID class_guid = { 0x3c0d501a, 0x140b, 0x11d1, {0xb4, 0xf, 0x0, 0xa0, 0xc9, 0x22, 0x31, 0x96} };
WCHAR interface_list[1024] = { 0 };
CONFIGRET status = CM_Get_Device_Interface_ListW(&class_guid, NULL, interface_list, 1024, CM_GET_DEVICE_INTERFACE_LIST_ALL_DEVICES);
if (status != CR_SUCCESS) {
printf("fail to get path\n");
return -1;
}
WCHAR* currInterface = interface_list;
while (*currInterface) {
printf("%ls\n", currInterface);
currInterface += wcslen(currInterface) + 1;
}
}
我们还需要了解一下DispatchDeviceControl 和 DispatchInternalDeviceControl
驱动程序的调度例程 (查看 DRIVER_DISPATCH) 处理 I/O 函数代码分别为 IRP_MJ_DEVICE_CONTROL 和 IRP_MJ_INTERNAL_DEVICE_CONTROL 的 I/O。
驱动程序通过调用 IoBuildDeviceIoControlRequest 为基础设备驱动程序创建 IRP。通过调用函数DeviceIoControl 后者又调用系统服务。 I/O 管理器设置 IRP,并将主要函数代码IRP_MJ_DEVICE_CONTROL和给定的 I/O 控制代码存储在 Parameters.DeviceIoControl.IoControlCode 的IO_STACK_LOCATION结构中。 然后,I/O 管理器调用链中最高级别驱动程序的 DispatchDeviceControl 例程。对于某些旨在与新驱动程序互操作和支持新驱动程序的系统提供的驱动程序,操作系统还为 IRP_MJ_INTERNAL_DEVICE_CONTROL 请求定义了一组 I/O 控制代码。
这样就可以通过发送对应的ioctl再通过DispatchDeviceControl定义的去处理,就可以去触发其中的函数了。
因为这个驱动就100多个函数,所以去查找可以通过ioctl去使用的并不费劲如下。
FSRendezvousServer::InitializeContext
FSRendezvousServer::InitializeStream
FSRendezvousServer::RegisterContext
FSRendezvousServer::RegisterStream
FSRendezvousServer::DrainTx
FSRendezvousServer::NotifyContext
FSRendezvousServer::PublishTx
FSRendezvousServer::PublishRx
FSRendezvousServer::ConsumeTx
FSRendezvousServer::ConsumeRx
这里来看一下漏洞点的FSRendezvousServer::PublishRx函数,
首先要知道mskssrv.sys驱动程序有两个对象:分别为 FSContextReg 对象和 FSStreamReg 对象。
PublishRx函数首先从FsContext2中拿到流对象FSRegObject *fsRegisterObject之后 -- > 调用函数FSRendezvousServer::FindObject来验证指针是否匹配FSRendezvousServer存储的两个列表中找到的对象。
然后再往下进行,流程将会走到FSStreamReg::PublishRx 函数,而其中的参数是pStreamObject 和 fsRegisterObject。
但是 FSRendezvousServer::FindObject 函数检查中确实检测了是否存在 FSStreamReg对象,随后fsRegisterObject对象最后又走到了FSStreamReg::PublishRx函数中,但这里却没有检查作为参数接收的对象的类型。 所以在 FSRendezvousServer::FindObject 函数这里就会出现类型混淆漏洞。这就会导致我们可以使用 FSContextReg 对象参数去调用FSStreamReg::PublishRx 函数。
然后走到FSStreamReg::PublishRx函数调用中,这时候参数正常接收到的对象是 FSStreamReg *fsRegisterObject,但我们修改导致传入的是FSContextReg对象,那么就会发生越界(Out of Bound)操作。
所以 我们使用DeviceIoControl --> IOCTL_FRAMESERVER_PUBLISH_RX IOCTL 去跟msrv通信时,调用过程为:FSRendezvousServer::PublishRx() -->FSRendezvousServer::FindObject--> FSStreamReg::PublishRx()
最后在PublishRx()函数中会处理FILE_OBJECT的FsContext2字段中的任何内容,并将用于FSStreamReg对象。
这样最后 FSStreamReg::PublishRx()函数就会对它认为是FSStreamReg对象去进行处理,就会导致越界写等漏洞的产生。
Exp分析:
这里采用的主要Spray the pool 技术。调用 NtFsControlFile向池喷射大小为 0x80 的对象。
然后我们知道可以常用的利用方法是通过NtQuerySystemInformation函数去泄露相关内核对象的地址。
这样就可以泄露出KTHREAD地址(PreviousMode的地址),当前进程和系统进程的EPROCESS地址(当前进程和系统进程的Token地址,用作替换完成提权操作),
然后漏洞利用是通过ObfDereferenceObject函数将KTHREAD的PreviousMode字段从1减少到0,这样如果PreviousMode为0,就是内核模式,则可以通过NtReadVirtualMemory函数和NtWriteVirtualMemory函数读写内核地址了。然后thread_main函数里边就是进行内存读写的操作。
而在随后的thread_sep函数,这里就用到了之前提到的DeviceIoControl去进行通信,并且设置了IOCTL为IOCTL_PUBLISH_RX。这样就可以去调用FSStreamReg::PublishRx函数了。
代码中在一个单独的线程thread_sep中调用 ioctl: 这将调用FSRendezvousServer::PublishRx和随后的FSStreamReg::PublishRx
然后FSStreamReg::PublishRx将在FsContext2字段中是一个FSStreamReg对象,它是0x1d8字节大,而实际上存在一个较小的FsContextReg对象,与受控数据相邻,具体内存布局的情况就是下图
然后,FSStreamReg::PublishRx将调用ObfDereferenceObject对从相邻对象中取出的超出边界的字段。这样再去把当前线程的PreviousMode地址放在那,当调用ObfDereferenceObject时候就会使主线程的PreviousMode减为0。然后另一个线程就会调用内核地址上的NtReadVirtualMemory和ntwritvirtualmemory去替换token了。
void thread_sep()
{
printf("\t[+] Loop Thread Start..\n");
ULONG_PTR InBuf[0x20] = { 0 };
InBuf[4] = 0x100000001;
DeviceIoControl(cReg, IOCTL_PUBLISH_RX, InBuf, 0x100, NULL, 0, NULL, NULL);
printf("\t\t[+] Loop thread loop finished..\n");
SetEvent(hEvent);
}
然后在一组喷射对象中创建一些(Holes)
执行 IOCTL 操作:通过 DeviceIoControl 执行 IOCTL_FS_INIT_CONTEXT 操作,将 FsContextReg 对象放置在喷射对象的hole中,上述内存布局中提到过。
从FILE_OBJECT中读取FsContext2字段的值,以获得FsContextReg对象的地址。读取并将相邻池块头中的processbilling覆盖为NULL。
通过CreateFileA函数中拿到设备接口从而去获取到句柄。并且使用 CreateThread 创建两个线程,还使用 WaitForSingleObject 等待这两个线程的结束。线程分别是 thread_main 和 thread_sep。
一个线程thread_main 就是使用NtReadVirtualMemory读取系统进程令牌,直到读取成功。然后当另一个线程成功覆盖PreviousMode之后,thread_main就完成读取操作随后使用NtWriteVirtualMemory将System-token写入当前进程的EPROCESS。
这样最后用替换好的system-token调用 system 函数执行cmd命令。复现成功 如下图所示