2020年3月12日微软确认在Windows 10最新版本中存在一个影响SMBv3协议的严重漏洞,并分配了CVE编号CVE-2020-0796(也可以被称为SMBGhost),该漏洞可允许攻击者在SMB服务器或客户端上远程执行代码。要在SMB服务器上利用该漏洞,未认证的攻击者需要发送一个特定的伪造的包给目标SMBv3服务器。要在SMB客户端上利用该漏洞,未认证的攻击者需要配置一个恶意SMBv3服务器,并让要被攻击的用户连接到该服务器。
在上一篇文章中,我们提到,尽管Microsoft安全通报将漏洞描述为远程代码执行(RCE)漏洞,但是没有可用的POC来演示。为此,我们开发的POC使用了不同的方法,且不涉及物理内存访问。相反,我们使用SMBleed(CVE-2020-1206)漏洞来开发这个POC。
无需身份验证的内存读取是在为RCE攻击做准备
上一篇文章我们分析了本地特权提升攻击,SMBGhost可用于RCE攻击,我们旨在通过本文章演示如何实现此攻击。对于RCE功能,我们需要知道在何处写入任意数据。由于现代Windows版本中的大多数内存布局都是随机的,因此在任何位置写入任意数据的能力仍然非常有限。在寻找另一种帮助攻击的功能时,我们发现了Microsoft SMB实施中的一个新漏洞。我们将其命名为SMBleed,因为它允许通过SMB远程泄漏内存的某些部分(类似于Heartbleed)。虽然概念相似,并且经过身份验证的用户可以读取大块未初始化的数据,但没有身份验证的攻击面受到更大的限制。由于我们的目标是未经身份验证的RCE利用,所以我们首先要寻找的是一种读取未经身份验证的内存的方法。
使用SMB技术
SMBleed漏洞允许攻击者发送消息,使其开始受到攻击者的控制,而消息的其余部分包含未初始化的数据,这些数据被视为消息的一部分。对于经过身份验证的用户,有一种简单的方法可以利用这一点,即使用SMB2 WRITE消息将未初始化的数据写入文件,然后使用SMB2 READ命令读取它。我们首先为未经身份验证的用户寻找一种类似的技术,即一种发送消息的方式,以便以后可以检索。
在浏览了协议规范并调试了几个会话之后,我们看到常规流程从客户端发送的以下命令开始:
SMB2 NEGOTIATE → SMB2 SESSION_SETUP → SMB2 SESSION_SETUP
如果使用了不正确的凭据,会话将在第二个SMB2 SESSION_SETUP请求之后中止。
通过使用SMB2协商- MSDN连接到一个共享
假设我们没有有效的凭据,因此我们检查了是否可以在不进行身份验证的情况下发送其他命令。经过测试,我们发现:
1. 要发送的第一个命令必须是SMB2协商,它还必须是会话期间唯一的SMB2协商命令。
2. 在身份验证成功完成之前,后续命令必须为SMB2 SESSION_SETUP。除非对命名管道或共享的匿名访问是不受限制的,而且是默认的。
由于未压缩SMB2协商消息,因此剩下的就是SMB2 SESSION_SETUP。因此,我们仔细研究了SMB2 SESSION_SETUP消息的格式,希望找到一种方法来获取正在发回的一些数据。
进一步了解SMB2 SESSION_SETUP
正如我们已经提到的,我们观察到的一个常规会话发送了两个SMB2 SESSION_SETUP命令。首先,我们检查对这些消息的响应是否返回了一些数据。如果是这样,我们可以尝试创建一条消息,使数据未初始化。遗憾的是,我们没有找到这样的数据。我们无法找到影响第一个响应的方法,而第二个响应的正文为空,数据包标头中的状态为0xC000006D(STATUS_LOGON_FAILURE)(请记住,我们假设没有有效凭据)。第一个SMB2 SESSION_SETUP请求包含一条NTLM协商消息,第二个SMB2 SESSION_SETUP请求包含一条NTLM身份验证消息。
NTLM身份验证消息
通过研究NTLM认证消息,我们得出结论,该消息最复杂的部分是NTLM2 V2响应结构,它最适合被利用。它是一个可变长度的字节数组,主要由NTLMv2_CLIENT_CHALLENGE结构组成。我们注意到,如果该结构未通过某些初始检查,则返回0xC000000D (STATUS_INVALID_PARAMETER)参数,而不是0xC000006D (STATUS_LOGON_FAILURE)。其中一些检查正在验证“AvPairss”字段。
AvPairs字段是一个可变长度的字节数组,它包含一个AV_PAIR结构序列。每个AV_PAIR结构定义了一个属性/值对,属性由AvId字段定义,AvLen字段以字节的形式定义值的长度,value字段是一个包含值本身的可变长度字节数组,属性为MsvAvEOL且长度为零的项表示数组的结束。
SMB2数据包内的AvPairs
身份验证消息由msv1_0.dll模块中的SsprHandleAuthenticateMessage函数处理,在初始检查中,该函数确保AvPairs数组包含以下属性:0x0001(MsvAvNbComputerName),0x0002(MsvAvNbDomainName)。该值未被检查,检查本身是通过遍历数组并检查请求的属性是否存在,以及它的长度是否在结构中完成的。如果长度太大,则停止遍历。因此,实际上,要使NTLM身份验证消息有效,并不需要MsvAvEOL项。
此时,我们认为可以创建一个请求来回答以下问题:给定偏移量x处的两个字节(解释为uint16),该值是否大于y?x和y由我们控制。资料包结构如下:
值0x0001 (MsvAvNbComputerName)的内容并不重要,因此我们可以使用它来调整第二个值的偏移量。对于第二个值,我们只将属性设置为0x0002 (MsvAvNbDomainName),而不初始化长度和值。我们还设置了整个数据包的大小,以便在length字段后面有y个字节。根据第二个值的长度字段的未初始化值,有两种可能的结果:
1. length <= y:在这种情况下,检查通过了,这是因为找到了一个有效的0x0002 (MsvAvNbDomainName)值。由于凭据不正确,服务器返回0xC000006D (STATUS_LOGON_FAILURE)。
2. length> y:在这种情况下,检查失败,因为第二个值的长度无效并被丢弃。对于这种情况,服务器返回0xC000000D (STATUS_INVALID_PARAMETER)。
根据服务器的响应,我们可以推断出问题的答案。
现在我们可以得到这一小段信息,对吧?没有那么快。不幸的是,NTLM身份验证消息被限制为0xB48字节,如果大于这个字节就会被丢弃。检查是由msv1_0.dll模块中的SspContextGetMessage函数完成的,我们可以通过只保留两个长度字节中的一个未初始化来解决这个问题吗?不幸的是,没有,因为uint16值被编码为小尾数,就我们目前所知,我们只能将第二个有效字节保留为未初始化状态,这并没有太大帮助。由于无法在单个SMB会话中更好地实现某些功能,我们需要继续研究。
观察#1:备用清单
正如我们在之前的研究中已经提到的那样,在内核中处理SMB的模块(srv2.sys和srvnet.sys)使用由srvnet.sys导出的自定义分配函数SrvNetAllocateBuffer。这个函数对小的分配使用备用列表进行优化。备用列表用于有效地为驱动程序保留一组可重用的、固定大小的缓冲区。
备用列表是在初始化时创建的,每个大小和逻辑处理器的列表,如下表所示:
每个带有“�”符号的单元格都是一个单独的备用列表。为了简化我们的分析,我们假设目标只有一个逻辑处理器(在本文的第三部分中,我们将为目标包含一个以上的逻辑处理器)。在这种情况下,只要分配了相同数量的字节,就使用相同的备用列表,并且一次又一次地重用相同的分配缓冲区。我们将使用该实现细节来对未初始化的数据进行一些控制,而且很快会看到。
观察#2:解压失败
让我们再来看看当压缩包被解压时会发生什么(关于更多细节和伪代码,请参考我们之前的研究):
如果CompressedData无效,则解压缩阶段将失败,复制阶段将不执行,连接将被删除。但是,只有在提取了有效的部分压缩数据之后,解压才可能失败。这允许我们创建一个请求,这样我们所选择的数据将被写入到我们所选择的偏移量中,就像这样:
返回到NTLM身份验证消息
我们可以通过两个步骤使用以上观察结果来使我们的技术起作用:
1. 发送包含无效压缩数据的消息,以便仅提取单个零字节。该字节将是AvPairs数组中第二个值的长度的最高有效字节。
2. 像前面一样发送消息,但请确保分配使用相同的备用列表,以便零字节在那里。
这次,该技术可以回答以下问题:给定偏移量x处的字节,该值是否大于y?和以前一样,x和y由我们控制。
由于我们可以通过确保使用相同的备用列表来重复使用缓冲区,因此我们可以在更改y的同时重复执行几次,最后推断出给定偏移量的字节值。
不幸的是,这种技术有一个限制。我们可以读取的字节的偏移量限制在从数据包缓冲区开始的0xADB字节。这是因为NTLM身份验证消息(AUTHENTICATE_MESSAGE)的偏移量在SMB2 SESSION_SETUP标头(由srv2.sys中的Smb2ValidateSessionSetup函数强制执行)的结尾之后被限制为0x40字节,并且NTLM身份验证消息(AUTHENTICATE_MESSAGE)的大小受到限制就像我们已经提到的0xB48字节。
克服偏移限制
假设我们要读取一个偏移量为0x1100的字节(我们将在写作的第三部分中了解为什么要走这么远)。我们不能直接用我们的技术来做到这一点,但是我们找到了以下解决方案:由于缓冲区从备用列表中重用,我们可以通过解压缩函数“提升”目标字节,方法是将偏移量字段设置为指向该字节之外。我们只需要确保位于那里的数据可以被解释为有效的压缩数据,否则不会发生复制。
传入的数据包缓冲区包含额外的16个标头字节,在进行解压缩时不会被复制。结果,将包括目标字节在内的复制数据复制到距离已分配缓冲区的开头更近16个字节的位置。我们可以重复几次,直到目标字节偏移足够低为止。
地址泄漏POC
你可以在这里找到一个演示上述技术的脚本,请记住,我们假设目标计算机只有一个逻辑处理器,因此必须正确配置VM才能使脚本工作。如果一切顺利,脚本将从NonPagedPoolNx池读取并打印一个地址。实际上,它是驻留在一个备用列表中的一个缓冲区的地址。
另一种方法:解压
随着研究的深入,我们意识到解压后的SMB包并不是唯一以各种方式无效的复杂结构。即使在处理所有与smb相关的结构之前,压缩的缓冲区也可能无效。如果解压缩失败,连接将被删除,这是可以检测到的。
微软的SMB实现提供了三种压缩算法供你选择:LZNT1,Plain LZ77和LZ77 + Huffman。我们查看了LZNT1,因为它是列表中的第一个,而且它相当简单,一个解压缩函数大约有80行Python代码。无需深入讨论细节,压缩数据由一系列压缩块组成,每个块以标记其长度的uint16变量开始。当遇到长度为0时,解压缩就完成(类似于以null结尾的字符串,但它是可选的)。同样,方便的是,0字节范围表示有效的压缩数据。通过上面的方法,我们成功地回答了与前面方法相同的问题:给定一个位于偏移位置x的字节,其值是否大于y?这里,x和y也是由我们控制的。
我们通过发送一个有效的压缩包,紧随其后的是类似于以下内容的字节范围(注意,这是一种简化,实际的字节值略有不同),从而实现了这一目标:
根据length字段中最不重要字节的未初始化值,有两种可能的结果:
length <= y:在这种情况下,整个压缩块将由零字节组成,这是完全有效的,而下一个块的长度将为零,从而成功完成了解压缩,服务器将返回一个响应。
length> y:在这种情况下,第一个或第二个压缩块将包含0xFF字节,这将使解压缩失败。服务器将断开连接。
与前面的技术一样,我们可以使用观察值#1和#2通过以下两个步骤在消息中间使用未初始化的字节来创建消息:
1. 发送包含无效压缩数据的消息,以便仅提取我们需要的部分,将要提取的字节是上图中的字节。
2. 发送第二条消息,但请确保分配使用相同的备用列表,以使步骤1中的字节在那里。
请注意,SMB数据包标头中的“偏移”值将指向压缩数据,该数据是否有效取决于初始化字节的值。有效的SMB数据包将未经压缩地发送。另请注意,由于“偏移”值大于消息本身,因此压缩数据大小的计算中会出现溢出,最终导致数量巨大。通常,这并不是问题,因为减压成功与否很快就结束了。但是有时系统会由于读取越界而崩溃,我们很少尝试解决此问题,因为这种情况很少发生,并且POC非常复杂。
与前一种技术相比,这种技术最显著的优点是不再有偏移量限制。即使我们设法克服了限制,它仍然需要发送大量数据包,从而损害了性能和稳定性。
ZecOps检测
ZecOps将与此问题相关的取证日志分类为以下标签#SMBGhost和#SMBleed,你可以找到有关如何将ZecOps解决方案用于端点和服务器,移动设备或应用程序的更多信息。
缓解法
你可以通过以下方法来弥补这两个问题的影响:
1. 应用最新的安全修复(推荐);
2. 阻塞端口445 /强制执行主机隔离;
3. 禁用SMBv3.1.1压缩;
总结
在本部分中,我们描述了如何利用SMBGhost和SMBleed从内核池中远程读取未初始化的数据,并且无需身份验证。在下一篇文章中,我们将展示它如何帮助我们实现RCE。
参考及来源:https://blog.zecops.com/vulnerabilities/smbleedingghost-writeup-part-ii-unauthenticated-memory-read-preparing-the-ground-for-an-rce/