Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / security

A Simple 32bit Ret2libc Attack in Linux

5.00/5 (6 votes)
27 May 2021CPOL12 min read 11.7K  
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.

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.

  • gets(char* buffer)

Let us get started with a simple program:

bug.c:

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:

Shell
/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:

C++
#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:

Shell
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.

Shell
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:

Shell
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:

Image 1

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:

Image 2

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:

Shell
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:

Shell
env

Image 3

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

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:

Shell
gcc get_env.c -o get_env.out

We can now go and use this program in order to find our target environment variable:

Image 4

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:

Image 5

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:

Image 6

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.

Image 7

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

Image 8

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:

Image 9

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

Image 10

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

Image 11

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:

Image 12

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:

Shell
export EGG="/bin/ls"

Image 13

We can now re-enter GDB and run the same payload as before. The addresses of system, exit and EGG should remain intact:

Image 14

History

  • 27th May, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)