소개

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