之前对于windows平台漏洞分析中我们都集中分析了本地提权类别的相关漏洞,很少有远程代码执行漏洞进行过分析,该系列中我们将分析4个windows内核远程代码执行漏洞,揭示这些很少被分析的攻击面。
CVE-2022-23253 是 Nettitude 在对 Windows Server 点对点隧道协议 (PPTP) 驱动程序进行模糊测试时发现的拒绝服务漏洞。 此漏洞可用于对目标服务器发起持续的拒绝服务攻击,该漏洞无需身份验证即可利用并影响 Windows Server vp////n 的所有默认配置,漏洞所在的模块位于raspptp.sys驱动。
PPTP 是一种 vp////n 协议,用于在客户端和 vp////n 服务器之间多路复用和转发虚拟网络数据。 该协议有两部分,TCP控制连接和GRE数据连接。 TCP控制连接主要负责客户端和服务器之间的网络数据缓存和复用的配置。 为了与 PPTP 服务器的控制连接进行对话,我们只需要连接到侦听套接字并发起协议握手即可。 之后,我们就可以开始与服务器的完整 PPTP 会话。
PPTP 握手过程
PPTP 实现了一个非常简单的控制连接握手过程。 所需要的只是客户端首先向服务器发送一个StartControlConnectionRequest请求,然后接收一个 StartControlConnectionReply 回应报文,表明没有问题并且控制连接已准备好开始处理命令。 StartControlConnectionRequest 的实际内容对测试用例没有影响,只需要有效地形成,以便服务器将连接状态推进到能够处理其余定义的控制连接帧。可以参考PPTP的RFC文档获取更多信息:https://datatracker.ietf.org/doc/html/rfc2637
PPTP 呼叫链接建立过程
为了将网络数据转发到 PPTP vp////n 服务器,控制连接需要与服务器建立虚拟呼叫。 与 PPTP 服务器通信时,有两种类型的虚拟呼叫,即呼出呼叫和呼入呼叫。 为了从客户端与 vp////n 服务器通信,我们通常使用传入呼叫类型。 最后,为了建立从客户端到服务器的传入呼叫,使用了三种控制消息类型:
- IncomingCallRequest – 客户端用于请求新的传入虚拟呼叫请求
- IncomingCallReply – 由服务器发送用于表示自己是否正在接受虚拟呼叫。 它还设置用于跟踪呼叫请求的请求ID(Call ID)。
- IncomingCallConnected – 客户端使用它来确认虚拟呼叫的连接并让服务器对其进行完全初始化以准备发送网络数据。
这里漏洞发现者使用了fuzz的方法发现了漏洞,触发该空指针引用的报文共有下面几个过程:
StartControlConnectionRequest() Client -> Server StartControlConnectionReply() Server -> Client IncomingCallRequest() Client -> Server IncomingCallReply() Server -> Client IncomingCallConnected() Client -> Server IncomingCallConnected() Client -> Server
可以发现出问题的地方就在最后两个包请求,也就是在Client发送第二次IncomingCallConnected请求的时候,内核发生了崩溃。
Sockets In The Windows Kernel – Winsock Kernel (WSK)
了解这个内容我们需要了解NDIS驱动的相关机制还有Windows内核Socket的相关细节,详细可以在MSDN上查找到。
下面的代码是raspptp.sys驱动创建内核socket还有某些回调函数的代码:
void __fastcall PptpCmRegSapPassive(__int64 a1) { ... v3 = PptpInitialize(CallMgrAfContext); // 创建第一个socket,可能是GRE的socket? if ( v3 && WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control && (HIDWORD(WPP_GLOBAL_Control->Timer) & 4) != 0 && BYTE1(WPP_GLOBAL_Control->Timer) ) { WPP_SF_(WPP_GLOBAL_Control->AttachedDevice, 26i64, &WPP_a989452d2ce136f272e3b9b86b31e890_Traceguids); } v4 = CtlListen(CallMgrAfContext); if ( v4 ) { if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control && (HIDWORD(WPP_GLOBAL_Control->Timer) & 4) != 0 && BYTE1(WPP_GLOBAL_Control->Timer) ) { WPP_SF_(WPP_GLOBAL_Control->AttachedDevice, 27i64, &WPP_a989452d2ce136f272e3b9b86b31e890_Traceguids); } if ( v4 < 0 ) goto LABEL_38; } ... } __int64 __fastcall CtlListen(__int64 CallMgrAfContext) { ... WORD1(v10) = __ROR2__(PptpControlPort, 8); // 看样子是端口号码,固定的 WORD1(v11) = WORD1(v10); LOWORD(v10) = 2; v9 = (__int64)&v10; v7[0] = 1; v8 = 1i64; v7[1] = 6; SockContext = (__int64)WskCreateAndIntializeSockContext(); v5 = SockContext; if ( SockContext ) { *(_QWORD *)(SockContext + 8) = CallMgrAfContext; *(_QWORD *)(SockContext + 32) = CtlConnectQueryCallback; *(_QWORD *)(SockContext + 24) = CtlDisconnectCallback; *(_QWORD *)(SockContext + 16) = CtlReceiveCallback; *(_QWORD *)(SockContext + 64) = CtlpSendMessageComplete; *(_QWORD *)(SockContext + 72) = CallMgrAfContext + 96; *(_QWORD *)(SockContext + 48) = CtlLogWskLibActivity; ServerSocket = WskCreateServerSocket((__int64)v7, v4, SockContext); if ( (ServerSocket & 0x80000000) == 0 ) *(_QWORD *)(CallMgrAfContext + 56) = v5; else WskDestroySockContext(v5); } else { ServerSocket = 0xC000009A; if ( WPP_GLOBAL_Control == (PDEVICE_OBJECT)&WPP_GLOBAL_Control ) return ServerSocket; if ( (HIDWORD(WPP_GLOBAL_Control->Timer) & 8) != 0 && BYTE1(WPP_GLOBAL_Control->Timer) ) WPP_SF_d( WPP_GLOBAL_Control->AttachedDevice, 88i64, &WPP_be37cabcf8053ca2192e70b03bf2a59b_Traceguids, 0xC000009Ai64); } ... }
在监听端口后,在 CtlListen 函数中注册了客户端连接到服务端的函数CtlConnectQueryCallback,还有服务端收到客户端数据的处理函数CtlReceiveCallback等,最终处理客户端发送的数据在CtlpEngine函数中进行。
Crash信息
... <- (Windows Bug check handling) NDIS!NdisMCmActivateVc+0x2d raspptp!CallEventCallInConnect+0x71 raspptp!CtlpEngine+0xe63 raspptp!CtlReceiveCallback+0x4b ... <- (TCP/IP Handling)
我们可以看到崩溃根本不是发生在 raspptp.sys 驱动程序中,而是发生在 ndis.sys 驱动程序中。raspptp.sys 被称为mini-port驱动,它实际上只实现了实现整个 vp////n 接口所需的一小部分功能,而其余的 vp////n 处理实际上由 NDIS 执行。raspptp.sys 充当 PPTP 的前端解析器,然后将封装的虚拟网络帧转发到NDIS,由 Windows vp////n 后端的其余部分路由和处理。
代码分析
第一段代码在PPTP控制连接状态机中。 此处理的第一部分是 switch 语句中的一个小部分,用于处理不同的控制消息。
对于 IncomingCallConnected 消息,我们可以看到所有代码最初所做的都是检查服务器上是否存在有效的Call ID和上下文结构。如果存在则去调用 CallEventCallInConnect 函数。
case IncomingCallConnected: // Ensure the client has sent a valid StartControlConnectionRequest message if ( lpPptpCtlCx->CtlCurrentState == CtlStateWaitStop ) { // BigEndian To LittleEndian Conversion CallIdSentInReply = (unsigned __int16)__ROR2__(lpCtlPayloadBuffer->IncomingCallConnected.PeersCallId, 8); if ( PptpClientSide ) // If we are the client CallIdSentInReply &= 0x3FFFu; // Maximum ID mask // Get the context structure for this call ID if it exists IncomingCallCallCtx = CallGetCall(lpPptpCtlCx->pPptpAdapterCtx, CallIdSentInReply); // Handle the incoming call connected event if ( IncomingCallCallCtx ) CallEventCallInConnect(IncomingCallCallCtx, lpCtlPayloadBuffer);
CallEventCallInConnect 函数执行两个任务,首先通过调用 NdisMCmActivateVc 激活虚拟连接,如果从该函数返回的状态不是 STATUS_PENDING,则调用 PptpCmActivateVcComplete 函数:
__int64 __fastcall CallEventCallInConnect(CtlCall *IncomingCallCallCtx, CtlMsgStructs *IncomingCallMsg) { unsigned int ActiveateVcRetCode; ... ActiveateVcRetCode = NdisMCmActivateVc(lpCallCtx->NdisVcHandle, (PCO_CALL_PARAMETERS)lpCallCtx->CallParams); if ( ActiveateVcRetCode != STATUS_PENDING ) { if... PptpCmActivateVcComplete(ActiveateVcRetCode, lpCallCtx, (PVOID)lpCallCtx->CallParams); } return 0i64; } ... NDIS_STATUS __stdcall NdisMCmActivateVc(NDIS_HANDLE NdisVcHandle, PCO_CALL_PARAMETERS CallParameters) { __int64 v2; // rbx PCO_CALL_PARAMETERS lpCallParameters; // rdi KIRQL OldIRQL; // al _CO_MEDIA_PARAMETERS *lpMediaParameters; // rcx __int64 v6; // rcx v2 = *((_QWORD *)NdisVcHandle + 9); lpCallParameters = CallParameters; OldIRQL = KeAcquireSpinLockRaiseToDpc((PKSPIN_LOCK)(v2 + 8)); *(_DWORD *)(v2 + 4) |= 1u; lpMediaParameters = lpCallParameters->MediaParameters; if ( lpMediaParameters->MediaSpecific.Length < 8 ) v6 = (unsigned int)v2; else v6 = *(_QWORD *)lpMediaParameters->MediaSpecific.Parameters; *(_QWORD *)(v2 + 136) = v6; *(_QWORD *)(v2 + 136) = *(_QWORD *)lpCallParameters->MediaParameters->MediaSpecific.Parameters; KeReleaseSpinLock((PKSPIN_LOCK)(v2 + 8), OldIRQL); return 0; }
我们可以看到 NdisMCMActivateVc 函数内部非常简单。 总是返回0,因此 CallEventCallInConnect 函数会继续调用 PptpCmActivateVcComplete
仔细分析我们发现在PptpCmActivateVcComplete函数中释放了CallParameters这个参数:
void __fastcall PptpCmActivateVcComplete(unsigned int OutGoingCallReplyStatusCode, CtlCall *CallContext, PVOID CallParams) { CtlCall *lpCallContext; // rdi ... if ( lpCallContext->UnkownFlag ) { if ( lpCallParams ) ExFreePoolWithTag((PVOID)lpCallContext->CallParams, 0); lpCallContext->CallParams = 0i64; ...
然而在CallEventCallInConnect调用 NdisMCmActivateVc 函数的时候并没有对Parameter做检查,导致二次调用的时候空指针引用。
这个漏洞的本质原因还是在协议实现的时候没有考虑到二次调用的问题,在协商的时候如果客户端第二次仍然发送同样的消息,如果服务端没有正确处理消息则可能有不可控的情况发生。
漏洞修复
最新的补丁看我们发现在CallEventCallInConnect 函数调用前会加入对象状态判断,同时也判断了Parameters是否创建,如果已创建过了则直接返回:
请登录/注册后在FreeBuf发布内容哦