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
nc 0.cloud.chals.io 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 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:
files: vader (ELF)
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 (
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
$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
$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
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:
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
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
nc 0.cloud.chals.io 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
Let's run this script on the server to see if we can get the flag:
nc 0.cloud.chals.io 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:
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
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
Let's follow this process for the actual plaintext/ciphertext:
Ciphertext: ESDK EDS NFIMNGDJTB XEZVZ OWV KOYRTI KT ZCT BOZ CDIY DIK Z PJ K UNMTV DIK J PJ K AKMD NSUN OWV GPXY TEQSGH PWDFX RXKR UNZ P RC B LJJI KOJ VDXXFX MXXRU GAIVB Plaintext: NICE JOB DECRYPTING INPUT THE ANSWER AS THE KEY WITH THE E AS A THREE THE O AS A ZERO WITH THE WORD ENGRAM AFTER WITH THE A AS A FOUR AND AOGNER RIGHT AFTER
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
Sending the string:
We just solved
Rahool's Challenge without needing to write any algorithms!