www.zerodayinitiative.com/advisories/ZDI-21-451/

 

Home | Zero Day Initiative

 

www.zerodayinitiative.com

This vulnerability allows local attackers to disclose sensitive information on affected installations of Oracle VirtualBox. An attacker must first obtain the ability to execute high-privileged code on the target guest system in order to exploit this vulnerability.

The specific flaw exists within the VMSVGA virtual device. The issue results from an integer truncation before reading from memory. An attacker can leverage this in conjunction with other vulnerabilities to escalate privileges and execute arbitrary code in the context of the hypervisor.

 

*No more details will be disclosed.

*This post is not a disclosure

 

전에 ZDI에 제보한 버그가 CVE를 받음과 동시에 이번 버전에서 패치되었다. (6.1.20) 본인이 제보한 것 중 최초로 돈을 받은 건이어서 인상이 깊다.

 

www.oracle.com/security-alerts/cpuapr2021.html

 

공개는 ZDI에서 해당 취약점을 공개한 뒤에 이루어질 예정이다. 부연 설명을 붙이는 방식으로 이루어질 것 같다.

 

The details of CVE-2021-2266 will be released after ZDI releases this bug.

'보안 > 일지' 카테고리의 다른 글

추가 버그  (1) 2021.04.15
CVE-2021-2127 VirtualBox  (1) 2021.01.20

Before going on content...

I originally mentioned that this bug will be disclosed 3 months after the patch. However, I decided to release it today since 3 months seems to be too long... As long as there is an another researcher who disclosed his bugs at the patch date, it would be fine releasing this bug today.


High-level overview of the vulnerability and the possible effect of using it

A divide by zero vulnerability exists in vmsvga3dSurfaceDMA, in SVGA3D of Oracle VirtualBox 6.1.16 Linux distributions. To use this vulnerability, 3D acceleration option should be turned for a guest OS, and an attacker must get a root privilege in the guest OS.

This vulnerability allows local or remote attackers to crash VMs with 3D acceleration, leading to a denial-of-service condition. Flaws exist in three points: (1) Invalid surfaces can be defined in vmsvga3dSurfaceDefine, (2) Textures can be bound to invalid surfaces, (3) No check for divisor in vmsvga3dSurfaceDMA.

Exact product that was found to be vulnerable including complete version information

Version Information

Oracle VirtualBox 6.1.16 releases (which is the latest release in 2020/11/17) except for Windows are affected by this vulnerability. PoC code is tested with the release and Ubuntu 20.04.1 desktop for host and guest OSes, but it is also expected to work with other operating systems, except with Windows.

Precondition

An attacker must get a root privilege in a guest OS in order to use this vulnerability. Also, 3D acceleration option must be turned on for the guest OS to use this vulnerability. Finally, graphics controller option must be set to VMSVGA, which is the default option for Linux guest OSes.

Root Cause Analysis

Overview

A divide by zero vulnerability exists in vmsvga3dSurfaceDMA, which handles the SVGA_3D_CMD_SURFACE_DMA command in the SVGA FIFO loop. A VM crashes when the vulnerability is triggered, leading to the denial-of-service condition on the software. The vulnerability lies in the simple overflow check of the guest offset, before the offset gets verified by vmsvgaR3GmrTransfer. (See Code 1)

// Code 1

// Devices/Graphics/DevVGA-SVGA3d.cpp:610
/* srcx, srcy and srcz values are used to calculate the guest offset.
 * The offset will be verified by vmsvgaR3GmrTransfer, so just check for overflows here.
 */
AssertReturn(srcz < UINT32_MAX / pMipLevel->mipmapSize.height / cbGuestPitch, VERR_INVALID_PARAMETER);

cbGuestPitch can be zero with forged surfaces, leading to a divide by zero vulnerability. No check exists for pMipLevel->cbSurfacePitch, so cbGuestPitch can be zero. AssertReturn function remains as an if-else statement in a release version, so the vulnerability exists in a release version.

Three steps are needed to reach the vulnerable condition. These steps each uses a bug contained in its process.

  1. Two invalid surfaces, which have invalid mipmap sizes, are defined using a SVGA_3D_CMD_SURFACE_DEFINE command. One of two surfaces may be a valid surface. It doesn't affect the next step.
  2. Textures are bound to two surfaces using SVGA_3D_CMD_SURFACE_STRETCHBLT command. Two surfaces are each sent as source and destination surfaces.
  3. Divide by zero is triggered by using SVGA_3D_CMD_SURFACE_DMA command with guest.pitch = 0

Code flow from input to vmsvga* functions

Commands are asynchronously handled with the FIFO loop. (vmsvgaR3FifoLoop in Devices/Graphics/DevVGA-SVGA.cpp) Commands sent via VMware SVGA II PCI device are first enqueued to a SVGA command FIFO buffer, and they are dequeued and handled asynchronously in FIFO order by the loop.

Parsing and executing commands is done in vmsvgaR3FifoLoop function, which contains a simple switch-case statement to handle commands. With a given command id, the loop selects the method of parsing and handling the command. The loop eventually calls vmsvga~~ function to handle a command.

Step 1. Definition of invalid surfaces

Definition of an invalid surface is needed for to reach the vulnerable condition. Our goal is to make a surface that has the following conditions:

  1. Mipmap size is not zero so the clip box is not considered as empty in DMA command.
  2. cbSurfacePitch (derived from cBlocksX, cBlocksY, ...) is zero to trigger divide by zero.
  3. Texture is bound to the surface so that oglId is not zero.

In this step, a surface is defined with a non-zero mipmap size and a zero cbSurfacePitch value. A surface should not be defined in this condition, so this can be considered as a bug.

Making an invalid surface is simple: a surface can be defined with a negative size. (i.e. (-1, -1, -1)) Then, the definition fails in the condition described below. (See Code 2)

// Code 2

