Demystifying ret2libc
Creating a vulnerable program in C and exploiting the buffer overflow with ret2libc

Table of contents
- Table of contents
- Creating the Vulnerable C Program & Compiling
- Analyzing the Binary with GDB
- Crafting the Exploit
- Closing
Interested in binary exploitation? Wonder where my GH username of "ret2c" comes from? Follow along to learn more about the infamous ret2libc exploit. Although it's not as widely used as it was in the early 2000s, it was the first exploit I learned when I discovered TeaMp0isoN's MLT at 12 years old, who (at the time) had the username "ret2mlt".
For this walkthrough, I'll be using a 64-bit install of Ubuntu 22.04 with ASLR (Address Space Layout Randomization), stack canaries, and position-independent executables disabled. The reason for this is to save time hunting for the return memory addresses and bypassing overflow protections seen on modern operating systems.
"What about NX??? How is this possible?!?!" Good comment! The fun part of ret2libc is its ability to reuse existing, legitimate code from the program's memory when libc (standard C library) is loaded. This exploit doesn't conduct any code injection, allowing us to bypass common security measures we see in modern operating systems.
Creating the Vulnerable C Program & Compiling
We'll start by writing our vulnerable program, utilizing strcpy()
as our vulnerable segment, which won't include any bounds checking, allowing us to trigger a buffer overflow and take advantage of libc.
Once our program is written, we'll compile it with gcc vuln.c -o vuln -m32 -no-pie -fno-stack-protector
.
- -m32: Used to compile this binary as 32-bit
- -no-pie: Disables position-independent execution, allows the program to have fixed memory addresses
- -fno-stack-protector: Disables security checks (stack canaries) for buffer overflows, allows us to overwrite addresses
chown
.
Analyzing the Binary with GDB
Let's start by opening our program in gdb
and disassembling the main function.
This starts to paint a picture of how we can exploit our program and start controlling registers post-overflow. If you're familiar with x86 ASM on older 32-bit systems, this may feel a little more verbose and complex than what we usually see. Even though we compiled this using the -m32
flag, since our host OS is a newer 64-bit system, other factors such as compiler version changes, Application Binary Interface (ABI) differences, newer stack unwinding techniques, and more influence what we see on these ASM dumps. It'll also play a role when we eventually craft our exploit, but we'll get to that soon.
What we really want to look at is that second to last line, 0x080491b9 <+67>: lea esp,[ecx-0x04]
. Since ecx
is stored on the stack, we can overwrite ecx
with any value we want. By controlling that register we can control esp
(the stack pointer), which is important since it points to the top of the stack where important data (like return addresses) are stored. This also means we don't have to worry about the return pointer, since if we control esp
the next return instruction will use whatever address is at the top of the stack as the next instruction to execute (the eip
value).
What we want to do next is calculate the offset where ecx
is stored, then overwrite it with the correct address, which we can then use to control what the return instruction will execute next. We can do this using tools from the Metasploit arsenal called pattern_create.rb
and pattern_offset.rb
. We'll create a random 130 character string, analyze the resulting segmentation fault, grab ecx
, and calculate our offset.
Now that we have the offset needed, let's grab some addresses for functions we'll need to pop a shell.
Let's start crafting the exploit, the only thing we need now is our desired stack address.
Crafting the Exploit
Have we lost the dolphins? We'll start by setting a breakpoint of *main+85
within gdb
to view the stack before ecx
gets popped off. Once we set a breakpoint, we'll convert those functions and the string we found into little endian to pass along using Perl.
- System: 0xf7dc6cd0 →
"\xd0\x6c\xdc\xf7"
- Exit: 0xf7db91f0 →
"\xf0\x91\xdb\xf7"
- "/bin/sh": 0xf7f380d5 →
"\xd5\x80\xf3\xf7"

Looks like we found system at
0xffffd154
on the stack, this is where we'll want to point esp
to. If you take note of the code section, we can see that the second to last instruction of 0x80491de <main+88>: lea esp,[ecx-0x4]
indicates that we'll want to do to fill that dummy 'BBBB' we pushed up above. We'll have to add that 0x04 to that system address, giving us 0xffffd158
, which we'll pass as "\x58\xd1\xff\xff"
. Let's clear that breakpoint and see what happens.
We got shell! We successfully took control of the pointer and instructed the program to utilize the already existing system() function alongside the "/bin/sh" string, allowing us to achieve access that wasn't previously expected by a developer.
Closing
Thank you for reading, this wouldn't have been possible without ret2rop's blog. I read over a dozen posts and labs about executing ret2libc, but this was the only blog that took modern 64-bit operating systems into consideration, which helped clear a lot of my confusion. Also, big thanks to Kay Lack for great content regarding ARM64 & x86-64 ASM. Which helped me in reading the dumps seen above.
For ret2libc, the core methodology will be the same as what's demonstrated here in this post, but any slight variation to your C code will change where to find offsets, how much padding is required to take control of key registers, and more. If you're interested in this exploit, be sure to jump around different blogs to see how they accomplished it, or check out some CTFs. There are a couple of other things to mention in this post (such as the multiple PIDs being created on shell spawn, further work for calling setuid(0)
, etc.), but that's for another time.
As always, if you have any questions or comments about what I wrote - please let me know! You can reach out to me via LinkedIn or Signal.
-connor