On the 20th of November 2020, HackFest held its annual conference, which included a capture the flag event. Two challenges, both of which were featured in the classic CTF, were created by me. Both write-ups are given in this article, starting off with the challenge description, after which the observations based on the description are given. Thirdly, the challenge’s solution is given in a step-by-step manner. At last, a rationale that covers the creation process is given to provide further insight into the challenge.
Table of contents
Russian Roulette
This challenge was created by myself, it was worth 125 points and was solved 64 times. As such, 79% of the teams that scored points solved the challenge. One can download the file here. The challenge description is given below.
You find this odd program on the computer of the Twelve Maffias. Are you lucky enough to crack the code, or is there more to it than pure luck?
Observations
As the file is a Java archive, one can execute the file to see what happens. The output is slowly printed on the screen and provides additional insight into the application. The output is given below.
The High Table of Twelve see that you decided to try your luck! It seems that you need to provide some more information when playing digital Russian roulette...
As this is a command-line interface based application, one can try to provide an argument to the program. In this example, the argument A is used. The output is given below.
The High Table of Twelve see that you decided to try your luck! Fortune favours the bold, but it seems that you weren't bold enough...
Analysis
The flag format for this competition is known, as it uses hf{…}, where the three dots can be any amount of human readable characters. When using the strings utility, one will see a lot of these flags. Either based upon assumption or by trying a few at random, one will observe that the flags are incorrect.
Looking at the code using a Java decompiler, one can see that it is indeed required to provide at least one command-line argument. The main function is given below.
public static void main(String[] args) throws InterruptedException { print("The High Table of Twelve see that you decided to try your luck!"); if (args.length <= 0) { System.out.println("It seems that you need to provide some more information when playing digital Russian roulette..."); System.exit(0); } Random random = new Random(1337); int number = 0; for (int i = 0; i < 11; i++) { number = random.nextInt(5000); } if (args[0].equals("" + number)) { print("You seem to have gotten the lucky number! Here is your prize:"); print(getFlags().get(number)); } else { print("Fortune favours the bold, but it seems that you weren't bold enough..."); } }
If there are no arguments, the program will shut down. A new random is then created, after which an integer is declared and initialised. Note that the random is seeded with 1337 as a value. The loop that follows provides a positive integer between 0 and 4999. The given random’s value is inclusive for the lower bound (which equals zero), whereas the upper bound is exclusive. Note that the function getFlags returns a list of all flags within the program.
If the first argument equals the value of that is assigned to the variable named number, the flag will be printed. In other words, the required command-line argument is the index of the flag in the list of all flags. By counting, looking at the line numbers, or checking the size of the list with all flags, one will see that there are 5000 entries, meaning index zero through 4999.
To obtain the flag, one needs to obtain the value that the integer named number contains at the moment it is compared to the given argument. Since the random is seeded, the values are the same every time. As such, one needs to obtain the value that is the 12th generated integer of a seeded random. By copying the code, one can simply execute this in a development environment. It then follows that the required number, and thus index, is 4406. Providing this as input to the program, or taking the value at that index of the list, will result in the the following flag: hf{67c510f94ad34a988d2e54868367de8d}.
Rationale
The reason to use a seeded random in this challenge is to show that the usage of a pseudo random number generator can be exactly predictable. This is something that is also used in some malware samples, especially the more simple loaders. The twelve iterations, from zero through 11, stand for the twelve challenges, which is the on-going theme of this CTF.
Exclusively Ordinary
This challenge was created by myself, it was worth 125 points and was solved 63 times. As such, 78% of the teams that scored points solved the challenge. One can download the file here. The challenge description is given below.
The Twelve Maffias of the High Table do not want others to crack their code and steal their money. Given how they are in police custody and you are the officer tasked to retrieve the money, you are the chosen person to crack their code. Can you?
Observations
When executing the program, one is greeted with a slowly printed message. The message is given below.
The Twelve Maffias of the High Table request an uncrackable yet human readable code to unlock their vault! All twelve of the Twelve Maffias voted for this secure code! Twelve out of twelve votes it got! Are you smarter than all twelve of their brains together?
When providing a command-line argument, the behaviour of the program changes. The outcome for 1 as the command-line argument is given below.
The Twelve Maffias of the High Table request an uncrackable yet human readable code to unlock their vault! Hint: The Wild Elves Live Very Ergonmic Uncrackable code: ��ȇP�:�s5&0��;~��ى\d;ݖ���X�
The uncrackable code contains unreadable characters. This is, based upon the given description and the printed output of the program, the place where the flag is to be printed. Additionally, there is a hint in the printed text: The Wild Elves Live Very Ergonomic.
Analysis
When looking at the code, it becomes apparent that the provided command-line argument should be an integer. The main function is given below.
public static void main(String[] args) throws InterruptedException { print("The Twelve Maffias of the High Table request an uncrackable yet human readable code to unlock their vault!"); if (args.length <= 0) { print("All twelve of the Twelve Maffias voted for this secure code! Twelve out of twelve votes it got! Are you smarter than all twelve of their brains together?"); System.exit(0); } print("Hint: The Wild Elves Live Very Ergonmic"); int seed = 0; try { seed = Integer.parseInt(args[0]); } catch (Exception e) { } byte[] data = {42, 106, -25, -10, 43, 56, -14, 60, -96, 74, -38, 124, 83, 15, 111, -55, -5, 64, 93, 113, 105, 13, 50, 7, 113, 66, -12, 89, 57, 121, -11, -14, 88, -31}; Random random = new Random(seed); byte[] output = new byte[data.length]; for (int i = 0; i < data.length; i++) { output[i] = (byte) (data[i] ^ random.nextInt(200)); } print("Uncrackable code: " + new String(output)); }
The given argument is used as a seed for a random function. The pseudo random number generator is then used, with an exclusive upper bound of 200, to xor each value in the byte array that is named data. The challenge in here is to uncover which seed is required to obtain the correct sequence of values to reconstruct the flag. This can be done in several ways, especially since the flag format provides the first three characters: hf{. One can re-use the code that is used to generate the uncrackable code and check the first three characters. A script to do so is given below.
public static void main(String[] args) { byte[] data = {42, 106, -25, -10, 43, 56, -14, 60, -96, 74, -38, 124, 83, 15, 111, -55, -5, 64, 93, 113, 105, 13, 50, 7, 113, 66, -12, 89, 57, 121, -11, -14, 88, -31}; for (int seed = 0; seed < Integer.MAX_VALUE; seed++) { Random random = new Random(seed); byte[] output = new byte[data.length]; byte[] array = new byte[3]; array[0] = (byte) (data[0] ^ random.nextInt(200)); array[1] = (byte) (data[1] ^ random.nextInt(200)); array[2] = (byte) (data[2] ^ random.nextInt(200)); String test = new String(array); if (test.equals("hf{")) { System.out.println("Found a hit for seed " + seed); random = new Random(seed); for (int i = 0; i < data.length; i++) { output[i] = (byte) (data[i] ^ random.nextInt(200)); } System.out.println("Uncrackable code: " + new String(output)); } } }
The partial output of running this code is given below.
Found a hit for seed 12 Uncrackable code: hf{s33d3d_r4nd0ms_4r3_pr3d1ct4bl3} Found a hit for seed 11249569 //...
The leetspeak in the flag stands for seeded randoms are predictable. Alternatively, one can guess the seed to be 12 based upon the hint. The capitalisation of the words in the hint spell TWELVE.
Rationale
The missing seed for the random makes it hard to find the xor key for all fields, but as the first few characters are known, one can initialise the correct seeded random, which in turn will produce the correct sequence of values to obtain the flag. Brute forcing a key is sometimes used within malware, as it can be used to frustrate researchers, but also to avoid detection by the antivirus software, as it can be used to bypass the sandbox duration of the application, and it might make the creation of an automated decryption method harder, as the key is not present in the code itself.
Conclusion
The two challenges that were included in this year’s iteration of HackFest’s CTF are both based upon pseudo random number generators and their seeds. Whereas the latter is slightly more based upon the CTF’s theme, both provide insight into possible uses for seeded randoms. One should not take the object’s type at face value, as there is more to it than meets the eye.
To contact me, you can e-mail me at [info][at][maxkersten][dot][nl], or DM me on Twitter @Libranalysis.