Search

[Virtualbox][CVE-2020-2674] 분석

Created Time
2022/10/06
Vector
USB Adaptor
취약점
double free
Tag
1day
Virtualbox
전 분석글에서 URB 패킷 전송에 대한 대략전인 흐름을 살펴보았다. 실제 USB 장치에서 데이터의 송수신이 필요할때 호출되는 함수는 아래와 같다.
VirtualBox-6.0.12 \ src \ VBox \ Devices \ USB \ DevOHCI.cpp : 3027
static bool ohciR3ServiceTd(POHCI pThis, VUSBXFERTYPE enmType, PCOHCIED pEd, uint32_t EdAddr, uint32_t TdAddr, uint32_t *pNextTdAddr, const char *pszListName) { ... /* * Allocate and initialize a new URB. */ PVUSBURB pUrb = VUSBIRhNewUrb(pThis->RootHub.pIRhConn, pEd->hwinfo & ED_HWINFO_FUNCTION, NULL, enmType, enmDir, Buf.cbTotal, 1, NULL); if (!pUrb) return false; /* retry later... */ ... /* * Submit the URB. */ ohciR3InFlightAdd(pThis, TdAddr, pUrb); Log(("%s: ohciR3ServiceTd: submitting TdAddr=%#010x EdAddr=%#010x cbData=%#x\n", pUrb->pszDesc, TdAddr, EdAddr, pUrb->cbData)); ohciR3Unlock(pThis); int rc = VUSBIRhSubmitUrb(pThis->RootHub.pIRhConn, pUrb, &pThis->RootHub.Led); ohciR3Lock(pThis); if (RT_SUCCESS(rc)) return true; ... }
C++
복사
전송할 데이터를 위해 URB를 만든다고 했다. VUSBIRhNewUrb() 함수에서 실제 그 동작이 수행된다. 그리고 VUSBIRhSubmitUrb() 함수에서 생성한 URB를 이제 제출한다.
VUSBIRhSubmitUrb() 함수는 실제 아래와 같이 구현되어있고
VirtualBox-6.0.12 \ include \ VBox \ vusb.h : 884
DECLINLINE ( int ) VUSBIRhSubmitUrb (PVUSBIROOTHUBCONNECTOR pInterface, PVUSBURB pUrb, struct PDMLED * pLed) { return pInterface-> pfnSubmitUrb (pInterface, pUrb, pLed); }
C++
복사
pfnSubmitUrb는 함수포인터로 다음과 같이 설정되어있다
VirtualBox-6.0.12 \ src \ VBox \ Devices \ USB \ DrvVUSBRootHub.cpp : 1339
pThis-> IRhConnector.pfnSubmitUrb = vusbRhSubmitUrb;
C++
복사
VirtualBox-6.0.12 \ src \ VBox \ Devices \ USB \ DrvVUSBRootHub.cpp : 661
static DECLCALLBACK(int) vusbRhSubmitUrb(PVUSBIROOTHUBCONNECTOR pInterface, PVUSBURB pUrb, PPDMLED pLed) { ... /* * The device was resolved when we allocated the URB. * Submit it to the device if we found it, if not fail with device-not-ready. */ int rc; if ( pUrb->pVUsb->pDev && pUrb->pVUsb->pDev->pUsbIns) { switch (pUrb->enmDir) // 디바이스로 나가는건지, 들어오는건지 판단 { case VUSBDIRECTION_IN: pLed->Asserted.s.fReading = pLed->Actual.s.fReading = 1; rc = vusbUrbSubmit(pUrb); pLed->Actual.s.fReading = 0; break; case VUSBDIRECTION_OUT: pLed->Asserted.s.fWriting = pLed->Actual.s.fWriting = 1; rc = vusbUrbSubmit(pUrb); pLed->Actual.s.fWriting = 0; break; default: rc = vusbUrbSubmit(pUrb); break; } if (RT_FAILURE(rc)) { LogFlow(("vusbRhSubmitUrb: freeing pUrb=%p\n", pUrb)); pUrb->pVUsb->pfnFree(pUrb); } } ... return rc; }
C++
복사
pUrb→enmDir 값에 따라서 디바이스로 나가는건지, 들어오는건지 판단이된다. 만약 vusbUrbSubmit() 함수가 submit 실패에 따른 값을 반환하면, if (RT_FAILURE(rc)) 요 조건문이 참이되어, pfnFree() 함수가 호출된다. 그리고 vusbRhSubmitUrb() 가 끝나고 다시 ohciR3ServiceTd() 로 돌아온다
static bool ohciR3ServiceTd(POHCI pThis, VUSBXFERTYPE enmType, PCOHCIED pEd, uint32_t EdAddr, uint32_t TdAddr, uint32_t *pNextTdAddr, const char *pszListName) { ... int rc = VUSBIRhSubmitUrb(pThis->RootHub.pIRhConn, pUrb, &pThis->RootHub.Led); ohciR3Lock(pThis); if (RT_SUCCESS(rc)) return true; /* Failure cleanup. Can happen if we're still resetting the device or out of resources. */ Log(("ohciR3ServiceTd: failed submitting TdAddr=%#010x EdAddr=%#010x pUrb=%p!!\n", TdAddr, EdAddr, pUrb)); VUSBIRhFreeUrb(pThis->RootHub.pIRhConn, pUrb); ohciR3InFlightRemove(pThis, TdAddr); return false; }
C++
복사
만약 rc가 failed 라면, 한번더 free를 한다.(VUSBIRhFreeUrb)
VirtualBox-6.0.12\include\VBox\vusb.h:2956
DECLINLINE(int) VUSBIRhFreeUrb(PVUSBIROOTHUBCONNECTOR pInterface, PVUSBURB pUrb) { return pInterface->pfnFreeUrb(pInterface, pUrb); }
C++
복사
VirtualBox-6.0.12\src\VBox\Devices\USB\DrvVUSBRootHub.cpp:1338
pThis->IRhConnector.pfnFreeUrb = vusbRhConnFreeUrb;
C++
복사
VirtualBox-6.0.12\src\VBox\Devices\USB\DrvVUSBRootHub.cpp:652
static DECLCALLBACK(int) vusbRhConnFreeUrb(PVUSBIROOTHUBCONNECTOR pInterface, PVUSBURB pUrb) { RT_NOREF(pInterface); pUrb->pVUsb->pfnFree(pUrb); return VINF_SUCCESS; }
C++
복사
따라서 전송에 실패한 URB가 두번 free된다. 결국 double free 취약점이 존재한다는 걸 의미함.
그럼 이걸 트리거 시켜야한다. 방법은 간단히 다음과 같다.
1.
게스트 OS가 부팅 될 때 HcControl 레지스터 값을 설정하여 OHCI 상태를 OHCI_USB_OPERATIONAL로 전환한다.
2.
OHCI_USB_OPERATIONAL 상태에선, ohciR3BusStart () 함수는 period 프레임 처리 스레드를 시작하고 스레드 worker는 프레임 period마다 프레임 처리 루틴을 시작한다. 프레임 처리 루틴은 다음과 같다
VirtualBox-6.0.12\src\VBox\Devices\USB\DevOHCI.cpp:4078
static void ohciR3StartOfFrame(POHCI pThis) { # ifdef LOG_ENABLED const uint32_t status_old = pThis->status; # endif /* * Update HcFmRemaining.FRT and update start of frame time. */ pThis->frt = pThis->fit; pThis->SofTime += pThis->cTicksPerFrame; /* * Check that the HCCA address isn't bogus. Linux 2.4.x is known to start * the bus with a hcca of 0 to work around problem with a specific controller. */ bool fValidHCCA = !( pThis->hcca >= OHCI_HCCA_MASK || pThis->hcca < ~OHCI_HCCA_MASK); # if 1 /* * Update the HCCA. * Should be done after SOF but before HC read first ED in this frame. */ if (fValidHCCA) ohciR3UpdateHCCA(pThis); # endif /* "After writing to HCCA, HC will set SF in HcInterruptStatus" - guest isn't executing, so ignore the order! */ ohciR3SetInterrupt(pThis, OHCI_INTR_START_OF_FRAME); if (pThis->fno) { ohciR3SetInterrupt(pThis, OHCI_INTR_FRAMENUMBER_OVERFLOW); pThis->fno = 0; } /* If the HCCA address is invalid, we're quitting here to avoid doing something which cannot be reported to the HCD. */ if (!fValidHCCA) { Log(("ohciR3StartOfFrame: skipping hcca part because hcca=%RX32 (our 'valid' range: %RX32-%RX32)\n", pThis->hcca, ~OHCI_HCCA_MASK, OHCI_HCCA_MASK)); return; } /* * Periodic EPs. */ if (pThis->ctl & OHCI_CTL_PLE) ohciR3ServicePeriodicList(pThis); /* * Control EPs. */ if ( (pThis->ctl & OHCI_CTL_CLE) && (pThis->status & OHCI_STATUS_CLF) ) ohciR3ServiceCtrlList(pThis); /* * Bulk EPs. */ if ( (pThis->ctl & OHCI_CTL_BLE) && (pThis->status & OHCI_STATUS_BLF)) ohciR3ServiceBulkList(pThis); else if ((pThis->status & OHCI_STATUS_BLF) && pThis->fBulkNeedsCleaning) ohciR3UndoBulkList(pThis); /* If list disabled but not empty, abort endpoints. */ ... }
C++
복사
ohciR3ServicePeriodicList(), ohciR3ServiceCtrlList(), ohciR3ServiceBulkList() 함수는 결국 아까처럼 URB 전송을 위해 VUSBIRhSubmitUrb() 함수를 호출한다.
VUSBIRhSubmitUrb() 내부에서 실제 vusbUrbSubmit() 함수가 호출되는데, 그 안을 보면
VirtualBox-6.0.12\src\VBox\Devices\USB\VUSBUrb.cpp:1091
int vusbUrbSubmit(PVUSBURB pUrb) { ... /* * Check that the device is in a valid state. */ const VUSBDEVICESTATE enmState = vusbDevGetState(pDev); if (enmState == VUSB_DEVICE_STATE_RESET) { LogRel(("VUSB: %s: power off ignored, the device is resetting!\n", pDev->pUsbIns->pszName)); pUrb->enmStatus = VUSBSTATUS_DNR; /* This will postpone the TDs until we're done with the resetting. */ return VERR_VUSB_DEVICE_IS_RESETTING; } ... if (pUrb->EndPt >= VUSB_PIPE_MAX) { Log(("%s: pDev=%p[%s]: SUBMIT: ep %i >= %i!!!\n", pUrb->pszDesc, pDev, pDev->pUsbIns->pszName, pUrb->EndPt, VUSB_PIPE_MAX)); return vusbUrbSubmitHardError(pUrb); } ... if (!pEndPtDesc) { Log(("%s: pDev=%p[%s]: SUBMIT: no endpoint!!! dir=%s e=%i\n", pUrb->pszDesc, pDev, pDev->pUsbIns->pszName, vusbUrbDirName(pUrb->enmDir), pUrb->EndPt)); return vusbUrbSubmitHardError(pUrb); } ... pUrb->enmState = VUSBURBSTATE_IN_FLIGHT; switch (pUrb->enmType) { case VUSBXFERTYPE_CTRL: rc = vusbUrbSubmitCtrl(pUrb); break; case VUSBXFERTYPE_BULK: rc = vusbUrbSubmitBulk(pUrb); break; case VUSBXFERTYPE_INTR: rc = vusbUrbSubmitInterrupt(pUrb); break; case VUSBXFERTYPE_ISOC: rc = vusbUrbSubmitIsochronous(pUrb); break; default: AssertMsgFailed(("Unexpected pUrb type %d\n", pUrb->enmType)); return vusbUrbSubmitHardError(pUrb); } ... else if ( RT_FAILURE(rc) && !ASMAtomicReadU32(&pDev->aPipes[pUrb->EndPt].async) /* && pUrb->enmType == VUSBXFERTYPE_BULK ?? */ && !vusbUrbErrorRh(pUrb)) { /* don't retry it anymore. */ pUrb->enmState = VUSBURBSTATE_REAPED; pUrb->enmStatus = VUSBSTATUS_CRC; vusbUrbCompletionRh(pUrb); return VINF_SUCCESS; } return rc; }
C++
복사
결국 최종적으로 double free를 트리거 시키려면,switch case문에서 vusbUrbSubmitHardError() 함수쪽으로 빠지게 만들어서 submit을 실패시키면 된다.