πŸ”’
❌
There are new articles available, click to refresh the page.
Before yesterdayNettitude Labs

CVE-2022-23270 – Windows Server VPN Remote Kernel Use After Free Vulnerability (Part 2)

11 May 2022 at 09:00

Following yesterday’s Microsoft VPN vulnerability, today we’re presenting CVE-2022-23270, which is another windows VPN Use after Free (UaF) vulnerability that was discovered through reverse engineering and fuzzing the raspptp.sys kernel driver. This presents attackers with another chance to perform denial of service and potentially even achieve remote code execution against a target server.

Affected Versions

The vulnerability 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:

The vulnerability affects both server and client use cases of the raspptp.sys driver and can potentially be triggered in both cases. This blog post will focus on triggering the vulnerability against a server target.

Introduction

CVE-2022-23270 is heavily dependent on the implementation of the winsock Kernel (WSK) layer in raspptp.sys, to be successfully triggered. If you want to learn more about the internals of raspptp.sys and how it interacts with WSK, we suggest you read our write up for CVE-2022-21972 before continuing:

CVE-2022-23270 is a Use after Free (UaF) resulting in Double Free that occurs as the result of a race condition. It resides in the implementation of PPTP Calls in the raspptp.sys driver.

PPTP implements two sockets; a TCP control connection and a GRE data connection. Calls are setup and managed by the control connection and are used to identify individual data streams handled by the GRE connection. The Call functionality makes it easy for PPTP to multiplex multiple different streams of VPN data over one connection.

Now we know in simple terms what PPTP calls are, lets see how they can be broken!

The Vulnerability

This section explores the underlying vulnerability.Β  We will then move on to triggering the vulnerable code on the target.

PPTP Call Context Objects

PPTP calls can be created through an IncomingCallRequest or an OutgoingCallRequest control message. The raspptp.sys driver creates a call context structure when either of these call requests are initiated by a connected PPTP client. The call context structures are designed to be used for tracking information and buffering GRE data for a call connection. For this vulnerability construction of the objects by raspptp.sys is unimportant we instead care about how they are accessed.

Accessing the Call Context

