Published on

Space Heroes CTF: Writeup Compilation



So me and a couple of LFGs (looking-for-groups) played Space Heroes CTF, organized by Florida Tech's FITSEC cybersecurity team. As one of the first CTFs I've played in over a year, it was an amazing learning experience for me being thrown into the mystical world of binary exploitation/pwn. I've made a couple of writeups for the cooler challenges I've solved; enjoy!

Guardians of the Galaxy

author: GlitchArchetype
points: 100
category: pwn
Ronan the Accuser has the Power Stone. Can Starlord find a successful distraction format?
nc 12690

Let's look at what happens when you run that binary given to us:

This error is because the binary is probably trying to reference a flag.txt within its directory that doesn't exist. Let's create one and run it again:

There, we got it to work locally. Since we know that this is problem a format string vulnerability from the "find a successful distraction format" part of the description, let's assume that the vulnerability is it writing our input to the stack with printf(). We will need to work our way up the stack with the format %n$s, where n is the decimal index of the argument you want, and s is the printf() specifier for a string of characters. Here is a Python script used to brute force our way up:

This script will send a UTF-8 encoded format string, with str(i) being the iterating variable. If its output contains the flag, the loop breaks and the script will stop. Let's run it:


author: v10l3nt
points: 100
category: pwn
files: vader (ELF)
Submit flag from /flag.txt from

As with any other sourceless pwn challenge, we first need to boot it up in the Ghidra disassembler for static analysis. Let's check what our main() function does:

Looks like it reads our input (stdin) with a fixed length of 256 through fgets(). Let's continue sifting around:

The goal is now clear: call the vader() function with five correct arguments to print the flag. Simple, right? Let's start building our chain.

Firstly, we need to calculate our offset. Although we can brute this by simply passing a cyclic string and seeing what's overwritten the $rsp register, we can see that in the main() function, 32 bytes are allocated to char local_28. We can assume this is the buffer, so if we overflow this and append an additional 8 bytes to cover the $rbp register, our offset is 40.

Next in line is the process of getting our arguments on the stack. Arguments to be passed into functions are also held in registers -- we need to figure out which ones we need to use to pass the correct arguments (DARK, S1D3, OF, TH3, FORC3) into vader(). Referencing this x64 cheatsheet (as the registers are different depending on the bitness/architecture of the ELF):

To call a function, the program should place the first six integer or pointer parameters in the registers $rdi, $rsi, $rdx, $rcx, $r8, and $r9; subsequent parameters (or parameters larger than 64 bits) should be pushed onto the stack, with the first argument topmost. The program should then execute the call instruction, which will push the return address onto the stack and jump to the start of the specified function.

Therefore, we need to put our arguments into $rdi, $rsi, $rdx, $rcx, and $r8. The main method of doing this is via gadgets, or simple assembly instructions that can be used to pop specific registers from the stack. After the pop, we can repopulate the register with our own address that represents the required string (this address will be located within the binary). Additionally, they almost always have a ret instruction at the end to return to even more gadgets, therefore creating a ROP chain.

Note: The program literally provides gadget functions for you:

Although you can use these, it's not really in the nature of a ROP challenge, so I will be finding the gadgets manually!

To find the gadgets we need, we will be utilizing a program called ropper and grep-ing the output:

Check it out -- at the bottom of the code block (0x40165b) there's a perfect gadget for us to use! Let's find ones for the rest of them:

The first pop rsi; pop r15; isn't ideal, as it's popping a redundant register -- we'll need to repopulate it with 8 bytes of garbage. On the other hand, the pop rcx; pop r8; takes care of two registers at once!

With that, we can draw up a visual of what our final payload will look like:

The last thing we need to do is to find the hex addresses of our argument strings:

Don't forget the address of vader() too!:

Here is my final script, which defines a variable for each section of our gigantic payload — this is for enhanced readability. I've also used the p64() function, which converts the address into little endian:

I don't usually do this, but here's a clip of me initially solving the challenge by running the above script:

This is considered a "simple" challenge for those experienced with the field of return-oriented programming within pwn/binary challenges. However, I had none prior to this competition, so Vader was one of the most time-consuming and annoying challenges to work with. Yet, it was probably the most satisfying solve throughout the entire competition, and it was my first time utilizing gadgets and building a ROP chain. I hope you enjoyed!

Warmup to the Dark Side

