By: unvariant

Tags: Pwn AmateursCTF-2023

Problem Description:


Reveal Hints None

Provided files

  • chal
  • Dockerfile


When performing allocations with malloc, any allocation greater than MMAP_THRESHOLD_MIN, which is set to 128kb by default, malloc will use mmap instead of its internal heap.


#define DEFAULT_MMAP_THRESHOLD_MIN (128 * 1024)

I do not remember why, but the second mmap will always be exactly below the libc in memory. This means that if was can control the size field of a mmapped chunk, we can unmap part of the libc.


static void
munmap_chunk (mchunkptr p)
  size_t pagesize = GLRO (dl_pagesize);
  INTERNAL_SIZE_T size = chunksize (p);

  assert (chunk_is_mmapped (p));

  uintptr_t mem = (uintptr_t) chunk2mem (p);
  uintptr_t block = (uintptr_t) p - prev_size (p);
  size_t total_size = prev_size (p) + size;
  /* Unfortunately we have to do the compilers job by hand here.  Normally
     we would test BLOCK and TOTAL-SIZE separately for compliance with the
     page size.  But gcc does not recognize the optimization possibility
     (in the moment at least) so we combine the two values into one before
     the bit test.  */
  if (__glibc_unlikely ((block | total_size) & (pagesize - 1)) != 0
      || __glibc_unlikely (!powerof2 (mem & (pagesize - 1))))
    malloc_printerr ("munmap_chunk(): invalid pointer");

  atomic_decrement (&mp_.n_mmaps);
  atomic_add (&mp_.mmapped_mem, -total_size);

  /* If munmap failed the process virtual memory address space is in a
     bad shape.  Just leave the block hanging around, the process will
     terminate shortly anyway since not much can be done.  */
  __munmap ((char *) block, total_size);

If we inspect the section layout of the libc in memory:

$ readelf -l libc.so.6

Elf file type is DYN (Shared object file)
Entry point 0x29f50
There are 14 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000310 0x0000000000000310  R      0x8
  INTERP         0x00000000001e3e30 0x00000000001e3e30 0x00000000001e3e30
                 0x000000000000001c 0x000000000000001c  R      0x10
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000027fe0 0x0000000000027fe0  R      0x1000
  LOAD           0x0000000000028000 0x0000000000028000 0x0000000000028000
                 0x00000000001944c1 0x00000000001944c1  R E    0x1000
  LOAD           0x00000000001bd000 0x00000000001bd000 0x00000000001bd000
                 0x00000000000578cc 0x00000000000578cc  R      0x1000
  LOAD           0x00000000002148f0 0x00000000002158f0 0x00000000002158f0
                 0x0000000000004f98 0x0000000000012560  RW     0x1000
  DYNAMIC        0x0000000000217bc0 0x0000000000218bc0 0x0000000000218bc0
                 0x00000000000001d0 0x00000000000001d0  RW     0x8
  NOTE           0x0000000000000350 0x0000000000000350 0x0000000000000350
                 0x0000000000000030 0x0000000000000030  R      0x8
  NOTE           0x0000000000000380 0x0000000000000380 0x0000000000000380
                 0x0000000000000044 0x0000000000000044  R      0x4
  TLS            0x00000000002148f0 0x00000000002158f0 0x00000000002158f0
                 0x0000000000000010 0x0000000000000090  R      0x8
  GNU_PROPERTY   0x0000000000000350 0x0000000000000350 0x0000000000000350
                 0x0000000000000030 0x0000000000000030  R      0x8
  GNU_EH_FRAME   0x00000000001e3e4c 0x00000000001e3e4c 0x00000000001e3e4c
                 0x00000000000070cc 0x00000000000070cc  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x00000000002148f0 0x00000000002158f0 0x00000000002158f0
                 0x0000000000003710 0x0000000000003710  R      0x1

Section to Segment mapping:
  Segment Sections...
   01     .interp 
   02     .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_d .gnu.version_r .rela.dyn .rela.plt 
   03     .plt .plt.got .plt.sec .text __libc_freeres_fn 
   04     .rodata .stapsdt.base .interp .eh_frame_hdr .eh_frame .gcc_except_table .hash 
   05     .tdata .init_array __libc_subfreeres __libc_atexit __libc_IO_vtables .data.rel.ro .dynamic .got .got.plt .data .bss 
   06     .dynamic 
   07     .note.gnu.property 
   08     .note.gnu.build-id .note.ABI-tag 
   09     .tdata .tbss 
   10     .note.gnu.property 
   11     .eh_frame_hdr 
   13     .tdata .init_array __libc_subfreeres __libc_atexit __libc_IO_vtables .data.rel.ro .dynamic .got

We can see that the .dynsym and .dynstr sections are located underneath the .text section. The .dynsym and .dynstr are used in lazy symbol resolution. If a external function called fgets is called in a binary compiled with lazy linking (indicated by PARTIAL RELRO), the linker will search shared libraries for a symbol defined with the name fgets and use the symbol information to retrieve the function address.

After unmapping part of the libc, we can perform another mmap to remap the lower part of the libc with data that we control. We can exploit this to provide malicious values for symbols in the libc to hijack lazy symbol resolution.


Leak flag by corrupting size

This unintended is a combination of three things:

  1. flag is allocated on the heap
  2. size field of chunks can be modified
  3. the input function does not perform null termination

One can abuse this to modify the size of a chunk to overlap with a flag chunk, then allocate that chunk in the third check and print out the flag in the error message.