There are two ways in which handling a PPTP control message can retrieve a call context structure. Both methods require the client to know the associated call ID for the call context structure. This ID is randomly generated by the server sent to the client within the reply to the Incoming or Outgoing call request. The client then uses that ID in all subsequent control messages sent to the server that relate to that specific call. See the PPTP RFC (https://datatracker.ietf.org/doc/html/rfc2637) for more information on how this is handled.

raspptp.sys uses two methods to access the call context structures when parsing control messages:

  • Globally accessible Call ID indexed array.
  • PPTP control connection context stored link list.

The difference between these two access methods is scope. The global array can retrieve any call allocated by any control connection, but the linked list only contains calls relating to the control connection containing it.

Let’s go a bit deeper into these access methods and see if they play nicely together…

Linked List Access

The linked list access method is performed through two functions within raspptp.sys. EnumListEntry which is used to iterate through each member of the control connection call linked list and EnumComplete which is used to end the current loop and reset state.

while ( 1 )
{
    EnumRecord = EnumListEntry(
    &lpPptpCtlCx->CtlCallDoubleLinkedList,
    (LIST_ENTRY *)&ListIterator,
    &lpPptpCtlCx->pPptpAdapterCtx->PptpAdapterSpinLock);
    if ( !EnumRecord )
        break;
    EnumCallCtx = (CtlCall *)(EnumRecord - 2);
    if ( EnumRecord != (PVOID *)16 && EnumCallCtx->CallAllocTag == 'CPTP' )
        CallEventOutboundTunnelEstablished(EnumCallCtx);
}
Itreator = (LIST_ENTRY *)&ListIterator;
EnumComplete(Itreator, (KSPIN_LOCK)&lpPptpCtlCx->pPptpAdapterCtx->PptpAdapterSpinLock);

The ListIterator variable is used to store the current linked list entry that has been reached in the list so that the loop can continue from this point on the next call to EnumListEntry. EnumComplete simply resets the ListIterator variable once it’s done with. The way in which this code appears in the raspptp.sys driver can change around slightly but the overall method is the same. Call EnumListEntry repeatedly until it returns null and then call EnumComplete to tidy up the iterator.

Global Call Array

The global array access method is handled through a function called CallGetCall:

CtlCall *__fastcall CallGetCall(PptpAdapterContext *AdapterCtx, unsigned __int64 CallId)
{
    PptpAdapterContext *lpAdapterCtx;
    unsigned __int64 lpCallId;
    CtlCall *CallEntry;
    KIRQL curAdaperIRQL;
    unsigned __int64 BaseCallID;
    unsigned __int64 CallIdMaskApplied;

    lpAdapterCtx = AdapterCtx;
    lpCallId = CallId;
    CallEntry = 0i64;
    curAdaperIRQL = KeAcquireSpinLockRaiseToDpc(&AdapterCtx->PptpAdapterSpinLock);
    BaseCallID = (unsigned int)PptpBaseCallId;
    lpAdapterCtx->HandlerIRQL = curAdaperIRQL;
    if ( lpCallId >= BaseCallID && lpCallId < (unsigned int)PptpMaxCallId )
    {
        if ( PptpCallIdMaskSet )
        {
            CallIdMaskApplied = (unsigned int)lpCallId & PptpCallIdMask;
            if ( CallIdMaskApplied < (unsigned int)PptpWanEndpoints )
            {
                CallEntry = lpAdapterCtx->PptpWanEndpointsArray + CallIdMaskApplied;
                if ( CallEntry )
                    {
                        if ( CallEntry->PptpWanEndpointFullCallId != lpCallId )
                            CallEntry = 0i64;
                    }
            }
        }
        else
        {
            CallEntry = lpAdapterCtx->PptpWanEndpointsArray + lpCallId - BaseCallID;
        }
    }
KeReleaseSpinLock(&lpAdapterCtx->PptpAdapterSpinLock, curAdaperIRQL);
return CallEntry;
}

This function effectively just retrieves the array slot that the call context structure should be stored in based on the provided call ID. It then returns the structure at that entry provided that it matches the specified ID and is in fact a valid entry.

So, what’s the issue? Both of these access methods look pretty harmless, right? There is one subtle and simple issue in the way these access methods are used. Locking!

Cross Thread Access?

CallGetCall is intended to be able to retrieve any call allocated by any currently connected control connection. Since a control connection doesn’t care about other control connection owned calls the control connection state machine should have no use for CallGetCall or at least, according to the PPTP RFC, it shouldn’t. However, this isn’t the case there are several control connection methods in raspptp.sys that use CallGetCall instead of referencing the internal control connection linked list!

If CallGetCall lets us access other control connection call context structures and certain parts of the PPTP handling can occur concurrently, then we can theoretically access the same call context structure in two different threads at the same time! This is starting to sound like a recipe for some racy memory corruption conditions.

Lock and Roll

Both the linked list access method and the CallGetCall function reference a PptpAdapterSpinLock variable on a global context structure. This is a globally accessible kernel spin lock that is to be used to prevent concurrent access to things which can be accessed globally. Using this should make any concurrent use of either call context list access method safe, right?

This isn’t the case at all. Looking at the above pseudo code the lock in CallGetCall is only actually held when we are searching through the list, which is great for the lookup but it’s not held once the call structure is returned. Unless the caller re locks the global lock before using the context structure (spoiler alert, it does not) then we have a potential window for unsafe concurrent access.

Concurrent access doesn’t necessarily mean we have a vulnerability. To prove that we have a vulnerability, we need two code locations that could cause a further issue when running with access to the object at the same time. For example, any form of free operation performed on the structure in this scenario could be a good source of an exploitable issue.

Getting Memory Corruption

Within the raspptp.sys driver there are many places where the kind of access we’re looking for can occur and cause different kinds of issues. Going over all of them is probably an entire series worth of blog posts that we can’t imagine anyone really wants. The one we ended up using for the Proof of Concept (PoC) involves the following two operations:

  • Closing A Control Connection
    • When a control connection is closed the control connections call linked list is walked and each call context structure is appropriately de-initialised and freed. This operation is performed by a familiar function, CtlpCleanup.
  • Sending an OutgoingCallReply control message with an error code set
    • If an OutgoingCallReply message is sent with an error set the call structure that it relates to is freed. The CallGetCall function is used for looking up the call context structure in this control message handling, which means we can use it to perform the free while the control connection close routine is running in a separate thread.

These two conditions create a scenario where if both were to happen consecutively, a call context structure is freed twice, causing a Use after Free/Double Free issue!

Race Against the Machine!

To trigger the race we need to take the following high level steps:

  • Create two control connections and initialise them so we can create calls.
  • On the first connection, we create the maximum allowed number of calls the server will allow us to.
  • We then consecutively close the first connection and start sending OutGoingCallReply messages for the allocated call IDs.
    • This realistically needs to be done in separate threads bound to separate CPU cores to guarantee true concurrency.
  • Then we sit back and wait for the race to be won?

In practice, reliably implementing these steps is a lot more difficult than it would initially seem. The window for reliably triggering the race condition and the amount of time we have to do something useful once the initial free occurs is incredibly small, even in the best case scenario.

However, this does not mean that it cannot be achieved. With a significant amount of effort it is possible to greatly increase the reliability of triggering the vulnerability. There are many different factors that can be played with to build a path towards successful exploitation.

One Lock, Two Lock, Three Lock, Four!

Let’s take a look at the two bits of code we’re hoping to get perfectly aligned and see just how tricky this race condition is actually going to be.

The CtlpCleanup Linked List Iteration

for ( ListIterator = (LIST_ENTRY *)EnumListEntry(
    &lpCtlCtxToCleanup->CtlCallDoubleLinkedList,
    &iteratorState,
    &gAdapter->PptpAdapterSpinLock);
    ListIterator;
    ListIterator = (LIST_ENTRY *)EnumListEntry(
    &lpCtlCtxToCleanup->CtlCallDoubleLinkedList,
    &iteratorState,
    &lpCtlCtxToCleanup->pPptpAdapterCtx->PptpAdapterSpinLock) )
    {
        lpCallCtx = (CtlCall *)&ListIterator[-1];
        if ( ListIterator != (LIST_ENTRY *)16 && lpCallCtx->CallAllocTag == 'CPTP' )
        {
            ...
        CallCleanup(lpCallCtx); // this will eventually free the call strructure
    }
}

We can see here that the loop is fairly small. The main part that we are interested in is the call to CallCleanup that is performed on each Call structure in the control context linked list. Now unfortunately this function is not as simple as we would like. The function contains a large number of different paths to execute and could potentially have a variety of ways that make our race condition harder or easier to exploit. The section that is most interesting for us in our PoC is the following pseudo code snippet.

lpIRQL = KeAcquireSpinLockRaiseToDpc(&lpCallToClean->CtlCallSpinLock_A);
lpCallToClean->NdisVcHandle = 0i64;
lpCallToClean->CurIRQL = lpIRQL;
CallDetachFromAdapter(lpCallToClean);
KeReleaseSpinLock(&lpCallToClean->CtlCallSpinLock_A, lpCallToClean->CurIRQL);
if...
    CtlDisconnectCall(lpCallToClean);
    CallpCancelCallTimers(lpCallToClean);
    DereferenceRefCount(lpCallToClean); // Decrement from Ctl loop
    lpCallToClean->CurIRQL = KeAcquireSpinLockRaiseToDpc(&lpCallToClean->CtlCallSpinLock_A);
}
}

KeReleaseSpinLock(&lpCallToClean->CtlCallSpinLock_A, lpCallToClean->CurIRQL);
return DereferenceRefCount(lpCallToClean); // Freeing decrement

Here, a set of detach operations are performed to remove the call structure from the lists its stored in and appropriately decrease its internal reference count. A side effect of this detach phase is that the call context structure is removed from both the linked list and global array. This means that if one thread gets to far through processing a call context structure free before the other one retrieves it from the respective list, the race will already be lost. This further adds to the difficulty in getting these two sections of code lined up.

Ultimately the final call to DereferenceRefCount causes the release of the underlying memory which in our scenario it does by calling the call context structures internal free function pointer to the CallFree function. Before we go over what CallFree does, lets look at the other half of the race condition.

OutgoingCallReply Handling

lpCallOutgoingCallCtx = CallGetCall(lpPptpCtlCx->pPptpAdapterCtx, ReasonCallIdMasked);
if ( lpCallOutgoingCallCtx )
{
    CallEventCallOutReply(lpCallOutgoingCallCtx, lpCtlPayloadBuffer);
}

The preceding excerpt of pseudo code is the bit of the OutgoingCallReply handling that we will be using to access the call context structures from a separate thread. Let’s take a look at the logic in this function which will also free the call context object!

lpCallCtx->CurIRQL = KeAcquireSpinLockRaiseToDpc(&lpCallCtx->CtlCallSpinLock_A); 
... 
KeReleaseSpinLock(&lpCallCtx->CtlCallSpinLock_A, lpCallCtx->CurIRQL); 
if ( OutGoingCallReplyStatusCode ) { 
    CallSetState(lpCallCtx, 0xBu, v8, 0); CallCleanup(lpCallCtx);
}

