CVE-2022-23253 是 Nettitude 在对 Windows Server 点对点隧道协议 (PPTP) 驱动程序进行模糊测试时发现的 Windows V**(远程访问服务)拒绝服务漏洞。此漏洞的含义是它可用于对目标服务器发起持续的拒绝服务攻击。该漏洞无需身份验证即可利用并影响 Windows ServerV** 的所有默认配置。
Nettitude 遵循了协调的披露流程,并向 Microsoft 报告了该漏洞。因此,最新版本的 MS Windows 现已修补,不再容易受到该问题的影响。
受影响的 Microsoft Windows Server 版本
该漏洞分别影响自 Windows Server 2008 和 Windows 7 以来的大多数 Windows Server 和 Windows Desktop 版本。要查看受影响的 Windows 版本的完整列表,请查看 MSRC 上的官方披露帖子:https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-23253 。
概述
PPTP 是一种 V**协议,用于在客户端和 V** 服务器之间多路复用和转发虚拟网络数据。该协议有两部分,TCP控制连接和GRE数据连接。TCP控制连接主要负责客户端和服务器之间的网络数据缓存和复用的配置。为了与 PPTP 服务器的控制连接进行对话,我们只需要连接到侦听套接字并发起协议握手即可。之后,我们就可以开始与服务器的完整 PPTP 会话。
在对漏洞进行模糊测试时,第一步通常是耐心等待崩溃发生。在对 PPTP 实现进行模糊测试的情况下,我们只需要等待三分钟,就可以在第一次可重现的崩溃之前!
我们的第一步是分析崩溃测试用例并将其最小化以创建可靠的概念证明。然而,在我们剖析测试用例之前,我们需要了解控制连接逻辑的几个关键部分正在尝试做什么!
PPTP 握手
PPTP 实现了一个非常简单的控制连接握手过程。所需要的只是客户端首先向StartControlConnectionRequest
服务器发送一个,然后接收一个StartControlConnectionReply
指示没有问题并且控制连接已准备好开始处理命令。的实际内容对StartControlConnectionRequest
测试用例没有影响,只需要有效地形成,以便服务器将连接状态推进到能够处理其余定义的控制连接帧。如果您对所有这些控制数据包帧应该做什么或包含什么感兴趣,您可以在 PPTP RFC ( https://datatracker.ietf.org/doc/html/rfc2637 ) 中找到详细信息。
PPTP 来电设置程序
为了将一些网络数据转发到 PPTP V** 服务器,控制连接需要与服务器建立虚拟呼叫。与 PPTP 服务器通信时,有两种类型的虚拟呼叫,即呼出呼叫和呼入呼叫。为了从客户端与 V** 服务器通信,我们通常使用传入呼叫类型。最后,为了建立从客户端到服务器的传入呼叫,使用了三种控制消息类型。
IncomingCallRequest
– 客户端用于请求新的传入虚拟呼叫。IncomingCallReply
– 由服务器用于指示是否正在接受虚拟呼叫。它还设置用于跟踪呼叫的呼叫 ID(这些 ID 然后也用于多路复用网络数据)。IncomingCallConnected
– 由客户端用于确认虚拟呼叫的连接并导致服务器对其进行完全初始化以准备网络数据。
在呼叫建立期间交换的最重要的信息位是呼叫 ID。这是客户端和服务器用来在特定调用中发送和接收数据的 ID。一旦建立了呼叫,就可以使用呼叫 ID 将数据发送到 PPTP 连接的 GRE 部分,以识别它所属的虚拟呼叫连接。
测试用例
减少测试用例后,我们可以看到导致服务器崩溃的控制消息交换如下:
StartControlConnectionRequest ()客户端 - >服务器
StartControlConnectionReply ()服务器 - >客户端
IncomingCallRequest ()客户端 - >服务端
IncomingCallReply ()服务器 - >客户端
IncomingCallConnected ()客户端 - >服务器
IncomingCallConnected ()客户端 - >服务器
测试用例最初看起来非常简单,实际上与我们期望的有效 PPTP 连接非常相似。不同之处在于第二条IncomingCallConnected
消息。出于某种原因,在接收到IncomingCallConnected
针对已连接呼叫 ID 的控制消息时,会触发空指针取消引用,从而导致系统崩溃。
让我们看看崩溃,看看我们是否能明白为什么这个相对简单的错误会导致如此大的问题。
迷恋;撞车;崩溃
查看崩溃的堆栈跟踪,我们得到以下信息:
代码语言:javascript复制... < - ( Windows Bug 检查处理)
NDIS!NdisMCmActivateVc 0x2d
raspptp!CallEventCallInConnect 0x71
raspptp!CtlpEngine 0xe63
raspptp!CtlReceiveCallback 0x4b
... < - ( TCP/IP 处理)
这里有趣的是,我们可以看到崩溃根本不是发生在raspptp.sys
驱动程序中,而是发生在ndis.sys
驱动程序中。是什么ndis.sys
?好吧,raspptp.sys
在所谓的微型端口驱动程序中,这意味着它实际上只实现了实现整个 V**接口所需的一小部分功能,而 V** 处理的其余部分实际上由 NDIS 驱动程序系统执行。raspptp.sys
充当 PPTP 的前端解析器,然后将封装的虚拟网络帧转发到 NDIS,由 Windows V**后端的其余部分路由和处理。
那么为什么会发生这种空指针取消引用呢?让我们看一下代码,看看是否可以收集更多细节。
代码
第一段代码在PPTP控制连接状态机中。此处理的第一部分是 switch 语句中的一个小存根,用于处理不同的控制消息。对于一条IncomingCallConnected
消息,我们可以看到所有代码最初所做的只是检查服务器上是否存在有效的调用 ID 和上下文结构。如果它们确实存在,则CallEventCallInConnect
使用消息有效负载和调用上下文结构对该函数进行调用。
案例 IncomingCallConnected:
// 确保客户端发送了一个有效的 StartControlConnectionRequest 消息
如果(lpPptpCtlCx- > CtlCurrentState == CtlStateWaitStop )
{
// BigEndian 到 LittleEndian 的转换
CallIdSentInReply = ( unsigned __int16 ) __ROR2__ ( lpCtlPayloadBuffer- > IncomingCallConnected.PeersCallId , 8 ) ;
if ( PptpClientSide ) // 如果我们是客户端
CallIdSentInReply &= 0x3FFFu; // 最大 ID 掩码
// 如果存在,则获取此调用 ID 的上下文结构
IncomingCallCallCtx = CallGetCall ( lpPptpCtlCx- > pPptpAdapterCtx, CallIdSentInReply ) ;
// 处理来电连接事件
如果( IncomingCallCallCtx )
CallEventCallInConnect ( IncomingCallCallCtx, lpCtlPayloadBuffer ) ;
该CallEventCallInConnect
函数执行两个任务;它通过调用来激活虚拟调用连接NdisMCmActivateVc
,然后如果从该函数返回的状态不是STATUS_PENDING
,它会调用该PptpCmActivateVcComplete
函数。
__int64 __fastcall CallEventCallInConnect ( CtlCall *IncomingCallCallCtx, CtlMsgStructs *IncomingCallMsg )
{
无符号整数 ActiveateVcRetCode;
...
ActiveateVcRetCode = NdisMCmActivateVc ( lpCallCtx- > NdisVcHandle, ( PCO_CALL_PARAMETERS ) lpCallCtx- > CallParams ) ;
如果(ActiveateVcRetCode!= STATUS_PENDING )
{
如果...
PptpCmActivateVcComplete ( ActiveateVcRetCode, lpCallCtx, ( PVOID ) lpCallCtx- > CallParams ) ;
}
返回0i64;
}
...
NDIS_STATUS __stdcall NdisMCmActivateVc ( NDIS_HANDLE NdisVcHandle, PCO_CALL_PARAMETERS CallParameters )
{
__int64 v2; // rbx
PCO_CALL_PARAMETERS lpCallParameters;// rdi
KIRQL OldIRQL; // 人
_CO_MEDIA_PARAMETERS *lpMediaParameters; // rcx
__int64 v6; // rcx
v2 = * (( _QWORD * ) NdisVcHandle 9 ) ;
lpCallParameters = 呼叫参数;
OldIRQL = KeAcquireSpinLockRaiseToDpc (( PKSPIN_LOCK )( v2 8 )) ;
* ( _DWORD * )( v2 4 ) |= 1u;
lpMediaParameters = lpCallParameters- > MediaParameters;
if ( lpMediaParameters- > MediaSpecific.Length < 8 ) _
v6 = (无符号整数) v2;
别的
v6 = * ( _QWORD * ) lpMediaParameters- > MediaSpecific。参数;
* ( _QWORD * )( v2 136 ) = v6;
* ( _QWORD * )( v2 136 ) = * ( _QWORD * ) lpCallParameters- > MediaParameters- > MediaSpecific。参数;
KeReleaseSpinLock (( PKSPIN_LOCK )( v2 8 ) , OldIRQL ) ;
返回0 ;
}
我们可以看到,实际上,这个NdisMCMActivateVc
功能非常简单。我们知道它总是返回,所以函数0
总是会继续调用。PptpCmActivateVcCompleteCallEventCallInConnect
查看堆栈跟踪,我们知道崩溃发生在函数的偏移处0x2d
,NdisMCmActivateVc
对应于伪代码中的以下行:
lpMediaParameters = lpCallParameters->MediaParameters;
由于NdisMCmActivateVc
不位于我们的主要目标驱动程序中raspptp.sys
,因此它主要是未逆向工程的,但很明显,主要目的是在结构上设置一些属性,该结构被跟踪为从raspptp.sys
. 由于这看起来并不直接导致问题,我们现在可以放心地忽略它。特定变量lpCallParameters
(也是CallParameters
参数)导致空指针取消引用,并通过raspptp.sys
;传递给函数。这表明漏洞必须发生在raspptp.sys
驱动程序代码的其他地方。
回顾来自的调用,CallEventCallInConnect
我们知道CallParmaters
参数实际上是存储在调用上下文结构中的指针raspptp.sys
。我们可以假设在调用PptpCmActivateVcComplete
这个结构的某个时刻被释放并且结构的指针成员被设置为零。所以让我们找到责任线!
void __fastcall PptpCmActivateVcComplete ( unsigned int OutGoingCallReplyStatusCode, CtlCall *CallContext, PVOID CallParams )
{
CtlCall *lpCallContext; // rdi
...
if ( lpCallContext- > UnkownFlag )
{
如果(lpCallParams )
ExFreePoolWithTag (( PVOID ) lpCallContext- > CallParams, 0 ) ;
lpCallContext- > CallParams = 0i64;
...
稍看之后,我们可以看到负责的代码部分。从逆向工程结构的设置中,CallContext
我们知道UnkownFlag
结构变量是1
通过处理最初分配和设置结构的IncomingCallRequest
帧来设置的。CallContext
对于我们的测试用例,此代码将始终执行,因此第二次调用CallEventCallInConnect
将触发空指针取消引用并使 NDIS 层中的机器崩溃,从而导致出现相应的蓝屏死机:
概念证明
我们将在 5 月 2 日发布概念验证代码,以便系统管理员有更多时间进行修补。