Buffer Overflow Vulnerability Lab
Buffer Overflow Vulnerability Lab
1 Lab Overview
The learning objective of this lab is for students to gain first-hand experience with a buffer-overflow vulner-
ability by putting what they have learned about the vulnerability from class into action. Buffer overflow is
defined as the condition in which a program attempts to write data beyond the boundaries of pre-allocated
fixed length buffers. This vulnerability can be utilized by a malicious user to alter the flow control of the
program, even execute arbitrary pieces of code. This vulnerability arises due to the mixing of the storage
for data (e.g. buffers) and the storage for controls (e.g. return addresses): an overflow in the data part can
affect the control flow of the program, because an overflow can change the return address.
This lab builds off of concepts introduced in the overrun lab. While the overrun lab is not a prerequisite
to performing this lab, it may help students are are new to low level references to data structures.
In this lab, students will be given a program with a buffer-overflow vulnerability; their task is to develop
a scheme to exploit the vulnerability and finally gain the root privilege. In addition to the attacks, students
will be guided to walk through several protection schemes that have been implemented in the operating
system to counter against the buffer-overflow attacks. Students need to evaluate whether the schemes work
or not and explain why.
2 Lab Tasks
2.1 Initial setup
The lab is started from the Labtainer working directory on your Docker-enabled host, e.g., a Linux VM.
From there, issue the command:
labtainer bufoverflow
The resulting virtual terminals will include a bash shell. The programs described below will be in your
home directory.
Address Space Randomization. Several Linux-based systems uses address space randomization to ran-
domize the starting address of heap and stack. This makes guessing the exact addresses difficult; guessing
addresses is one of the critical steps of buffer-overflow attacks. In this lab, we disable these features using
the following commands:
The StackGuard Protection Scheme. The GCC compiler implements a security mechanism called ”Stack
Guard” to prevent buffer overflows. In the presence of this protection, buffer overflow will not work. You
can disable this protection if you compile the program using the -fno-stack-protector switch. For example,
to compile a program example.c with Stack Guard disabled, you may use the following command:
Note we use the ”-m32” switch to create 32 bit executables, which are required for this lab.
Non-Executable Stack. Ubuntu used to allow executable stacks, but this has now changed: the binary
images of programs (and shared libraries) must declare whether they require executable stacks or not, i.e.,
they need to mark a field in the program header. Kernel or dynamic linker uses this marking to decide
whether to make the stack of this running program executable or non-executable. This marking is done
automatically by the recent versions of gcc, and by default, the stack is set to be non-executable. To change
that, use the following option when compiling programs:
2.2 Shellcode
Before you start the attack, you need a shellcode. A shellcode is the code to launch a shell. It has to be
loaded into the memory so that we can force the vulnerable program to jump to it. Consider the following
program:
#include <stdio.h>
int main( ) {
char *name[2];
name[0] = ‘‘/bin/sh’’;
name[1] = NULL;
execve(name[0], name, NULL);
}
The shellcode that we use is just the assembly version of the above program. The following program
shows you how to launch a shell by executing a shellcode stored in a buffer. Please compile and run the
following code, and see whether a shell is invoked.
/* call_shellcode.c */
/*A program that creates a file containing code for launching shell*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
SEED Labs – Buffer Overflow Vulnerability Lab 3
Note: In this lab we have relaced the bin/sh program with an older insecure shell that will inherit the
setuid permissions assocated with the stack program. Modern shells will use the process real uid as their
effective id, thereby making it more difficult to obtain root shells from setuid programs. However, more
sophisticated shell code can run the following program to turn the real user id to root. This way, you
would have a real root process.
void main()
{
setuid(0); system("/bin/sh");
}
For this lab, we’ll use the simpler shell code and and insecure /bin/sh program.
Please use the following command to compile the code (don’t forget the execstack option):
A few places in this shellcode are worth mentioning. First, the third instruction pushes “//sh”, rather
than “/sh” into the stack. This is because we need a 32-bit number here, and “/sh” has only 24 bits. Fortu-
nately, “//” is equivalent to “/”, so we can get away with a double slash symbol. Second, before calling the
execve() system call, we need to store name[0] (the address of the string), name (the address of the
array), and NULL to the %ebx, %ecx, and %edx registers, respectively. Line 5 stores name[0] to %ebx;
Line 8 stores name to %ecx; Line 9 sets %edx to zero. There are other ways to set %edx to zero (e.g.,
xorl %edx, %edx); the one (cdq) used here is simply a shorter instruction: it copies the sign (bit 31) of
the value in the EAX register (which is 0 at this point) into every bit position in the EDX register, basically
setting %edx to 0. Third, the system call execve() is called when we set %al to 11, and execute “int
$0x80”.
SEED Labs – Buffer Overflow Vulnerability Lab 4
return 1;
}
Compile the above vulnerable program and make it set-root-uid. You can achieve this by compiling it
in the root account, and chmod the executable to 4755 (don’t forget to include the execstack and
-fno-stack-protector options to turn off the non-executable stack and StackGuard protections):
$ sudo su
# gcc -m32 -o stack -z execstack -fno-stack-protector stack.c
# chmod 4755 stack
# exit
The above program has a buffer overflow vulnerability. It first reads an input from a file called “badfile”,
and then passes this input to another buffer in the function bof(). The original input can have a maximum
length of 517 bytes, but the buffer in bof() has only 12 bytes long. Because strcpy() does not check
boundaries, buffer overflow will occur. Since this program is a set-root-uid program, if a normal user can
exploit this buffer overflow vulnerability, the normal user might be able to get a root shell. It should be
noted that the program gets its input from a file called “badfile”. This file is under users’ control. Now, our
objective is to create the contents for “badfile”, such that when the vulnerable program copies the contents
into its buffer, a root shell can be spawned.
SEED Labs – Buffer Overflow Vulnerability Lab 5
/* exploit.c */
After you finish the above program, compile and run it. This will generate the contents for “badfile”.
Then run the vulnerable program stack. If your exploit is implemented correctly, you should be able to
get a root shell:
Important: Please compile your vulnerable program first. Please note that the program exploit.c, which
generates the bad file, can be compiled with the default Stack Guard protection enabled. This is because we
SEED Labs – Buffer Overflow Vulnerability Lab 6
are not going to overflow the buffer in this program. We will be overflowing the buffer in stack.c, which is
compiled with the Stack Guard protection disabled.
While in the root shell, you are required to display the content of a secret file:
cat /root/.secret
It should be noted that although you have obtained the “#” prompt, your real user id is still yourself (the
effective user id is now root). You can check this by typing the following:
# id
uid=(500) euid=0(root)
If running the vulnerable code once does not get you the root shell, how about running it for many
times? You can run ./stack using the whilebash.sh script, and see what will happen. If your exploit
program is designed properly, you would eventually be able to get the root shell. You can modify your
exploit generation program to increase the probability of success (i.e., reduce the time that you have to
wait). You may get lucky and get a root prompt before too long. Otherwise use ctrl c to break out of the
whilebash.sh script.
It should be noted that non-executable stack only makes it impossible to run shellcode on the stack, but it
does not prevent buffer-overflow attacks, because there are other ways to run malicious code after exploiting
a buffer-overflow vulnerability. The return-to-libc attack is an example. We have designed a seperate lab for
that attack. If you are interested, please see the Return-to-Libc Attack Lab for details.
3 Guidelines
We can load the shellcode into “badfile”, but it will not be executed because our instruction pointer will not
be pointing to it. One thing we can do is to change the return address to point to the shellcode. But we have
two problems: (1) we do not know where the return address is stored, and (2) we do not know where the
shellcode is stored. To answer these questions, we need to understand the stack layout the execution enters
a function. The following figure gives an example.
High Address
void func (char *str) {
char buffer[12];
int variable_a;
strcpy (buffer, str); str (a pointer to a string)
Current Frame
} Return Address
Low Address
Finding the address of the memory that stores the return address. From the figure, we know, if we
can find out the address of buffer[] array, we can calculate where the return address is stored. Since
the vulnerable program is a Set-UID program, you can make a copy of this program, and run it with your
own privilege; this way you can debug the program (note that you cannot debug a Set-UID program).
In the debugger, you can figure out the address of buffer[], and thus calculate the starting point of the
malicious code. You can even modify the copied program, and ask the program to directly print out the
address of buffer[]. The address of buffer[] may be slightly different when you run the Set-UID
copy, instead of of your copy, but you should be quite close.
SEED Labs – Buffer Overflow Vulnerability Lab 8
If the target program is running remotely, and you may not be able to rely on the debugger to find out
the address. However, you can always guess. The following facts make guessing a quite feasible approach:
• Stack usually starts at the same address.
• Stack is usually not very deep: most programs do not push more than a few hundred or a few thousand
bytes into the stack at any one time.
• Therefore the range of addresses that we need to guess is actually quite small.
Finding the starting point of the malicious code. If you can accurately calculate the address of buffer[],
you should be able to accurately calcuate the starting point of the malicious code. Even if you cannot accu-
rately calculate the address (for example, for remote programs), you can still guess. To improve the chance
of success, we can add a number of NOPs to the beginning of the malcious code; therefore, if we can jump
to any of these NOPs, we can eventually get to the malicious code. The following figure depicts the attack.
NOP
NOP
…… (many NOP’s)
NOP
str str
Previous FP Previous FP
buffer [0] …... buffer [11] buffer [0] …... buffer [11]
Storing an long integer in a buffer: In your exploit program, you might need to store an long integer (4
bytes) into an buffer starting at buffer[i]. Since each buffer space is one byte long, the integer will actually
SEED Labs – Buffer Overflow Vulnerability Lab 9
occupy four bytes starting at buffer[i] (i.e., buffer[i] to buffer[i+3]). Because buffer and long are of different
types, you cannot directly assign the integer to buffer; instead you can cast the buffer+i into an long pointer,
and then assign the integer. The following code shows how to assign an long integer to a buffer starting at
buffer[i]:
char buffer[20];
long addr = 0xFFEEDD88;
4 Submission
When the lab is completed, or you’d like to stop working for a while, run
stoplab
from the host Labtainer working directory. You can always restart the Labtainer to continue your work.
When the lab is stopped, a zip file is created and copied to a location displayed by the stoplab command.
When the lab is completed, send that zip file to the instructor, or submit it via Sakai.
References
[1] Aleph One. Smashing The Stack For Fun And Profit. Phrack 49, Volume 7, Issue 49. Available at
http://www.cs.wright.edu/people/faculty/tkprasad/courses/cs781/alephOne.html