// Devices/Graphics/DevVGA-SVGA3d.cpp:278
if (RT_LIKELY(pSurface->cxBlock == 1 && pSurface->cyBlock == 1))
{
    cBlocksX = pMipmapLevel->mipmapSize.width;
    cBlocksY = pMipmapLevel->mipmapSize.height;
}
else
{
    (...)
}

(...)

const uint32_t cMaxBlocksX = cbMemRemaining / pSurface->cbBlock;
if (cBlocksX > cMaxBlocksX)
    return VERR_INVALID_PARAMETER;

cBlocksX becomes a negative value (i.e. -1), which is given as the argument of the command. cMaxBlocksX is 0x80000000 / n since cbMemRemaining remains 0x80000000 and pSurface->cbBlock becomes a small integer which is calculated from a given surface format. (i.e. 1, 2, 4, ..., see DevVGA-SVGA3d-shard.cpp:263 for details) Since the check is done with an unsigned comparison, cBlocksX is always bigger than cMaxBlocksX, so the check fails and the function returns.

A mipmap is already allocated before checking the validity of cBlocks, and the default value of a mipmap is zero for all other remaining values that are not initialized. Therefore, *cbSurfacePitch also becomes zero since it is not initialized. Mipmap size is already assigned before validation, so it is not zero. (See Code 3)

// Code 3

// Devices/Graphics/DevVGA-SVGA3d.cpp:223
for (uint32_t i=0; i < cMipLevels; i++)
    pSurface->paMipmapLevels[i].mipmapSize = paMipLevelSizes[i];

The outcome of step 1 is an object that looks like below. (Size is given as (-1, -1, -1)) Note that mipmapSize is not zero, and cbSurfacePitch is zero.

[*] surface
$2441 = 0x7fff6c7c49a0
$2442 = {
  id = 0x0,
  idWeakContextAssociation = 0xffffffff,
  surfaceFlags = 0x2,
  format = SVGA3D_BUFFER,
  internalFormatGL = 0xffffffff,
  formatGL = 0xffffffff,
  typeGL = 0xffffffff,
  enmOGLResType = VMSVGA3D_OGLRESTYPE_NONE,
  oglId = {
    texture = 0x0,
    buffer = 0x0,
    renderbuffer = 0x0
  },
  targetGL = 0x0,
  bindingGL = 0x0,
  fEmulated = 0x0,
  idEmulated = 0x0,
  faces = {{
      numMipLevels = 0x1
    }, {
      numMipLevels = 0x0
    }, {
      numMipLevels = 0x0
    }, {
      numMipLevels = 0x0
    }, {
      numMipLevels = 0x0
    }, {
      numMipLevels = 0x0
    }},
  cFaces = 0x1,
  cMipmapLevels = 0x1,
  paMipmapLevels = 0x7fff6fca48a0,
  multiSampleCount = 0x0,
  autogenFilter = SVGA3D_TEX_FILTER_NONE,
  cbBlock = 0x1,
  cxBlock = 0x1,
  cyBlock = 0x1,
  cbBlockGL = 0x1,
  fDirty = 0x0
}
[*] paMipmapLevel[0]
$2443 = {
  mipmapSize = {
    width = 0xffffffff,
    height = 0xffffffff,
    depth = 0xffffffff
  },
  cBlocksX = 0x0,
  cBlocksY = 0x0,
  cBlocks = 0x0,
  cbSurfacePitch = 0x0,
  cbSurfacePlane = 0x0,
  cbSurface = 0x0,
  pSurfaceData = 0x0,
  fDirty = 0x0
}

Step 2. Texture binding

Texture binding to the surface made in step 1 is needed to reach the vulnerable condition. In step 2, SVGA_3D_CMD_SURFACE_STRETCHBLT command is used to bind a texture. Draw primitive functions may be used to bind textures, but they are more complicated than vmsvga3dSurfaceStretchBlt function, so the function is used in step 2. Also, a buffer may be bound to a surface instead of binding a texture to it, but binding a texture is chosen because of the same reason.

vmsvga3dSurfaceStretchBlt needs two surfaces: one as a source surface and the other as a destination surface. One of two surfaces may be valid, but one of them must be the surface which is made in step 1 to exploit the vulnerability.

Codes described in Code 4 is used to create and bind the texture to the surfaces. Even if a surface is invalid, a texture will be created and bound to the surface. vmsvga3dBackCreateTexture function will create a texture and bind it to the surface.

// Code 4

// Devices/Graphics/DevVGA-SVGA3d.cpp:437
if (!VMSVGA3DSURFACE_HAS_HW_SURFACE(pSrcSurface))
{
    /* Unknown surface type; turn it into a texture, which can be used for other purposes too. */
    LogFunc(("unknown src sid=%u type=%d format=%d -> create texture\n", sidSrc, pSrcSurface->surfaceFlags, pSrcSurface->format));
    rc = vmsvga3dBackCreateTexture(pState, pContext, pContext->id, pSrcSurface);
    AssertRCReturn(rc, rc);
}

if (!VMSVGA3DSURFACE_HAS_HW_SURFACE(pDstSurface))
{
    /* Unknown surface type; turn it into a texture, which can be used for other purposes too. */
    LogFunc(("unknown dest sid=%u type=%d format=%d -> create texture\n", sidDst, pDstSurface->surfaceFlags, pDstSurface->format));
    rc = vmsvga3dBackCreateTexture(pState, pContext, pContext->id, pDstSurface);
    AssertRCReturn(rc, rc);
}

A texture should not be bound if a surface is invalid. Therefore, this can be also considered as a bug.

The outcome of step 2 is an object that looks like below. Note that oglId is not zero anymore.

