전 분석글에서 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을 실패시키면 된다.