(1e4000 EP가 클리어라고 하는 사람들도 있겠지만 본인은 도전과제를 다 깬 시점에서 클리어라고 생각한다)
게임을 열심히 하면서 느낀 것은 다른 키우기류 게임들과 다르게 게임 디자인이 깔끔하다는 것이다. 게임에 대한 어느정도의 이해도를 요구하긴 하지만 엄청 높은 이해도를 요구하는 것도 아니고, 환생 시스템과 비스무리한건 있지만 끔찍하게 반복적인 것을 요구하는 것이 아니며, 지루하려고 하면 새로운 컨텐츠를 소개하는 완급조절이 잘 되어 있어서 게임을 재밌게 할 수 있었다. 모바일 게임치고 광고를 많이 띄우거나 요구하는 것도 아니고... 쿠키 클리커나 페이퍼클립 게임과 비슷한 류의 게임이지만 그 둘보다는 훨씬 재미있는 게임이라고 생각하면 될 것 같다.
깨는데 26일정도 걸린 것 같은데 처음 시작해서 클리어까지는 보통이라면 훨씬 더 오래 걸리는 것이 정상일 것이다. 본인의 경우에는 거의 여기에 매달린 것 같아서... 아무튼 재밌게 플레이했다.
요새 커널 공부를 열심히 하고 있다. procfs님 자료보면서 열심히 삽질하며 공부하고 있고, 나름대로 공부가 잘 진행되어 동아리 세미나도 2번 진행할 수 있었다.
정말 기초적인 내용이었던 LKM과 같은 내용은 건너뛸 수 있었지만 바로 return-to-user부터 모르는 내용이 나와 좀 막혔다. Return to user, SMEP, KPTI까지는 공부를 했는데 KASLR부터 이것저것 찾아보니 혼돈이 왔다. 버전마다 구현이 다르다, 이게 맞다, 등, 어... 뭐가 맞는거지? 하는 생각밖에 안 들었다. 처음에 환경세팅할 때 디버깅 세팅을 안해서 망했다.
디버깅 세팅을 하려고 열심히 buildroot 설정 건드려보고 디버거 바꿔보고 삽질을 했다. 근데 아무리 해도 vmlinux는 심볼을 어딘가에 버리고 오고, 디버거는 이번 기회에 pwndbg를 쓰게 되었지만 별 효과는 없었다. 하루 종일 삽질했는데, 결국 멘탈이 나가 워게임을 풀러가 돌아오지 못했다.
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.
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.
Textures are bound to two surfaces using SVGA_3D_CMD_SURFACE_STRETCHBLT command. Two surfaces are each sent as source and destination surfaces.
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:
Mipmap size is not zero so the clip box is not considered as empty in DMA command.
cbSurfacePitch (derived from cBlocksX, cBlocksY, ...) is zero to trigger divide by zero.
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)
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.
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.
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.
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.
An invalid surface, which has a non-zero mipmap size and zero cBlocks* and cbSurfacePitch, can be defined.
A texture can be bound to an invalid surface.
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.
Deallocate the surface when defining a surface fails.
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.
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)
요새 꾸준히 하던 것들이 손에 안 잡힌다는 느낌이 크다. 학부 생활을 3년 반 동안 하면서 좌로 구르고 우로 구르고 쉬지 않고 완전 갈릴대로 갈려서 지친게 큰 것 같다. 학교나 과 특성상 학생들을 미친듯이 갈아버리기 때문에 (한 학기에 과제가 15개? 20개? 그 정도 나오는데 하나하나가 미친 난이도를 자랑한다) 이를 따라가다가 많이 지쳤다. 학부 3학년 때 하려고 마음 먹었던 BoB를 졸업을 한 학기 놔두고 하게 되어 좀 쉬는 시간을 가지게 되었지만 그래도 지친건 지친 것 같다. (참고로 저 때는 무슨 과제를 하다가 원서를 못 냈다) (다른 사람들에게 BoB는 과제 많이 내고 교육생들 굴리는 프로그램으로 느껴지는 것 같지만 지금까지 갈릴대로 갈린 나에게는 거의 쉬는 기간이었다. 그렇다고 과제를 성실하게 낸건 아니지만 이는 나중에 올릴 다른 이야기에서...)
요새 CTF도 할 의욕이 잘 안 나고 코드도 잘 안 읽힌다. 동아리에서 대회 하자고 해도 할 의욕이 안 나고 BoB에서 하는 과제나 프로젝트도 손에 잘 안 잡혔다. (그래도 할 일을 하고 없는 의욕 전부 짜내서 했지만 적극적으로 뭔갈 하진 못한 것 같다. 팀원들에게 괜히 미안하다...) 프로젝트가 끝난 지금도 버박 코드 열어놓고 보거나 write-up들을 깨작깨작 보면서 공부하고 있긴 한데 이건 단순히 습관에 의한 것일 뿐 본인이 뭔가 적극적으로 뭔가를 하고 있다는 느낌이 없다.
그래도 요즘 조금씩 회복되는 느낌이 있어 그나마 나은 것 같다. 계속 미뤄뒀던 커널 공부도 다시 하려 하고, 동아리 세미나도 되도록이면 많이 하려 한다. 대학원도 좀 준비해야 될 것 같고, 남겨둔 일들이 많다. 조급해하지 않고 차근차근 이들을 해나가려 한다.