[*] pSurface
$2453 = {
  id = 0x1,
  idWeakContextAssociation = 0xffffffff,
  surfaceFlags = 0x22,
  format = SVGA3D_BUFFER,
  internalFormatGL = 0xffffffff,
  formatGL = 0xffffffff,
  typeGL = 0xffffffff,
  enmOGLResType = VMSVGA3D_OGLRESTYPE_TEXTURE,
  oglId = {
    texture = 0x38,
    buffer = 0x38,
    renderbuffer = 0x38
  },
  targetGL = 0x806f,
  bindingGL = 0x806a,
  fEmulated = 0x0,
  idEmulated = 0x0,
  faces = {{
      numMipLevels = 0x1
    }, {
      numMipLevels = 0x0
    }, {
      numMipLevels = 0x0
    }, {
      numMipLevels = 0x0
    }, {
      numMipLevels = 0x0
    }, {
      numMipLevels = 0x0
    }},
  cFaces = 0x1,
  cMipmapLevels = 0x1,
  paMipmapLevels = 0x7fff6c7c17a0,
  multiSampleCount = 0x0,
  autogenFilter = SVGA3D_TEX_FILTER_NONE,
  cbBlock = 0x1,
  cxBlock = 0x1,
  cyBlock = 0x1,
  cbBlockGL = 0x1,
  fDirty = 0x0
}
[*] pMipLevel
$2454 = {
  mipmapSize = {
    width = 0xffffffff,
    height = 0xffffffff,
    depth = 0xffffffff
  },
  cBlocksX = 0x0,
  cBlocksY = 0x0,
  cBlocks = 0x0,
  cbSurfacePitch = 0x0,
  cbSurfacePlane = 0x0,
  cbSurface = 0x0,
  pSurfaceData = 0x0,
  fDirty = 0x0
}

Step 3. Triggering the bug

Divide by zero bug in vmsvga3dSurfaceDMA can now be triggered with the surface made in step 1 and step 2. The problematic code is described in Code 5.

// Code 5

// Devices/Graphics/DevVGA-SVGA3d.cpp:610
/* srcx, srcy and srcz values are used to calculate the guest offset.
 * The offset will be verified by vmsvgaR3GmrTransfer, so just check for overflows here.
 */
AssertReturn(srcz < UINT32_MAX / pMipLevel->mipmapSize.height / cbGuestPitch, VERR_INVALID_PARAMETER);

Here, divide by zero occurs when cbGuestPitch is zero. cbGuestPitch becomes zero if we give guest.pitch = 0. (guest.pitch is an argument of a DMA command given by the user) AssertReturn function remains as an if-else statement in a release version, so the vulnerability exists in a release version. See Code 6.

// Code 6

// Devices/Graphics/DevVGA-SVGA3d.cpp:597
uint32_t cbGuestPitch = guest.pitch;
if (cbGuestPitch == 0)
{
    /* Host must "assume image is tightly packed". Our surfaces are. */
    cbGuestPitch = pMipLevel->cbSurfacePitch;
}
else { (...) }

There are two checks that need to be bypassed to reach the vulnerable condition. One (Code 7) is bypassed by defining an invalid surface in step 1, and the other (Code 8) is bypassed by binding a texture to the invalid surface defined in step 2. The check in Code 7 requires a non-zero mipmap size, and the other check in Code 8 requires a non-zero oglId.

// Code 7

// Devices/Graphics/DevVGA-SVGA3d.cpp:536
/* The copybox's "dest" is coords in the host surface. Verify them against the surface's mipmap size. */
SVGA3dBox hostBox;
hostBox.x = paBoxes[i].x;
hostBox.y = paBoxes[i].y;
hostBox.z = paBoxes[i].z;
hostBox.w = paBoxes[i].w;
hostBox.h = paBoxes[i].h;
hostBox.d = paBoxes[i].d;
vmsvgaR3ClipBox(&pMipLevel->mipmapSize, &hostBox);

if (   !hostBox.w
    || !hostBox.h
    || !hostBox.d)
{
    Log(("Skip empty box\n"));
    continue;
}

// Devices/Graphics/DevVGA-SVGA.cpp:5274
/**
 * Unsigned coordinates in pBox. Clip to [0; pSize).
 *
 * @param   pSize   Source surface dimensions.
 * @param   pBox    Coordinates to be clipped.
 */
void vmsvgaR3ClipBox(const SVGA3dSize *pSize, SVGA3dBox *pBox)
{
    /* x, w */
    if (pBox->x > pSize->width)
        pBox->x = pSize->width;
    if (pBox->w > pSize->width - pBox->x)
        pBox->w = pSize->width - pBox->x;

    /* y, h */
    if (pBox->y > pSize->height)
        pBox->y = pSize->height;
    if (pBox->h > pSize->height - pBox->y)
        pBox->h = pSize->height - pBox->y;

    /* z, d */
    if (pBox->z > pSize->depth)
        pBox->z = pSize->depth;
    if (pBox->d > pSize->depth - pBox->z)
        pBox->d = pSize->depth - pBox->z;
}
// Code 8

// Devices/Graphics/DevVGA-SVGA3d.cpp:504
if (!VMSVGA3DSURFACE_HAS_HW_SURFACE(pSurface))
{
    /*
     * Not realized in host hardware/library yet, we have to work with
     * the copy of the data we've got in VMSVGA3DMIMAPLEVEL::pSurfaceData.
     */
    AssertReturn(pMipLevel->pSurfaceData, VERR_INTERNAL_ERROR);
}

// Devices/Graphics/DevVGA-SVGA3d-internal.h:683 (VMSVGA3D_DIRECT3D not defined in linux release)
/** @def VMSVGA3DSURFACE_HAS_HW_SURFACE
 * Checks whether the surface has a host hardware/library surface.
 * @returns true/false
 * @param   a_pSurface      The VMSVGA3d surface.
 */