This small code snippet from CallEventCallOutReply represents the code that is relevant for our PoC. Effectively if the status field of the OutgoingCallReply message is set then a call to CallCleanup happens and again will eventually result in CallFree being hit.

CallFree

The call free function releases resources for multiple sub objects stored in the call context as well as the call context itself:

void __fastcall CallFree(CtlCall *CallToBeFreed)
{
    CtlCall *lpCallToBeFreed;
    _NET_BUFFER_LIST *v2;
    NDIS_HANDLE v3;
    NDIS_HANDLE v4;
    PNDIS_HANDLE v5;
    PNDIS_HANDLE v6;
    PNDIS_HANDLE v7;

    if ( CallToBeFreed )
    {
        lpCallToBeFreed = CallToBeFreed;
         ...
         v2 = lpCallToBeFreed->CtlNetBufferList_A;
 Β   if ( v2 )
  Β       ChunkLChunkength(v2);
  Β       v3 = lpCallToBeFreed->CtlCallWorkItemHandle_A;
    if ( v3 )
    Β     NdisFreeIoWorkItem(v3);
    Β     v4 = lpCallToBeFreed->CtlCallWorkItemHandle_B;
    if ( v4 )
        NdisFreeIoWorkItem(v4);
        v5 = lpCallToBeFreed->hCtlCallCloseTimeoutTimerObject;
    if ( v5 )
        NdisFreeTimerObject(v5);
        v6 = lpCallToBeFreed->hCtlCallAckTimeoutTimerObject;
    if ( v6 )
        NdisFreeTimerObject(v6);
        v7 = lpCallToBeFreed->hCtlDieTimeoutTimerObject;
    if ( v7 )
        NdisFreeTimerObject(v7);
        ExFreePoolWithTag(lpCallToBeFreed, 0);
    }
}

In CallFree, none of the sub-objects have their pointers Nulled out by raspptp.sys. This means that any one of these objects will cause potential double free conditions to occur, giving us a few different locations where we can expect a potential issue to occur when triggering the vulnerability.

Something that you may notice looking at the code snippets for this vulnerability is that there are large portions of overlapping locks. These will in effect cause each thread not to be able to enter certain sections of the cleanup and freeing process at the same time, which makes the race condition harder to predict. However, it does not prevent it from being possible.

We have knowingly not included many of the other hazards and caveats for triggering this vulnerability, as there are just too many different factors to go over, and in actuality a lot of them are self-correcting (luckily for us). The main reason we can ignore a lot of these hazards is that none of them truly stop the two threads from entering the vulnerable condition!

Proof of Concept

We will not yet be publishing our PoC for this vulnerability to allow time for patches to be fully adopted. This unfortunately makes it hard to show the exact process we took to trigger the vulnerability, but we will release the PoC script at a later date! For now here is a little sneak peak at the outputs:

[+] Race Condition Trigger Attempt: 1, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 2, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 3, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 4, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 5, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 6, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 7, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 8, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 9, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 10, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 11, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 12, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 13, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 14, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 15, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 16, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 17, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 18, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 19, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 20, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 21, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 22, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 23, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 24, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 25, With spacing 0 and sled 25
[+] Race Condition Trigger Attempt: 26, With spacing 0 and sled 25
[****] The Server Has Crashed!

A Wild Crash Appeared!

The first step in PoC development is achieving a successful trigger of a vulnerability and usually for kernel vulnerabilities this means causing a crash! Here it is. A successful trigger of our race condition causing the target server to show us the iconic Blue Screen of Death (BSOD):

Now this crash has the following vulnerability check analysis and its pretty conclusive that we’ve caused one of the intended double free scenarios.

*******************************************************************************
* *
* Vulnerabilitycheck Analysis *
* *
*******************************************************************************

KERNEL_SECURITY_CHECK_FAILURE (139)
A kernel component has corrupted a critical data structure. The corruption
could potentially allow a malicious user to gain control of this machine.
Arguments:
Arg1: 0000000000000003, A LIST_ENTRY has been corrupted (i.e. double remove).
Arg2: ffffa8875b31e820, Address of the trap frame for the exception that caused the vulnerabilitycheck
Arg3: ffffa8875b31e778, Address of the exception record for the exception that caused the vulnerabilitycheck
Arg4: 0000000000000000, Reserved

Devulnerabilityging Details:
------------------

KEY_VALUES_STRING: 1

Key : Analysis.CPU.mSec
Value: 5327

Key : Analysis.DevulnerabilityAnalysisManager
Value: Create

Key : Analysis.Elapsed.mSec
Value: 22625

Key : Analysis.Init.CPU.mSec
Value: 46452

Key : Analysis.Init.Elapsed.mSec
Value: 9300845

Key : Analysis.Memory.CommitPeak.Mb
Value: 82

Key : FailFast.Name
Value: CORRUPT_LIST_ENTRY

Key : FailFast.Type
Value: 3

Key : WER.OS.Branch
Value: fe_release

Key : WER.OS.Timestamp
Value: 2021-05-07T15:00:00Z

Key : WER.OS.Version
Value: 10.0.20348.1

VULNERABILITYCHECK_CODE: 139

VULNERABILITYCHECK_P1: 3

VULNERABILITYCHECK_P2: ffffa8875b31e820

VULNERABILITYCHECK_P3: ffffa8875b31e778

VULNERABILITYCHECK_P4: 0

