I still can’t believe our first win as .;,;. was on corCTF of all CTFs! That’s actually insane.
Really really happy with how we did, and I can’t believe this is how we qualify for DiceCTF finals 😂. Anyway, here are writeups for the challenges I did. Even though one of them is forensics actually they were all reverse engineering.
forensics/schitty-challenge
author: plastic
26 solves / 216 points
We managed to capture the terminal output of a user while trying to obfuscate a shell script.
We get an ELF file, a short C File, and finally some terminal output.
The C file isn’t really useful. It’s mostly just there to show that the ./file_information
command run in the terminal output prints the results of the stat
function on the original /bin/sh
.
On the other hand, the terminal output tells us a lot. First, it tells us that the flag binary was generated from running shc
on a shell script called flag.sh
. The goal of the challenge is probably to recover this shell script to get the flag.
shc
is a shell script compiler. It takes a shell script, and “compiles” it into a binary. However, by “compile”, it actually means encrypting the original script and embedding it into the binary.
In this case, we have a few flags that change the behavior of shc
:
-U
tellsshc
to create an untraceable (no strace/ptrace) binary-H
tellsshc
to create a “hardened” binary, which only works for the default shell and supposedly makes it harder to reverse engineer.-v
is for verbose, and-o
and-f
are for output and input files respectively.
rev in disguise
If we toss the binary into a decompiler, we see a fairly interesting main function:
If we try running it, we fail a check in sub_32eb
, the first function:
$ ./flag
Operation not permitted
Killed
All we need to do is patch out the call to this function and the binary will continue.
Here’s what it does after ignoring the first function:
$ ./flag_patched
sh: 1: .����ޙC��Wjm�b˪�#��s��E۶$�Бt�: not found
sh: 2: Syntax error: word unexpected (expecting ")")
Clearly something is going wrong in the decryption here. Thankfully, we have a cool trick up our sleeve that means we don’t need to reverse anything else!
who actually wants to rev though
Yep! We can use our good ol’ friend LD_PRELOAD
to intercept the library calls used by the binary. I won’t go over the technique since there’s lots of better tools to learn it elsewhere, but essentially it let’s use provide our own definition for functions like execve
and stat
that the binary uses.
Additionally I’ll skip over the process of enumerating each and every function since only a few are useful. Here’s the final code I ended up using:
At some point in the binary, it uses the results of the stat
function on /bin/sh
to help decrypt the shell script. However, since we know the outputs of this function from the terminal output, we can override this specific call to return the proper inputs to successfully decrypt the shell script.
Putting it all together, we can compile the above code to a shared object, and run the patched flag binary with LD_PRELOAD
properly set, and we get the flag!
The final shell script has the flag as a comment, meaning we had to actually retrieve the whole script, but the system
call sees it all anyway, so we’re good to go!
rev/cormine
Note: This is a 2-part challenge, but my solution solves both parts so I’ll just be writing as one challenge. Both challenges use the same binary.
author: clubby, strellic
8 solves / 362 points
Crusaders of Rust Gaming Division is proud to present our first totally original title: corMine! Please enjoy exclusive access to this alpha test!
Controls:
- WASD - Movement
- Space - Jump
- F9 - Save level to
game.cms
- Esc - Release/grab mouse
- Hold Esc - Quit
Remember, if you spot any bugs, no you didn’t.
./cormine load cormine1.cms
Troubleshooting: If you’re unable to run the challenge, try the below steps.
- If using Wayland, don’t!
- Try
export WGPU_BACKEND=vulkan
before the challenge
As a warning, this challenge is a Rust Bevy binary, meaning it is HUGE. I won’t pasting whole decompilations, so it might be a bit hard to find exactly where I’m talking about (I’ll give addresses though). also binja op
Saving the level
First things first, obviously the flag is somehow stored inside our given .cms
files. We don’t want to look for this logic just yet, since the binary is really big, but there is a trick we can use.
Since we know F9
saves the level to game.cms
, it must run the same logic as loading a save, just in reverse.
Good thing for us, searching for the string game.cms
gives us pretty close to our target location.
We can see that a file is being opened, presumably game.cms
, and then later written to.
Scrolling a bit further down, we see something very intimdating:
At first glance, this is definitely some inlined operation. Thankfully, if we extract the two constants 0x5851f42d4c957f2d
and -0x5e89ab1b9041e80d
, we can google their numerical values 6364136223846793005
and 11634580027462260723
, and we find this result:
That means this function must be creating some PRNG with a seed. Problem is, what’s the PRNG or the seed?
Slide to the left…
Scrolling down just a tiny bit more, we see two function calls:
We can probably guess that the we have some ChaCha based PRNG, and by doing a quick search for the rand_chacha
crate and with the information of the second function call having an argument of 6, we can assume that we are seeding a ChaCha12Rng. If you’re aware of how the SALSA/ChaCha family of PRNGs work, this makes sense since the argument being 6 is the number of odd/even rounds, which means 12 rounds total, so ChaCha12Rng
.
…and slide to the right
Now, we could keep going and see how the PRNG is used to help save the file, but remember, our goal is to load the file, not save it. So, let’s look at other places these functions are called.
As you can see from the amazingly labeled diagram, there’s just one other place where the function is called 4 times, and that’s where the save is being loaded.
Going to the load
calls, we can scroll up to see where it opens and first reads from the file. This function is really long and complex, so here’s a basically condensed version of the important bit:
This means the first 4 bytes of the file are actually our seed for the ChaCha PRNG! Right after this section it reads in 2 bytes. I assume this is for the size of the world, but it’s not super important and I didn’t look into it.
Next, scrolling past the inline seed_from_u64
and init_chacha
, we get to this section (again, this is heavily condensed):
It reads in 3 values, XORs them with the PRNG output to get the position of the block, then XORs just a single byte with the PRNG output to get the type of the block.
As for how it reads in the values, here’s what the decomp of the function looks like:
Here’s my reimplementation in Rust, do note that since each XOR is done with one uint32, we can return a u64 and just cast it to a u32 when needed.
Putting it all together
Now all we have to do is basically reimplement the reader and just dump the map to a file. Here’s my final parser, yes i know i suck at rust ok i just wanted to solve the challenge:
Running this on the cormine1.cms
file, we get the following output (download):
Now, remember this isn’t the flag, its just the blocks we have. All we need to do to get the flag is to just visualize the block id 0s, and we get the flag!
And our flag is corctf{w4llh4cks}
.
For the second part, we can repeat the exact same thing, getting this output (download):
As you can see, the old blocks are being overwritten with an invalid block id. All we have to do is just ignore these and only plot the id 3, and we get the flag!
And our flag is corctf{0v3rwrit3}
.