#ifdef VMSVGA3D_DIRECT3D
# define VMSVGA3DSURFACE_HAS_HW_SURFACE(a_pSurface) ((a_pSurface)->u.pSurface != NULL)
#else
# define VMSVGA3DSURFACE_HAS_HW_SURFACE(a_pSurface) ((a_pSurface)->oglId.texture != OPENGL_INVALID_ID)
#endif

Vulnerable code can be reached by bypassing these two conditions. VM crashes with divide by zero, trying to divide with cbGuestPitch, which has zero value.

gdb-peda$ c
Continuing.

Thread 24 "VMSVGA FIFO" received signal SIGFPE, Arithmetic exception.
[Switching to Thread 0x7f3acc41f700 (LWP 73906)]
[----------------------------------registers-----------------------------------]
RAX: 0x1 
RBX: 0x0 
RCX: 0xffffffff 
RDX: 0x0 
RSI: 0x0 
RDI: 0x0 
RBP: 0x7f3acc41ed10 --> 0x7f3acc41ee50 --> 0x7f3acc41ee90 --> 0x7f3acc41eeb0 --> 0x7f3acc41eef0 --> 0x0 
RSP: 0x7f3acc41ec10 --> 0x400000001 
RIP: 0x7f3acd87b333 (div    ebx)
R8 : 0x0 
R9 : 0x0 
R10: 0x1 
R11: 0x2a ('*')
R12: 0x0 
R13: 0x7f3a94603ed0 --> 0xffffffff00000001 
R14: 0x7f3a9775f260 --> 0xffffffffffffffff 
R15: 0x7f3a94000b80 --> 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7f3acd87b32b:    mov    eax,ecx
   0x7f3acd87b32d:    div    DWORD PTR [r14+0x4]
   0x7f3acd87b331:    xor    edx,edx
=> 0x7f3acd87b333:    div    ebx
   0x7f3acd87b335:    cmp    eax,DWORD PTR [rbp-0xbc]
   0x7f3acd87b33b:    jbe    0x7f3acd87b568
   0x7f3acd87b341:    mov    eax,ecx
   0x7f3acd87b343:    xor    edx,edx
[------------------------------------stack-------------------------------------]
0000| 0x7f3acc41ec10 --> 0x400000001 
0008| 0x7f3acc41ec18 --> 0x7f3a94000b64 --> 0xfffffffe 
0016| 0x7f3acc41ec20 --> 0x7f3ad8134568 --> 0xffffeeee 
0024| 0x7f3acc41ec28 --> 0x22057251 
0032| 0x7f3acc41ec30 --> 0x0 
0040| 0x7f3acc41ec38 --> 0x7f3ad8134250 --> 0x5000000010 
0048| 0x7f3acc41ec40 --> 0x7f3acc6e4140 --> 0x7f3ab8000000 --> 0xff1d1d1dff1d1d1d 
0056| 0x7f3acc41ec48 --> 0x7f3acc6e4800 --> 0x8000000 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGFPE
0x00007f3acd87b333 in ?? () from /usr/lib/virtualbox/VBoxDD.so
gdb-peda$ bt
#0  0x00007f3acd87b333 in  () at /usr/lib/virtualbox/VBoxDD.so
#1  0x00007f3acd876bcb in  () at /usr/lib/virtualbox/VBoxDD.so
#2  0x00007f3b10724c8a in  () at /usr/lib/virtualbox/components/VBoxVMM.so
#3  0x00007f3b21f879e8 in  () at /usr/lib/virtualbox/VBoxRT.so
#4  0x00007f3b2204b452 in  () at /usr/lib/virtualbox/VBoxRT.so
#5  0x00007f3b2235f609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#6  0x00007f3b22280293 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
gdb-peda$ vmmap
(...)
0x00007f3acd7c6000 0x00007f3acd7e8000 r--p    /usr/lib/virtualbox/VBoxDD.so
0x00007f3acd7e8000 0x00007f3acd94d000 r-xp    /usr/lib/virtualbox/VBoxDD.so
0x00007f3acd94d000 0x00007f3acdb7f000 r--p    /usr/lib/virtualbox/VBoxDD.so
0x00007f3acdb7f000 0x00007f3acdb80000 ---p    /usr/lib/virtualbox/VBoxDD.so
0x00007f3acdb80000 0x00007f3acdb93000 r--p    /usr/lib/virtualbox/VBoxDD.so
0x00007f3acdb93000 0x00007f3acdb9b000 rw-p    /usr/lib/virtualbox/VBoxDD.so
0x00007f3acdb9b000 0x00007f3ace201000 rw-p    mapped
(...)

Suggested Fixes

Three main causes exist in this vulnerability.

  1. An invalid surface, which has a non-zero mipmap size and zero cBlocks* and cbSurfacePitch, can be defined.
  2. A texture can be bound to an invalid surface.
  3. No check for the divisor, which is cbGuestPitch, in DMA command.

Fixing this vulnerability depends on the design choice that developers make. There are two options to fix this vulnerability.

  1. Deallocate the surface when defining a surface fails.
  2. Validate mipmaps of surfaces whether they have proper fields whenever surfaces are used in commands. For this vulnerability, there are problems in vmsvga3dSurfaceStretchBlt, vmsvga3dBackCreateTexture, and vmsvga3dSurfaceDMA. However, other commands may also contain problems.

Proof-of-Concept

Environment

Proof-of-Concept code is tested on VirtualBox 6.1.16 version (recent version in 2020-11-20), Linux distribution. Version for host and guest OSes are Ubuntu 20.04.1 desktop, but other OSes except Windows are supposed to work as well. 3D acceleration under display option should be turned on, and graphics controller option should remain VMSVGA, which is the default setting for display in Linux distribution.

Running PoC code

https://github.com/candymate/pwn/tree/master/CVE/CVE-2021-2127

PoC code should be run inside a guest. Thus, download the attached file inside a guest and extract the file.

libpciaccess-dev should be installed first to run the attached code. Run sudo apt install libpciaccess-dev first before running the code.

