Join the discord

Ceaser Creek Software RE challenges

26 Nov, 2017 23:00
Just recently I found this challenge released by Ceaser Creek Software - a Ohio based software security company.
The challenge is designed as entry level "interview" for potentially new employees.

From their blog posts recently, it turned out they had to release a easier version of the original 2015 challenge, because attendees complained by the difficulty level of the first one.
And like the Top Gear's host Jeremy Clarkson says minutes before everything goes awfully wrong - how hard could it be?

For the sake of linearity (does that word exist?) of the events, I'll start with the 2015 version that was supposed to be too hard, and continue with the nerfed 2017 one.



Ceaser Creek Software, 2015 reverse engineering challenge - challenge1.bin


The challenge was released on 1st of October 2015 and of course I'm too late to qualify for it.

Originally the file comes with a ".bin" extension, but throwing it in HxD shows what the real format actually is:
challenge1.bin header, HxDOffset      00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000    42 5A 68 39 31 41 59 26 53 59 DB 47 5B 63 04 00    BZh91AY&SYЫG[c..
00000010    FB 7F FF FF FF FF FF FF FF FF FF FF FF FF FF FF    ................
00000020    FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF    ................
The BZ signature of a typical .bzip archive is pretty obvious.

Extracting the challenge1 file and again viewing it in HxD, shows it's an ELF executable:
challenge1 header, HxDOffset      00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000    7F 45 4C 46 01 01 01 03 00 00 00 00 00 00 00 00    .ELF..........
00000000    02 00 03 00 01 00 00 00 0C 8D 04 08 34 00 00 00    ................
00000000    38 2E 1C 00 00 00 00 00 34 00 20 00 06 00 28 00    ................

I rarely do reverse engineering in Linux, but oh well - edb will be the debugger and IDA will be its sidekick.

Running the executable in Kali didn't print any output and I seriously thought it's broken, so I throw it in IDA to see for obvious things.

IDA couldn't resolve the main() procedure, but edb did break there, so it easily eliminated the "fun" of digging around the initialization code and did that for me:


From here I can locate it in IDA and use the decompiler:
main(), IDA decompileint __cdecl sub_8049140(int a1, int a2) {
    v17 = 0;
    v16 = 0;
    memset(&v8, 0, 0x64u);
    v4 = 0;
    v5 = 0;
    v6 = 0;
    v7 = 0;
    result = sub_8122A20(0x4000);
    v16 = result;
    if (result) {
        result = sub_81145A0(*(_DWORD *)(a2 + 4), &unk_8192DEC);
        v17 = result;
        if (result) {
            result = sub_8114790(v16, 0x4000, 1, v17);
            if (result == 1) {
                result = sub_8048E24(0, v16, 0x4000);
                v15 = result;
                if (result == 0x63637377) {
                    sub_8049910(&v14);
                    sub_8049560(&v14, v16, 1);
                    sub_8049740(&v13, &v14);
                    result = sub_8048220(&v13, &unk_8208480, 16);
                    if (!result) {
                        sub_8049910(&v14);
                        sub_8049560(&v14, v16 + 1, 5);
                        sub_8049740(&v12, &v14);
                        result = sub_8048220(&v12, &unk_8208490, 16);
                        if (!result) {
                            sub_8049910(&v14);
                            sub_8049560(&v14, v16 + 6, 3);
                            sub_8049740(&v11, &v14);
                            result = sub_8048220(&v11, &unk_82084A0, 16);
                            if (!result) {
                                sub_8049910(&v14);
                                sub_8049560(&v14, v16 + 9, 6);
                                sub_8049740(&v10, &v14);
                                result = sub_8048220(&v10, &unk_82084B0, 16);
                                if (!result) {
                                    sub_8126500(&v8, v16, 6);
                                    sub_8126500(&v9, v16 + 9, 6);
                                    *(_WORD *)((char *)&v8 + strlen((const char *)&v8)) = 49;
                                    sub_8115030(&v8);
                                    sub_8126500(&v4, v16, 15);
                                    *(_WORD *)((char *)&v4 + strlen((const char *)&v4)) = 33;
                                    sub_804901C(&v4, &v3);
                                    sub_8048E70(&unk_82084C0, 36, &v3);
                                    result = sub_8115030(&unk_82084C0);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    if (v17)
        result = sub_8114030(v17);
    if (v16)
        result = sub_8122DA0(v16);
    return result;
}
Lots of nested IFs and I don't see any console output functions, which explains the lack of messages when I first run it in the console.

Except the memset() in the beginning there's no other resolved functions, so I started debugging it in edb.

The first function - sub_8122A20() got one parameter - 0x4000, and after executing it, EAX contained a buffer of 0x4000 zero bytes, so that's clearly a malloc().
Next sub_81145A0() follows, and it's last parameter - unk_8192DEC was the key to get that right with no need of tracing it.
This unk_8192DEC is pointing to a "rb" string, so sub_81145A0() must be a fopen().
That was confirmed when I first traced it over and EAX was 0, so the program exited.

Creating a simple text file and passing it as parameter to challenge1 solved that, because now EAX was always bigger than 0.

Anton Chekhov said "If you say in the first chapter that there is a rifle hanging on the wall, in the second or third chapter it absolutely must go off."
So, if there's a fopen() in the code, there must be also an fread()/fwrite() and fclose() in there.

Let's find them, by using parameters and return values of what I already know.
v17 is the return value of fopen(), so it must be a handle.
It's used as 4th parameter by sub_8114790() and as only parameter for sub_8114030(), so the second function should be fclose().
The first one however can be fread() or fwrite() but knowing the v16 is a buffer and it's also passed to sub_8114790() makes it pretty obvious it's a fread().
Also, the last function sub_8122DA0() takes v16 as only parameter, and since it's last and its parameter is checked before executing it, it's pretty obvious this one is free().

Let's wrap up everything I know so far:
main(), IDA decompileint __cdecl sub_8049140(int a1, int a2) {
    v17 = 0;
    v16 = 0;
    memset(&v8, 0, 0x64u);
    v4 = 0;
    v5 = 0;
    v6 = 0;
    v7 = 0;
    result = malloc(0x4000);
    v16 = result;
    if (result) {
        result = fopen(*(_DWORD *)(a2 + 4), &"rb");
        v17 = result;
        if (result) {
            result = fread(v16, 0x4000, 1, v17);
            if (result == 1) {
                // trimmed
            }
        }
    }
    if (v17)
        result = fclose(v17);
    if (v16)
        result = free(v16);
    return result;
}

Alright. Having this information I build myself a 0x4000 bytes long file and continue down with sub_8048E24():
sub_8048E24(), IDA decompileint __cdecl sub_8048E24(int a1, int a2, int a3) {
    v6 = a2;
    for (i = ~a1; ;i = ((unsigned int)i >> 8) ^ dword_8208080[(unsigned __int8)(i ^ *(_BYTE *)v3)]) {
        v4 = a3--;
        if (!v4)
            break;
        v3 = v6++;
    }
    return ~i;
}
That binary NOT operator on the initialization of the for loop and on the result value is pretty damn familiar.

The two parameters the function takes are also a good hint, but the pre-calculated crc32 table that dword_8208080 is the fat spoiler here:
dword_8208080, IDA disassembly.data:08208080 dword_8208080   dd 0, 77073096h, 0EE0E612Ch, 990951BAh, 76DC419h, 706AF48Fh, 0E963A535h, 9E6495A3h
.data:08208080                 dd 0EDB8832h, 79DCB8A4h, 0E0D5E91Eh, 97D2D988h, 9B64C2Bh, 7EB17CBDh, 0E7B82D07h, 90BF1D91h
.data:08208080                 dd 1DB71064h, 6AB020F2h, 0F3B97148h, 84BE41DEh, 1ADAD47Dh, 6DDDE4EBh, 0F4D4B551h, 83D385C7h
.data:08208080                 dd 136C9856h, 646BA8C0h, 0FD62F97Ah, 8A65C9ECh, 14015C4Fh, 63066CD9h, 0FA0F3D63h, 8D080DF5h
.data:08208080                 dd 3B6E20C8h, 4C69105Eh, 0D56041E4h, 0A2677172h, 3C03E4D1h, 4B04D447h, 0D20D85FDh, 0A50AB56Bh
.data:08208080                 dd 35B5A8FAh, 42B2986Ch, 0DBBBC9D6h, 0ACBCF940h, 32D86CE3h, 45DF5C75h, 0DCD60DCFh, 0ABD13D59h
.data:08208080                 dd 26D930ACh, 51DE003Ah, 0C8D75180h, 0BFD06116h, 21B4F4B5h, 56B3C423h, 0CFBA9599h, 0B8BDA50Fh
.data:08208080                 dd 2802B89Eh, 5F058808h, 0C60CD9B2h, 0B10BE924h, 2F6F7C87h, 58684C11h, 0C1611DABh, 0B6662D3Dh
; trimmed
So sub_8048E24() is actually a crc32() function that calculates the CRC32 checksum (duh!) of the data I pass to the challenge1 executable.

Due to CRC32's nature (fast algorithm, short 32bit result, etc.), there is a slight possibility to perform a collision attack and actually construct a file that has a 0x63637377 checksum, but it will still take some significant amount of time.
Therefore, I'll patch this on the run in edb, and just NOP the check. That shouldn't be an issue in future, because the CRC32 checksum is used once - only here.

With some logic and few runs in edb, I was able to nail the next four functions - sub_8049910(), sub_8049560(), sub_8049740() and sub_8048220().
The decompiled sub_8049910() helped a lot here:
sub_8049910(), IDA decompileint __cdecl sub_8049910(int a1) {
    if ( sub_804B630() && !sub_8112EC0("OPENSSL_FIPS_NON_APPROVED_MD5_ALLOW") )
        sub_804A600((unsigned int)"md5_dgst.c", 80, (int)"Digest MD5 forbidden in FIPS mode!");
    return sub_8049880(a1);
}
A MD5 initialization routine. Nice.
Like before with the fopen() - if there's a MD5_init(), there also should be a set of additional functions with it.

Few executions back and forward in edb, and observing the input arguments to the rest of the functions and I was able to figure it out:
1. sub_8049910(arg1) is MD5_init(md5_object), like I already determined above, is creating the hashing procedure object;

2. sub_8049560(arg1, arg2, arg3) is MD5_assign(md5_object, data, length), assigning the length number of bytes from my input data to the initialized md5_object;

3. sub_8049740(arg1, arg2) is MD5_hash(md5_result, md5_object), hashing the data and putting it in the md5_result;

4. and finally sub_8048220(arg1, arg2, arg3) that turned out to be a memcmp(buff_a, buff_b, length), comparing my data's hash with the one stored in buff_b.

There are four checks using this same method and each of them uses a different chunk of my input data file.
Since I have the offsets of the hashed data and the comparison hashes I can take them and try to crack the MD5 hashes:
data offsetdata lengthhashcracked
0x000x01865C0C0B4AB0E063E5CAA3387C1A8741i
0x010x055E93DE3EFA544E85DCD6311732D28F95pwned
0x060x038FC42C6DDF9966DB3B09E84365034357the
0x090x062D8AA42A0347C2D66CC86A0138DC9664puzzle

Nice, all of the words were simple enough so simple google search can do the job.
So, according to these last checks, the first bytes of my data should be "ipwnedthepuzzle" and the rest doesn't matter as long the whole file is 0x4000 bytes or more long.

Putting this to the test (and again NOP-ing the CRC32 check) gave me this result:


Challenge completed. Just for the heck of it, I looked around a bit more to see how the "success" message is constructed.
That's done inside sub_8048E70():
sub_8048E70(), IDA decompileunsigned int __cdecl decrypt(unsigned int *a1, unsigned int a2, int a3) {
    v3 = a1[a2 + 0x3FFFFFFF];
    v8 = *a1;
    result = 0x9E3779B9 * (0x34 / a2 + 6);
    for (i = 0x9E3779B9 * (0x34 / a2 + 6); i; i += 0x61C88647) {
       v5 = (i >> 2) & 3;
       for ( j = a2 - 1; (signed int)j > 0; --j ) {
           a1[j] -= ((4 * v8 ^ (a1[j + 0x3FFFFFFF] >> 5)) + ((v8 >> 3) ^ 16 * a1[j + 0x3FFFFFFF])) ^ ((v8 ^ i)
                    + (a1[j + 0x3FFFFFFF] ^ *(_DWORD *)(4 * (v5 ^ j & 3) + a3)));
           v8 = a1[j];
       }
       *a1 -= ((4 * v8 ^ (a1[a2 + 0x3FFFFFFF] >> 5)) + ((v8 >> 3) ^ 16 * a1[a2 + 0x3FFFFFFF])) ^ ((v8 ^ i)
                    + (a1[a2 + 0x3FFFFFFF] ^ *(_DWORD *)(4 * (v5 ^ j & 3) + a3)));
       v8 = *a1;
       result = 0x9E3779B9;
    }
    return result;
}

Looks a bit messy, but that's only because the imperfections of IDA's decompiler.
The algorithm used by the authors is XXTEA, and the decryption implementation I shamelessly stole from Wikipedia proves that:
PoC, using XXTEA implementation taken from Wikipedia.org#include <windows.h>
#include <stdio.h>
#include <stdint.h>

#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))

void btea(uint32_t *v, int n, uint32_t const key[4]) {
    uint32_t y, z, sum;
    unsigned p, rounds, e;
    n = -n;
    rounds = 6 + 52/n;
    sum = rounds*DELTA;
    y = v[0];
    do {
        e = (sum >> 2) & 3;
        for (p=n-1; p>0; p--) {
            z = v[p-1];
            y = v[p] -= MX;
        }
        z = v[n-1];
        y = v[0] -= MX;
        sum -= DELTA;
    } while (--rounds);
}

int main() {
    byte data[] = "\xC3\xF5\x63\x8D\xBB\x07\xD7\xDE\x96\xE6\xEE\x73\x33\xCB\x28\x87"\
                  "\xBB\x6A\xD4\xE4\xCC\x26\x7A\xAB\xB2\x53\x00\x9D\x4F\x70\xBF\x58"\
                  "\x70\xE3\xB5\x65\x5E\x15\x5B\x55\x75\x18\x02\x03\xA7\x1B\x4B\x34"\
                  "\x73\xC0\x88\xE8\x69\xE7\x8B\x66\x77\x25\xE3\xD2\xA7\x65\x65\x90"\
                  "\x13\xDF\xD9\xF0\x0D\x4E\xD6\xA8\xF6\x86\xBC\xFE\x1A\x1F\x62\x52"\
                  "\xB9\x47\x20\x55\x9B\x72\x6F\x1E\xF6\x62\x3D\xF9\x83\x84\xAC\x51"\
                  "\x32\xC3\xA5\x0B\x61\xB6\x18\x71\xE7\xC7\xD9\x86\x87\x26\xF4\x8E"\
                  "\xC2\x05\x7E\x79\x37\xAF\xA5\x0B\xFA\x5A\x71\x43\xBF\x5C\x88\x22"\
                  "\x14\xC2\x91\xF1\x02\x6D\x8C\x70\x9B\x98\x30\xFF\x16\x65\xD6\x43";
    byte key[] = "ipwnedthepuzzle!ipwnedthepuzzle!";
    int data_len = -(sizeof(data)/sizeof(uint32_t));

    btea((uint32_t*)data, data_len, (uint32_t*)key);

    printf("Decrypted: \"%s\"\n", data);

    return 0;
}


And that's all about the 2015 challenge.

Time to steam-roll the 2017 one!



Ceaser Creek Software, 2017 reverse engineering challenge - qwertyuiop.xf3


Like before, first I loaded the file in HxD to determine its format:
qwertyuiop.xf3 header, HxDOffset      00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000    50 4B 03 04 3F 00 00 00 0E 00 04 72 2C 4B 03 08    PK..?......r,K..
00000010    5D 1F 98 23 00 00 00 58 00 00 0C 00 00 00 71 77    ]..#...X......qw
00000020    65 72 74 79 75 69 6F 70 2E 31 10 04 05 00 5D 00    ertyuiop.1....].

This time we have a zip archive containing one file named "qwertyuiop.1", so let's unzip it and check it in HxD:
qwertyuiop.1 header, HxDOffset      00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000    4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00    MZђ.........яя..
00000000    B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00    ё.......@.......
00000000    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................

Alright, it's a Windows executable this time, so let's run it:


Unlike the previous one, here the authors made a nice CLI interface, so that's good.
Because of the CLI interface however, the IDA decompile is quite bulky, so I snipped most of the UI related code:
main(), IDA decompileint __cdecl main(char a1) {
    // trimmed - function initialization, variables, etc.
    v61 = 0x33323130; // "0123456789abcdef"
    v62 = 0x37363534;
    v63 = 0x62613938;
    v64 = 0x66656463;
    v65 = 0;
    v52 = GetStdHandle(0xFFFFFFF5);
    v51 = GetStdHandle(0xFFFFFFF6);
    hConsoleOutput = v2;
    v3 = "d9a5b7aa4fb65c8c2953abed47ec9109c8082e6f"; // what's this?
	
    for ( i = 0; ; v3 = off_4065C0[i >> 1] ) {
        *(&Str2 + i) = v3[i];
        v5 = v3[i + 1];
        i += 2;
        byte_40703F[i] = v5;
        if (i == 40)
            break;
    }
    *(_WORD *)Str1 = 0;

    // trimmed - console initialization and top border
    WriteConsoleA(v6, &unk_406612, 1, &NumberOfCharsWritten, 0);
    sub_401610((const char *)&unk_4060FD, 0);
    sub_401610("Welcome to outLink!", 0);
    sub_401610((const char *)&unk_4060FD, 0);
    sub_401610("  We were recently made aware that our password database may have been", 1);
    sub_401610("  compromised. Please enter your previous password to verify your identity", 1);
    sub_401610("  in order to begin the process of changing your password.", 1);
    sub_401610(" ", 0);
    // trimmed - output bottom part of the interface

    // start the user input and code validation loop
    while (2) {
        NumberOfCharsWritten = 0;
        // trimmed - more console related code
        while (ReadConsoleA(hConsoleInput, &Buffer, 1u, &NumberOfCharsWritten, 0)) {
            // trimmed - handle the CLI interface for the user input
        }
        // trimmed - more console code

        hConsoleInputa = sub_4030F0(pbData, 256); // that's a strlen()
        pdwDataLen = 20;
        if (!sub_401890(pbData, &pdwDataLen, hConsoleInputa, 0x8004u)) { // I should check this procedure
            // trimmed - simple "byte array" to "hex string" routine
        }
        // trimmed
        if (strncmp(Str1, (const char *)lpConsoleScreenBufferInfo, nNumberOfCharsToWrite)) {
            sub_401BA0("Incorrect Password, Try Again");
            continue; // loop will continue therefore, being here means I've entered a wrong password
        }
        break; // this break will get me out of the here if I've entered a valid password
    }
    // trimmed - rest of the code
    // I'll get back to it, after checking the code so far

    // to be continued

That v3 variable holding "d9a5b7aa4fb65c8c2953abed47ec9109c8082e6f" in the beginning looks suspicious.

The function sub_401890() is right before the strncmp(), so let's look what it holds:
sub_401890(), IDA decompileDWORD __usercall sub_401890@<eax>(BYTE *pbData@<ecx>, DWORD *pdwDataLen@<edx>, BYTE *a3@<eax>, DWORD dwDataLen, ALG_ID Algid) {
    v5 = pdwDataLen;
    v6 = a3;
    v7 = pbData;
    phProv = 0;
    phHash = 0;
    NumberOfCharsWritten = 0;
    v8 = GetStdHandle(0xFFFFFFF4);
    if (CryptAcquireContextA(&phProv, 0, 0, 0x18u, 0xF0000000)) {
        if (CryptCreateHash(phProv, Algid, 0, 0, &phHash)) {
            if (CryptHashData(phHash, v7, dwDataLen, 0)) {
                if (CryptGetHashParam(phHash, 2u, v6, v5, 0)) {
                    CryptReleaseContext(phProv, 0);
                    CryptDestroyHash(phHash);
                    return 0;
                }
                v14 = GetLastError();
                v15 = sub_401860(&Buffer, 0x100u, "%s: 0x%lx\n", (unsigned int)"CryptGetHashParam failed");
            } else {
                v14 = GetLastError();
                v15 = sub_401860(&Buffer, 0x100u, "%s: %lu\n", (unsigned int)"CryptHashData failed");
            }
            WriteConsoleA(v8, &Buffer, v15, &NumberOfCharsWritten, 0);
            CryptReleaseContext(phProv, 0);
            CryptDestroyHash(phHash);
            result = v14;
        } else {
            v10 = GetLastError();
            v11 = sub_401860(&Buffer, 0x100u, "%s: %lu\n", (unsigned int)"CryptCreateHash failed");
            WriteConsoleA(v8, &Buffer, v11, &NumberOfCharsWritten, 0);
            CryptReleaseContext(phProv, 0);
            result = v10;
        }
    } else {
        v12 = GetLastError();
        v13 = sub_401860(&Buffer, 0x100u, "%s: %lu\n", (unsigned int)"CryptAcquireContext failed");
        WriteConsoleA(v8, &Buffer, v13, &NumberOfCharsWritten, 0);
        result = v12;
    }
    return result;
}

Nice. It's a hashing function, using Wincrypt API.
The algorithm used is passed by its ALG_ID, and for this call it's 0x8004 which is CALG_SHA1.
That explains the hash stored in v3 at the beginning.

From here, I started x86dbg and set a breakpoint at the call where sub_401890() is, to see what's getting hashed.
Turned out it's just the user input code. I actually expected some modifications to it, like salt but there were none.

So, the user code is SHA1 hashed and then compared against something. My first guess would be that something to be the hardcoded "d9a5b7aa4fb65c8c2953abed47ec9109c8082e6f" string.

Cracking hashes can be really time consuming and the authors knew that. They intentionally picked a easy enough password, so a simple google search gave me the "rehash" answer.

But that's just too easy, right?
It's a wrong answer, so let's get back to the debugger, this time placing a breakpoint at the strncmp():
x86dbg, stack0022FC50  0022FD32  "d9a5b7aa4fb65c8c2953abed47ec9109c8082e6f" ; that's "rehash"
0022FC54  00407040  "d920dede0c7be37fa17525c2447e79fabd775d1b" ; but what the heck is this and where did it came from?
Ok, there's some traps, and I just fell in one.

But why did that happened? The v3 was quite obvious, and it would be surprising if it was the correct password.
There was a for() loop right after v3 that I kinda ignored (whoops), so let's take a closer look:
Hash loop, IDA decompilev3 = "d9a5b7aa4fb65c8c2953abed47ec9109c8082e6f";
for (i = 0; ; v3 = off_4065C0[i >> 1]) {    // grab a data from the off_4065C0 array
    *(&Str2 + i) = v3[i];                   // save byte 1 at Str2
    v5 = v3[i + 1];                         // save byte 2 at v5
    i += 2;                                 // increment the iterator with 2
    byte_40703F[i] = v5;                    // finally store the second byte here
    if (i == 40)                            // terminate the loop at its 20'th iteration (because i += 2)
       break;
}
*(_WORD *)Str1 = 0;                         // zero termination? could be!
Ok, note to self - pay more attention. It's obvious that v3 is overwritten multiple times by whatever off_4065C0 holds.

Speaking of off_4065C0, let's see what this array holds:
off_4065C0, IDA disassembly.rdata:004065C0 off_4065C0      dd offset aD9a5b7aa4fb65c ; "d9a5b7aa4fb65c8c2953abed47ec9109c8082e6f"
.rdata:004065C4                 dd offset aAc20d084c192f0 ; "ac20d084c192f03422d4d714dfe9c4179807ef63"
.rdata:004065C8                 dd offset a1683de731aea95 ; "1683de731aea959389f83044cdf3c9ca70a2aaf3"
.rdata:004065CC                 dd offset aBdb480de655aa6 ; "bdb480de655aa6ec75ca058c849c4faf3c0f75b1"
.rdata:004065D0                 dd offset aCc1e0b1e0cf8af ; "cc1e0b1e0cf8af90df54d8b53107f825fe26ba8f"
.rdata:004065D4                 dd offset aF870b0c75c7ba9 ; "f870b0c75c7ba9eb427ba81f42c4fd01c26c6bc6"
.rdata:004065D8                 dd offset a25fe336ebc9fe3 ; "25fe336ebc9fe3a74c5a17b8c2e2c3d1227a8128"
.rdata:004065DC                 dd offset aAf9b6d7b20c0af ; "af9b6d7b20c0af7fd4c308ead21aba9612cb86b8"
.rdata:004065E0                 dd offset a7af78c911d5b48 ; "7af78c911d5b48bea1dc2449d9d89513abeb4be5"
.rdata:004065E4                 dd offset a74f55636889c72 ; "74f55636889c72163a7508fad64f18c9c6a5e882"
.rdata:004065E8                 dd offset a7c34d3efccfda4 ; "7c34d3efccfda40a760025ab90250bec2fb008e8"
.rdata:004065EC                 dd offset aE852191079ea08 ; "e852191079ea08b654ccf4c2f38a162e3e84ee04"
.rdata:004065F0                 dd offset aE97ea9f3e0a8b3 ; "e97ea9f3e0a8b30d4a233d1d448f4a8708b9dc13"
.rdata:004065F4                 dd offset aF564946202b118 ; "f564946202b1182fc8aaf567507e6d9e02c99f24"
.rdata:004065F8                 dd offset a24ac4f3e49e7fb ; "24ac4f3e49e7fb12c8d6f54a0da37973473a2e10"
.rdata:004065FC                 dd offset a897d21056a3413 ; "897d21056a341314b60764c31b36c1fad542e78a"
.rdata:00406600                 dd offset aE4bbe5b7a4c1eb ; "e4bbe5b7a4c1eb55652965aee885dd59bd2ee7f4"
.rdata:00406604                 dd offset a441cb0ed81450a ; "441cb0ed81450a3d0c3c137fc364d9cb4877a696"
.rdata:00406608                 dd offset a8bfe65513ae6e6 ; "8bfe65513ae6e6a1af42c06d65fe37c9eec15df8"
.rdata:0040660C                 dd offset a54fd1711209fb1 ; "54fd1711209fb1c0781092374132c66e79e2241b"

An array of strings.
And where are Str2 and byte_40703F located in the memory:
Str2 and byte_40703F, IDA disassembly.bss:0040703F byte_40703F     db ?
.bss:00407040 Str2            db ?
.bss:00407041                 db    ? ;
.bss:00407042                 db    ? ;
; trimmed
Alright, everything is getting clear now.

The loop iterates through the array of hashes, taking two bytes at a time, and move them to Str2 and byte_40703F that are two consecutive pointers.
In other words, it takes these bytes:
hash slicingd9a5b7aa4fb65c8c2953abed47ec9109c8082e6f
ac20d084c192f03422d4d714dfe9c4179807ef63
1683de731aea959389f83044cdf3c9ca70a2aaf3
bdb480de655aa6ec75ca058c849c4faf3c0f75b1
cc1e0b1e0cf8af90df54d8b53107f825fe26ba8f
f870b0c75c7ba9eb427ba81f42c4fd01c26c6bc6
25fe336ebc9fe3a74c5a17b8c2e2c3d1227a8128
af9b6d7b20c0af7fd4c308ead21aba9612cb86b8
7af78c911d5b48bea1dc2449d9d89513abeb4be5
74f55636889c72163a7508fad64f18c9c6a5e882
7c34d3efccfda40a760025ab90250bec2fb008e8
e852191079ea08b654ccf4c2f38a162e3e84ee04
e97ea9f3e0a8b30d4a233d1d448f4a8708b9dc13
f564946202b1182fc8aaf567507e6d9e02c99f24
24ac4f3e49e7fb12c8d6f54a0da37973473a2e10
897d21056a341314b60764c31b36c1fad542e78a
e4bbe5b7a4c1eb55652965aee885dd59bd2ee7f4
441cb0ed81450a3d0c3c137fc364d9cb4877a696
8bfe65513ae6e6a1af42c06d65fe37c9eec15df8
54fd1711209fb1c0781092374132c66e79e2241b

And the correct hash "d920dede0c7be37fa17525c2447e79fabd775d1b" is constructed here.

Again, using google, I got "applesauce" and trying it this time was a success:


Hooray me.
Out of curiosity, I checked these hashes and it turned out, they are all valid:
hashdata
d9a5b7aa4fb65c8c2953abed47ec9109c8082e6frehash
ac20d084c192f03422d4d714dfe9c4179807ef63compiler
1683de731aea959389f83044cdf3c9ca70a2aaf3featureless
bdb480de655aa6ec75ca058c849c4faf3c0f75b1cc
cc1e0b1e0cf8af90df54d8b53107f825fe26ba8fampere
f870b0c75c7ba9eb427ba81f42c4fd01c26c6bc6cipherer
25fe336ebc9fe3a74c5a17b8c2e2c3d1227a8128desperate
af9b6d7b20c0af7fd4c308ead21aba9612cb86b8sweetroot
7af78c911d5b48bea1dc2449d9d89513abeb4be5cisco
74f55636889c72163a7508fad64f18c9c6a5e882diligent
7c34d3efccfda40a760025ab90250bec2fb008e8mayonnaise
e852191079ea08b654ccf4c2f38a162e3e84ee04crack
e97ea9f3e0a8b30d4a233d1d448f4a8708b9dc13tamper
f564946202b1182fc8aaf567507e6d9e02c99f24sleight
24ac4f3e49e7fb12c8d6f54a0da37973473a2e10covertly
897d21056a341314b60764c31b36c1fad542e78asocket
e4bbe5b7a4c1eb55652965aee885dd59bd2ee7f4android
441cb0ed81450a3d0c3c137fc364d9cb4877a696dissembler
8bfe65513ae6e6a1af42c06d65fe37c9eec15df8crypt
54fd1711209fb1c0781092374132c66e79e2241bg


Now that I have the valid password, the only thing that left is to check where and how the success message is constructed:
main() second stage, IDA decompile
    // ... continuing

    sub_401BA0("Thank you. Decrypted Message Below.");
    sub_401570();
    v58 = 32;
    // again hashing is used, this time with ALG_ID = 0x800C, that is CALG_SHA_256
    if (!sub_401890(pbData, &v58, hConsoleInputa, 0x800Cu)) {
        // trimmed - byte array to hex string
    }
    sub_401E00(&NumberOfCharsWritten, &unk_406640, v71, 33);    // decrypt part1 of the success message
    sub_401E00(&NumberOfAttrsWritten, &unk_406620, v71, 33);    // and part2

    // trimmed - final console output of the success message

    WriteConsoleA(v52, "Press any key to end the program...\n", 36, &NumberOfCharsRead, 0);
    ReadConsoleA(v51, &Buffer, 1u, &NumberOfCharsRead, 0);
    return 0;
}
So the success message is decrypted in two parts, by sub_401E00(), using the SHA256 hash of "applesauce" as key.

The IDA decompiled decryptor is one giant mess, but it turned out, the message is decrypted by a simple XOR like this:
Success message decryptor, C#include <windows.h>
#include <stdio.h>

int main() {
    // the data is concatenated unk_406640 and unk_406620
    byte data[] = "\x34\xE1\x88\x60\x4C\xE4\x54\xB3\xCB\xBC\xB3\x0D\x9E\xF6\x2E\xC9"\
                  "\x23\xAE\x92\x9D\xA0\x4B\xC2\xAA\xAA\xBB\x1B\x59\x6D\xCA\x1B\x9F"\
                  "\x00\xA9\x97\x7B\x42\xE3\x54\xB0\x8E\xFB\xFC\x3A\xD3\xEE\x28\xD0"\
                  "\x71\xEF\x8C\x88\xB4\x5A\x91\xA8\xB7\xB9\x12\x6A\x39\x95\x3C\x99";
	// the keys is SHA256("applesauce")
    byte key[] = "\x63\x84\xE4\x0C\x6C\x80\x3B\xDD\xAE\x9D\x93\x48\xF3\x97\x47\xA5"\
                 "\x03\xCF\xE2\xED\xCC\x2E\xB1\xCB\xDF\xD8\x7E\x06\x5C\xFB\x5B\xFC";

    for(int i = 0; i < 0x40; i++) {
        printf("%c", data[i] ^ key[i%0x20]);
    }

    return 0;
}

And that's all about this challenge!

Comments

* You have an opinion? Let us all hear it!

XpoZed 31 Aug, 2023 22:15
The polytable of crc32 is very easy to recognize, just because it always start with a 0 DWORD.
So is MD5, once you start noticing its four init vars being assigned in an array.
RC4 is also easy to recognize by its matrix building loop, followed by the PRGA using the key, and so on.
Through experience, you start recognizing certain patterns.
Guest 26 Aug, 2023 11:01
Wow! This is phenomenal work!

I kind of know what your answer will be, but how were you able to determine which actions looked like what under the hood?
E.g. - "Oh, this has GOTTA be a malloc, or oh this is almost CERTAINLY a MD5 init routine?

It's well done regardless - Thanks for a great (well broken down) read!
© nullsecurity.org 2011-2024 | legal | terms & rules | contacts