
[CVE-2017-10235] Описание и PoC переполнения буфера устройства E1000 в VirtualBox
В следующем документе описывается ошибка, обнаруженная в VirtualBox v5.1.22 (исправлена в v5.1.24) в компоненте эмуляции гостевого устройства DevE1000 (Эмуляция контроллера Ethernet Intel 82540EM), в функции e1kFallbackAddToFrame, что приводит к переполнению буфера на хосте, когда гостевая ОС находится под контролем злоумышленника.
Ошибка была признана Oracle в CPU за июль 2017 года с присвоенным CVE-2017-10235.
Уязвимость была подтверждена как на хосте с Linux (Ubuntu 16.04), так и на хосте с Windows (v8.1) при запуске гостевой Linux (также Ubuntu 16.04), но уязвимость может быть воспроизведена во многих различных комбинациях хоста и гостя. Во всех сценариях предполагается конфигурация сети по умолчанию: только один сетевой адаптер подключенный к NAT типа Intel PRO/1000 MT Desktop (82540EM).
Поскольку управляющие структуры (включая указатели на функции) могут быть перезаписаны данными, контролируемыми злоумышленником, можно с уверенностью предположить, что во многих сценариях может быть достигнуто удаленное выполнение кода. Oracle присвоила этой ошибке низкий балл CVSS, посчитав, что она имеет риск конфиденциальности None и целостности Low, что, по нашему мнению, не отражает полный потенциал компрометации этой ошибки (объяснение возможности RCE приведено ниже).
Код VirtualBox, реализующий эмуляцию [контроллера Ethernet Intel 82540EM][intel_manual] (в файле [src/VBox/Devices/Network/DevE1000.cpp][DevE1000_cpp]), в функции [e1kFallbackAddToFrame][e1kFallbackAddToFrame], реализует аппаратную сегментацию TCP:
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc,
bool fOnWorkerThread)
{
#ifdef VBOX_STRICT
PPDMSCATTERGATHER pTxSg = pThis->CTX_SUFF(pTxSg);
Assert(e1kGetDescType(pDesc) == E1K_DTYP_DATA);
Assert(pDesc->data.cmd.fTSE);
Assert(!e1kXmitIsGsoBuf(pTxSg));
#endif
uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN +
pThis->contextTSE.dw3.u16MSS;
Assert(u16MaxPktLen != 0);
Assert(u16MaxPktLen < E1K_MAX_TX_PKT_SIZE);
Эта функция правильно проверяет, что максимальная длина TX-пакета (u16MaxPktLen) ниже стандартного максимума в 16288 байт (E1K_MAX_TX_PKT_SIZE), но делает это в виде макроса Assert, который будет отключен в релизной сборке, фактически делая проверку бесполезной для конечного пользователя. Это можно сравнить с аналогичной функцией [e1kAddToFrame][e1kAddToFrame], которая выполняет проверку с помощью явного if вместо Assert:
static bool e1kAddToFrame(PE1KSTATE pThis, RTGCPHYS PhysAddr,
uint32_t cbFragment)
{
PPDMSCATTERGATHER pTxSg = pThis->CTX_SUFF(pTxSg);
bool const fGso = e1kXmitIsGsoBuf(pTxSg);
uint32_t const cbNewPkt = cbFragment + pThis->u16TxPktLen;
if (RT_UNLIKELY( !fGso && cbNewPkt > E1K_MAX_TX_PKT_SIZE ))
{
E1kLog(("%s Transmit packet is too large: %u > %u(max)\n",
pThis->szPrf, cbNewPkt, E1K_MAX_TX_PKT_SIZE));
return false;
}
Различие между использованием обычной функции (e1kAddToFrame) и резервной (e1kFallbackAddToFrame) определяется в e1kXmitDesc() и зависит от двух факторов: флаг TSE должен быть включен в дескрипторах данных/контекста (управляется ОС в гостевой машине) и флаг GSO должен быть выключен. Последний зависит от многих факторов, поэтому существует много способов его отключить, но самый удобный — включить режим loopback, который настраивается через регистр управления приемом (биты RCTL.LBM), также управляемый гостевой ОС.
Включение режима loopback приведет к тому, что функция [e1kXmitAllocBuf][e1kXmitAllocBuf] будет использовать буфер aTxPacketFallback (Буфер пакетов передачи, используемый для резервного режима TSE и loopback) для размещения PDM scatter/gather буфера, с упомянутой длиной 16288 байт (E1K_MAX_TX_PKT_SIZE), и сигнализировать, что GSO будет отключен (путем установки NULL в pvUser).
if (RT_LIKELY(GET_BITS(RCTL, LBM) != RCTL_LBM_TCVR))
{
...
}
else
{
/* Create a loopback using the fallback buffer and preallocated SG. */
AssertCompileMemberSize(E1KSTATE, uTxFallback.Sg, 8 * sizeof(size_t));
pSg = &pThis->uTxFallback.Sg;
pSg->fFlags = PDMSCATTERGATHER_FLAGS_MAGIC |
PDMSCATTERGATHER_FLAGS_OWNER_3;
pSg->cbUsed = 0;
pSg->cbAvailable = 0;
pSg->pvAllocator = pThis;
pSg->pvUser = NULL; /* No GSO here. */
pSg->cSegs = 1;
pSg->aSegs[0].pvSeg = pThis->aTxPacketFallback;
pSg->aSegs[0].cbSeg = sizeof(pThis->aTxPacketFallback);
}
Это приведет к тому, что вызов функции e1kXmitIsGsoBuf (внутри [e1kXmitDesc][e1kXmitDesc]) вернет False, и при включенном TSE в дескрипторе данных поток выполнения пойдет в [e1kFallbackAddToFrame][e1kFallbackAddToFrame_call] (вместо более безопасной функции e1kAddToFrame с правильной проверкой).
/*
* Add the descriptor data to the frame. If the frame is complete,
* transmit it and reset the u16TxPktLen field.
*/
if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg)))
{
...
}
else if (!pDesc->data.cmd.fTSE)
{
...
}
else
{
STAM_COUNTER_INC(&pThis->StatTxPathFallback);
rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread);
}
Внутри e1kFallbackAddToFrame, при отключенной в релизной сборке вышеупомянутой проверке, MSS может быть произвольно большим (до 64K минус HDRLEN), что позволяет передать произвольно большое DTALEN в [e1kFallbackAddSegment][e1kFallbackAddSegment_call]:
/*
* Carve out segments.
*/
int rc;
do
{
/* Calculate how many bytes we have left in this TCP segment */
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
if (cb > pDesc->data.cmd.u20DTALEN)
{
/* This descriptor fits completely into current segment */
cb = pDesc->data.cmd.u20DTALEN;
rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb,
pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
Функция [e1kFallbackAddSegment][e1kFallbackAddSegment] будет использовать это значение (теперь как аргумент u16Len) для копирования из гостевой памяти в буфер aTxPacketFallback в памяти хоста (через PDMDevHlpPhysRead) без дополнительных проверок этой длины, что вызовет переполнение буфера (емкость буфера 16288 байт при размере памяти до 64К).
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr,
uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
int rc = VINF_SUCCESS;
/* TCP header being transmitted */
struct E1kTcpHeader *pTcpHdr = (struct E1kTcpHeader *)
(pThis->aTxPacketFallback + pThis->contextTSE.tu.u8CSS);
/* IP header being transmitted */
struct E1kIpHeader *pIpHdr = (struct E1kIpHeader *)
(pThis->aTxPacketFallback + pThis->contextTSE.ip.u8CSS);
E1kLog3(("%s e1kFallbackAddSegment: Length=%x, remaining payload=%x,
header=%x, send=%RTbool\n", pThis->szPrf, u16Len,
pThis->u32PayRemain, pThis->u16HdrRemain, fSend));
Assert(pThis->u32PayRemain + pThis->u16HdrRemain > 0);
PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);