Cookie Consent by
Posts NSEC 2020 - Crackme

NSEC 2020 - Crackme


With the ongoing craziness of Covid-19 this year's NSEC CTF was moved from its usual in-person venue to a fully online environment. Among the offerings were a series of crackme binary reverse-engineering challenges. With increasing difficulty, and not necessarily with the reversing portion itself, I was able to finish all 6 for my team. Follow along as I step through my solutions and add some personal comments.

Crackme 0

Simply put, trivial. The first command I ran, strings, returned the flag.

root@kali:~/Desktop/NSEC2020/cracks# strings crackme0 | grep FLAG

Crackme 1

While this challenge should have also been trivial, I decided to complicate my life and ignored the obvious - story of my life. Running strings against the binary did not return our flag as in crackme0, so I booted up ghidra and took a look at the code.

Now a normal person would have seen that atoi then 0x539 comparison and quickly realized that the password was 1337. My mind decided to ignore that and approach it by loading the binary in gdb and seeing if I could bypass the check altogether.

As I knew the offset to call the print_flag() was 957 and I knew the dynamic address space from the break at main I was able to jump directly to the function.

Sometimes you are your own worst enemy. I ended up with the same result but definitely went about it in a more complicated way.

Crackme 2

Now it's starting to get interesting. Crackme2 still wasn't too complicated but required analyzing the code to understand what the program was doing when processing your input. Unlike the previous examples we could not just jump to win since as we shall see the code was using our input to check the flag validity.

I popped it into ghidra and started stepping through the logic, starting at main.

Then if we look at compute_flag we notice it is first setting up a few variables then processing our input. Let's keep those variables in mind for later.

So what can we tell at this point? It looks like the program takes the following steps:

  • Base64 encodes our input
  • Checks that the b64 encoded length is 20 characters - otherwise prints unauthorized
  • Iterates through the previously initialized variables
  • Compares the variables with our B64 input char by char - printing unauthorized if it doesn’t match.

Excellent, so we should be able to recreate the base64 string and decode it, which should give us the required password.

Crackme 3

Here be dragons! The previous crackmes were all x86-64 architecture compiled binaries. Now we're moving into MIPS64 architecture and with that we have a new set of issues to deal through.

Unfortunately since we are not running on a MIPS system, when we attempt to execute the binary.

padraig@kali:~/Desktop/NSEC2020/cracks$ ./crackme3
bash: ./crackme3: cannot execute binary file: Exec format error

Side note: the next part was completely unnecessary to finish the challenge, however I wanted to get my environment working rather than just complete the challenge.

After some background reading of this article I understood what I needed to be able to run the program.

  • Install qemu to emulate various architectures
  • Install the mips64 libraries
  • Link our library to be used by the emulator

Alright, we got this!

sudo apt-get install qemu qemu-user qemu-user-static
sudo apt-get install libc6-mips64-cross
ln -s /usr/mips64-linux-gnuabi64/lib64/ /lib64/

Then if we give it a shot.

root@kali:~/Desktop/NSEC2020/cracks# qemu-mips64 crackme3
crackme3: error while loading shared libraries: cannot open shared object file: No such file or directory

Almost... Let's add one more linking.

sudo ln -s /usr/mips64-linux-gnuabi64/lib/ /lib/mips64-linux-gnuabi64/
root@kali:~/Desktop/NSEC2020/cracks# ./crackme3
Computing secret...

Excellent! It does seem like we will need to take a look under the hood as the binary isn't asking for any input nor providing us with any meaningful output. It IS running however and I will take the minor win in getting this setup done... for now at least.

If we load the binary into ghidra and take a poke at main we notice something interesting.

So what do we see? The program is initializing a string of 'A' characters. Then it is adding values to these A's in memory. I'm willing to wager that this will be our flag!

This was the only part necessary to complete the challenge. But why stop here! Similar to how I set up the environment to be able to run the binary I wanted to learn how to properly debug something of a different architecture.

This part required a few things to work correctly. You need to add the -g flag to qemu-mips64 to indicate you will be attaching it to a gdb session. Secondly you need to start gdb-multi arch with the binary, set the right architecture configurations, then connect to the listening session.

root@kali:~/Desktop/NSEC2020/cracks# qemu-mips64 -g 12345 crackme3 &
[1] 5146
root@kali:~/Desktop/NSEC2020/cracks# gdb-multiarch crackme3
GNU gdb (Debian 9.1-3) 9.1
Reading symbols from crackme3...
(No debugging symbols found in crackme3)

gef➤  set arch mips
The target architecture is assumed to be mips
gef➤  set endian big
The target is assumed to be big endian
gef➤  target remote localhost:12345
Remote debugging using localhost:12345

Alright, now what? If we take a look at the decompiled version one more time we can see that register $v0 is storing the value of the additions.

If we look at it through gdb we can see the value of $v0.

Finally if we iterate through the logic we should be able to gather all the values necessary.

Excellent! Or almost... Seems a few characters are missing. Since we started with a string of all 'A' characters it makes sense that these did not need to be modified, therefore we missed them in the logic flow.