TRAP_FRAME: ffffa8875b31e820 -- (.trap 0xffffa8875b31e820)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=0000000000000000 rbx=0000000000000000 rcx=0000000000000003
rdx=ffffcf88f1a78338 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8025f8d8ae1 rsp=ffffa8875b31e9b0 rbp=ffffcf88f1ae0602
r8=0000000000000010 r9=000000000000000b r10=fffff8025b0ddcb0
r11=0000000000000001 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
NDIS!ndisFreeNblToNPagedPool+0x91:
fffff802`5f8d8ae1 cd29 int 29h
Resetting default scope

EXCEPTION_RECORD: ffffa8875b31e778 -- (.exr 0xffffa8875b31e778)
ExceptionAddress: fffff8025f8d8ae1 (NDIS!ndisFreeNblToNPagedPool+0x0000000000000091)
ExceptionCode: c0000409 (Security check failure or stack buffer overrun)
ExceptionFlags: 00000001
NumberParameters: 1
Parameter[0]: 0000000000000003
Subcode: 0x3 FAST_FAIL_CORRUPT_LIST_ENTRY

PROCESS_NAME: System

ERROR_CODE: (NTSTATUS) 0xc0000409 - The system detected an overrun of a stack-based buffer in this application. This overrun could potentially allow a malicious user to gain control of this application.

EXCEPTION_CODE_STR: c0000409

EXCEPTION_PARAMETER1: 0000000000000003

EXCEPTION_STR: 0xc0000409

STACK_TEXT:
ffffa887`5b31dcf8 fffff802`5b354ea2 : ffffa887`5b31de60 fffff802`5b17bb30 ffff9200`174e5180 00000000`00000000 : nt!DbgBreakPointWithStatus
ffffa887`5b31dd00 fffff802`5b3546ed : ffff9200`00000003 ffffa887`5b31de60 fffff802`5b22c910 00000000`00000139 : nt!KiVulnerabilityCheckDevulnerabilityBreak+0x12
ffffa887`5b31dd60 fffff802`5b217307 : ffffa887`5b31e4e0 ffff9200`1732a180 ffffcf88`ef584700 fffffff6`00000004 : nt!KeVulnerabilityCheck2+0xa7d
ffffa887`5b31e4c0 fffff802`5b229d69 : 00000000`00000139 00000000`00000003 ffffa887`5b31e820 ffffa887`5b31e778 : nt!KeVulnerabilityCheckEx+0x107
ffffa887`5b31e500 fffff802`5b22a1b2 : 00000000`00000000 fffff802`5f5a1285 ffffcf88`edd5c210 fffff802`5b041637 : nt!KiVulnerabilityCheckDispatch+0x69
ffffa887`5b31e640 fffff802`5b228492 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiFastFailDispatch+0xb2
ffffa887`5b31e820 fffff802`5f8d8ae1 : ffffcf88`ef584c00 ffffcf88`ef584700 00000000`00000000 00000000`00000000 : nt!KiRaiseSecurityCheckFailure+0x312
ffffa887`5b31e9b0 fffff802`5f8d5d3d : ffffcf88`f1a78350 00000000`00000000 ffffcf88`f1ae06b8 01000000`000002d0 : NDIS!ndisFreeNblToNPagedPool+0x91
ffffa887`5b31e9e0 fffff802`62bd2f7d : ffffcf88`f1ae06b8 fffff802`62bda000 ffffcf88`f1a78050 ffffcf88`f202dd70 : NDIS!NdisFreeNetBufferList+0x11d
ffffa887`5b31ea20 fffff802`62bd323f : ffffcf88`f202dd70 ffffcf88`ef57f1a0 ffffcf88`ef1fc7e8 ffffcf88`f1ae0698 : raspptp!CallFree+0x65
ffffa887`5b31ea50 fffff802`62bd348e : ffffcf88`f1a78050 00000000`00040246 ffffa887`5b31eaa0 00000000`00000018 : raspptp!CallpFinalDerefEx+0x7f
ffffa887`5b31ea80 fffff802`62bd2bad : ffffcf88`f1ae06b8 ffffcf88`f1a78050 00000000`0000000b ffffcf88`f1a78050 : raspptp!DereferenceRefCount+0x1a
ffffa887`5b31eab0 fffff802`62be37b2 : ffffcf88`f1ae0660 ffffcf88`f1ae0698 ffffcf88`f1ae06b8 ffffcf88`f1a78050 : raspptp!CallCleanup+0x61d
ffffa887`5b31eb00 fffff802`62bd72bd : ffffcf88`00000000 ffffcf88`f15ce810 00000000`00000080 fffff802`62bd7290 : raspptp!CtlpCleanup+0x112
ffffa887`5b31eb90 fffff802`5b143425 : ffffcf88`ef586040 fffff802`62bd7290 00000000`00000000 00000000`00000000 : raspptp!MainPassiveLevelThread+0x2d
ffffa887`5b31ebf0 fffff802`5b21b2a8 : ffff9200`1732a180 ffffcf88`ef586040 fffff802`5b1433d0 00000000`00000000 : nt!PspSystemThreadStartup+0x55
ffffa887`5b31ec40 00000000`00000000 : ffffa887`5b31f000 ffffa887`5b319000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x28

SYMBOL_NAME: raspptp!CallFree+65

MODULE_NAME: raspptp

IMAGE_NAME: raspptp.sys

STACK_COMMAND: .thread ; .cxr ; kb

BUCKET_ID_FUNC_OFFSET: 65

FAILURE_BUCKET_ID: 0x139_3_CORRUPT_LIST_ENTRY_raspptp!CallFree

OS_VERSION: 10.0.20348.1

BUILDLAB_STR: fe_release

OSPLATFORM_TYPE: x64

OSNAME: Windows 10

FAILURE_ID_HASH: {5d4f996e-8239-e9e8-d111-fdac16b209be}

Followup: MachineOwner
---------

It turns out that the double free trigger here is triggering a kernel assertion to be raised on a linked list. The cause of this is one of those sub objects on the call context structure we mentioned earlier. Now, while crashes are great for PoC’s they are not great for exploits, so what do we need to do next if we want to look at further exploitation more seriously?

Exploitation – Next Steps

The main way in which this particular double free scenario can be exploited would be to attempt to spray objects into the kernel heap that will instead be incorrectly freed by our second free instead of causing the above kernel vulnerability check.

The first object that might make a good contender is the call context structure itself. If we were to spray a new call context into the freed memory between the two frees being run then we would have a freed call context structure still connected to a valid and accessible control connection. This new call context structure would be comprised of mostly freed sections of memory that can then be used to cause further memory corruption and potentially achieve kernel RCE against a target server!

Conclusion

Race conditions are a particularly tricky set of vulnerabilities, especially when it comes to getting reliable exploitation. In this scenario we have a remarkably small windows of opportunity to do something potentially dangerous. Exploit development, however, is the art of taking advantage of small opportunities. Achieving RCE with this vulnerability might seem like an unlikely event but it is certainly possible! RCE is also not the only use of this vulnerability with local access to a target machine; it doubles as an opportunity for Local Privilege Escalation (LPE). All this makes CVE-2022-23270 something that in the right hands could be very dangerous.

Timeline

  • Vulnerability Reported To Microsoft – 29 October 2021
  • Vulnerability Acknowledged – 29 October 2021
  • Vulnerability Confirmed – 11 November 2021
  • Patch Release Date Confirmed – 12 January 2022
  • Patch Release – 10 May 2022

The post CVE-2022-23270 – Windows Server VPN Remote Kernel Use After Free Vulnerability (Part 2) appeared first on Nettitude Labs.

CVE-2022-21972: Windows Server VPN Remote Kernel Use After Free Vulnerability (Part 1)

10 May 2022 at 09:00

CVE-2022-21972 is a Windows VPN Use after Free (UaF) vulnerability that was discovered through reverse engineering the raspptp.sys kernel driver. The vulnerability is a race condition issue and can be reliably triggered through sending crafted input to a vulnerable server. The vulnerability can be be used to corrupt memory and could be used to gain kernel Remote Code Execution (RCE) or Local Privilege Escalation (LPE) on a target system.

Affected Versions

The vulnerability 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-21972

The vulnerable code is present on both server and desktop distributions, however due to configuration differences, only the server deployment is exploitable.

Overview

This vulnerability is based heavily on how socket object life cycles are managed by the raspptp.sys driver. In order to understand the vulnerability we must first understand some of the basics in the kernel driver interacts with sockets to implement network functionality.

Sockets In The Windows Kernel – Winsock Kernel (WSK)

WSK is the name of the Windows socket API that can be used by drivers to create and use sockets directly from the kernel. Head over to https://docs.microsoft.com/en-us/windows-hardware/drivers/network/winsock-kernel-overview to see an overview of the system.

The way in which the WSK API is usually used is through a set of event driven call back functions. Effectively, once a socket is set up, an application can provide a dispatch table containing a set of function pointers to be called for socket related events. In order for an application to be able to maintain its own state through these callbacks, a context structure is also provided by the driver to be given to each callback so that state can be tracked for the connection throughout its life-cycle.

raspptp.sys and WSK

Now that we understand the basics of how sockets are interacted with in the kernel, let’s look at how the raspptp.sys driver uses WSK to implement the PPTP protocol.

The PPTP protocol specifies two socket connections; a TCP socket used for managing a VPN connection and a GRE (Generic Routing Encapsulation) socket used for sending and receiving the VPN network data. The TCP socket is the only one we care about for triggering this issue, so lets break down the life cycle of how raspptp.sys handles these connections with WSK

  1. A new listening socket is created by the WskOpenSocket function in raspptp.sys.Β  This function is passed a WSK_CLIENT_LISTEN_DISPATCH dispatch table with the WskConnAcceptEvent function specified as the WskAcceptEven handler. This is the callback that handles a socket accept event, aka new incoming connection.
  2. When a new client connects to the server the WskConnAcceptEvent function is called.Β  This function allocates a new context structure for the new client socket and registers a WSK_CLIENT_CONNECTION_DISPATCH dispatch table with all event callback functions specified. These are WskConnReceiveEvent, WskConnDisconnectEvent and WskConnSendBacklogEvent for receive, disconnect and send events respectively.
  3. Once the accept event is fully resolved, WskAcceptCompletion is called and a callback is triggered (CtlConnectQueryCallback) which completes initialisation of the PPTP Control connection and creates a context structure specifically for tracking the state of the clients PPTP control connection. This is the main object which we care about for this vulnerability.

The PPTP Control connection context structure is allocated by the CtlAlloc function. Some abbreviated pseudo code for this function is:

PptpCtlCtx *__fastcall CtlAlloc(PptpAdapterContext *AdapterCtx)
{
    PptpAdapterContext *lpPptpAdapterCtx;
    PptpCtlCtx *PptpCtlCtx;
    PptpCtlCtx *lpPptpCtlCtx;
    NDIS_HANDLE lpNDISMiniportHandle;
    PDEVICE_OBJECT v6;
    __int64 v7;
    NDIS_HANDLE lpNDISMiniportHandle_1;
    NDIS_HANDLE lpNDISMiniportHandle_2;
    struct _NDIS_TIMER_CHARACTERISTICS TimerCharacteristics;

    lpPptpAdapterCtx = AdapterCtx;
    PptpCtlCtx = (PptpCtlCtx *)MyMemAlloc(0x290ui64, 'TPTP'); // Actual name of the allocator function in the raspptp.sys code
    lpPptpCtlCtx = PptpCtlCtx;
    if ( PptpCtlCtx )
    {
        memset(PptpCtlCtx, 0, 0x290ui64);
        ReferenceAdapter(lpPptpAdapterCtx);
        lpPptpCtlCtx->AllocTagPTPT = 'TPTP';
        lpPptpCtlCtx->CtlMessageTypeToLength = (unsigned int *)&PptpCtlMessageTypeToSizeArray;
        lpPptpCtlCtx->pPptpAdapterCtx = lpPptpAdapterCtx;
        KeInitializeSpinLock(&lpPptpCtlCtx->CtlSpinLock);
        lpPptpCtlCtx->CtlPptpWanEndpointsEntry.Blink = &lpPptpCtlCtx->CtlPptpWanEndpointsEntry;
        lpPptpCtlCtx->CtlCallDoubleLinkedList.Blink = &lpPptpCtlCtx->CtlCallDoubleLinkedList;
        lpPptpCtlCtx->CtlCallDoubleLinkedList.Flink = &lpPptpCtlCtx->CtlCallDoubleLinkedList;
        lpPptpCtlCtx->CtlPptpWanEndpointsEntry.Flink = &lpPptpCtlCtx->CtlPptpWanEndpointsEntry;
        lpPptpCtlCtx->CtlPacketDoublyLinkedList.Blink = &lpPptpCtlCtx->CtlPacketDoublyLinkedList;
        lpPptpCtlCtx->CtlPacketDoublyLinkedList.Flink = &lpPptpCtlCtx->CtlPacketDoublyLinkedList;
        lpNDISMiniportHandle = lpPptpAdapterCtx->MiniportNdisHandle;
        TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpEchoTimeout;
        *(_DWORD *)&TimerCharacteristics.Header.Type = 0x180197;
        TimerCharacteristics.AllocationTag = 'TMTP';
        TimerCharacteristics.FunctionContext = lpPptpCtlCtx;
        if ( NdisAllocateTimerObject(
            lpNDISMiniportHandle,
            &TimerCharacteristics,
            &lpPptpCtlCtx->CtlEchoTimeoutNdisTimerHandle) )
        {
        ...
        }
        else
        {
            lpNDISMiniportHandle_1 = lpPptpAdapterCtx->MiniportNdisHandle;
            TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpWaitTimeout;
            if ( NdisAllocateTimerObject(
            lpNDISMiniportHandle_1,
            &TimerCharacteristics,
            &lpPptpCtlCtx->CtlWaitTimeoutNdisTimerHandle) )
            {
                ...
            }
            else
            {
                lpNDISMiniportHandle_2 = lpPptpAdapterCtx->MiniportNdisHandle;
                TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpStopTimeout;
                if ( !NdisAllocateTimerObject(
                lpNDISMiniportHandle_2,
                &TimerCharacteristics,
                &lpPptpCtlCtx->CtlStopTimeoutNdisTimerHandle) )
                {
                    KeInitializeEvent(&lpPptpCtlCtx->CtlWaitTimeoutTriggered, NotificationEvent, 1u);
                    KeInitializeEvent(&lpPptpCtlCtx->CtlWaitTimeoutCancled, NotificationEvent, 1u);
                    lpPptpCtlCtx->CtlCtxReferenceCount = 1;// Set reference count to an initial value of one
                    lpPptpCtlCtx->fpCtlCtxFreeFn = (__int64)CtlFree;
                    ExInterlockedInsertTailList(
                    (PLIST_ENTRY)&lpPptpAdapterCtx->PptpWanEndpointsFlink,
                    &lpPptpCtlCtx->CtlPptpWanEndpointsEntry,
                    &lpPptpAdapterCtx->PptpAdapterSpinLock);
                    return lpPptpCtlCtx;
                }
                ...
            }
        }
        ...
    }
    if...
        return 0i64;
}

The important parts of this structure to note are the CtlCtxReferenceCount and CtlWaitTimeoutNdisTimerHandle structure members. This new context structure is stored on the socket context for the new client socket and can then be referenced for all of the events relating to the socket it binds to.

The only section of the socket context structure that we then care about are the following fields:

00000008 ContextPtr dq ? ; PptpCtlCtx
00000010 ContextRecvCallback dq ? ; CtlReceiveCallback
00000018 ContextDisconnectCallback dq ? ; CtlDisconnectCallback
00000020 ContextConnectQueryCallback dq ? ; CtlConnectQueryCallback
  • PptpCtlCtx – The PPTP specific context structure for the control connection.
  • CtlReceiveCallback – The PPTP control connection receive callback.
  • CtlDisconnectCallback – The PPTP control connection disconnect callback.
  • CtlConnectQueryCallback – The PPTP control connection query (used to get client information on a new connection being complete) callback.

raspptp.sys Object Life Cycles

The final bit of background information we need to understand before we delve into the vulnerability is the way that raspptp keeps these context structures alive for a given socket. In the case of the PptpCtlCtx structure, both the client socket and the PptpCtlCtx structure have a reference count.

This reference count is intended to be incremented every time a reference to either object is created. These are initially set to 1 and when decremented to 0Β the objects are freed by calling a free callback stored within each structure. This obviously only works if the code remembers to increment and decrement the reference counts properly and correctly lock access across multiple threads when handling the respective structures.

Within raspptp.sys, the code that performs the reference increment and de-increment functionality usually looks like this:

// Increment code
_InterlockedIncrement(&Ctx->ReferenceCount);

// Decrement Code
if ( _InterlockedExchangeAdd(&Ctx->ReferenceCount, 0xFFFFFFFF) == 1 )
    ((void (__fastcall *)(CtxType *))Ctx->fpFreeHandler)(Ctx);

As you may have guessed at this point, the vulnerability we’re looking at is indeed due to incorrect handling of these reference counts and their respective locks, so now that we have covered the background stuff let’s jump into the juicy details!

The Vulnerability

The first part of our use after free vulnerability is in the code that handles receiving PPTP control data for a client connection. When new data is received by raspptp.sys the WSK layer will dispatch a call the the appropriate event callback. raspptp.sys registers a generic callback for all sockets called ReceiveData. This function parses the incoming data structures from WSK and forwards on the incoming data to the client sockets contexts own receive data call back. For a PPTP control connection, this callback is the CtlReceiveCallback function.

The section of the ReceiveData function that calls this callback has the following pseudo code. This snippet includes all the locking and reference increments that are used to protect the code against multi threaded access issues…

_InterlockedIncrement(&ClientCtx->ConnectionContextRefernceCount);
((void (__fastcall *)(PptpCtlCtx *, PptpCtlInputBufferCtx *, _NET_BUFFER_LIST *))ClientCtx->ContextRecvCallback)(
ClientCtx->ContextPtr,
lpCtlBufferCtx,
NdisNetBuffer);

the CtlReceiveCallback function has the following pseudo code:

__int64 __fastcall CtlReceiveCallback(PptpCtlCtx *PptpCtlCtx, PptpCtlInputBufferCtx *PptpBufferCtx, _NET_BUFFER_LIST *InputBufferList)
{
    PptpCtlCtx *lpPptpCtlCx;
    PNET_BUFFER lpInputFirstNetBuffer;
    _NET_BUFFER_LIST *lpInputBufferList;
    ULONG NetBufferLength;
    PVOID NetDataBuffer;

    lpPptpCtlCx = PptpCtlCtx;
    lpInputFirstNetBuffer = InputBufferList->FirstNetBuffer;
    lpInputBufferList = InputBufferList;
    NetBufferLength = lpInputFirstNetBuffer->DataLength;
    NetDataBuffer = NdisGetDataBuffer(lpInputFirstNetBuffer, lpInputFirstNetBuffer->DataLength, 0i64, 1u, 0);
    if ( NetDataBuffer )
        CtlpEngine(lpPptpCtlCx, (uchar *)NetDataBuffer, NetBufferLength);
        ReceiveDataComplete(lpPptpCtlCx->CtlWskClientSocketCtx, lpInputBufferList);
        return 0i64;
}

The CtlpEngine function is the state machine responsible for parsing the incoming PPTP control data. Now there is one very important piece of code that is missing from these two sections and that is any form of reference count increment or locking for the PptpCtlCtx object!

Neither of the callback handlers actually increment the reference count for the PptpCtlCtx or attempt to lock access to signify that it is in use; this is potentially a vulnerability because if at any point the reference count was to be decremented then the object would be freed! However, if this is so bad, why isnt every PPTP server just crashing all the time? The answer to this question is that the CtlpEngine function actually uses the reference count correctly.

This is where things get confusing. Assuming that the raspptp.sys driver was completely single threaded, this implementation would be 100% safe as no part of the receive pipeline for the control connection decrements the object reference count without first performing an increment to account for it. In reality however, raspptp.sys is not a single threaded driver. Looking back at the initialization of the PptpCtlCtx object, there is one part of particular interest.

TimerCharacteristics.FunctionContext = PptpCtlCtx;
TimerCharacteristics.TimerFunction = (PNDIS_TIMER_FUNCTION)CtlpWaitTimeout;
if ( NdisAllocateTimerObject(
    lpNDISMiniportHandle_1,
    &TimerCharacteristics,
    &lpPptpCtlCtx->CtlWaitTimeoutNdisTimerHandle) )

Here we can see the allocation of an Ndis timer object. The actual implementation of these timers isn’t important, but what is important is that these timers dispatch there callbacks on a separate thread to that of which WSK dispatches the ReceiveData callback. Another interesting point is that both use the PptpCtlCtx structure as their context structure.

So what does this timer callback do and when does it happen? The code that sets the timer is as follows:

NdisSetTimerObject(newClientCtlCtx->CtlWaitTimeoutNdisTimerHandle, (LARGE_INTEGER)-300000000i64, 0, 0i64);// 30 second timeout timer

We can see that a 30 second timer trigger is set and when this 30 seconds is up, the CtlpWaitTimeout callback is called. This 30 second timer can be canceled but this is only done when a client performs a PPTP control handshake with the server, so assuming we never send a valid handshake after 30 seconds the callback will be dispatched. But what does this do?

The CtlpWaitTimeout function is used to handle the timer callback and it has the following pseudo code:

LONG __fastcall CtlpWaitTimeout(PVOID Handle, PptpCtlCtx *Context)
{
    PptpCtlCtx *lpCtlTimeoutEvent;

    lpCtlTimeoutEvent = Context;
    CtlpDeathTimeout(Context);
    return KeSetEvent(&lpCtlTimeoutEvent->CtlWaitTimeoutTriggered, 0, 0);
}

As we can see the function mainly serves to call the eerily named CtlpDeathTimeout function, which has the following pseudo code:

void __fastcall CtlpDeathTimeout(PptpCtlCtx *CtlCtx)
{
    PptpCtlCtx *lpCtlCtx;
    __int64 Unkown;
    CHAR *v3;
    char SockAddrString;

    lpCtlCtx = CtlCtx;
    memset(&SockAddrString, 0, 65ui64);
    if...
        CtlSetState(lpCtlCtx, CtlStateUnknown, Unkown, 0);
        CtlCleanup(lpCtlCtx, 0);
}

This is where things get even more interesting. The CtlCleanup function is the function responsible for starting the process of tearing down the PPTP control connection. This is done in two steps. First, the state of the Control connection is set to CtlStateUnknown which means that the CtlpEngine function will be prevented from processing any further control connection data (kind of). The second step is to push a task to run the similarly named CtlpCleanup function onto a background worker thread which belongs to the raspptp.sys driver.

The end of the CtlpCleanup function contains the following code that will be very useful for us being able to trigger a use after free as it will always run on a different thread to the CtlpEngine function.

result = (unsigned int)_InterlockedExchangeAdd(&lpCtlCtxToCleanup->CtlCtxReferenceCount, 0xFFFFFFFF);
if ( (_DWORD)result == 1 )
    result = ((__int64 (__fastcall *)(PptpCtlCtx *))lpCtlCtxToCleanup->fpCtlCtxFreeFn)(lpCtlCtxToCleanup);

It decrements the reference count on the PptpCtlCtx object and even better is that no part of this timeout pipeline increments the reference count in a way that would prevent the free function from being called!

So, theoretically, all we need to do is find some way of getting the CtlpCleanup and CtlpEngine function to run at the same time on seperate threads and we will be able to cause a Use after Free!

However, before we celebrate too early, we should take a look at the function that actually frees the PptpCtlCtx function because it is yet another callback. The fpCtlCtxFreeFn property is a callback function pointer to the CtlFree function. This function does a decent amount of tear down as well but the bits we care about are the following lines

WskCloseSocketContextAndFreeSocket(CtlWskContext);/
lpCtlCtxToFree->CtlWskClientSocketCtx = 0i64;
...
ExFreePoolWithTag(lpCtlCtxToFree, 0);

Now there is more added complication in this code that is going to make things a little more difficult. The call to WskCloseSocketContextAndFreeSocket actually closes the client socket before freeing the PptpCtlCtx structure. This means that at the point the PptpCtlCtx structure is freed, we will no longer be able to send new data to the socket and trigger any more calls into CtlpEngine. However, this doesn’t mean that we can’t trigger the vulnerability, since if data is already being processed by CtlpEngine when the socket is closed we simply need to hope the thread stays in the function long enough for the free to occur in CtlFree and boom – we have a UAF.

Now that we have a good old fashioned kernel race condition, let’s take a look at how we can try to trigger it!

The Race Condition

Like any good race condition, this one contains a lot of moving parts and added complication which make triggering it a non trivial task, but it’s still possible! Let’s take a look at what we need to happen.

  1. 30 second timeout is triggered and eventually runs CtlCleanup, pushing a CtlpCleanup task onto a background worker thread queue.
  2. Background worker thread wakes up and starts processing the CtlpCleanup task from its task queue.
  3. CtlpEngine starts or is currently processing data on a WSK dispatch thread when the CtlpCleanup function frees the underlying PptpCtlCtx structure from the worker thread!
  4. Bad things happen…

Triggering the Race Condition

The main parts of this race condition to consider are what are the limits on the data can we send to the server to spend as much time as possible in CtlpEngine parsing loop and can we do this without cancelling the timeout?

Thankfully as previously mentioned the only way to cancel the timeout is to perform a PPTP control connection handshake, which technically means we can get the CtlpEngine function to process any other part of the control connection, as long as we don’t start the handshake. However the state machine within CtlpEngine needs the handshake to take place to enable any other part of the control connection!

There is one part of the CtlpEngine state machine that can still be partially validly hit (without triggering an error) before the handshake has taken place. This is the EchoRequest control message type. Now we can’t actually enter the proper handling of the message type before the handshake has taken place but what we can do is use it to iterate through all the sent data in the parsing loop without triggering a parsing error. This effectively forms a way of us spinning inside the CtlpEngine function without cancelling the timeout which is exactly what we want. Even better is that this remains true when the CtlStateUnknown state is set by the CtlCleanup function.

Unfortunately the maximum amount of data we can process in one WSK receive data event callback trigger is limited to the maximum data that can be received in one TCP packet. In theory this is 65,535 bytes but due to the size limitation of Ethernet frames to 1,500 bytes we can only send ~1,450 bytes (1,500 minus the headers of the other network layer frames) of PPTP control messages in a single request. This works out at around 90 EchoRequest messages per callback event trigger. For a modern CPU this is not a lot to churn through before hopping out of the CtlpEngine function.

Another thing to consider is how do we know if the race condition was successful or a failure? Thankfully in this regard the server socket being closed on timeout works in our favour as this will cause a socket exception on the client if we attempt to send any more data once the server closes the socket. Once the socket is closed we know that the race is finished but we don’t necessarily know if we did or didn’t win the race.

With these considerations in place, how do we trigger the vulnerability? It actually becomes a simple proof of concept. Effectively we just continually send EchoRequest PPTP control frames in 90 frame bursts to a server until the timeout event occurs and then we hope that we’ve won the race.

We won’t be releasing the PoC code until people have had a chance to patch things up but when the PoC is successful we may see something like this on our target server:

Because the PptpCtlCtx structure is de-initialised there are a lot of pointers and properties that contain invalid values that, if used at different parts of the Receive Event handling code, will cause crashes in non fun ways like Null pointer deference’s. This is actually what happened in the Blue Screen of Death above, but the CtlpEngine function did still process a freed PptpCtlCtx structure.

Can we use this vulnerability for anything more than a simple BSOD?

Exploitation

Due to the state of mitigation in the Windows kernel against memory corruption exploits and the difficult nature of this race condition, achieving useful exploitation of the vulnerability is not going to be easy, especially if seeking to obtain Remote Code Execution (RCE). However, this does not mean it is not possible to do so.

Exploitability – The Freed Memory

In order to asses the exploitability of the vulnerability, we need to look at what our freed memory contains and where about it is in the Windows kernel heap. In windbg we can use the !pool command to get some information on the allocated chunk that will be freed in our UaF issue.

ffff828b17e50d20 size: 2a0 previous size: 0 (Allocated) *PTPT

We can see here that the size of the freed memory block is 0x2a0 or 672 bytes. This is important as it puts us in the allocation size range for the variable size kernel heap segment. This heap segment is fairly nice for use after free exploitation as the variable size heap also maintains a free list of chunks that have been freed and their sizes. When a new chunk is allocated this free list is searched and if a chunk of an exact or greater size match is found it will be used for the new allocation. Since this is the kernel, any other part of the kernel that allocates non paged pool memory allocations of this or a similar size could end up using this freed slot as well.

So, what do we need in order to start exploiting this issue? ideally we want to find some allocated object in the kernel that we can control the contents of and allocate at 0x2a0 bytes in size. This would allow us to create a fake PptpCtlCtx object, which we can then use to control the CtlpEngine state machine code. Finding an exact size match allocation isn’t the only way we could groom the heap for a potential exploit but it would certainly be the most reliable method.

If we can take control of a PptpCtlCtx object what can we do? One of the most powerful bits of this vulnerability from an exploit development perspective are the callback functions located inside the PptpCtlCtx structure. Usually a mitigation called Control Flow Guard (CFG) or Xtended Flow Guard (XFG) would prevent us from being able to corrupt and use these callback pointers with an arbitrary executable kernel address. However CFG and XFG are not enabled for the raspptp.sys driver (as of writing this blog) meaning we can point execution to any instruction located in the kernel. This gives us plenty of things to abuse for exploitation purposes. A caveat to this is that we are limited to the number of these gadgets we can use in one trigger of the vulnerability, meaning we would likely need to trigger the vulnerability multiple times with different gadgets to achieve a full exploit or at least that’s the case on a modern Windows kernel.

Exploitability – Threads

Allocating an object to fill our freed slot and take control of kernel execution through a fake PptpCtlCtx object sounds great, but one additional restriction on the way in which we do this is that we only have access to CtlpEngine using the freed object for a short period of CPU time. We can’t use the same thread that is processing the CtlpEngine to allocate objects to fill the empty slot, and if we do it would be after the thread has returned from CtlpEngine. At this point the vulnerability will no longer be exploitable.

What this means is that we would need the fake object allocations to be happening in a separate thread in the hope that we can get one of our fake objects allocated and populated with our fake object contents while the vulnerable kernel thread is still in CtlpEngine, allowing us to then start doing bad things with the state machine. All of this sounds like a lot to try and get done in relatively small CPU windows, but it is possible that it could be achieved. The issue with any exploit attempting to do this is going to be reliability, since there is a fairly high chance a failed exploit would crash the target machine and retrying the exploit would be a slow and easily detectable process.

Exploitability – Local Privilege Escalation vs Remote Code Execution

The ability to exploit this issue for LPE is much more likely to be successful over the affected Windows kernel versions than exploiting it for RCE. This is largely due to the fact that an RCE exploit will need to be able to first leak information about the kernel using either this vulnerability or another one before any of the potential callback corruption uses would be viable. There are also far fewer parts of the kernel accessible remotely, meaning finding a way of spraying a fake PptpCtlCtx object into the kernel heap remotely is going to be significantly harder to achieve.

Another reason that LPE is a much more viable exploit route is that the localhost socket or 127.0.0.1 allows for far more data than the ethernet frame capped 1,500 bytes we get remotely, to be processed by each WSK Receive event callback. This significantly increases most of the variables for achieving successful exploitation!

Conclusion

Wormable Kernel Remote Code Execution vulnerabilities are the holy grail of severity in modern operating systems. With great power however comes great responsibility. While this vulnerability could be catastrophic in its impact ,the skill to pull off a successful and undetected exploit is not to be underestimated. Memory corruption continues to become a harder and harder art form to master, however there are definitely those out there with the ability and determination to achieve the full potential of this vulnerability. For these reasons CVE-2022-21972 is a vulnerability that represents a very real threat to internet connected Microsoft based VPN infrastructure. We recommend that this vulnerability is patched with priority in all environments.

Timeline

  • Vulnerability Reported To Microsoft – 29 Oct 2021
  • Vulnerability Acknowledged – 29 Oct 2021
  • Vulnerability Confirmed – 11 November 2021
  • Patch Release Date Confirmed – 12 November 2021
  • Patch Release – 10 May 2022

The post CVE-2022-21972: Windows Server VPN Remote Kernel Use After Free Vulnerability (Part 1) appeared first on Nettitude Labs.

CVE-2022-23253 – Windows VPN Remote Kernel Null Pointer Dereference

22 March 2022 at 09:00

CVE-2022-23253 is a Windows VPN (remote access service) denial of service vulnerability that Nettitude discovered while fuzzing the Windows Server Point-to-Point Tunnelling Protocol (PPTP) driver. The implications of this vulnerability are that it could be used to launch a persistent Denial of Service attack against a target server. The vulnerability requires no authentication to exploit and affects all default configurations of Windows Server VPN.

Nettitude has followed a coordinated disclosure process and reported the vulnerability to Microsoft. As a result the latest versions of MS Windows are now patched and no longer vulnerable to the issue.

Affected Versions of Microsoft Windows Server

The vulnerabilityΒ 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 GRE 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 vulnerabilities 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 GRE 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; // 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;
}

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 vulnerability 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

  • Vulnerability reported To Microsoft – 29 Oct 2021
  • Vulnerability acknowledged – 29 Oct 2021
  • Vulnerability confirmed – 11 Nov 2021
  • Patch release date confirmed – 18 Jan 2022
  • Patch released – 08 March 2022
  • Blog released – 22 March 2022

The post CVE-2022-23253 – Windows VPN Remote Kernel Null Pointer Dereference appeared first on Nettitude Labs.

  • There are no more articles
❌