A fun CTF that has unreachable code, although I wonder how practical this is in real life!
Static Analysis with Ghidra
We throw the binary into Ghidra to analyze the main function
It’s pretty complicated.
On line 13, its creating a value based off srand() from iVar2
On line 15, it checks if there are command line parameters passed into the binary. If there is, it adds 8 to it, calls atoi (which converts a string to an integer), and assigns it to local_9
If the value of local_9 is lesser than 0 or equals to 0x7f (or 127 dec), then local_9 is assigned to the random variable initialized earlier.
Two more functions are called below, first of which is an unconditional call, and the second depends on the output of the prior, and takes in some data and local_9 as it’s inputs.
Lets analyze the first function:
This function tries to open the file “/tmp/secret”. If it does not exists, the program exits.
If the contents of the file is equal to “VerySuperSeKretKey”, it also exits. This key was meant as a misdirection.
It will only return 1 when the value of the file is -0x22, -0x53, -0x42, -0x11. Converting them to their unsigned equivalent, we need to have 0xDE, 0xAD, 0xBE, 0xEF (A nice play on 0xDEADBEEF). We can achieve this by using Python to write bytes to a file
After creating this file, it will return 1, which allows the execution of the second function
This function XORs each character of the RO data (param_1) with the value that was created earlier (param_2). It then prints out the hex equivalent using printf(“%x”). It does it 0x12 times (or 18 times), because that’s the length of the RO data
After the XORing, there is an anti-debug technique with PTRACE that we can overcome by patching EAX to 0.
Dynamic Analysis with IDA
Once we have analyzed the code and created the prerequisites like the “/tmp/secret” file, we then perform dynamic analysis of the binary.
The trouble here is that because param_2 (the value to be XORed with param_1) is a wide range, we cannot guess what value it is. The constrains of param_2 only says that it has to be larger than 0, and cannot be 127, if not, it becomes a random value.
We brute force XOR with to try to figure out what param_2 should be. We first convert it from it’s Hex values to string equivalent, and try various XOR values.
We discover the parameter to enter to be 0x58, or the character “X”. But since param_2 is passed as an int, we need to enter the integer equivalent of X, which is 88.
However, the output is going to be the hex value of “VerySuperSeKretKey”, which is wrong.
It’s only after switching to text view did I see this piece of unreachable code after it prints out “Are you sure it’s the right one? . .”
We see bunch of weird strings and another PTRACE anti-debug. One trick I learnt from this CTF is that you can set your instruction pointer to the line of code by right clicking, and Set IP. This set the RIP to the start of the function, and allows us to run the chunk of code.
When we run the code and bypass PTRACE, we see this long string being printed out:
Or if it’s too small: ffffffe3fffffffcfffffffdfffffffbfffffff7fffffffcfffffffdffffffeaffffffc7ffffffebffffffecfffffffdfffffffefffffff9fffffff6fffffff7ffffffa9ffffffa9ffffffa0ffffffc7ffffffb9ffffffb9ffffffc7ffffffe5
These are signed hex values, and even if we take the right most value, this does not make any sense.
Trying another method, we use the earlier XOR key we found to be “X”, and try to XOR the string and see what we get.
Sure enough, we get something! However, this key is still wrong, although very close.
If we look at the text “<=;7<=*\a+,=>967ii`\ayy\a%”, “\a” is actually a bell sound, and not to be interpreted as 2 separate characters which decodes to “.9”
We convert all of the character to their hex values, and replace “\a” with the hex value of BELL.
23 3c 3d 3b 37 3c 3d 2a 5c 61 2b 2c 3d 3e 39 36 37 69 69 60 5c 61 79 79 5c 61 25
Replacing 5c 61 with 07
23 3c 3d 3b 37 3c 3d 2a 07 2b 2c 3d 3e 39 36 37 69 69 60 07 79 79 07 25
And now convert from HEX to ASCII and XOR it with “X” to get the flag!