Crackme 4

Similar to crackme 3 we ran into a different architecture - this time ARM based. Also similar to the previous crackme I made it much harder on myself than was needed.

In the interest of knowledge and learning a bit more I followed the same steps as crackme3 to get the ARM based libraries and debuggers working in my environments. While this wasn't necessarily required for crackme4, it was paramount in solving crackme5.

sudo apt-get install qemu-system-arm
sudo apt-get install gcc-arm-linux-gnueabihf

Now if we try and run the binary.

root@kali:~/Desktop/NSEC2020/cracks# qemu-arm crackme4
/lib/ Invalid ELF image for this architecture

Argh... so similar to the previous crackme I need to get the proper libaries lined up. Unfortunately I already have a library configured at the location for a different architecture. If I were to symlink the ARM library I would most likely end up screwing other things up. A bit of reading later and I learned I can supply the library location directly to qemu.

root@kali:~/Desktop/NSEC2020/cracks# qemu-arm -L /usr/arm-linux-gnueabihf/ crackme4

Excellent we have it running! At least I think so... Let's take a look at it with ghidra.

Interesting, those hex characters seem readable. If gather them up in the proper order and convert them.

root@kali:~/Desktop/NSEC2020/cracks# ../askgod submit FLAG-HtWywoKQraYiWbW
Congratulations, you score your team 1 points!

Beautiful, one more crackme down.

Crackme 5

The grand daddy of the set. I enjoyed this one the most, but it also caused me the most pain. This crackme5 is also an ARM based binary.

With the setup done for crackme4 we should be able to execute this binary.

root@kali:~/Desktop/NSEC2020/cracks# qemu-arm -L /usr/arm-linux-gnueabihf/ crackme5
Enter passcode: 1234

Well that went smoothly! Alright we need to spend some time understanding what it is looking for as far as "authorization" though. Let's pop it open in ghidra.

This is a bit to follow so breaking it down our value first goes through a strtol to convert the supplied string to a base 10 integer. Secondly it calls validate which has three sub-functions. If we put all the functions together the neat formula that needs to be satisfied becomes the following.

$$ \begin{align*} [pass\ \newcommand*\xor{\oplus}\ 0x1a75]\ \newcommand*\ShiftLeft{\ll}\ [(pass\ \newcommand*\BitAnd{\mathbin{\&}}\ 1) + 2] = pass + 0x21a3937 \end{align*} $$

Or since we are working with decimal.

$$ \begin{align*} [pass\ \newcommand*\xor{\oplus}\ 6773]\ \newcommand*\ShiftLeft{\ll}\ [(pass\ \newcommand*\BitAnd{\mathbin{\&}}\ 1) + 2] = pass + 35273015 \end{align*} $$

This may look confusing but if we break it down by chunk.

  • Our pass input is XOR’d with 6773
  • Our pass is AND’d with 1 (which keeps the least significant bit)
  • The XOR result is then Left Bit Shifted by either 2 or 3 (essentially multiplying the result by 4 or 8)
  • This result needs to equal our pass input added to 35273015

Now I spent probably too much time trying to reduce this to a simpler equation but in the end I broke it down to possibilities. Since we knew that the input was limited to 10 characters that allowed for only 100 million options - not bad! Here are some other assumptions I made to reduce it further.

  • Since we need to add 35 million to our input on the right side and the left side total is being multiplied by only 4 or 8 we are dealing with numbers in the millions.
  • As a result the XOR action would negligible for our loose approximations.

With the XOR assumption our equivalent equation becomes:

$$ \begin{align*} pass\ * [4|8] = pass + 35273015 \end{align*} $$

That seems much easier to approximate! If I do it by million chunks and take the lower and upper bounds of multiplying by 4 or 8:

M - 4 / 8
4 - 16/32 -> possibility
5 - 20/40 -> possibility
6 - 24/48
7 - 28/56
8 - 32/64
9 - 36/72
10 - 40/80
11 - 44/88 -> possibility
12 - 48/96
13 - 52/104
14 - 56/112
15 - 60/120

What this is telling us is that our answer is somewhere in the 4-6 million or 11 million range. That's still not an answer, but 3 million possibilities is better than 100 million!

At this point I did not have a better idea to optimize further so I decided to bite the bullet and bruteforce the 3 million possibilities. Using seq to create three 1-million digit files I pumped them through a simple one liner to grep for the answer, then walked away to have a drink thinking this wouldn't work. Lo and behold I walked back to the following.

Hot diggity daffodils! Alright I still don't trust myself so I decided to test it manually.

Excellent, all that is left is to submit!


Overall I had a great time working on the various crackmes and definitely learned a thing or two throughout. Unfortunately due to amount of challenges available during the CTF and real life priorities I had to limit myself to a narrow subset and was only able to tackle these six crackmes and the two dreamcast (writeup soon to follow) challenges. I'm looking forward to reading other writeups of crackme5 and seeing if others had a more eloquent solution than my optimized bruteforce approach.

As always thanks folks, until next time!

This post is licensed under CC BY 4.0 by the author.