After installing the library, run make to compile the code. Next, run the compiled code with root privilege. (Do sudo ./poc)

Additional Information

Most of the codes are based on one of released PoC codes, which are in https://github.com/renorobert/virtualbox-vmsvga-bugs.

Software Download Link

VirtualBox release can be downloaded in https://www.virtualbox.org/wiki/Downloads. Linux distribution needs to be downloaded for this vulnerability. (PoC code is tested on Ubuntu 20.04.1 desktop for guest and host OSes)

Ubuntu 20.04.1 desktop image can be downloaded in https://releases.ubuntu.com/20.04/. This may be optional.


Patch

Patched at 6.1.18. Allocation (or Definition) of invalid surfaces is now prohibited. (See VBox/Devices/Graphics/DevVGA-SVGA3d.cpp)

 

VirtualBox SVGA에서 버그를 하나 발견하여 벤더에 제보했고, CVE 번호를 하나 받게 되었다. 버그를 제보하고 CVE까지 받는 것은 처음인데, 내 이름이 패치 내역에 있는 것이 상당히 기분이 좋았다. 프로젝트로 진행해서 열심히 삽질하고 한 것이 나름대로 보상이 된 것 같다.

 

www.oracle.com/security-alerts/cpujan2021.html

크레딧 내용에 있는 본인 CVE

 

저 자료를 보면 VirtualBoBs 팀으로 버그가 3개가 들어가 있는데, 팀에서 벤더에 직접 제보하여 이번에 패치된 3건, ZDI 제보해서 오퍼까지 받은게 3건으로 총 6건을 발견하여 제보하였다. 버그 헌팅 처음 하는 사람들끼리 모여서 단기간에 으쌰으쌰해서 열심히 삽질한 것이 어떻게 성과가 잘 나온 것 같다. 처음에 VM 관련해서 아무런 지식이 없어 원데이로 삽질하고, CVE 훑어보고, 심지어는 방법론 자체를 공부하는 등 여러 삽질을 했다. 그럼에도 이런 활동들을 나름 계획을 세우고, 서로 자료 공유도 열심히 하고, 코로나 때문에 얼굴 한번 보지 못했어도 간단하게 인력 관리 봇 하나 구현해서 운영까지 해가며 어케어케 하다보니 프로젝트가 잘 된 것 같다.

 

버그에 대한 자세한 내용은 3달 정도 더 기다린 후에 공개하려 한다. 어제 저녁쯤에 패치가 되어 현재 해결이 된 상태지만 패치 갭을 생각해서 3달 정도 있다가 하는게 맞는 것 같다.


The details of CVE-2021-2127 will be released after 3 months, considering patch gaps of softwares. Even though the bug was fixed yesterday, I think it is right to disclose the bug after 3 months, considering the gaps.

'보안 > 일지' 카테고리의 다른 글

CVE-2021-2266 VirtualBox (ZDI-CAN-13464)  (2) 2021.04.22
추가 버그  (1) 2021.04.15

소개

VirtualBox의 디스플레이 세팅을 보게 되면 Graphicss Controller와 Acceleration 세팅이 있다. 해당 컨트롤러에서 VMSVGA를 선택하면 SVGA를 사용하게 된다. Linux를 OS로 선택하면 3D acceleration 옵션까지 켤 수 있다.

이 문서에서는 SVGA의 내부 구조와 작동 방식에 대해 설명한다.

Contents

Graphics Device Architecture

SVGA는 해당 옵션이 켜져 있을 때 게스트에서 해당 디바이스의 커널 드라이버를 통해 접근할 수 있다. 크게 두 방법으로 접근 가능한데, (1) MMIO를 통한 FIFO 메모리 접근, (2) Port I/O를 통한 SVGA Register 접근이 가능하다. 해당 두 상태를 설정해놓으면 VM 구현 코드에서 데이터를 가져가 상태에 맞는 기능을 수행하게 된다. 위 그림의 경우 VMware 기준으로 설명이 되어 있지만 VirtualBox도 동일한 방식으로 작동한다.

SVGA Structure and Port I/O

SVGA의 경우 게스트 입장에서는 PCI 디바이스를 통해 접근할 수 있다. 위 도식에서 BAR 0, 1, 2는 각각 I/O 포트 번호, 글로벌 프레임 버퍼의 물리 주소, FIFO의 물리 주소를 나타낸다. IRQ의 경우 Interrupt Request의 약자로 인터럽트가 들어오는 것을 처리하는데 사용된다. I/O를 처리하는 것을 아래 코드로 살펴보자.

// VBox/Graphics/DevVGA.cpp:6661
pThis->hIoPortVmSvga    = NIL_IOMIOPORTHANDLE;
pThis->hMmio2VmSvgaFifo = NIL_PGMMMIO2HANDLE;
if (pThis->fVMSVGAEnabled)
{
    /* Register the io command ports. */
    rc = PDMDevHlpPCIIORegionCreateIo(pDevIns, pThis->pciRegions.iIO, 0x10, vmsvgaIOWrite, vmsvgaIORead, NULL /*pvUser*/,
                                      "VMSVGA", NULL /*paExtDescs*/, &pThis->hIoPortVmSvga);
    AssertRCReturn(rc, rc);

    rc = PDMDevHlpPCIIORegionCreateMmio2Ex(pDevIns, pThis->pciRegions.iFIFO, pThis->svga.cbFIFO,
                                           PCI_ADDRESS_SPACE_MEM, 0 /*fFlags*/, vmsvgaR3PciIORegionFifoMapUnmap,
                                           "VMSVGA-FIFO", (void **)&pThisCC->svga.pau32FIFO, &pThis->hMmio2VmSvgaFifo);
    AssertRCReturn(rc, PDMDevHlpVMSetError(pDevIns, rc, RT_SRC_POS,
                                           N_("Failed to create VMSVGA FIFO (%u bytes)"), pThis->svga.cbFIFO));

    pPciDev->pfnRegionLoadChangeHookR3 = vgaR3PciRegionLoadChangeHook;
}