author: v10l3nt
points: 100
category: pwn
Once you start down the dark path, forever will it dominate your destiny. (And yes, the binary isn't included)
nc 30096

Let's run that netcat link to see what's going on:

We're given an address of the win() function... and that's it. If this is a ret2win challenge, how are we meant to find the offset of the $rip register and overflow it with our code? Of course... we need to brute force it.

In the code snippet below, I got the address provided in the prompt by reading the line and taking its substring (ASLR is enabled, so it's different each time). Then, I slowly increased the buffer of the payload with a loop until I found the correct offset of the $rip:

Let's run this script on the server to see if we can get the flag:

Rahool's Challenge

author: excaligator
points: 331
category: crypto
nc 10294

Let's open that netcat link to see what's going on:

For themed CTFs, I find it really fun to figure out the cultural references in the challenge before solving them. In this case, Rahool is a vendor in the Destiny 2 Tower that can decrypt legendary engrams (purple) and sell exotic engrams (gold). Uncoincidentally, that's what we'll be doing here.

Immediately, we can tell that the ciphertext underneath the giant Rahool ASCII is substitution. This means that the plaintext is simply substituted by a value determined by the algorithm. Throwing it into this cipher identifier, we find that it is a Vigenère cipher.

Before moving on, we need to figure out what the hell a Vigenère is.

The Vigenère Cipher

A Vigenère cipher is a type of encryption that uses both plaintext and a key. There are many ways to use this encryption method, but the most common is via addition and table/tabula recta.

To encrypt using addition, take the position in the alphabet of the first letter in your plaintext (make sure it starts at 0, i.e. A = 0, B = 1, C = 2) and add it with the position of your key (if the key was "key", the position would be 10 as K = 10). Then, take the modulo 26 (divide by 26 to get the remainder, symbol %), as some numbers add up to greater than 26.

Plaintext: hello
Key: key
h (07) + k (10) = r (17 % 26 = 17)
e (04) + e (04) = i (08 % 26 = 08)
l (11) + y (24) = j (35 % 26 = 09)
l (11) + k (10) = v (21 % 26 = 21) <- Note that the key cycles
o (14) + e (04) = s (18 % 26 = 18)
Ciphertext: rijvs

In a formula, where A is the plaintext's alphabetic position and B is the key's alphabetic position, in that would be:

C=(A+B)mod26C = (A + B) \bmod 26

It'll be a more manual process (albeit more fun) for encrypting via table/tabula recta. Let's check out what it looks like (Source: Wikipedia):


Each of the 26 rows contains the same alphabet, except shifted to the left by one position. At the top, each column is associated with a letter in the alphabet. To the left, each row is associated with a letter in the key.

If I wanted to encrypt HELLO with WORLD as the key, I would find the cell that intersects with column H and row W. In that case, it would be D. Then, I would find the cell that intersects with column E and row O. In that case, it would be S. Rinse and repeat for the entire phrase.

Cheaters Never Win...

But how are we supposed to decrypt vigenere without a key? Let's do some "OSINT" and Google the crap out of it. DCode, which can keylessly decrypt substitution ciphers, is the first option. Click, clack, Ctrl + Shift + C, Ctrl + V later and we have solved it!!1!1!

Or not. Wait... the plaintext is telling me to replace my E with a 3 and my O with an 0. Those aren't in RKBGVP. What's going on? Is the website wrong?

...Or Do They?

Let's go back to the drawing board and look at the problem again.

We've found ourselves an encrypted engram - Can you break the (new and improved) indecipherable cipher? Message:A + Key:B = 0 + B = O

Since this cipher is "new and improved", we can assume it's not just your normal Vigenère. However, the A + B = O is the biggest giveaway of what we are meant to do for this challenge.

Take it literally. The letter A (plaintext) plus the letter B (key) is equal to the letter O (ciphertext).

I solved this challenge via known-plaintext attack. Yeah, it sounds fancy. But here's what I actually did:

This is a tabula recta with a modified offset. You see how intersecting column A and row B would return O?

Since we know our plaintext, we can use this table "backwards" to find the key. If you go down the column of your letter and find its equivalent ciphertext letter, it would be on the same row as the key for that letter!

For example, if C is our plaintext and Q is our ciphertext, the key would be B.

Let's follow this process for the actual plaintext/ciphertext:

E + N -> E
S + I -> X
D + C -> O
K + E -> T
E + J -> I
D + O -> C
S + B -> E
N + D -> X
F + E -> O
I + C -> T
M + R -> I
N + Y -> C
G + P -> E

The key is EXOTIC, as in how Master Rahool sells exotic engrams. Very funny. We can now follow the instructions in the plaintext and send it to the server with an unnecessary pwntools script:

Sending the string:

We just solved Rahool's Challenge without needing to write any algorithms!