Published on

PicoCTF 2022: Buffer Overflow Series



This is a writeup for the buffer overflow series during the picoCTF 2022 competition. This was arguably my favorite set of challenges, as beforehand I'd never stepped into the realm of binary exploitation/pwn. I learned a lot from this, so I highly recommend solving it by yourself before referencing this document. Cheers!

Buffer overflow 0

authors:- Alex Fulton
- Palash Oswal points: 100
category: pwn
Smash the stack! Let's start off simple: can you overflow the correct buffer? The program is available here. You can view source here, and connect with it using:
nc 65535

Let's check out our source code:

The first thing we should do is check how the flag is printed. Looks like it's handled in a sigsegv_handler() function:

Researching online, a "SIGSEGV" stands for a segmentation fault, which is an error raised by memory-protected hardware whenever it tries to access a memory address that is either restricted or does not exist. If the flag printf() resides within sigsegv_handler(), then we can safely assume that we must figure out how to trigger a segmentation fault.

We see that on line 40, the horrible gets() is called, and reads buf1 (the user input) onto the stack. This function sucks, as it will write the user's input to the stack without regard to its allocated length. The user can simply overflow this length, and the program will pass their input into the vuln() function to trigger a segmentation fault:

Buffer overflow 1

authors:- Sanjay C.
- Palash Oswal points: 200
category: pwn
Control the return address.
Now we're cooking! You can overflow the buffer and return to the flag function in the program. You can view source here. And connect with it using:
nc [PORT]
Warning: This is an instance-based challenge. Port info will be redacted alongside the last eight characters of the flag, as they are dynamic.

Let's check out our source code:

In the vuln() function, we see that once again, the gets() function is being used. However, instead of triggering a segmentation fault like Buffer overflow 0, we will instead utilize its vulnerability to write our own addresses onto the stack, changing the return address to win() instead.

I: Explaining the Stack 💬

Before we get into the code, we need to figure out how to write our own addresses to the stack. Let's start with a visual:

Whenever we call a function, multiple items will be "pushed" onto the top of the stack (in the diagram, that will be on the right-most side). It will include any parameters, a return address back to main(), a base pointer, and a buffer. Note that the stack grows downwards, towards lower memory addresses, but the buffer is written upwards, towards higher memory addresses.

We can "smash the stack" by exploiting the gets() function. If we pass in a large enough input, it will overwrite the entire buffer and start overflowing into the base pointer and return address within the stack:

If we are deliberate of the characters we pass into gets(), we will be able to insert a new address to overwrite the return address to win(). Let's try!

II: Smashing the Stack 🔨

To start, we first need to figure out our "offset". The offset is the distance, in characters, between the beginning of the buffer and the position of the $eip. This can be visualized with the gdb-gef utility by setting a breakpoint (a place to pause the runtime) in the main() function:

Analyzing this breakpoint, if we look at the arrow on the assembly code, we can see that its address is the exact same as the $eip (0x80492d7). Let's try overflowing this register by passing an unhealthy amount of As into the program:

Look what happened: our program threw a SIGSEGV (segmentation) fault, as it is trying to reference the address 0x41414141, which doesn't exist! This is because our $eip was overwritten by all our As (0x41 in hex = A in ASCII).

III: Finessing the Stack 🛠️

Although we've managed to smash the stack, we still don't know the offset (how many As we need to pass in order to reach the $eip). To solve this problem, we can use the pwntools cyclic command, which creates a string with a recognizable cycling pattern for it to identify:

We can see that $eip is currently overflowed with the pattern 0x6161616c (laaa). let's search for this pattern using pattern search:

To figure out which offset we need to use, we can use readelf to analyze header of the vuln executable:

Our binary is in little endian, we know that 44 As are needed in order to reach the $eip. The only thing we need now before we create our exploit is the address of the win() function, which will be appended to the end of our buffer to overwrite the $eip on the stack:

Win is at 0x80491f6, but we need to convert it to the little endian format. You can do this with the pwntools p32() command, which results in \xf6\x91\x04\x08. Let's make a final visual of our payload:

Let's write our payload and send it to the remote server with Python3/pwntools:

Let's try running the script on the server:

We have completed our first ret2win buffer overflow on a x32 binary! Yet, this is just the beginning. How about we spice things up a little bit?

IV: Automating the Stack 🔧

Although the concept of buffer overflows can seem daunting to newcomers, experienced pwners will often find these sorts of challenges trivial, and don't want to spend the effort manually finding offsets and addresses just to send the same type of payload. This is where our best friend comes in: pwntools helper functions and automation! Let's start with the first part - the $eip offset for x32 binaries.

The main helper we will be using is pwnlib.elf.corefile. It can parse core dump files, which are generated by Linux whenever errors occur during a running process. These files take an image of the process when the error occurs, which may assist the user in the debugging process. Remember when we sent a large cyclic pattern which was used to cause a segmentation fault? We'll be using the core dump to view the state of the registers during that period, without needing to step through it using GDB. We'll be using the coredump to eventually find the offset!

Info: Many Linux systems do not have core dumps properly configured. For bash, run ulimit -c unlimited to generate core dumps of unlimited size. For tsch, run limit coredumpsize unlimited. By default, cores are dumped into either the current directory or /var/lib/systemd/coredump.

