CVE-2023-29360 mskssrv.sys漏洞分析与利用
CVE-2023-29360是微软驱动程序mskssrv.sys的一个权限提升漏洞,mskssrv.sys是微软流媒体服务相关组件,漏洞在6月份已被修复.本文分析调试环境为win11 22h2 22000.51
1.漏洞分析
该漏洞的修复版本是10.0.22621.1848,之前一个版本为10.0.22621.1,两个版本改动非常小,只有FsAllocAndLockMdl函数发生了变化.
在FsAllocAndLockMdl函数中调用MmProbeAndLockPages时,第二个参数rdx中的值由原来的0,修改为了1.也就是从KernelMode修改为了UserMode.
通过补丁来看,可以基本确定此函数引发了漏洞.
void MmProbeAndLockPages(
[in, out] PMDL MemoryDescriptorList,
[in] KPROCESSOR_MODE AccessMode,
[in] LOCK_OPERATION Operation
);
MmProbeAndLockPages函数用于探测指定的虚拟内存页,使其常驻,并将其锁定在内存中(比如在驱动开发ring3和ring0进行数据通信时,使用MDL锁定内存,可以在直接进行内存访问,不需要进行额外的内存copy)这样可以确保在设备驱动程序中仍在使用这些页时,停止分页产生的页交换,并且页无法被释放和重新分配.
MemoryDescriptorList:指向虚拟内存缓冲区的指针,如果函数成功将页面锁定在内存中,则会更新MDL以描述底层物理页面
AccessMode:检测参数的访问模式,KernelMode:0,UserMode:1
Operation:调用者希望检测问权限并锁定页面的操作类型,在上面的调用中,这里是IoWriteAccess表示驱动可以读写缓冲区.
MDL
关于mdl,微软官方文档是这样解释的,跨连续虚拟内存地址范围的IO缓冲区可以分布在多个物理页上,而且这些页是可以不连续的.操作系统使用内存描述符表MDL,来描述这些虚拟内存缓冲区的物理页布局.
MDL由一个MDL结构组成,后面是一个数据数组,用于描述Io缓冲区所在的物理内存,大小随着MDL所描述的io缓冲区的特性变化.
MDL的结构体,驱动程序只能访问其中的Next和MdlFlags成员,其余成员是不透明的,无法访问.
typedef struct _MDL {
struct _MDL *Next;//指向MDL链中下一个MDL的指针
CSHORT Size;
CSHORT MdlFlags;//锁定状态
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;
MDL的创建通过函数IoAllocateMdl进行分配,释放则使用IoFreeMdl.
在mskssrv.sys的函数FsAllocAndLockMdl中,对Mdl相关操作做了封装,内部调用IoAllocateMdl和MmProbeAndLockPages锁定指定长度的虚拟内存.
看一下FsAllocAndLockMdl的调用链.
SrvDispatchIoControl
FSRendezvousServer::PublishTx
FSStreamReg::PublishTx
FSFrameMdl::AllocateMdl
FsAllocAndLockMdl
在SrvDispatchIoControl中可以计算出FSRendezvousServer::PublishTx对应的io code为0x2f0408
然后在FSRendezvousServer::PublishTx函数中,将SystemBuffer数据传入FSStreamReg::PublishTx->FSFrameMdl::AllocateMdl->FsAllocAndLockMdl
SystemBuffer偏移0x88*i+0x28
从上面的流程中可知 FsAllocAndLockMdl的创建MDL时,是由ring3的SystemBuffer中一些配置参数创建的,而在FsAllocAndLockMdl中 MmProbeAndLockPages的参数却为KernelMode,因此这里的检测实际上是无效的.ring3可以传递过来一个内核的内存地址.
在修复补丁中设置为了UserMode,修复了漏洞.
2.漏洞利用
目前为止,由于MmProbeAndLockPages的错误检查,如果可以成功的调用到此函数,可以通过该漏洞创建一个内核地址的MDL.
如果有方法将这个MDL映射到ring3,那么将可以实现任意内核地址的读写,完成提权.而在这个驱动中存在通过函数
FSRendezvousServer::ConsumeTx是可以完成这个操作的,它对应的iocode为0x2f0410,在其内部,会调用MmMapLockedPagesSpecifyCache将Mdl映射到ring3.
目前为止,FSRendezvousServer::PublishTx和FSRendezvousServer::ConsumeRx函数是无法直接调用的.需要初始化一个结构,每次在调用函数时,都会通过FsGetRendezvousServer去获取这个结构,否则会返回0xC0000010.
并且传入的inputbuffer数据的结构需要逆向出来.让其可以正常的执行到目标位置
所以要通过调用一些前置的iocode来初始化.
首先看下FSRendezvousServer::PublishTx接收的InputBuff的结构,从判断条件可以分析出:
该结构的大小是大于0xb0的结构体.
offset_0x20-1<=0x12B
offset_0x24<=offset_0x20的
inputBuf长度>=0x88*(offset_0x20-1+0xb0)
在FSStreamReg::CheckRecycle中对以下偏移位置进行计算,0x5c,0x7c,0x24.,
在FSStreamReg::PublishTx函数中,可以确定偏移0x24处应该是一个循环次数,偏移0x70处是一个大于0的值.
然后将偏移0x28指针和申请的内存池传入FSFrameMdl::AllocateMdl.最后将这个内存池添加进链表.
然后在FSFrameMdl::AllocateMdl中将偏移为0x38,0x48,0x58,0x68,0x78,0x88,0x98,0xa8的值写入至了前面创建的内存池中.
然后通过偏移0x70处的值执行FsAllocLockMdl,如果为1,那么会将偏移0x60值作为创建MDL的虚拟地址.
如果为4,8,那么会将偏移0x48值和偏移0x60值作为创建MDL的虚拟地址
偏移0x6c和0x5c是长度.
在这个函数执行完后,inputbuf种0x28之后的数据会写入到内存池中,然后添加进链表.
在FSRendezvousServer::ConsumeTx的调用过程中,会从链表中取出,并将input_buf偏移0x70处8字节数据作为MmMapLockedPagesSpecifyCache的参数,这个参数非常重要,MmMapLockedPagesSpecifyCache函数在这里将MDL描述的物理内存映射到了ring3的一个虚拟地址.最后一个参数如果被设置为了MdlMappingNoWrite标志,那么映射的ring3虚拟地址,将没有写入权限.
#define MdlMappingNoWrite 0x80000000 // Create the mapping as nowrite
#define MdlMappingNoExecute 0x40000000 // Create the mapping as noexecute
这里的数据是结构体偏移0x70的8字节数据,右移1d位后,取反,然后与运算0C0000010,or 0x40000010.
得出的4字节数据,最高位不为1就可以了.
这里将8字节数据第32位设置位1,即可以通过验证.
通过以上对其结构的分析,可以总结出如下结构体
typedef struct _EvilBuffer {
uint64_t size; //0x0
uint64_t txsize; //0x8
uint64_t rxsize; //0x10
uint32_t txcount; //0x18
uint32_t rxcount; //0x1c
uint64_t value; //0x20
uint64_t value2; //0x28
uint64_t virtualAddress1; //0x30
uint64_t timestamp; //0x38
uint64_t field9; //0x40
uint64_t virtualAddress2; //0x48
uint64_t field10; //0x50
uint64_t size1; //0x58
uint64_t virtualAddress3; //0x60
uint64_t size2; //0x68
uint32_t Priority; //0x70
uint32_t flag; //0x74
uint64_t resolution; //0x78
uint64_t field11; //0x80
uint64_t field12; //0x88
uint64_t format; //0x90
uint64_t field13; //0x98
uint64_t dimension; //0xA0
uint64_t field14; //0xA8
uint8_t reserved2[0x110]; //0xB0
} EvilBuffer;
通过这个结构体调用FSRendezvousServer::PublishTx创建MDL,然后再调用FSRendezvousServer::ConsumeTx映射回ring3就可以通过写入任意内核地址完成提权了.
至于前置通过FSRegisterStream,FSInitializeStream,FSInitializeContextRendezvous,FSRendezvousServerRegisterContext进行初始化需要的一些结构体就不再另行分析了.
这个漏洞原理并不复杂,只是想要完成利用,需要逆向很多结构,设置一些固定的标志位...