Long time no see, eh? Seemed like I had nothing better to do this weekend so I've decided to check what's new (and unsolved) on Crackmes.one
I've picked "
pranav's FinalFight crackme", due to the high difficulty rating, but it turned out it's more annoying than difficult.
So, without further ado, let's do this.
Initial analysis
The crackme comes with a cheesy readme:
Readme.txt"Hello, son. As you know by now, the coloradians have taken down all the 6 outpost planets, leaving us with just this home planet, and now they are coming for this. What little defence we have left will be no match for those damn battlestations of theirs. I had fear that this moment may come, and as a last resort, I had planted 2 of our best stealth operatives on one of their battleships at the cost of ten thousand soldiers. Their mission is, to access the nuclear warhead control facility and launch their own warheads at them, so that we can hopefully get an edge in the final fight. One of them managed to access and send their Nuclear warheads control program, but the other one was captured and killed while trying to obtain the secret code to launch the missiles. This is the fight to save us from extinction and we cannot stop no matter the loss."
"And that is where you come in, mister. You are the greatest hacker known in our entire system and it is time to put your skills for our homeworld. You should analyse the program and identify the correct code of the program, or else they will destroy our homeworld too, ending our civilization. There is no time to waste, our remaining operative, and you are the last hope of our civilization. May luck be with us..."
Mission: Find the correct code and upload it along with how you cracked it in text/pdf file to the main server. Make sure to include the details.
Restrictions: No patching is allowed. Patching it here won't change the actual program in their battlestation.
So, no patching, which is good because patching means you are halfassing it.
The crackme is tiny - less than 50 kilobytes, and it seems it's written in C (MinGW's GCC).
Directly running it prints the input prompt and entering a random code gives us the bad ending as expected:
Because there was no packer that DiE could recognize, I just throw it in IDA as usual.
The code doesn't looked obfuscated and the entire main() procedure looked like this:
Obviously there are some loops going on in the middle but let's start from the top:
main() start.text:004015D1 mov dword ptr [esp], offset Buffer ; "Colorado Military Systems v.3.1"
.text:004015D8 call _puts
.text:004015DD mov dword ptr [esp], offset aTargetsAssigne ; "Targets assigned.. Requesting Final con"...
.text:004015E4 call _puts
.text:004015E9 mov dword ptr [esp], offset Format ; "Enter secret code:"
.text:004015F0 call _printf
.text:004015F5 mov byte ptr [esp+3Bh], 0BFh
.text:004015FA mov byte ptr [esp+3Ch], 0A3h
.text:004015FF mov byte ptr [esp+3Dh], 43h
.text:00401604 mov byte ptr [esp+3Eh], 0ABh
.text:00401609 mov byte ptr [esp+3Fh], 99h
.text:0040160E mov dword ptr [esp], 186A0h
.text:00401615 call _malloc
.text:0040161A mov edi, eax
.text:0040161C mov [esp+4], eax
.text:00401620 mov dword ptr [esp], offset a100000sC ; "%100000s%*c"
.text:00401627 call _scanf
The "welcome" message is printed out, some unknown buffer is filled with 0xBF, 0xA3, 0x43, 0xAB and 0x99, and the user input is requested in the end.
Skipping the loops, I went to the end of the code:
main() end.text:00401849 cmp byte ptr [esp+14h], 0BFh
.text:0040184E jnz short loc_401886
.text:00401850 mov eax, 1
.text:00401855 movzx ebx, byte ptr [esp+eax+36h]
.text:0040185A cmp [esp+eax+3Bh], bl
.text:0040185E jnz short loc_401886
.text:00401860 add eax, 1
.text:00401863 cmp eax, 5
.text:00401866 jnz short loc_401855
.text:00401868 mov dword ptr [esp], offset aCodeAcceptedLa ; "\nCode Accepted. Launching Nuclear warh"...
.text:0040186F call _printf
.text:00401874 call _getchar
.text:00401879 mov eax, 0
.text:0040187E lea esp, [ebp-0Ch]
.text:00401881 pop ebx
.text:00401882 pop esi
.text:00401883 pop edi
.text:00401884 pop ebp
.text:00401885 retn
.text:00401886 mov dword ptr [esp], offset aSecretCodeWron ; "\nSecret code wrong! Initiating Facilit"...
.text:0040188D call _printf
.text:00401892 call _getchar
.text:00401897 mov dword ptr [esp], 0
.text:0040189E call _exit
Some check against 0xBF, a loop that compares bytes and depending on the final result we'll either get the good ending or the bad one.
The final loop compares 5 bytes (including the first 0xBF), and if we start from the second, it obviously compares the something with the five bytes stored in the beginning of the procedure.
Solving it
Using IDA's graph view, I can easily distinguish 5 separate loops, performing some basic math calculations on the user input.
The first one is pretty simple:
first crc loop.text:00401665 loc_401665:
.text:00401665 mov edx, ecx
.text:00401667 add ecx, 1
.text:0040166A movsx eax, byte ptr [edi+edx] ; eax = data[i]
.text:0040166E movsx edx, byte ptr [edi+edx+1] ; edx = data[i+1]
.text:00401673 imul eax, edx ; eax * edx
.text:00401676 cdq
.text:00401677 idiv ecx ; divided by i
.text:00401679 add ebx, eax ; ebx is the result
.text:0040167B cmp ecx, esi
.text:0040167D jnz short loc_401665
Past the loop, is this entire code:
basically % 0xFF.text:0040167F mov edx, 80808081h
.text:00401684 mov eax, ebx
.text:00401686 mul edx
.text:00401688 shr edx, 7
.text:0040168B mov eax, edx
.text:0040168D shl eax, 8
.text:00401690 sub eax, edx
.text:00401692 sub ebx, eax
.text:00401694 mov [esp+14h], ebx
Looks like it does complicated stuff, but this is basically implementing a modulo of 0xFF over the result of the loop.
And before you think I figure that by myself - no, I've used IDA's decompiler.
The exact code is used multiple times (well, five times to be precise) with each loop, so from now on just keep in mind that each loop result will perform a
modulo(0xFF) to its result.
Now back to the code.
Taking a quick glance of the rest of the loops, I basically have five different custom checksum algorithms, performed over the user input.
The LOBYTE result of each of these should be equal to the appropriate byte from that mysterious buffer from the beginning.
So, for the first loop, the LOBYTE(result) of it should be equal to 0xBF, the second loop should result in 0xA3 and so on.
To solve this I have to basically bruteforce it, which is not cool and I will totally do it in the worst possible way.
First of all I have to implement each checksum algorithm
crc_p1() to
crc_p5().
Next, if i just try to blindly permutate user code and run it through all those five checksum algorithms, it will take days if not ages in order to get a correct hit.
So, instead of this, I'll do it like that:
test_code()BOOL test_code(char *code_str) {
if (crc_p1(code_str) != 0xBF) {
return FALSE;
}
if (crc_p2(code_str) != 0xA3) {
return FALSE;
}
if (crc_p3(code_str) != 0x43) {
return FALSE;
}
if (crc_p4(code_str) != 0xAB) {
return FALSE;
}
if (crc_p5(code_str) != 0x99) {
return FALSE;
}
return TRUE;
}
That way if some of the check stages fail, I won't have to spend time calculating the rest.
Finally I need a good permutator, but I'm not a good coder, so in my case any would work:
my terrible permutatorvoid permutator(char *code_base, int max_depth) {
char code_str[0x20];
char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-";
memcpy(code_str, code_base, 0x20);
int cur_depth = strlen(code_base);
if (cur_depth == max_depth) {
return;
}
for (int c = 0; c < strlen(charset); c++) {
code_str[cur_depth] = charset[c];
if (test_code(code_str) == TRUE) {
printf("Valid code: '%s'\n", code_str);
}
if (cur_depth + 1 < max_depth) {
permutator(code_str, max_depth);
}
}
}
My code is ready, so the final step is play the waiting game.
I didn't care that much to wait for a second collision that will produce the correct results, so the first one was good enough for me - "
aaaaxqB7ch":
Hooray! Good job! Whatever.
Final rant
And here's where I should start explaining why a good, difficult crackme shouldn't rely on brute forcing.
See, if I give you the following code:
epic_crackme.pyimport hashlib
user_code = bytearray(input("Enter your password:"), "utf-8")
if hashlib.md5(user_code).hexdigest() == "73d5ebecde712e8e9d1c4d01fd6bf589":
print("Good job, you solve my epic crackme!")
else:
print("Fail! Try again.")
This is hard. You will never figure that the secret password is actually randomly tapping my keyboard to produce the string "
asdjkashdaskjhdjksakhdsakjdhjsad".
However, this is not a quality crackme.
I hope you get the hint.
Code implementation can be found here:
http://nullsecurity.org/download/1cce1ff0936f6689aeac0debc1a62624
Comments
* You have an opinion? Let us all hear it!