위 코드에서 I/O 핸들링을 어떻게 하는지, FIFO MMIO를 어떻게 처리할지에 대한 설정을 하고 있음을 확인할 수 있다. VRAM의 경우에는 VMSVGA 세팅에 상관없이 VGA에서 처리한다.

// VBox/Devices/Graphics/DevVGA.cpp:6680
/*
 * Allocate VRAM and create a PCI region for it.
 */
rc = PDMDevHlpPCIIORegionCreateMmio2Ex(pDevIns, pThis->pciRegions.iVRAM, pThis->vram_size,
                                       PCI_ADDRESS_SPACE_MEM_PREFETCH, 0 /*fFlags*/, vgaR3PciIORegionVRamMapUnmap,
                                       "VRam", (void **)&pThisCC->pbVRam, &pThis->hMmio2VRam);
AssertLogRelRCReturn(rc, PDMDevHlpVMSetError(pDevIns, rc, RT_SRC_POS,
                                                 N_("Failed to allocate %u bytes of VRAM"), pThis->vram_size));

SVGA Handling

SVGA I/O나 MMIO로 접근하게 되면 vmsvgaIORead, vmsvgaIOWrite 콜백 함수들에서 해당 데이터들을 처리하게 된다. 들어오는 포트에 따라 이를 처리하는 루틴이 달라진다.

// VBox/Devices/Graphics/DevVGA-SVGA.cpp:2037
/**
 * @callback_method_impl{FNIOMIOPORTNEWIN}
 */
DECLCALLBACK(VBOXSTRICTRC) vmsvgaIORead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t *pu32, unsigned cb)
{
    PVGASTATE   pThis = PDMDEVINS_2_DATA(pDevIns, PVGASTATE);
    RT_NOREF_PV(pvUser);

    /* Only dword accesses. */
    if (cb == 4)
    {
        switch (offPort)
        {
            case SVGA_INDEX_PORT:
                *pu32 = pThis->svga.u32IndexReg;
                break;

            case SVGA_VALUE_PORT:
                return vmsvgaReadPort(pDevIns, pThis, pu32);

            case SVGA_BIOS_PORT:
                Log(("Ignoring BIOS port read\n"));
                *pu32 = 0;
                break;

            case SVGA_IRQSTATUS_PORT:
                LogFlow(("vmsvgaIORead: SVGA_IRQSTATUS_PORT %x\n", pThis->svga.u32IrqStatus));
                *pu32 = pThis->svga.u32IrqStatus;
                break;

            default:
                ASSERT_GUEST_MSG_FAILED(("vmsvgaIORead: Unknown register %u was read from.\n", offPort));
                *pu32 = UINT32_MAX;
                break;
        }
    }
    else
    {
        Log(("Ignoring non-dword I/O port read at %x cb=%d\n", offPort, cb));
        *pu32 = UINT32_MAX;
    }
    return VINF_SUCCESS;
}

vmsvgaReadPortvmsvgaWritePort에서는 SVGA Register 값들을 세팅하게 된다. 예를들어, SVGA_REG_ID로 값을 읽으려 시도하면 SVGA id가 리턴된다.

반면에 FIFO 메모리와 VRAM의 경우 직접 해당 메모리 영역에 값을 쓰게 된다. (각각 pThisCC->svga.pau32FIFO, pThisCC->pbVRam) FIFO의 경우 이후 FIFO loop에서 커맨드를 하나씩 잘라서 처리하게 된다.

SVGA PCI Device

게스트 입장에서 SVGA 디바이스에 접근할 때에는 PCI 디바이스를 통해 접근할 수 있다. 게스트 입장에서 볼 수 있는 PCI 디바이스는 다음과 같다.

vm@vm:~$ lspci
(...)
00:02.0 VGA compatible controller: VMware SVGA II Adapter
(...)

코드상으로 해당 PCI 디바이스를 찾아 연결하기 위해서 vendor와 device id를 맞춰주어야 한다. 해당 id들은 svga_reg.h에 정의되어 있다.

// VBox/Devices/Graphics/vmsvga/svga_reg.h:41
/*
 * PCI device IDs.
 */
#define PCI_VENDOR_ID_VMWARE            0x15AD
#define PCI_DEVICE_ID_VMWARE_SVGA2      0x0405

코드상으로 연결하는 방법은 다양한 방법이 있지만 libpciaccess를 이용하여 연결하는 예시 코드는 아래와 같다.

// https://github.com/renorobert/virtualbox-vmsvga-bugs/blob/master/CVE-2017-10210/svga.c
int conf_svga_device(void)
{
    struct pci_device *dev;
    struct pci_device_iterator *iter;
    struct pci_id_match match;
    uint16_t command;

    if (getuid() != 0 || geteuid() != 0) 
        errx(EXIT_FAILURE, "[!] Run program as root");

    iopl(3);

    if (pci_system_init())
        return -1;

    match.vendor_id = PCI_VENDOR_ID_VMWARE;
    match.device_id = PCI_DEVICE_ID_VMWARE_SVGA2;
    match.subvendor_id = PCI_MATCH_ANY;
    match.subdevice_id = PCI_MATCH_ANY;
    match.device_class = 0;
    match.device_class_mask = 0;

    iter = pci_id_match_iterator_create(&match);
    dev = pci_device_next(iter);

    if (dev == NULL) {
        pci_cleanup(iter);
        return -1;
    }

    pci_device_probe(dev);

    gSVGA.ioBase = dev->regions[0].base_addr;
    gSVGA.fbMem = (void *)dev->regions[1].base_addr;
    gSVGA.fifoMem = (void *)dev->regions[2].base_addr;

    command = pci_device_cfg_read_u16(dev, 0, 4);
    pci_device_cfg_write_u16(dev, command | 7, 4);

    SVGA_WriteReg(SVGA_REG_ID, SVGA_ID_2);
    SVGA_WriteReg(SVGA_REG_ENABLE, true);

    gSVGA.vramSize = SVGA_ReadReg(SVGA_REG_VRAM_SIZE);
    gSVGA.fbSize = SVGA_ReadReg(SVGA_REG_FB_SIZE);
    gSVGA.fifoSize = SVGA_ReadReg(SVGA_REG_MEM_SIZE);

    pci_device_map_range(dev, (pciaddr_t)gSVGA.fbMem, (pciaddr_t)gSVGA.fbSize,
            PCI_DEV_MAP_FLAG_WRITABLE, (void *)&gSVGA.fbMem);
    pci_device_map_range(dev, (pciaddr_t)gSVGA.fifoMem, (pciaddr_t)gSVGA.fifoSize,
            PCI_DEV_MAP_FLAG_WRITABLE, (void *)&gSVGA.fifoMem);

    pci_cleanup(iter);

    return 0;
}

