소개
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;
}
vmsvgaReadPort
나 vmsvgaWritePort
에서는 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
)에서 커맨드로 잘라서 해당하는 커맨드를 수행하게 된다. 기본적으로 커맨드 구조는 아래를 따른다.
커맨드 넘버
// 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
커맨드 헤더 (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;
커맨드 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
- VMware SVGA Device Developer Kit: https://sourceforge.net/projects/vmware-svga/ (Alternative: https://github.com/prepare/vmware-svga)
- 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
- https://github.com/renorobert/virtualbox-vmsvga-bugs
'보안 > Bug Hunting' 카테고리의 다른 글
QEMU USB Analysis and Fuzzing (0) | 2021.01.19 |
---|