CVE-2022-23253 is a Windows VPN (Remote access services or RaS) Denial of Service (DoS) bug that Nettitude discovered while fuzzing the Windows Server Point-to-Point Tunnelling Protocol (PPTP) driver. The implications of this bug are that it could be used to launch a persistent Denial of Service attack against a target server. The bug requires no authentication and affects all default configurations of Windows Server VPN.
Nettitude has followed a coordinated disclosure process and reported the bug to Microsoft. As a result the latest versions of Windows are now patched and no longer vulnerable to the issue.
Affected Versions of Microsoft Windows Server
The bug affects most versions of Windows Server and Windows Desktop since Windows Server 2008 and Windows 7 respectively. To see a full list of affected windows versions check the official disclosure post on MSRC: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-23253.
Overview
PPTP is a VPN protocol used to multiplex and forward virtual network data between a client and VPN server. The protocol has two parts, a TCP control connection and a UDP data connection. The TCP control connection is mainly responsible for the configuring of buffering and multiplexing for network data between the client and server. In order to talk to the control connection of a PPTP server, we only need to connect to the listening socket and initiate the protocol handshake. After that we are able to start a complete PPTP session with the server.
When fuzzing for bugs the first step is usually a case of waiting patiently for a crash to occur. In the case of fuzzing the PPTP implementation we had to wait a mere three minutes before our first reproducible crash!
Our first step was to analyse the crashing test case and minimise it to create a reliable proof of concept. However before we dissect the test case we need to understand what a few key parts of the control connection logic are trying to do!
The PPTP Handshake
PPTP implements a very simple control connection handshake procedure. All that is required is that a client first sends a StartControlConnectionRequest
to the server and then receives a StartControlConnectionReply
indicating that there were no issues and the control connection is ready to start processing commands. The actual contents of the StartControlConnectionRequest
has no effect on the test case and just needs to be validly formed in order for the server to progress the connection state into being able to process the rest of the defined control connection frames. If you’re interested in what all these control packet frames are supposed to do or contain you can find details in the PPTP RFC (https://datatracker.ietf.org/doc/html/rfc2637).
PPTP IncomingCall Setup Procedure
In order to forward some network data to a PPTP VPN server the control connection needs to establish a virtual call with the server. There are two types of virtual call when communicating with a PPTP server, these are outgoing calls and incoming calls. To to communicate with a VPN server from a client we typically use the incoming call variety. Finally, to set up an incoming call from a client to a server, three control message types are used.
-
IncomingCallRequest
– Used by the client to request a new incoming virtual call. -
IncomingCallReply
– Used by the server to indicate whether the virtual call is being accepted. It also sets up call ID’s for tracking the call (these ID’s are then used for multiplexing network data as well). -
IncomingCallConnected
– Used by the client to confirm connection of the virtual call and causes the server to fully initialise it ready for network data.
The most important bit of information exchanged during call setup is the call ID. This is the ID used by the client and server to send and receive data along that particular call. Once a call is set up data can then be sent to the UDP part of the PPTP connection using the call ID to identify the virtual call connection it belongs to.
The Test Case
After reducing the test case, we can see that at a high level the control message exchanges that cause the server to crash are as follows:
StartControlConnectionRequest() Client -> Server StartControlConnectionReply() Server -> Client IncomingCallRequest() Client -> Server IncomingCallReply() Server -> Client IncomingCallConnected() Client -> Server IncomingCallConnected() Client -> Server
The test case appears to initially be very simple and actually mostly resembles what we would expect for a valid PPTP connection. The difference is the second IncomingCallConnected
message. For some reason, upon receiving an IncomingCallConnected
control message for a call ID that is already connected, a null pointer dereference is triggered causing a system crash to occur.
Let’s look at the crash and see if we can see why this relatively simple error causes such a large issue.
The Crash
Looking at the stack trace for the crash we get the following:
... <- (Windows Bug check handling) NDIS!NdisMCmActivateVc+0x2d raspptp!CallEventCallInConnect+0x71 raspptp!CtlpEngine+0xe63 raspptp!CtlReceiveCallback+0x4b ... <- (TCP/IP Handling)
What’s interesting here is that we can see that the crash does not not take place in the raspptp.sys
driver at all, but instead occurs in the ndis.sys
driver. What is ndis.sys
? Well, raspptp.sys
in what is referred to as a mini-port driver, which means that it only actually implements a small part of the functionality required to implement an entire VPN interface and the rest of the VPN handling is actually performed by the NDIS driver system. raspptp.sys
acts as a front end parser for PPTP which then forwards on the encapsulated virtual network frames to NDIS to be routed and handled by the rest of the Windows VPN back-end.
So why is this null pointer dereference happening? Let’s look at the code to see if we can glean any more detail.
The Code
The first section of code is in the PPTP control connection state machine. The first part of this handling is a small stub in a switch statement for handling the different control messages. For an IncomingCallConnected
message, we can see that all the code initially does is check that a valid call ID and context structure exists on the server. If they do exist, a call is made to the CallEventCallInConnect
function with the message payload and the call context structure.
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);
The CallEventCallInConnect
function performs two tasks; it activates the virtual call connection through a call to NdisMCmActivateVc
and then if the returned status from that function is not STATUS_PENDING
it calls the PptpCmActivateVcComplete
function.
__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; // rcxv2 = *((_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;
}
We can see that in reality, the NdisMCMActivateVc
function is surprisingly simple. We know that it always returns 0
so there will always be a proceeding call to PptpCmActivateVcComplete
by the CallEventCallInConnect
function.
Looking at the stack trace we know that the crash is occurring at an offset of 0x2d
into the NdisMCmActivateVc
function which corresponds to the following line in our pseudo code:
lpMediaParameters = lpCallParameters->MediaParameters;
Since NdisMCmActivateVc
doesn’t sit in our main target driver, raspptp.sys
, it’s mostly un-reverse engineered, but it’s pretty clear to see that the main purpose is to set some properties on a structure which is tracked as the handle to NDIS from raspptp.sys
. Since this doesn’t really seem like it’s directly causing the issue we can safely ignore it for now. The particular variable lpCallParameters
(also the CallParameters
argument) is causing the null pointer dereference and is passed into the function by raspptp.sys
; this indicates that the bug must be occurring somewhere else in the raspptp.sys
driver code.
Referring back to the call from CallEventCallInConnect
we know that the CallParmaters
argument is actually a pointer stored within the Call Context structure in raspptp.sys
. We can assume that at some point in the call to PptpCmActivateVcComplete
this structure is freed and the pointer member of the structure is set to zero. So lets find the responsible line!
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; ...
After a little bit of looking we can see the responsible sections of code. From reverse engineering the setup of the CallContext
structure we know that the UnkownFlag
structure variable is set to 1
by the handling of the IncomingCallRequest
frame where the CallContext
structure is initially allocated and setup. For our test case this code will always execute and thus the second call to CallEventCallInConnect
will trigger a null pointer dereference and crash the machine in the NDIS layer, causing the appropriate Blue Screen Of Death to appear:
Proof Of Concept
We will release proof of concept code on May 2nd to allow extra time for systems administrators to patch.
Timeline
- Bug Reported To Microsoft – 29 Oct 2021
- Bug Acknowledged – 29 Oct 2021
- Bug Confirmed – 11 Nov 2021
- Patch Release Date Confirmed – 18 Jan 2022
- Patch Released – 08 March 2022
- Blog Released – 09 March 2022
The post CVE-2022-23253 – Windows VPN Remote Kernel Null Pointer Dereference appeared first on Nettitude Labs.
Article Link: CVE-2022-23253 - Windows VPN Remote Kernel Null Pointer Dereference - Nettitude Labs