In this typical instance, we will be looking at how an attacker would circumvent DEP (Data execution prevention) by redirecting the return address of a C function to a function contained in libC (C standard library) in a 32bit binary.
As any exploit, as time goes by, new methods of prevention and detection are introduced. In this article, we will be looking at a more advanced version of a buffer overflow attack. In this typical instance, we will be looking at how an attacker would circumvent DEP (Data Execution Prevention) by redirecting the return address of a C function to a function contained in libC (C standard library) in a 32bit binary. If we want to, we can additionally push a chain of libC based functions to the stack in order to execute them one after the other which is ROP (Return Oriented Programming).
A reason for not using a 64bit binary in this article is that
Ret2libc in its purest form was during the 32bit binary era. As building up the attack is based on you creating a fake stack frame after the
$EBP register value for a specific funtion. This includes pushing the function arguments into the fake stack frame using an overflow. A
Ret2libc attack on 64 bit binaries relies on pushing function arguments onto registers using gadgets and has similarities to ROP or so Return Oriented Programming.
How can we spawn a shell without using direct shellcode? or list a directory? A
Ret2libC attack allows us to call the C function
system and a function called
exit in order to spawn a shell and thereafter allow the program to exit cleanly without arousing any suspicion.
It is highly reccomended that you have experience with buffer overflows or have read my previous article specifically on Buffer overflows as this article is an extension of it. In this article, we will be using the following tools:
- Ubuntu 8 (An installation guide is available in my Formatted String exploit article which I do reccomend you follow as we must install extra components from the CD image file. You may find the ISO image here).
- A basic knowledge of C
- Knowledge of Endianness (Big Endian/Little Endian) concepts.
In this article, we will be exploiting the non length checked function
gets is not recommended for usage in modern programs, programmer or rookie error may slip through the cracks. The
gets function allows us to take a user input from the standard input (STDIN) and store it into a character buffer without being length checked which is a definite recipe for disaster concerning buffer overflows.
Let us get started with a simple program:
int main(int argc, char* argv)
This program allows us to call a function
buggy_function containing the vulnerable
gets function. We will use the PERL programming language in order to create a
string which we will direct as user input to our vulnerable program using some simple BASH command notations.
Our goal in this exploit will be to overflow the return address for the
buggy_function stack frame and substitute it with the address of the
system function. We will also pass the address of the
exit function and the argument to execute a new shell instance
"/bin/sh" for the
system function, all of which are contained within
Before we get started, let's turn off memory randomization or ASLR which would interfere with our exploit. ASLR helps programs protect themselves against such attacks by constantly moving memory around using a "canary variable". This helps in also checking whether memory has been tampered with or so providing integrity checks. you may switch ASLR off on your system by modifying the following file's value from
NOTE: Do remember to set the value of the randomize_va_space file back to 2 once you are done with your exploit.
What is libC?
libC as it is known refers to the standard library for C which contains all functions, type definitions and macros related to the C programming language. In this case, it defines the types
libC is a major component for C's functioning and libraries are included into your program using the following notation:
In this case, the
system functions are included in the
stdlib library which contains basic standard functionality for our C program. This means that when the program is run, we can use functions which are included in the library even if we haven't used them directly in our program, as they will be loaded as well into memory. In fact, you will find that a lot of functions in
libC are available to us even though we haven't included/used them in our program. We can especially see this if we debug our program in GDB by using the
print command with the name of the function while setting a breakpoint in our program:
This availability is due to the fact that these libraries and functions are built in which allows multiple programs to point to a specific pointer in memory in order to call a function that is frequently used with low overhead.
libC is very complex and very extensive. It defines memory allocation, file managment and so forth.
How a Ret2libC Attack Works in Memory
Let's go ahead and understand how a
Ret2libC attack would work in memory. As we know, functions are organized into stack frames (Read my article on Buffer Overflows to know more). A frame is composed of a function prologue which helps set up the frame for use. A body and a return statement. The top of the frame is denoted by the Stack pointer (
$ESP) and the bottom by the Base pointer (
$EBP). A return address is stored after the base pointer in order to signify where the program should return to after executing the function as shown in the example below.
In order to execute a
Ret2libC attack, we will create a fake stack frame right after
$EBP pointer in order for it to look something like this:
Example Exploitable Function fnc(char* arg1, char* arg2, n...)
|-------------------------------| ← Start of frame for buggy_function(). Referenced to by $ESP
| //function prologue |
| buffer = 'A' x 128 | ← Our buffer which contains random 'A's
| 'AAAA' | ← $EBP replaced by 4 random bytes
| system() | ← Replaced the return address with the system call
| exit() | ← Allows us to exit cleanely after system()
| address of "/bin/sh" | ← Argument for system()
In order to run our exploit successfully, we will have to overrun our buffer and the
$EBP pointer with 4 random bytes. We will then go ahead and override the return address with the address of
system and subsequently
exit and finally the address of
"/bin/sh" which for this exploit will be contained in an environment variable. If you would like to add another function instead of system, you may do so. Do remember that unlike ROP which allows you to do multiple operations, here we are limited to only calling two functions, any other addresses placed after will not be executed and likely end up with an error.
Exploitation: Preparing Ourselves
In order to execute our
Ret2libC attack, we will be using GDB. As beginners, this will help us to navigate the complex world of C binaries, of course, as we get better at these exploits, our use of GDB will deminish. GDB is known as the GNU debuger and will help us view memory during execution, create breakpoints and understand our vulnerable binary further. There are various other programs one may use such as NM or hexdump. For this article, we will stick with GDB.
In order to include extra symbols which GDB can pick up on within our binary, we will go ahead and compile our binary using the
-g flag which will include debug symbols into our binary.
gcc -g bug.c -o bug.out -fno-stack-protector
-o flag allows us to define the name for our resultant binary. We will be using the
-fno-stack-protector flag in order to compile our binary. This will disable any stack protection for our program or so any protection that helps in detecting stack smashing (buffer overflows).
Let's now go ahead and open our vulnerable program with GDB:
gdb -q ./bug.out
-q flag suppresses any welcome messages that may be displayed by GDB. Let's go ahead and prepare ourselves by finding our the real address of
exit. We will need to find them while running our program through GDB. We can set a breakpoint during execution in order to do so on the function
main which will allow us to observe other values in memory including the locations of the
exit functions. We can set a break point by using the following command:
Let's go ahead and run our program using the following command:
We have now encountered our breakpoint which we set in
Now we have access to our programs memory at runtime. Let's go ahead and figure out where the functions
system is and where
Perfect! Now that we have the addresses of
exit, we can go ahead and use them in our exploit. Yet so, we still need to set an argument for
In this case, we will be creating an environment variable named
EGG which will contain
"/bin/sh", this will be the argument of which address we will push onto our fake stack frame as an argument for our call to the
system function. We can exit GDB using the key '
Q'. Now we enter the following command in order to export our environment variable:
Perfect! We must have now successfully exported our environment variable. You may verify it by finding it in the list of environment variables using:
As we can see above, the
EGG environment variable has been successfully created. In GDB, we could go through memory in order to find any environment variable which is as not hard as they are usually located at a very similar area of memory, but in this instance, let's go ahead instead and create a program that will get the location of our environment variables directly and infact return the very environment variable we would like. We will be using the C function
getenv which allows us to get an environment variable by name. We will directly print it out using the "%8p" format parameter for
printf which means that
printf should print the result of
getenv or so our variable
EGG as a pointer (or so the address of
EGG in Hex):
int main(int argc, char* argv)
We can go ahead and compile our program using gcc and without any flags such as the
-g flag nor the
-fno-stack-protector flags as we will not be needing them for this simple program:
gcc get_env.c -o get_env.out
We can now go and use this program in order to find our target environment variable:
As we can see, the address of
Let's go ahead and run our program (bug.out) with GDB and create a breakpoint in the
main function. We will now inspect whether the
EGG environment variable is infact at
As we can see,
EGG is infact not at
0xbffff860. This is because there is another environment variable that is pushed before
EGG on the stack. This variable contains the full path and the full name of our executable which means that the location of
EGG is dependent on the length of this variable due to the varying size of our executable depending on our path and name preference:
At this point,
EGG has moved up by 27bytes. We need to subtract that from the address we got with the
EGG is so located at
Let's go ahead now and try our exploit by inserting the corrected address of
EGG into our payload. This should get us a new shell.
Running Our Exploit
In order to run our buffer overflow exploit, we must go ahead and overwrite our return address with the address of
system so that instead of pointing to its prevoius location inside of
main, it will go ahead and call the system function. We can then push the 4 bytes representing the address of
exit and further the address of
Let's figure out the location of
buffer and the location of our base pointer
$EBP in order to figure out the total length required in order to override the return address.
In GDB, let's go ahead and set a break point in
buggy_function right before the
gets function is called. In order to know where, we can use the
Let's go ahead and create a breakpoint on line 9. GDB will break execution before code on line 9 is executed. This will allow the
buffer array to be created so we can figure out its location:
Here, we can see that buffer starts at
0xbffff484. We must now go and figure out where our base pointer is in order to get a correct length for our payload. We can figure out the location of our base pointer
$EBP by using the following command:
info registers $ebp
We additionally can run the following GDB examine command in order to see the following 4bytes after
$EBP which represent the return address for
In fact, as we can see,
$EBP is stored at
0xbffff508 and the memory location after contains the return address pointing to
0x080483f3 is the point of execution in
main after calling
buggy_function. We can see the return point in the
main function by disassembling it and looking for the address
As you can see,
0x080483f3 refers to the next execution point after calling
Let's go and find out the exact size required of our payload by substracting the larger address or so that of our base pointer
$EBP with that of the smaller address at hand or so that of
As we can see, we need our payload to be 132bytes in order to reach
$EBP, we will need 4 more bytes in order to ovverride
$EBP and 12 more in order to insert the addresses of
EGG. In total, our payload length should be 148bytes. Perfect, let's go ahead and use perl to print our payload into our program as
STDIN containing 132 garbage bytes + the 4 that are meant to overwrite
$EBP and there after, our exploit critical byte information, remember that you will have to write the addresses in Big Endian:
r <<< $(perl -e 'print "A" x 132 . "B" x 4 .
"\x90\xfa\xea\xb7" . "\xe0\x4c\xea\xb7" . "\x45\xf8\xff\xbf")
The program should now exit normally and give you a /bin/sh shell. If you forget the
exit function, the program will not exit gracefully and give you an error but will still spawn a shell. In order to avoid suspicion of our exploited system, it is best to include the
Another way to have fun is to list the current directory In fact, you can run any executable of your choice. In this case, let's go and change
EGG outside of GDB to contain the
ls executable in the linux /bin/ folder:
We can now re-enter GDB and run the same payload as before. The addresses of
EGG should remain intact:
- 27th May, 2021: Initial version