FIFO Command Handling

FIFO buffer로 MMIO를 수행하게 되면 FIFO loop (vmsvgaR3FifoLoop)에서 커맨드로 잘라서 해당하는 커맨드를 수행하게 된다. 기본적으로 커맨드 구조는 아래를 따른다.

  1. 커맨드 넘버

    // VBox/Devices/Graphics/vmsvga/svga_reg.h:1010
    typedef enum {
    SVGA_CMD_INVALID_CMD           = 0,
    SVGA_CMD_UPDATE                = 1,
    SVGA_CMD_RECT_COPY             = 3,
    SVGA_CMD_DEFINE_CURSOR         = 19,
    SVGA_CMD_DEFINE_ALPHA_CURSOR   = 22,
    SVGA_CMD_UPDATE_VERBOSE        = 25,
    SVGA_CMD_FRONT_ROP_FILL        = 29,
    SVGA_CMD_FENCE                 = 30,
    SVGA_CMD_ESCAPE                = 33,
    SVGA_CMD_DEFINE_SCREEN         = 34,
    SVGA_CMD_DESTROY_SCREEN        = 35,
    SVGA_CMD_DEFINE_GMRFB          = 36,
    SVGA_CMD_BLIT_GMRFB_TO_SCREEN  = 37,
    SVGA_CMD_BLIT_SCREEN_TO_GMRFB  = 38,
    SVGA_CMD_ANNOTATION_FILL       = 39,
    SVGA_CMD_ANNOTATION_COPY       = 40,
    SVGA_CMD_DEFINE_GMR2           = 41,
    SVGA_CMD_REMAP_GMR2            = 42,
    SVGA_CMD_MAX
    } SVGAFifoCmdId;
    
    // VBox/Devices/Graphics/vmsvga/svga3d\_reg.h:1031  
    #define SVGA\_3D\_CMD\_LEGACY\_BASE 1000  
    #define SVGA\_3D\_CMD\_BASE 1040
    
    #define SVGA\_3D\_CMD\_SURFACE\_DEFINE SVGA\_3D\_CMD\_BASE + 0 // Deprecated  
    #define SVGA\_3D\_CMD\_SURFACE\_DESTROY SVGA\_3D\_CMD\_BASE + 1  
    #define SVGA\_3D\_CMD\_SURFACE\_COPY SVGA\_3D\_CMD\_BASE + 2  
    #define SVGA\_3D\_CMD\_SURFACE\_STRETCHBLT SVGA\_3D\_CMD\_BASE + 3  
    #define SVGA\_3D\_CMD\_SURFACE\_DMA SVGA\_3D\_CMD\_BASE + 4  
    #define SVGA\_3D\_CMD\_CONTEXT\_DEFINE SVGA\_3D\_CMD\_BASE + 5  
    #define SVGA\_3D\_CMD\_CONTEXT\_DESTROY SVGA\_3D\_CMD\_BASE + 6  
    #define SVGA\_3D\_CMD\_SETTRANSFORM SVGA\_3D\_CMD\_BASE + 7  
    #define SVGA\_3D\_CMD\_SETZRANGE SVGA\_3D\_CMD\_BASE + 8  
    #define SVGA\_3D\_CMD\_SETRENDERSTATE SVGA\_3D\_CMD\_BASE + 9  
    #define SVGA\_3D\_CMD\_SETRENDERTARGET SVGA\_3D\_CMD\_BASE + 10  
    #define SVGA\_3D\_CMD\_SETTEXTURESTATE SVGA\_3D\_CMD\_BASE + 11  
    #define SVGA\_3D\_CMD\_SETMATERIAL SVGA\_3D\_CMD\_BASE + 12  
    #define SVGA\_3D\_CMD\_SETLIGHTDATA SVGA\_3D\_CMD\_BASE + 13  
    #define SVGA\_3D\_CMD\_SETLIGHTENABLED SVGA\_3D\_CMD\_BASE + 14  
    #define SVGA\_3D\_CMD\_SETVIEWPORT SVGA\_3D\_CMD\_BASE + 15  
    #define SVGA\_3D\_CMD\_SETCLIPPLANE SVGA\_3D\_CMD\_BASE + 16  
    #define SVGA\_3D\_CMD\_CLEAR SVGA\_3D\_CMD\_BASE + 17  
    #define SVGA\_3D\_CMD\_PRESENT SVGA\_3D\_CMD\_BASE + 18 // Deprecated  
    #define SVGA\_3D\_CMD\_SHADER\_DEFINE SVGA\_3D\_CMD\_BASE + 19  
    #define SVGA\_3D\_CMD\_SHADER\_DESTROY SVGA\_3D\_CMD\_BASE + 20  
    #define SVGA\_3D\_CMD\_SET\_SHADER SVGA\_3D\_CMD\_BASE + 21  
    #define SVGA\_3D\_CMD\_SET\_SHADER\_CONST SVGA\_3D\_CMD\_BASE + 22  
    #define SVGA\_3D\_CMD\_DRAW\_PRIMITIVES SVGA\_3D\_CMD\_BASE + 23  
    #define SVGA\_3D\_CMD\_SETSCISSORRECT SVGA\_3D\_CMD\_BASE + 24  
    #define SVGA\_3D\_CMD\_BEGIN\_QUERY SVGA\_3D\_CMD\_BASE + 25  
    #define SVGA\_3D\_CMD\_END\_QUERY SVGA\_3D\_CMD\_BASE + 26  
    #define SVGA\_3D\_CMD\_WAIT\_FOR\_QUERY SVGA\_3D\_CMD\_BASE + 27  
    #define SVGA\_3D\_CMD\_PRESENT\_READBACK SVGA\_3D\_CMD\_BASE + 28 // Deprecated  
    #define SVGA\_3D\_CMD\_BLIT\_SURFACE\_TO\_SCREEN SVGA\_3D\_CMD\_BASE + 29  
    #define SVGA\_3D\_CMD\_SURFACE\_DEFINE\_V2 SVGA\_3D\_CMD\_BASE + 30  
    #define SVGA\_3D\_CMD\_GENERATE\_MIPMAPS SVGA\_3D\_CMD\_BASE + 31  
    #define SVGA\_3D\_CMD\_ACTIVATE\_SURFACE SVGA\_3D\_CMD\_BASE + 40  
    #define SVGA\_3D\_CMD\_DEACTIVATE\_SURFACE SVGA\_3D\_CMD\_BASE + 41  
    #define SVGA\_3D\_CMD\_MAX SVGA\_3D\_CMD\_BASE + 42
  2. 커맨드 헤더 (id (지워짐), header size)

    // VBox/Devices/Graphics/vmsvga/svga3d_reg.h:1126
    /*
    * The data size header following cmdNum for every 3d command
    */
    typedef
    struct {
    /* uint32_t               id; duplicate*/
    uint32_t               size;
    } SVGA3dCmdHeader;
  3. 커맨드 body

    // example of command body
    
    STRUCTURE <svga3d\_reg.struct\_c\_\_SA\_SVGA3dCmdSetRenderState object at 0x7fbbcbe0a7b8>  
    FIELD cid (type=<class 'ctypes.c\_uint'>, bitlen=32)  
    VALUE = 103 (type=<class 'int'>)  
    STRUCTURE <svga3d\_reg.struct\_c\_\_SA\_SVGA3dRenderState object at 0x7fbbcbe0a7b8>  
    FIELD state (type=<class 'ctypes.c\_int'>, bitlen=32)  
    VALUE = 700967841 (type=<class 'int'>)  
    FIELD \_1 (type=<class 'svga3d\_reg.union\_c\_\_SA\_SVGA3dRenderState\_0'>, bitlen=32)  
    STRUCTURE <svga3d\_reg.union\_c\_\_SA\_SVGA3dRenderState\_0 object at 0x7fbbcbe0a8c8>  
    FIELD uintValue (type=<class 'ctypes.c\_uint'>, bitlen=32)  
    VALUE = 1482925787 (type=<class 'int'>)  
    FIELD floatValue (type=<class 'ctypes.c\_float'>, bitlen=32)  
    VALUE = 1001223113146368.0 (type=<class 'float'>)