Before we start, let's work through the steps with command-line Python. First, let's import the pwntools global namespace and generate an elf object using pwntool's ELF():

We can then generate a cyclic() payload and start a local process referencing the aforementioned elf object. Sending the payload and using the .wait() method will throw an exit code -11, which signals a segmentation fault and generates a core dump.

We can now create a corefile object and freely reference registers! To find the offset, we can simply call the object key within cyclic_find().

Now that we know how ELF objects and core dumps work, let's apply them to our previous script. Another cool helper I would like to implement is flat() (which has a great tutorial here, referred to by the legacy alias fit()), which flattens arguments given in lists, tuples, or dictionaries into a string with pack(). This will help us assemble our payload without needing to concatenate seemingly random strings of As and little-endian addresses, increasing readability.

This is my final, completely automated script:

Let's run the script on the server:

We've successfully automated a solve on a simple x32 buffer overflow!

Buffer overflow 2

authors:- Sanjay C.
- Palash Oswal points: 300
category: pwn
Control the return address and arguments.
This time you'll need to control the arguments to the function you return to! Can you get the flag from this program?
You can view source here. And connect with it using:
nc [PORT]
Warning: This is an instance-based challenge. Port info will be redacted alongside the last eight characters of the flag, as they are dynamic.

Let's check out our source code:

Looking at the win() function, we can see that two arguments are required that need to be passed into the function to receive the flag. Two guard clauses lay above the flag print:

The goal is simple: call win(0xCAFEF00D, 0xF00DF00D)! We'll be doing it the hard way (for a learning experience), in addition to a more advanced easy way. Let's get started.

I: The Hard Way 🐢

We can apply a lot from what we learned in Buffer overflow 1. The first thing we should do is find the offset, which requires no hassle with pwntools helpers! Although we'll get actual number here, I won't include it in the final script for the sake of not leaving out any steps. Simply segfault the process with a cyclic string, read the core dump's fault address ($eip) and throw it into cyclic_find():

The next thing we need to know about is the way functions are laid out on the stack. Let's recall the diagram I drew out earlier:

If we want to call a function with parameters, we'll need to include the base pointer alongside a return address, which can simply be main(). With this, we can basically copy our script over from Buffer overflow 1 with a few tweaks to the payload:

Let's run it on the remote server:

II: The Easy Way 🐇

But... what if you wanted to be an even more lazy pwner? Well, you're in luck, because I present to you: the pwntools ROP object! By throwing our elf object into ROP() it transforms, and we can use it to automatically call functions and build chains! Here it is in action:

Let's run it on the remote server:

We've successfully called a function with arguments through buffer overflow!

Buffer overflow 3

authors:- Sanjay C.
- Palash Oswal points: 300
category: pwn
Do you think you can bypass the protection and get the flag? It looks like Dr. Oswal added a stack canary to this program to protect against buffer overflows. You can view source here. And connect with it using:
nc [PORT]
Warning: This is an instance-based challenge. Port info will be redacted alongside the last eight characters of the flag, as they are dynamic.

I: Finding the Canary 🐦

So, Dr. Oswal apparently implemented a stack canary, which is just a dynamic value appended to binaries during compilation. It helps detect and mitigate stack smashing attacks, and programs can terminate if they detect the canary being overwritten. Yet, checksec didn't find a canary. That's a bit suspicious... but let's check out our source code first:

If you look closely, you might be able to see why checksec didn't find a stack canary. That's because it's actually a static variable, being read from a canary.txt on the host machine. Canaries that aren't implemented by the compiler are not really canaries!

Knowing that the canary will be four bytes long (defined by CANARY_SIZE) and immediately after the 64-byte buffer (defined by BUFSIZE), we can write a brute forcing script that can determine the correct canary with a simple trick: by not fully overwriting the canary the entire time! Check out this segment of source code:

This uses memcmp() to determine if the current canary is the same as the global canary. If it's different, then the program will run exit(-1), which is a really weird/invalid exit code and supposedly represents "abnormal termination":

However, if we theoretically overwrite the canary with a single correct byte, memcmp() won't detect anything!:

II: Bypassing the Canary 💨

We can now start writing our script! My plan is to loop through all printable characters for each canary byte, which can be imported from string. Let's include that in our pwn boilerplate alongside a simple function that allows us to swap between a local and remote instance:

Here's the big part: the get_canary() function. I'll be using pwnlib.log for some spicy status messages. My general process for the brute force is visualized here if you're having trouble:

I'll be initially sending 64 + 1 bytes, and slowly appending the correct canary to the end of my payload until the loop has completed four times:

The final thing we need to figure out is the offset between the canary to $eip, the pointer register, which we will repopulate with the address of win(). We can do this by appending a cyclic pattern to the end of our current payload (64 + 4 canary bytes) and reading the Corefile's crash location, which will be the $eip:

Note: My canary is abcd because I put that in my canary.txt. It will be different on the remote server!

The offset is 16, so we'll have to append that amount of bytes to the payload followed by the address of win(). I'll combine all sections of our payload together with flat(), and then hopefully read the flag from the output:

Here is my final script with all of its components put together:

Running the script:

We've successfully performed a brute force on a vulnerable static canary!