Join the discord

crackmes.one : pranav's FinalFight crackme

14 Jun, 2021 14:10
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!

There's no comments on this article yet. Be the first!
© nullsecurity.org 2011-2024 | legal | terms & rules | contacts