4바이트씩 잘라서 MMIO로 데이터를 넘기면 FIFO buffer에 차례대로 들어간다. 이후 FIFO loop에서 커맨드 번호를 읽고, 헤더 사이즈를 읽은 뒤 헤더사이즈에 맞게 FIFO buffer에서 dequeue를 해서 커맨드를 읽는다. 읽은 커맨드는 커맨드 종류에 맞춰 기능을 수행하게 된다.

SVGA state에 따라 external command를 수행하는 경우도 있다. External command에는 reset, power off, save / load state 등이 있는데, SVGA 커맨드에 상관 없이 외부 요인에 의해 실행된다. (PCI command로 실행시킬 수도 있다. ex - /sys/bus/pci/devices/<device no.>/power)

// VBox/Devices/Graphics/DevVGA-SVGA.cpp:3529 (inside vmsvgaR3FifoLoop)
/*
 * Special mode where we only execute an external command and the go back
 * to being suspended.  Currently, all ext cmds ends up here, with the reset
 * one also being eligble for runtime execution further down as well.
 */
if (pThis->svga.fFifoExtCommandWakeup)
{
    vmsvgaR3FifoHandleExtCmd(pDevIns, pThis, pThisCC);
    while (pThread->enmState == PDMTHREADSTATE_RUNNING)
        if (pThis->svga.u8FIFOExtCommand == VMSVGA_FIFO_EXTCMD_NONE)
            PDMDevHlpSUPSemEventWaitNoResume(pDevIns, pThis->svga.hFIFORequestSem, RT_MS_1MIN);
        else
            vmsvgaR3FifoHandleExtCmd(pDevIns, pThis, pThisCC);
    return VINF_SUCCESS;
}

References

  1. VMware SVGA Device Developer Kit: https://sourceforge.net/projects/vmware-svga/ (Alternative: https://github.com/prepare/vmware-svga)
  2. Straight outta VMware: Modern exploitation of the SVGA device for guest-to-host escape exploits (Blackhat Europe 2018): https://i.blackhat.com/eu-18/Thu-Dec-6/eu-18-Sialveras-Straight-Outta-VMware-Modern-Exploitation-Of-The-SVGA-Device-For-Guest-To-Host-Escapes.pdf
  3. https://github.com/renorobert/virtualbox-vmsvga-bugs

'보안 > Bug Hunting' 카테고리의 다른 글

QEMU USB Analysis and Fuzzing  (0) 2021.01.19

+ Recent posts