1
概述
2020年3月10日是微软补丁日,安全社区注意到Microsoft发布并立即删除了有关CVE-2020-0796的信息;
2020年3月11日早上,Microsoft发布了可纠正SMBv3协议如何处理特制请求的修补程序;
2020年03月12日微软发布安全公告声称Microsoft 服务器消息块 3.1.1 (SMBv3) 协议处理某些请求的方式中存在远程执行代码漏洞。成功利用此漏洞的攻击者可以获取在目标服务器或客户端上执行代码的能力。要利用针对服务器的漏洞,未经身份验证的攻击者可以将特制数据包发送到目标 SMBv3 服务器。要利用针对客户端的漏洞,未经身份验证的攻击者将需要配置恶意的 SMBv3 服务器,并说服用户连接到该服务器。此安全更新通过更正 SMBv3 协议处理这些特制请求的方式来修复此漏洞。
此缺陷可影响SMB协商中的客户端和服务端。服务端漏洞位于srv2.sys中,客户端漏洞位于mrxsmb.sys中,这两个漏洞最终都在SmbCompressDecompress中调用了相同的代码。
本文试以CVE-2020-0796为例,为读者呈现漏洞分析工作视角。
2
受影响的系统
Windows 10 Version 1903 for 32-bit Systems
3
分析
首先我们来执行CVE-2020-0796的PoC
PS C:\Users\admin\CVE-2020-0796\> python .\poc.py 192.168.0.10
图 1
如果目标系统未处于调试状态,我们将观察到目标设备如图1所示进入蓝屏状态。待Windows系统重启后,我们会使用WinDBG打开C:\Windows\System32\MEMORY.DMP文件,通过分析内存转储文件尝试找到触发蓝屏的原因。
如果目标系统处于调试状态,将会在WinDBG中观测到如图2所示的中断:
图 2
3.1
释放内存的错误
无论是任何一种情况,大多时候在WinDBG中首选执行!analyze -v,尝试由WinDBG自动分析导致问题的模块。
或者查看栈回溯
kd> kn
如上文0x0C号栈帧所示,srvnet模块中的SmbCompressionDecompress函数在调用ExFreePool时是触发蓝屏的直接因素。
同时,我们注意到上文0x0D号栈帧所示的返回函数是模块名+偏移量的形式,这是因为WinDBG没有加载srv2模块的的符号文件。加载srv2模块的符号之后,栈回溯更有可读性:
kd> lml
kd> kn
根据函数名称字面理解或参考DDK文档ExFreePool是释放内存的函数,一般不会有什么问题。这个涉及Windows内核的Pool内存管理机制及结构。过往经验告诉我们,ExFreePool需要操作的内存结构被破坏掉了,即这可能是个Windows内核中的内存破坏漏洞(Memory Corruption)。
人生终极三问:你是谁?从哪里来?到哪里去?在漏洞分析领域同样适用。
为搞明白ExFreePool要释放的内存,来自哪里,又是被谁搞坏的。我们需要在IDA Pro中看看srvnet模块中的SmbCompressionDecompress函数。
图 3
当然如果你那边IDA Pro显示的和图3所示不同,没有这些可读性较好的变量名,而是像图4这样
图 4
也不必惊讶,后续我们会解释,如何通过公开的文档、符号文件或者数据流,注解IDA Pro函数名或者变量名,使得显示更加友好,以便开展分析工作。这个过程有点像Windows系统自带的扫雷游戏。
IDA Pro显示srvnet模块中的SmbCompressionDecompress函数主要流程十分清晰:申请内存(ExAllocatePoolWithTag)、解压处理(RtlDecompressBufferEx2)、释放内存(ExFreePoolWithTag)。
我们现在已知蓝屏的直接原因是释放内存的操作引起的,那么问题就显然出现在成功申请内存之后,到释放内存之间的这个过程中。我们看到这个过程中只有一个处理函数,即RtlDecompressBufferEx2。
现在所有的疑点都集中在了RtlDecompressBufferEx2函数上,
图 5
我们来看看这个ntoskrnl模块中的RtlDecompressBufferEx2函数。
图 6
在图6中IDA Pro显示RtlDecompressBufferEx2函数是根据参数CompressionFormat的一个跳转函数。
图 7
RtlDecompressBufferProcs数组前2个QWORD元素为0。即当CompressionFormat取值为3时,函数最终转向RtlDecompressBufferXpressLz函数中。
图 8
图 9
在图8和9中,IDA Pro显示RtlDecompressBufferXpressLz函数是一个300多行伪代码的复杂函数。
静态分析有点吃力,为了快速定位问题,让我们来试试用WinDBG动态调试一下。
还是执行PoC,windbg中断时执行kn或者!analyze -v。这次我们试试!analyze -v。
FOLLOWUP_IP:
太棒了,我们和WinDBG达成了共识。它直接提示可能是nt!RtlDecompressBufferXpressLz+2d0处出了问题。
图 10
图 11
如图10和11所示,现在我们了解到nt!RtlDecompressBufferXpressLz+2d0处是一个内存复制函数qmemcpy。这符合往常的漏洞构成的元素。
我们需要再了解一下qmemcpy里面的这3个参数。
kd> !pool 0xffffe402f06b3000
kd> !pool 0xffffe402f06b3000+0xef30
我们设置一个这样的断点:
bp nt!RtlDecompressBufferXpressLz+0x2D0 ".printf \"RtlDecompressBufferXpressLz(), qmemcpy(dst=0x%I64x, src=0x%I64x, count=0x%I64x)\", rdi, rsi, r9;.echo"
当WinDBG中断下来时,我们就能得到感兴趣的qmemcpy的3个参数:
kd> r
伪代码如下:
qmemcpy(dst=0xffffe402ec9f4439, src=0xffffe402ec9f4438, count=0x8483ffff)
查看一下目的内存的pool信息:
kd> !pool 0xffffe402ec9f4439
这是一个0x1280大小的large page allocation非分页池内存。qmemcpy函数准备向其中写入0x8483ffff大小的数据。很显然会溢出。
kd> !pool 0xffffe402ec9f4000+0x1280
对于Pool内存的大小不超过一个页面长度(PAGE_SIZE,即4K字节)时,可以通过使用POOL_HEADER结构体来查看pool块信息。而对于large page allocation的内存却不行。
我们注意到0xffffe402ec9f4000之后在ffffe402ec9f5280 处是一个0x700大小的空闲块,再之后ffffe402ec9f5990 处是一个0x290 大小的已被分配使用的块。
kd> !poolval 0xffffe402ec9f4000+0x1280
在qmemcpy函数执行后,我们发现ffffe402ec9f5280处的_POOL_HEADER确实被写入了数据。
3.2
复制数据的大小
现在我们需要搞明白,复制数据大小和目的地址的来源。
图 12
经过类似的断点和调试,我们在nt!RtlDecompressBufferXpressLz+0x2AA处,观察到qmemcpy中的count数据来自于RtlDecompressBufferXpressLz收到的参数CompressedBuffer的最后4个字节与3之和(图12所示)。因此操作压缩数据末尾的4个字节,可以控制复制数据的大小。
复制数据大小的来源已经清楚了,就剩下最后一个谜团--目的地址的来源。
3.3
目的地址的来源
图 13
我们根据设置的WinDBG断点日志,整理了图13所示的函数调用及数据传递过程。也顺便介绍前文所述的如何通过公开的文档、符号文件或者数据流,注解IDA Pro函数名或者变量名,使得显示更加友好,以便开展分析工作。入手点是https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-rtldecompressbufferex2查阅到的关于RtlDecompressBufferEx2的定义或NT之前泄露的源码中的相关函数定义。
从日志上来看,qmemcpy的目的地址正是UncompressedBuffer偏移1的地方。
Srv2DecompressData+0x85处的ExAllocatePoolWithTag() 返回值是0xffffa28f92503000,位于UncompressedBuffer之后0x370CBC8的位置。
即qmemcpy写入数据大小范围内有其他的Pool块时,将会导致ExFreePoolWithTag()时出错。
3.4
任意地址写入
如果size大小合适或者其范围内没有在用的Pool块,如0x1100+0n24大小时,则会有下述情况:
图 14
我们根据相关函数调用,绘制了图14所示的内存布局图。
当srv2!Srv2DecompressData+0x79处 SrvNetAllocateBuffer((unsigned int)(hdr.OriginalCompressedSegmentSize + offset)申请内存时,返回值设定AllocateBuf,简称A点。B点至U点正是SMB协议头中的offset值0x03e8(0n1000)。
图 15
OriginalCompressedSegmentSize值(图15中Wireshark所示的OriginalSize)过大,与offset相加导致整数溢出。最终申请了一个较小的内存。即B点至A点的内存。内存的起始地址被写在AllocateBuf+0n24的P点。
当解压函数把超量数据写入U点时,如果超过了之前申请的内存(B点至A点的内存),也会覆盖原本存放在P处的指针。
srv2!Srv2DecompressData+0x108处的memmove会读取P点的指针作为目的地址,写入原始数据中offset之前的数据,从而完成预定的解压逻辑。当P处的指针可以被改写后,攻击者就获得了一次任意地址写入任意数据的能力。
srv2!memmove(Src=0xffffcb0558c5f060, Dst=0x4141414141414141, Size=1000)
kd> db 0x0xffffcb0558c5f060
kd> !pool 0x0xffffcb0558c5f060
至此漏洞分析视角下的工作基本完成,撰写分析报告时,我们会用倒叙的方法,就是大家经常看到的文章形式。后续文章我们再谈谈漏洞补丁分析和漏洞利用。
4
解决方案
尽快安装微软官方补丁或在网络出入口上阻止TCP端口445,以防止SMB流量进出互联网。此外,我们建议您进行内部网络分段,并禁止终端之间的SMB连接,以防止横向移动。
禁用SMBv3压缩将防止利用易受攻击的SMB服务器。要禁用SMBv3压缩,可以在PowerShell中运行以下命令:
Set-ItemProperty -Path
5
综述
此漏洞对攻击者具有很高的价值,可使得攻击者很容易触及分配内存的函数,并且可以控制触发溢出的数据大小。蓝屏(BSOD)一般是远程代码执行的前兆,从其进化到远程代码执行(RCE)会更具挑战性,因为需要借助其他漏洞以便绕过Windows最新的缓解技术(KASLR)。应警惕漏洞利用难度稍小的本地权限提升情景,请尽快安装官方补丁。
6
参考 &引用
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/1d435f21-9a21-4f4c-828e-624a176cf2a0
7
命令&断点
sxe ld srv2
8
思考&讨论
能坚持读到这里确属不易:),我们思考讨论以下3个问题有没有较好的解决办法?
如何像查看小于PAGE_SIZE的内存使用dt nt!_POOL_HEADER那样,查看large page allocation内存的size结构?
对于较大的内存范围,如大于8字节,使用内存访问断点?
对于较复杂的执行流程,从最终的数据,如何便捷的向前溯源过往有关分支?有点类似反向污点数据追踪。