Buffers located in dynamic memory are also vulnerable to overflow. Many programmers, lazy by nature, first allocate a fixed-size buffer and then define how much memory they actually use. They typically forget to handle correctly situations, in which there isn't enough memory. Buffers of two types are usually encountered in the heap: structure elements and dynamically-allocated memory blocks.
Assume that there is a structure called demo in the program, which contains a fixed-size buffer:
Listing 4.10: An example of a structure with an overflowing buffer (highlighted in bold)
struct demo
{
int a;
char buf[8];
int b;
}
Casual handling of the data being processed (for example, lack of required checks in the required place) can result in overflow of the buf buffer and, consequently, in overwriting of the variables that follow it. These are member variables of the structure itself (in this case, variable b), the strategy of whose modification will be typical and will observe the rules common for all overflowing buffers. The possibility of overwriting memory cells located beyond the limits of the allocated memory block is less evident. By the way, for the buffers that have monopolistic access to the entire allocated memory block, this is the only possible strategy. Consider the code in Listing 4.11. In your opinion, is there anything that can overflow?
Listing 4.11: An example of a dynamic memory block vulnerable to overflow
#define MAX_BUF_SIZE 8
For a long time, it was assumed that here there was nothing to overflow. At most, it was possible to organize a trivial DoS attack. However, it was thought to be impossible to gain control over the target computer because of the chaotic distribution of dynamic blocks over the memory. The base address of the p block is generally arbitrary, and practically anything can be located beyond its end, including an unallocated memory region. Any attempt at accessing such a region results in an immediate exception, which in turn results in abnormal program termination.
However, this common point of view is erroneous. Currently, no one would be surprised by overflow of dynamic buffers. For a long time, this technology was used as a universal technique of gaining control, and not without success. For example, the much-talked-of Slapper worm, which is one of the few worms that infect UNIX machines, propagates in this manner. How is it possible? Consider the propagation mechanism of this worm in more detail.
Allocation and release of the dynamic memory takes place chaotically, and at any given instance any allocated block can be followed by another block. Even if several memory blocks are allocated sequentially, no one can guarantee that they will be allocated in the same order at every program start-up. This is because the order of memory block
allocation depends on the size of the released memory buffers and the order, according to which they were freed. Nevertheless, the structure of auxiliary data structures that runs through dynamic memory as a kind of supporting framework is easily predictable, although it may differ from compiler to compiler.
There are lots of dynamic memory implementations. Different manufacturers use
different algorithms. Allocated memory blocks may be supported by a tree or by a linked or double-linked list, references to which might be represented both by pointers and by indexes stored in the beginning or end of each allocated block or in a separate data structure. The latter method of implementation is encountered rarely.
Without diving deep into the technical details of the dynamic memory manager, it is
possible to say that at least two auxiliary variables are related to each allocated memory block: the pointer (index) to the next block and the block allocation flag. These variables can be located before the allocated block, after it, or in a different location. When
releasing the memory block, the free function checks the allocation flag of the next block and, if it is free, joins these two blocks together, updating the pointer. And, where there is a pointer, there practically always is the poke function. In other words, by
overwriting the data after the end of the allocated block in strictly-measured doses, it is possible to modify practically any cell of the vulnerable program, for example, by
redirecting some pointer to the shellcode.
Consider the dynamic memory organization, according to which all allocated blocks are connected using double-linked lists, the pointers to which are located in the beginning of every block (Fig. 4.4). In addition, adjacent memory blocks need not reside in adjacent elements of the list. This means that in the course of multiple allocation-and-release operations the list inevitably becomes fragmented, and constant defragmentation of this list is too inconvenient.
Figure 4.4: Dynamic memory blocks
Buffer overflow overwrites auxiliary structures of the next memory block, thus providing the possibility of modifying them (Fig. 4.5). However, what benefit will the attacker receive? Access to the cells of every block is carried out by the pointer returned to the program at the instance of its allocation, not by the "auxiliary" pointer that the intruder is going to overwrite. Auxiliary pointers are used exclusively by malloc/free (and similar functions). Modifying the pointer to the next or previous block allows the intruder to force the function to accept the address of the next allocated block, for example, by
superimposing it over the available buffer. However, the hacker has no guarantees that this operation will be successful because, when allocating a memory block, the malloc function looks for the most suitable (from its point of view) memory region. As a rule, this is the first free block in a chunk matching the size of the requested one. Thus, there is no guarantee that the desired region will be suitable for it. Briefly speaking, the prospect is not too optimistic.
Pointer to the next block in chunk
Memory block 1 Pointer to the previous block in chunk
Size
Status (allocated/free) Memory allocated to the block Pointer to the next block in chunk
Memory block 2 Pointer to the previous block in chunk
Size
Status (allocated/free) Memory allocated to the block
Figure 4.5: Approximate map of dynamic memory allocation
Release of memory blocks is a different matter. To reduce the fragmentation of the dynamic memory, the free function automatically joins the block currently being
released with the next one, provided that the next block also is free. Because adjacent blocks might be located on opposite ends of the list that links them, the free function before connecting the foreign block must remove it from the chunk. This is carried out by concatenation of the previous and next pointers. In pseudocode, this operation appears approximately as follows: pointer to the next block in the chunk = pointer to the
previous block in the chunk. Yet, this is nothing but the analogue of the poke function in Basic, which allows modification of any cell of the vulnerable program.
More details on this topic can be found in the "Once upon a free()…" article published in
issue 39h of the Phrack e-zine (http://www.phrack.org). This article is overstuffed with technical details of dynamic memory implementation in different libraries, but it is useful reading.
As a rule, the possibility of writing into the memory is used for modifying the import table to replace some API function, which is guaranteed to be called by the vulnerable
program soon after the overflow takes place. The fate of the program is predefined, because the integrity of the supporting framework of dynamic memory is already violated and this instable construction can crash at any moment. It probably will be impossible to pass control to the overflowing buffer, however, because its address isn't known beforehand. The hacker must improvise under these circumstances. First, it is possible to place the shellcode in any other available buffer with a known address (see the next section). Second, among the functions of the vulnerable program it is possible to encounter the ones that pass control to the pointer passed to them, along with some argument (conventionally, denote such a function the f-function). After that, the only thing that remains for the hacker is to find an API function that takes the pointer to the
overflowing buffer and replaces its address with that of the f-function. In C++ programs, with their virtual functions and this pointers, such situation is not rare, although it
cannot be called common. However, when designing shellcode, it is not recommended that you rely on the standard solutions. Hackers have to be creative.
Be prepared that, in some implementations of the heap, indexes instead of pointers might be encountered. In general, indexes are relative addresses counted either from the first byte of the heap or from the current memory cell. The latter case is encountered most frequently (in particular, the library of the Microsoft Visual C++ 6.0 compilers is built exactly in this way). Thus, it is expedient to consider it in more detail. As was already mentioned, absolute addresses of the overflowing buffers are not known beforehand and change unpredictably depending on specific circumstances. However, the addresses of the cells that are the most desirable for modification are absolute addresses. What could be done about this? It is possible to investigate the strategy of allocation and release of the memory for the current application to detect the most probable combination, because surely some patterns in assigning addresses to the overflowing buffers can be detected. By carefully testing all possible variants one after another, the attacker sooner or later will succeed in gaining control over the server.
However, before this successful attempt of the attack, the server might freeze a couple of times, which will disclose the attack and make administrators vigilant.