mirror of
https://github.com/fdiskyou/Zines.git
synced 2025-03-09 00:00:00 +01:00
857 lines
34 KiB
Text
857 lines
34 KiB
Text
==Phrack Inc.==
|
|
|
|
Volume 0x0b, Issue 0x39, Phile #0x09 of 0x12
|
|
|
|
|=---------------------=[ Once upon a free()... ]=-----------------------=|
|
|
|=-----------------------------------------------------------------------=|
|
|
|=--------------=[ anonymous <d45a312a@author.phrack.org> ]=-------------=|
|
|
|
|
|
|
On the Unix system, and later in the C standard library there are functions
|
|
to handle variable amounts of memory in a dynamic way. This allows programs
|
|
to dynamically request memory blocks from the system. The operating system
|
|
only provides a very rough system call 'brk' to change the size of a big
|
|
memory chunk, which is known as the heap.
|
|
|
|
On top of this system call the malloc interface is located, which provides
|
|
a layer between the application and the system call. It can dynamically
|
|
split the large single block into smaller chunks, free those chunks on
|
|
request of the application and avoid fragmentation while doing so. You can
|
|
compare the malloc interface to a linear file system on a large, but
|
|
dynamically sized raw device.
|
|
|
|
There are a few design goals which have to be met by the malloc interface:
|
|
|
|
- stability
|
|
- performance
|
|
- avoidance of fragmentation
|
|
- low space overhead
|
|
|
|
There are only a few common malloc implementations. The most common ones
|
|
are the System V one, implemented by AT&T, the GNU C Library implementation
|
|
and the malloc-similar interface of the Microsoft operating systems
|
|
(RtlHeap*).
|
|
|
|
Here is a table of algorithms and which operating systems use them:
|
|
|
|
Algorithm | Operating System
|
|
------------------------+--------------------------------------------------
|
|
BSD kingsley | 4.4BSD, AIX (compatibility), Ultrix
|
|
BSD phk | BSDI, FreeBSD, OpenBSD
|
|
GNU Lib C (Doug Lea) | Hurd, Linux
|
|
System V AT&T | Solaris, IRIX
|
|
Yorktown | AIX (default)
|
|
RtlHeap* | Microsoft Windows *
|
|
------------------------+--------------------------------------------------
|
|
|
|
|
|
It is interesting to see that most of the malloc implementations are very
|
|
easy to port and that they are architecture independent. Most of those
|
|
implementations just build an interface with the 'brk' system call. You can
|
|
change this behaviour with a #define. All of the implementations I have
|
|
come across are written in ANSI C and just do very minimal or even no
|
|
sanity checking. Most of them have a special compilation define that
|
|
includes asserts and extra checks. Those are turned off by default in the
|
|
final build for performance reasons. Some of the implementations also
|
|
offer extra reliability checks that will detect buffer overflows. Those
|
|
are made to detect overflows while development, not to stop exploitation
|
|
in the final release.
|
|
|
|
|
|
Storing management info in-band
|
|
|
|
Most malloc implementations share the behaviour of storing their own
|
|
management information, such as lists of used or free blocks, sizes of
|
|
memory blocks and other useful data within the heap space itself. Since the
|
|
whole idea of malloc/free is based on the dynamic requirements the
|
|
application has, the management info itself occupies a variable amount of
|
|
data too. Because of this, the implementation can seldomly just reserve a
|
|
certain amount of memory for its own purposes, but stores the management
|
|
information "in-band", right after and before the blocks of memory that are
|
|
used by the application.
|
|
|
|
Some applications do request a block of memory using the malloc interface,
|
|
which later happens to be vulnerable to a buffer overflow. This way, the
|
|
data behind the chunk can be changed. Possibly the malloc management
|
|
structures can be compromised. This has been demonstrated first by Solar
|
|
Designer's wizard-like exploit [1].
|
|
|
|
The central attack of exploiting malloc allocated buffer overflows is to
|
|
modify this management information in a way that will allow arbitrary
|
|
memory overwrites afterwards. This way pointers can be overwritten within
|
|
the writeable process memory, hence allowing modification of return
|
|
addresses, linkage tables or application level data.
|
|
|
|
To mount such an attack, we have to take a deep look within the internal
|
|
workings of the implementation we want to exploit. This article discusses
|
|
the commonly used GNU C Library and the System V implementation and how to
|
|
gain control over a process using buffer overflows which occur in malloced
|
|
buffers under Linux, Solaris and IRIX systems.
|
|
|
|
|
|
System V malloc implementation
|
|
==============================
|
|
|
|
IRIX and Solaris use an implementation which is based on self-adjusting
|
|
binary trees. The theoretical background of this implementation has been
|
|
described in [2].
|
|
|
|
The basic idea of this implementation is to keep lists of equally sized
|
|
malloc chunks within a binary tree. If you allocate two chunks of the
|
|
same size, they will be within the same node and within the same list of this
|
|
node. The tree is ordered by the size of its elements.
|
|
|
|
|
|
The TREE structure
|
|
|
|
The definition of the TREE structure can be found in the mallint.h, along
|
|
with some easy-to-use macros to access its elements. The mallint.h file
|
|
can be found in the source distribution of the Solaris operating system
|
|
[4]. Although I cannot verify that IRIX is based on the same source, there
|
|
are several similarities which indicated this. The malloc interface
|
|
internally creates the same memory layout and functions, besides some 64
|
|
bit alignments. You can utilize the Solaris source for your IRIX exploits,
|
|
too.
|
|
|
|
To allow each tree element to be used for a different purpose to avoid
|
|
overhead and force an alignment, each TREE structure element is defined
|
|
as a union:
|
|
|
|
|
|
/* the proto-word; size must be ALIGN bytes */
|
|
typedef union _w_ {
|
|
size_t w_i; /* an unsigned int */
|
|
struct _t_ *w_p; /* a pointer */
|
|
char w_a[ALIGN]; /* to force size */
|
|
} WORD;
|
|
|
|
|
|
Central TREE structure definition:
|
|
|
|
/* structure of a node in the free tree */
|
|
typedef struct _t_ {
|
|
WORD t_s; /* size of this element */
|
|
WORD t_p; /* parent node */
|
|
WORD t_l; /* left child */
|
|
WORD t_r; /* right child */
|
|
WORD t_n; /* next in link list */
|
|
WORD t_d; /* dummy to reserve space for self-pointer */
|
|
} TREE;
|
|
|
|
|
|
The 't_s' element of the chunk header contains the rounded up value of the
|
|
size the user requested when he called malloc. Since this size is always
|
|
rounded up to a word boundary, at least the lower two bits of the 't_s'
|
|
elements are unused - they normally would have the value of zero all the
|
|
time. Instead of being zero, they are ignored for all size-related
|
|
operations. They are used as flag elements.
|
|
|
|
From the malloc.c source it reads:
|
|
|
|
BIT0: 1 for busy (block is in use), 0 for free.
|
|
|
|
BIT1: if the block is busy, this bit is 1 if the preceding block in
|
|
contiguous memory is free. Otherwise, it is always 0.
|
|
|
|
|
|
TREE Access macros:
|
|
|
|
/* usable # of bytes in the block */
|
|
#define SIZE(b) (((b)->t_s).w_i)
|
|
|
|
/* free tree pointers */
|
|
#define PARENT(b) (((b)->t_p).w_p)
|
|
#define LEFT(b) (((b)->t_l).w_p)
|
|
#define RIGHT(b) (((b)->t_r).w_p)
|
|
|
|
/* forward link in lists of small blocks */
|
|
#define AFTER(b) (((b)->t_p).w_p)
|
|
|
|
/* forward and backward links for lists in the tree */
|
|
#define LINKFOR(b) (((b)->t_n).w_p)
|
|
#define LINKBAK(b) (((b)->t_p).w_p)
|
|
|
|
|
|
For all allocation operations a certain alignment and minimum size is
|
|
enforced, which is defined here:
|
|
|
|
#define WORDSIZE (sizeof (WORD))
|
|
#define MINSIZE (sizeof (TREE) - sizeof (WORD))
|
|
#define ROUND(s) if (s % WORDSIZE) s += (WORDSIZE - (s % WORDSIZE))
|
|
|
|
|
|
The tree structure is the central element of each allocated chunk. Normally
|
|
only the 't_s' and 't_p' elements are used, and user data is stored from
|
|
't_l' on. Once the node is freed, this changes and the data is reused to
|
|
manage the free elements more efficiently. The chunk represents an element
|
|
within the splay tree. As more chunks get freed, the malloc implementation
|
|
tries to merge the free chunks right next to it. At most FREESIZE (32 by
|
|
default) chunks can be in this dangling free state at the same time. They
|
|
are all stored within the 'flist' array. If a call to free is made while
|
|
the list is already full, the old element at this place falls out and is
|
|
forwarded to realfree. The place is then occupied by the newly freed
|
|
element.
|
|
|
|
This is done to speed up and avoid defragmentation in cases where a lot of
|
|
calls to free are made in a row. The real merging process is done by
|
|
realfree. It inserts the chunk into the central tree, which starts at the
|
|
'Root' pointer. The tree is ordered by the size of its elements and
|
|
is self-balancing. It is a so called "splay tree", in which the elements
|
|
cycle in a special way to speed up searches (see google.com "splay tree"
|
|
for further information). This is not much of importance here, but keep in
|
|
mind that there are two stages of free chunks: one being within the flist
|
|
array, and one within the free-elements tree starting at 'Root'.
|
|
|
|
There are some special management routines for allocating small chunks of
|
|
memory, which happen to have a size below 40 bytes. Those are not
|
|
considered here, but the basic idea is to have extra lists of them, not
|
|
keeping them within a tree but in lists, one for each WORD matching size
|
|
below 40.
|
|
|
|
There is more than one way to exploit a malloc based buffer overflow,
|
|
however here is one method which works against both, IRIX and Solaris.
|
|
|
|
As a chunk is realfree'd, it is checked whether the neighbor-chunks are
|
|
already within the realfree'd tree. If it is the case, the only thing
|
|
that has to be done is to logically merge the two chunks and reorder its
|
|
position within the tree, as the size has changed.
|
|
|
|
This merging process involves pointer modification within the tree, which
|
|
consists of nodes. These nodes are represented by the chunk header
|
|
itself. Pointers to other tree elements are stored there. If we can
|
|
overwrite them, we can possibly modify the operation when merging the
|
|
chunks.
|
|
|
|
Here is, how it is done in malloc.c:
|
|
(modified to show the interesting part of it)
|
|
|
|
static void
|
|
realfree(void *old)
|
|
{
|
|
TREE *tp, *sp, *np;
|
|
size_t ts, size;
|
|
|
|
/* pointer to the block */
|
|
tp = BLOCK(old);
|
|
ts = SIZE(tp);
|
|
if (!ISBIT0(ts))
|
|
return;
|
|
CLRBITS01(SIZE(tp));
|
|
|
|
/* see if coalescing with next block is warranted */
|
|
np = NEXT(tp);
|
|
if (!ISBIT0(SIZE(np))) {
|
|
if (np != Bottom)
|
|
t_delete(np);
|
|
SIZE(tp) += SIZE(np) + WORDSIZE;
|
|
}
|
|
|
|
We remember NEXT points to the chunk directly following the current one. So
|
|
we have this memory layout:
|
|
|
|
tp old np
|
|
| | |
|
|
[chunk A header] [chunk A data] | [chunk B or free ....]
|
|
|
|
|
chunk boundary
|
|
|
|
In the usual situation the application has allocated some space and got a
|
|
pointer (old) from malloc. It then messes up and allows a buffer overflow
|
|
of the chunk data. We cross the chunk boundary by overflowing and hit the
|
|
data behind, which is either free space or another used chunk.
|
|
|
|
np = NEXT(tp);
|
|
|
|
Since we can only overflow data behind 'old', we cannot modify the header
|
|
of our own chunk. Therefore we cannot influence the 'np' pointer in any
|
|
way. It always points to the chunk boundary.
|
|
|
|
Now a check is made to test if it is possible to merge forward, that is our
|
|
chunk and the chunk behind it. Remember that we can control the chunk
|
|
to the right of us.
|
|
|
|
if (!ISBIT0(SIZE(np))) {
|
|
if (np != Bottom)
|
|
t_delete(np);
|
|
SIZE(tp) += SIZE(np) + WORDSIZE;
|
|
}
|
|
|
|
BIT0 is zero if the chunk is free and within the free elements tree. So if
|
|
it is free and not the last chunk, the special 'Bottom' chunk, it is
|
|
deleted from the tree. Then the sizes of both chunks are added and later in
|
|
the code of the realfree function the whole resized chunk is reinserted
|
|
into the tree.
|
|
|
|
One important part is that the overflowed chunk must not be the last chunk
|
|
within the malloc space, condition:
|
|
|
|
1. Overflowed chunk must not be the last chunk
|
|
|
|
Here is how the 't_delete' function works:
|
|
|
|
static void
|
|
t_delete(TREE *op)
|
|
{
|
|
TREE *tp, *sp, *gp;
|
|
|
|
/* if this is a non-tree node */
|
|
if (ISNOTREE(op)) {
|
|
tp = LINKBAK(op);
|
|
if ((sp = LINKFOR(op)) != NULL)
|
|
LINKBAK(sp) = tp;
|
|
LINKFOR(tp) = sp;
|
|
return;
|
|
}
|
|
|
|
There are other cases, but this is the one easiest to exploit. As I am
|
|
already tired of this, I will just explain this one here. The others are
|
|
very similar (look at malloc.c).
|
|
|
|
ISNOTREE compares the 't_l' element of the TREE structure with -1. -1 is
|
|
the special marker for non-tree nodes, which are used as doubly linked list,
|
|
but that does not matter.
|
|
|
|
Anyway, this is the first condition we have to obey:
|
|
|
|
2. fake->t_l = -1;
|
|
|
|
Now the unlinking between FOR (t_n) and BAK (t_p) is done, which can be
|
|
rewritten as:
|
|
|
|
t1 = fake->t_p
|
|
t2 = fake->t_n
|
|
t2->t_p = t1
|
|
t1->t_n = t2
|
|
|
|
Which is (written in pseudo-raw-assignments which happen at the same time):
|
|
|
|
[t_n + (1 * sizeof (WORD))] = t_p
|
|
[t_p + (4 * sizeof (WORD))] = t_n
|
|
|
|
This way we can write to arbitrary addresses together with valid
|
|
addresses at the same time. We choose to use this:
|
|
|
|
t_p = retloc - 4 * sizeof (WORD)
|
|
t_n = retaddr
|
|
|
|
This way retloc will be overwritten with retaddr and *(retaddr + 8) will be
|
|
overwritten with retloc. If there is code at retaddr, there should be a
|
|
small jump over the bytes 8-11 to not execute this address as code. Also,
|
|
the addresses can be swapped if that fits the situation better.
|
|
|
|
Finally our overwrite buffer looks like this:
|
|
|
|
| <t_s> <t_p> <t_l> <j: t_r> <t_n> <j: t_d>
|
|
|
|
|
chunk boundary
|
|
|
|
Where: t_s = some small size with lower two bits zeroed out
|
|
t_p = retloc - 4 * sizeof (WORD)
|
|
t_l = -1
|
|
t_r = junk
|
|
t_n = retaddr
|
|
t_d = junk
|
|
|
|
Note that although all of the data is stored as 32 bit pointers, each
|
|
structure element occupies eight bytes. This is because of the WORD
|
|
union, which forces at least ALIGN bytes to be used for each element.
|
|
ALIGN is defined to eight by default.
|
|
|
|
So a real overflow buffer behind the chunk boundary might look like:
|
|
|
|
ff ff ff f0 41 41 41 41 ef ff fc e0 41 41 41 41 | ....AAAA....AAAA
|
|
ff ff ff ff 41 41 41 41 41 41 41 41 41 41 41 41 | ....AAAAAAAAAAAA
|
|
ef ff fc a8 41 41 41 41 41 41 41 41 41 41 41 41 | ....AAAAAAAAAAAA
|
|
|
|
All 'A' characters can be set arbitrarily. The 't_s' element has been
|
|
replaced with a small negative number to avoid NUL bytes. If you want to use
|
|
NUL bytes, use very few. Otherwise the realfree function will crash later.
|
|
|
|
The buffer above will overwrite:
|
|
|
|
[0xeffffce0 + 32] = 0xeffffca8
|
|
[0xeffffca8 + 8] = 0xeffffce0
|
|
|
|
See the example code (mxp.c) for a more in-depth explanation.
|
|
|
|
To summarize down the guts if you happen to exploit a malloc based buffer
|
|
overflow on IRIX or Solaris:
|
|
|
|
1. Create a fake chunk behind the one you overflow
|
|
2. The fake chunk is merged with the one you overflow as it is
|
|
passed to realfree
|
|
3. To make it pass to realfree it has to call malloc() again or
|
|
there have to be a lot of successive free() calls
|
|
4. The overflowed chunk must not be the last chunk (the one before
|
|
Bottom)
|
|
5. Prepend the shellcode/nop-space with jump-aheads to not execute
|
|
the unavoidable unlink-overwrite address as code
|
|
6. Using the t_splay routines attacks like this are possible too, so
|
|
if you cannot use the attack described here (say you cannot
|
|
write 0xff bytes), use the source luke.
|
|
|
|
|
|
There are a lot of other ways to exploit System V malloc management, way
|
|
more than there are available in the GNU implementation. This is a result
|
|
of the dynamic tree structure, which also makes it difficult to understand
|
|
sometimes. If you have read until here, I am sure you can find your own
|
|
ways to exploit malloc based buffer overflows.
|
|
|
|
|
|
GNU C Library implementation
|
|
============================
|
|
|
|
The GNU C library keeps the information about the memory slices the
|
|
application requests in so called 'chunks'. They look like this (adapted
|
|
from malloc.c):
|
|
|
|
+----------------------------------+
|
|
chunk -> | prev_size |
|
|
+----------------------------------+
|
|
| size |
|
|
+----------------------------------+
|
|
mem -> | data |
|
|
: ... :
|
|
+----------------------------------+
|
|
nextchunk -> | prev_size ... |
|
|
: :
|
|
|
|
Where mem is the pointer you get as return value from malloc(). So if you
|
|
do a:
|
|
|
|
unsigned char * mem = malloc (16);
|
|
|
|
Then 'mem' is equal to the pointer in the figure, and (mem - 8) would be
|
|
equal to the 'chunk' pointer.
|
|
|
|
The 'prev_size' element has a special function: If the chunk before the
|
|
current one is unused (it was free'd), it contains the length of the chunk
|
|
before. In the other case - the chunk before the current one is used -
|
|
'prev_size' is part of the 'data' of it, saving four bytes.
|
|
|
|
The 'size' field has a special meaning. As you would expect, it contains
|
|
the length of the current block of memory, the data section. As you call
|
|
malloc(), four is added to the size you pass to it and afterwards the size
|
|
is padded up to the next double-word boundary. So a malloc(7) will become a
|
|
malloc(16), and a malloc(20) will become malloc(32). For malloc(0) it will
|
|
be padded to malloc(8). The reason for this behaviour will be explained in
|
|
the latter.
|
|
|
|
Since this padding implies that the lower three bits are always zero and
|
|
are not used for real length, they are used another way. They are used to
|
|
indicate special attributes of the chunk. The lowest bit, called
|
|
PREV_INUSE, indicates whether the previous chunk is used or not. It is set
|
|
if the next chunk is in use. The second least significant bit is set if the
|
|
memory area is mmap'ed -- a special case which we will not consider. The
|
|
third least significant bit is unused.
|
|
|
|
To test whether the current chunk is in use or not, we have to check the
|
|
next chunk's PREV_INUSE bit within its size value.
|
|
|
|
Once we free() the chunk, using free(mem), some checks take place and the
|
|
memory is released. If its neighbour blocks are free, too (checked using
|
|
the PREV_INUSE flag), they will be merged to keep the number of reuseable
|
|
blocks low, but their sizes as large as possible. If a merge is not
|
|
possible, the next chunk is tagged with a cleared PREV_INUSE bit, and the
|
|
chunk changes a bit:
|
|
|
|
+----------------------------------+
|
|
chunk -> | prev_size |
|
|
+----------------------------------+
|
|
| size |
|
|
+----------------------------------+
|
|
mem -> | fd |
|
|
+----------------------------------+
|
|
| bk |
|
|
+----------------------------------+
|
|
| (old memory, can be zero bytes) |
|
|
: :
|
|
|
|
nextchunk -> | prev_size ... |
|
|
: :
|
|
|
|
You can see that there are two new values, where our data was previously
|
|
stored (at the 'mem' pointer). Those two values, called 'fd' and 'bk' -
|
|
forward and backward, that is, are pointers. They point into a double
|
|
linked list of unconsolidated blocks of free memory. Every time a new free
|
|
is issued, the list will be checked, and possibly unconsolidated blocks
|
|
are merged. The whole memory gets defragmented from time to time to release
|
|
some memory.
|
|
|
|
Since the malloc size is always at least 8 bytes, there is enough space for
|
|
both pointers. If there is old data remaining behind the 'bk' pointer, it
|
|
remains unused until it gets malloc'd again.
|
|
|
|
The interesting thing regarding this management, is that the whole internal
|
|
information is held in-band -- a clear channeling problem.
|
|
(just as with format string commands within the string itself, as control
|
|
channels in breakable phonelines, as return addresses within stack memory,
|
|
etc).
|
|
|
|
Since we can overwrite this internal management information if we can
|
|
overwrite a malloced area, we should take a look at how it is processed
|
|
later on. As every malloc'ed area is free()'d again in any sane program,
|
|
we take a look at free, which is a wrapper to chunk_free() within malloc.c
|
|
(simplified a bit, took out #ifdef's):
|
|
|
|
static void
|
|
chunk_free(arena *ar_ptr, mchunkptr p)
|
|
{
|
|
size_t hd = p->size; /* its head field */
|
|
size_t sz; /* its size */
|
|
int idx; /* its bin index */
|
|
mchunkptr next; /* next contiguous chunk */
|
|
size_t nextsz; /* its size */
|
|
size_t prevsz; /* size of previous contiguous chunk */
|
|
mchunkptr bck; /* misc temp for linking */
|
|
mchunkptr fwd; /* misc temp for linking */
|
|
int islr; /* track whether merging with last_remainder */
|
|
|
|
check_inuse_chunk(ar_ptr, p);
|
|
|
|
sz = hd & ~PREV_INUSE;
|
|
next = chunk_at_offset(p, sz);
|
|
nextsz = chunksize(next);
|
|
|
|
Since the malloc management keeps chunks within special structures called
|
|
'arenas', it is now tested whether the current chunk that should be free
|
|
directly borders to the 'top' chunk -- a special chunk. The 'top' chunk is
|
|
always the top-most available memory chunk within an arena, it is the border
|
|
of the available memory. The whole if-block is not interesting for typical
|
|
buffer overflows within the malloc space.
|
|
|
|
if (next == top(ar_ptr)) /* merge with top */
|
|
{
|
|
sz += nextsz;
|
|
|
|
if (!(hd & PREV_INUSE)) /* consolidate backward */
|
|
{
|
|
prevsz = p->prev_size;
|
|
p = chunk_at_offset(p, -(long)prevsz);
|
|
sz += prevsz;
|
|
unlink(p, bck, fwd);
|
|
}
|
|
|
|
set_head(p, sz | PREV_INUSE);
|
|
top(ar_ptr) = p;
|
|
|
|
if ((unsigned long)(sz) >= (unsigned long)trim_threshold)
|
|
main_trim(top_pad);
|
|
return;
|
|
}
|
|
|
|
Now the 'size' of the current chunk is tested whether the previous chunk is
|
|
unused (testing for the PREV_INUSE flag). If this is the case, both chunks
|
|
are merged.
|
|
|
|
islr = 0;
|
|
|
|
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);
|
|
}
|
|
|
|
Now the same is done vice versa. It is checked whether the chunk in front
|
|
of the current chunk is free (testing for the PREV_INUSE flag of the size
|
|
two chunks ahead). If this is the case the chunk is also merged into the
|
|
current one.
|
|
|
|
if (!(inuse_bit_at_offset(next, nextsz))) /* consolidate forward */
|
|
{
|
|
sz += nextsz;
|
|
|
|
if (!islr && next->fd == last_remainder(ar_ptr))
|
|
/* re-insert last_remainder */
|
|
{
|
|
islr = 1;
|
|
link_last_remainder(ar_ptr, p);
|
|
}
|
|
else
|
|
unlink(next, bck, fwd);
|
|
|
|
next = chunk_at_offset(p, sz);
|
|
}
|
|
else
|
|
set_head(next, nextsz); /* clear inuse bit */
|
|
|
|
set_head(p, sz | PREV_INUSE);
|
|
next->prev_size = sz;
|
|
if (!islr)
|
|
frontlink(ar_ptr, p, sz, idx, bck, fwd);
|
|
}
|
|
|
|
As Solar Designer showed us, it is possible to use the 'unlink' macro to
|
|
overwrite arbitrary memory locations. Here is how to do:
|
|
|
|
A usual buffer overflow situation might look like:
|
|
|
|
mem = malloc (24);
|
|
gets (mem);
|
|
...
|
|
free (mem);
|
|
|
|
This way the malloc'ed chunk looks like this:
|
|
|
|
[ prev_size ] [ size P] [ 24 bytes ... ] (next chunk from now)
|
|
[ prev_size ] [ size P] [ fd ] [ bk ] or [ data ... ]
|
|
|
|
As you can see, the next chunk directly borders to our chunk we overflow.
|
|
We can overwrite anything behind the data region of our chunk, including
|
|
the header of the following chunk.
|
|
|
|
If we take a closer look at the end of the chunk_free function, we see this
|
|
code:
|
|
|
|
if (!(inuse_bit_at_offset(next, nextsz))) /* consolidate forward */
|
|
{
|
|
sz += nextsz;
|
|
|
|
if (!islr && next->fd == last_remainder(ar_ptr))
|
|
/* re-insert last_remainder */
|
|
{
|
|
islr = 1;
|
|
link_last_remainder(ar_ptr, p);
|
|
}
|
|
else
|
|
unlink(next, bck, fwd);
|
|
|
|
next = chunk_at_offset(p, sz);
|
|
}
|
|
|
|
The inuse_bit_at_offset, is defined as macro in the beginning of malloc.c:
|
|
|
|
#define inuse_bit_at_offset(p, s)\
|
|
(((mchunkptr)(((char*)(p)) + (s)))->size & PREV_INUSE)
|
|
|
|
Since we control the header of the 'next' chunk we can trigger the whole if
|
|
block at will. The inner if statement is uninteresting, except our chunk is
|
|
bordering to the top-most chunk. So if we choose to trigger the outer if
|
|
statement, we will call unlink, which is defined as macro, too:
|
|
|
|
#define unlink(P, BK, FD) \
|
|
{ \
|
|
BK = P->bk; \
|
|
FD = P->fd; \
|
|
FD->bk = BK; \
|
|
BK->fd = FD; \
|
|
}
|
|
|
|
The unlink is called with a pointer to a free chunk and two temporary
|
|
pointer variables, called bck and fwd. It does this to the 'next' chunk
|
|
header:
|
|
|
|
*(next->fd + 12) = next->bk
|
|
*(next->bk + 8) = next->fd
|
|
|
|
They are not swapped, but the 'fd' and 'bk' pointers point to other chunks.
|
|
This two chunks being pointed to are linked, zapping the current chunk from
|
|
the table.
|
|
|
|
So to exploit a malloc based buffer overflow, we have to write a bogus
|
|
header in the following chunk and then wait for our chunk getting free'd.
|
|
|
|
[buffer .... ] | [ prev_size ] [ size ] [ fd ] [ bk ]
|
|
|
|
'|' is the chunk boundary.
|
|
|
|
The values we set for 'prev_size' and 'size' do not matter, but two
|
|
conditions have to be met, in case it should work:
|
|
|
|
a) the least significant bit of 'size' has to be zero
|
|
b) both, 'prev_size' and 'size' should be add-safe to a pointer that is
|
|
read from. So either use very small values up to a few thousand, or -
|
|
to avoid NUL bytes - use big values such as 0xfffffffc.
|
|
c) you have to ensure that at (chunk_boundary + size + 4) the lowest bit
|
|
is zeroed out (0xfffffffc will work just fine)
|
|
|
|
'fd' and 'bk' can be set this way (as used in Solar Designers Netscape
|
|
Exploit):
|
|
|
|
fd = retloc - 12
|
|
bk = retaddr
|
|
|
|
But beware, that (retaddr + 8) is being written to and the content there
|
|
will be destroyed. You can circumvent this by a simple '\xeb\x0c' at
|
|
retaddr, which will jump twelve bytes ahead, over the destroyed content.
|
|
|
|
Well, however, exploitation is pretty straight forward now:
|
|
|
|
<jmp-ahead, 2> <6> <4 bogus> <nop> <shellcode> |
|
|
\xff\xff\xff\xfc \xff\xff\xff\xfc <retloc - 12> <retaddr>
|
|
|
|
Where '|' is the chunk boundary (from that point we overflow). Now, the
|
|
next two negative numbers are just to survive a few checks in free() and to
|
|
avoid NUL bytes. Then we store (retloc - 12) properly encoded and then the
|
|
return address, which will return to the 'jmp-ahead'. The buffer before the
|
|
'|' is the same as with any x86 exploit, except for the first 12 bytes,
|
|
because we have to take care of the extra write operation by the unlink
|
|
macro.
|
|
|
|
|
|
Off-by-one / Off-by-five
|
|
------------------------
|
|
|
|
It is possible to overwrite arbitrary pointers, even in cases where you can
|
|
overwrite only five bytes, or - in special cases - only one byte. When
|
|
overwriting five bytes the memory layout has to look like:
|
|
|
|
[chunk a] [chunk b]
|
|
|
|
Where chunk a is under your control and overflowable. Chunk b is already
|
|
allocated as the overflow happens. By overwriting the first five bytes of
|
|
chunk b, we trash the 'prev_size' element of the chunks header and the
|
|
least significant byte of the 'size' element. Now, as chunk b is free()'d,
|
|
backward consolidation pops in, since 'size' has the PREV_INUSE flag
|
|
cleared (see below). If we supply a small value for 'prev_size', which is
|
|
smaller than the size of chunk a, we create a fake chunk structure:
|
|
|
|
[chunk a ... fakechunk ...] [chunk b]
|
|
|
|
|
p
|
|
|
|
Where prev_size of chunk b points relativly negative to the fake chunk.
|
|
The code which is exploitable through this setting was already discussed:
|
|
|
|
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);
|
|
}
|
|
|
|
'hd' is the size element of chunk b. When we overwrite it, we clear out the
|
|
lower two bits, so PREV_INUSE is cleared and the if condition is matched
|
|
(NUL will do it in fact). In the next few instructions 'p', which was a
|
|
pointer to chunk b originally, is relocated to our fakechunk. Then the
|
|
unlink macro is called and we can overwrite the pointers as usual. We use
|
|
backward consolidation now, while in the previous description we have used
|
|
forward consolidation. Is this all confusing? Well, when exploiting malloc
|
|
overflows, do not worry about the details, they will become clearer as you
|
|
understand the malloc functions from a broader scope.
|
|
|
|
For a really well done overview and description of the malloc
|
|
implementation in the GNU C Library, take a look at the GNU C Library
|
|
reference manual [3]. It makes a good read for non-malloc related things,
|
|
too.
|
|
|
|
|
|
Possible obstacles and how to get over with them
|
|
================================================
|
|
|
|
As with any new exploitation technique people will show up which have the
|
|
'perfect' solution to the problem in their head or in form of a patch to
|
|
the malloc functions. Those people - often ones who have never written
|
|
an exploit themselves - are misleading into a wrong sense of security and I
|
|
want to leave a few words about those approaches and why they seldomly work.
|
|
|
|
There are three host based stages where you can stop a buffer overflow
|
|
resulting in a compromise:
|
|
|
|
1. The bug/overflow stage
|
|
|
|
This is the place where the real overflow happens, where data is
|
|
overwritten. If this place is known, the origin of the problem can be fixed
|
|
(at source level). However, most approaches argue that this place is not
|
|
known and therefore the problem cannot be fixed yet.
|
|
|
|
2. The activation stage
|
|
|
|
After the overflow happened parts of the data of the application are
|
|
corrupted. It does not matter what kind of data, whether it is a stack
|
|
frame, a malloc management record or static data behind a buffer. The
|
|
process is still running its own path of code, the overwritten data is
|
|
still passive. This stage is everything after the overflow itself and
|
|
before the seize of execution control. This is where the natural,
|
|
non-artificially introduced hurdles for the attacker lies, code which must
|
|
be passed in a certain way.
|
|
|
|
3. The seized stage
|
|
|
|
This is everything after control has been redirected from its original
|
|
path of execution. This is the stage where nopspace and shellcode is
|
|
executed, where no real exploitation hurdles are left.
|
|
|
|
|
|
Now for the protection systems. Most of the "non-exec stack" and "non-exec
|
|
heap" patches try to catch the switch from stage two to three, where
|
|
execution is seized, while some proprietary systems check for the origin of
|
|
a system call from within kernel space. They do not forbid you to run code
|
|
this way, they try to limit what code can be run.
|
|
|
|
Those systems which allow you to redirect execution in the first place are
|
|
fundamentally flawed. They try to limit the exploitation in a black-listing
|
|
way, by trying to plug the places you may want to go to. But if you can
|
|
execute legal code within the process space its almost everytime enough to
|
|
compromise the process as a whole.
|
|
|
|
Now for the more challenging protections, which try to gripe you in stage
|
|
two. Those include - among others - libsafe, StackGuard, FormatGuard, and
|
|
any compiler or library based patches. They usually require a recompilation
|
|
or relinking of your existing code, to insert their security 'measures'
|
|
into your code. This includes canary values, barriers of check bytes or
|
|
reordering and extensive checking of sanity before doing things which might
|
|
be bad. While sanity checking in general is a good policy for security, it
|
|
cannot fix stuff that was broken before. Every of those protections is
|
|
assuming a certain situation of a bug which might appear in your program
|
|
and try to predict the results of an attacker abusing the bug. They setup
|
|
traps which they assume you will or have to trigger to exploit the bug.
|
|
This is done before your control is active, so you cannot influence it
|
|
much except by choosing the input data. Those are, of course much more
|
|
tight than protection systems which only monitor stage three, but still
|
|
there are ways around them. A couple of ways have been discussed in the
|
|
past, so I will not go into depth here. Rather, I will briefly address on a
|
|
protection which I already see on the horizon under a name like
|
|
'MallocGuard'.
|
|
|
|
Such a protection would not change the mechanism of malloc management
|
|
chunks much, since the current code has proved to be effective. The malloc
|
|
function plays a key role in overall system performance, so you cannot
|
|
tweak freely here. Such a protection can only introduce a few extra checks,
|
|
it cannot verify the entire consistency everytime malloc() is called. And
|
|
this is where it is flawed: Once you seize control over one malloc chunk
|
|
information, you can seize control over other chunks too. Because chunks
|
|
are 'walked' by using either stored pointers (SysV) or stored lengths
|
|
(GlibC), it is possible to 'create' new chunks. Since a sanity check would
|
|
have to assume inconsistency of all chunks in the worst case, it would have
|
|
to check all chunks by walking them. But this would eat up too much
|
|
performance, so its impossible to check for malloc overflows easily while
|
|
still keep a good performance. So, there will be no 'MallocGuard', or it
|
|
will be a useless guard, in the tradition of useless pseudo protections. As
|
|
a friend puts it - 'for every protection there is an anti-protection'.
|
|
|
|
|
|
Thanks
|
|
======
|
|
|
|
I would like to thank all proofreaders and correctors. For some really
|
|
needed corrections I thank MaXX, who wrote the more detailed article about
|
|
GNU C Library malloc in this issue of Phrack, kudos to him ! :)
|
|
|
|
|
|
References
|
|
==========
|
|
|
|
[1] Solar Designer,
|
|
http://www.openwall.com/advisories/OW-002-netscape-jpeg.txt
|
|
[2] DD Sleator, RE Tarjan, "Self-Adjusting Binary Trees", 1985,
|
|
http://www.acm.org/pubs/citations/journals/jacm/1985-32-3/p652-sleator/
|
|
http://www.math.tau.ac.il/~haimk/adv-ds-2000/sleator-tarjan-splay.pdf
|
|
[3] The GNU C Library
|
|
http://www.gnu.org/manual/glibc-2.2.3/html_node/libc_toc.html
|
|
[4] Solaris 8 Foundation Source Program
|
|
http://www.sun.com/software/solaris/source/
|
|
|
|
|=[ EOF ]=---------------------------------------------------------------=|
|
|
|