受影响版本:
系统 | 版本 |
---|---|
Microsoft Windows 10 | |
Windows 10 | 1607 |
Windows 10 | 1709 |
Windows 10 | 1803 |
Windows 10 | 1809 |
Windows 10 | 1903 |
Windows 10 | 1909 |
Windows 7 SP1 | |
Windows 8.1 | |
Windows RT 8.1 | |
Windows Server | 2008 SP2 |
Windows Server | 2008 R2 SP1 |
Windows Server | 2012 |
Windows Server | 2012 R2 |
Windows Server 2016 | |
Windows Server 2019 | |
Windows Server | 1803 |
Windows Server | 1903 |
Windows Server | 1909 |
此漏洞只会影响SMB v3.1.1
客户端与服务端都存在此漏洞
服务端漏洞位于srv2.sys内核模块中,客户端漏洞位于mrxxmb.sys模块
漏洞描述
从win10 1903/win server 1903开始对SMB v3.1.1进行数据压缩的支持
包格式
翻译后内容
此次漏洞触发原因是因为客户端/服务端在进行数据解压是未对OriginalCompressedSegmentSize与Offset/Length 进行合理的长度检查造成的
先来大致梳理一下函数的调用关系:
DriverEntry=>Srv2DeviceControl=>Srv2ProcessFsctl=>Srv2StartDriver=>Srv2StartInstance=>Srv2ReceiveHandler
然后Srv2ReceiveHandler函数会将Srv2DecompressMessageAsync函数放入SLIST_ENTRY链表中进行回调异步调用
然后Srv2DecompressMessageAsync函数会调用去调用Srv2DecompressData函数
Srv2DecompressData函数会根据OriginalCompressedSegmentSize与Offset/Length进行内存分配
_mm_srli_si128函数是一个与XMM寄存器相关的函数,此函数让第一个参数v3逻辑运算向右移8个字节,要注意他的移动单位是字节不是位
此时Size指向ProtocolId,v4指向CompressionAlgorithm,Size偏移1个32位即4字节便是OriginalCompressedSegmentSize,v4偏移一个4字节便是Offset/Length
然后SrvNetAllocateBuffer函数申请内存空间其大小等于OriginalCompressedSegmentSize+Offset而这两个值都是可控的,然后进入SrvNetAllocateBuffer查看如何进行内存分配,要注意此函数与之后要看SmbCompressionDecompress的位于sysnet.sys模块中
进入SrvNetAllocateBuffe后他会先判断SrvDisableNetBufferLookAsideList是否为真,或者,参数1即要分配的内存大小是否大于0x100100
如果一方成立就进入if在判断参数1是否大于0x100100如果大于的话就返回失败,如果仅仅是SrvDisableNetBufferLookAsideList为真那就调用SrvNetAllocateBufferFromPool进行内存分配,再来看看SrvDisableNetBufferLookAsideList是如何初始化的
可以看出SrvDisableNetBufferLookAsideList是在函数SrvNetRefreshLanmanServerParameters中进行初始化的
可以看出SrvDisableNetBufferLookAsideList的值肯定为一个布尔值即真或假,SrvLibGetDWord函数会去调用ZwOpenKey打开注册表键值然后使用ZwQueryValueKey去读取注册表如果读取成功则返回一个指定值,如果读取失败则返回ZwQueryValueKey的返回值即失败原因,在我的系统里没有在注册表找到这个项,所以SrvDisableNetBufferLookAsideList的值默认为false,也就是说SrvNetAllocateBuffer的第一个if正常情况下不会去执行,顺着流程往下走可以看到
他会先判断参数1是否大于0x1100,然后求出到底用哪个值做SrvNetBufferLookasides的下标来获取内存,如果不大于0x1100则默认下标为0,再来看看SrvNetBufferLookasides是如何初始化的
进入SrvNetCreateBufferLookasides函数,一直追下去会发现PplCreateLookasideList内部其实还是调用ExInitializeLookasideListEx函数来进行LookasideList列表的初始化,我们直接进入SrvNetBufferLookasideAllocate查看分配了新的LookasideList列表的函数,这里(1<<(v3+12))+256是要分配内存的大小,根据计算此大小依次为[0x900,0x1100,0x2100,0x4100,0x8100,0x10100......0x80100]
SrvNetBufferLookasideAllocate在内部又调用了SrvNetAllocateBufferFromPool函数
在SrvNetAllocateBufferFromPool函数中调用了ExAllocatePoolWithTag函数来分配指定类型的内存
分配大小v7我重命名为size,然后会发现size=v6+v3=(2*(MmSizeOfMdl+8))+(lParam2 + 232)
最后要返回的数据我重命名为backdata,刚刚从ExAllocatePoolWithTag函数获取到的数据重命名为ExAllocData
可以看出backdata=&ExAllocData[lParam2+0x57]&0xFFFFFFFFFFFFFFF8ui64
假设lparam2为0x1100,那0x1100+0x57=0x1157,0x1157&0xFFFFFFFFFFFFFFF8ui64=0x1150,也就是说返回的数据是从ExAllocatePoolWithTag函数获取到的数据的0x1150偏移处开始的
根据上面可以总结出,SrvNetAllocateBuffer函数最后会创建一个‘结构体+数据’这种类型的一块内存,这块内存结构大致如下
回到srv2.sys中的Srv2DecompressData,在用SrvNetAllocateBuffer申请过内存后会调用SmbCompressionDecompress函数来解压缩数据,此函数也在srvnet.sys中,其本质上是调用RtlDecompressBufferEx2函数来进行数据解压缩的
这里解释一下几个重要参数,方便与Srv2DecompressData中的传入的参数一一对应
- CompressionFormat:解压缩算法,此参数不用过多关注,他对应SmbCompressionDecompress的第一个参数
- UncompressedBuffer:解压后数据存放的缓冲区地址,对应SmbCompressionDecompress的第四个参数
- UncompressedBufferSize:解压数据缓冲区大小,对应SmbCompressionDecompress的第五个参数
- CompressedBuffer:待解压数据,对应SmbCompressionDecompress的第二个参数
-
CompressedBufferSize:待解压数据大小,对应SmbCompressionDecompress的第三个参数
返回值便是RtlDecompressBufferEx2函数的返回值
再回到Srv2DecompressData看看是如何调用SmbCompressionDecompress的
可以看出他会从
*(_QWORD *)(*(_QWORD *)(v1 + 240) + 24i64) + Size.m128i_u32[3] + 16i64
处获取压缩数据,经过解压放入
Size.m128i_u32[3] + *(_QWORD *)(backdata + 24)
backdata + 24指向刚刚SrvNetAllocateBuffer申请内存的起始位置,在这里也就是将解压后数据放入‘内存起始位置+SMB数据包offset/length’处,第六个参数v11用于接收解压后的数据大小,当SmbCompressionDecompress函数调用失败或者解压后的数据大小与SMB包中OriginalCompressedSegmentSize的值不一致时(不过如果RtlDecompressBufferEx2调用成功的话OriginalCompressedSegmentSize的值就会赋给v11),否则继续往后运行,接着往后看
这段代码可以解释为,如果offset/length不为0,则从
(v1 + 240) + 24i64) + 16i64)
处获取数据后放入
(v8 + 24)
指向的地址,根据分析上面SmbCompressionDecompress函数的调用可知
(v1 + 240) + 24i64) + 16i64)
大致指向压缩数据内存位置,
(v8 + 24)
指向内存起始的位置。
由于OriginalCompressedSegmentSize与Offset/Length长度我们可控,且SrvNetAllocateBuffer函数会根据他们俩来申请一块‘数据+结构体’形式的内存,我们可以申请一块较小的内存,将我们想要让重新赋值的某块内存的地址想办法构造payload填充到(v8 + 24)处,然后在momove函数执行时就会将我们想要写入的数据写入到(v8 + 24)处