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 offset | data length | hash | cracked |
---|
0x00 | 0x01 | 865C0C0B4AB0E063E5CAA3387C1A8741 | i |
0x01 | 0x05 | 5E93DE3EFA544E85DCD6311732D28F95 | pwned |
0x06 | 0x03 | 8FC42C6DDF9966DB3B09E84365034357 | the |
0x09 | 0x06 | 2D8AA42A0347C2D66CC86A0138DC9664 | puzzle |
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:
hash | data |
---|
d9a5b7aa4fb65c8c2953abed47ec9109c8082e6f | rehash |
ac20d084c192f03422d4d714dfe9c4179807ef63 | compiler |
1683de731aea959389f83044cdf3c9ca70a2aaf3 | featureless |
bdb480de655aa6ec75ca058c849c4faf3c0f75b1 | cc |
cc1e0b1e0cf8af90df54d8b53107f825fe26ba8f | ampere |
f870b0c75c7ba9eb427ba81f42c4fd01c26c6bc6 | cipherer |
25fe336ebc9fe3a74c5a17b8c2e2c3d1227a8128 | desperate |
af9b6d7b20c0af7fd4c308ead21aba9612cb86b8 | sweetroot |
7af78c911d5b48bea1dc2449d9d89513abeb4be5 | cisco |
74f55636889c72163a7508fad64f18c9c6a5e882 | diligent |
7c34d3efccfda40a760025ab90250bec2fb008e8 | mayonnaise |
e852191079ea08b654ccf4c2f38a162e3e84ee04 | crack |
e97ea9f3e0a8b30d4a233d1d448f4a8708b9dc13 | tamper |
f564946202b1182fc8aaf567507e6d9e02c99f24 | sleight |
24ac4f3e49e7fb12c8d6f54a0da37973473a2e10 | covertly |
897d21056a341314b60764c31b36c1fad542e78a | socket |
e4bbe5b7a4c1eb55652965aee885dd59bd2ee7f4 | android |
441cb0ed81450a3d0c3c137fc364d9cb4877a696 | dissembler |
8bfe65513ae6e6a1af42c06d65fe37c9eec15df8 | crypt |
54fd1711209fb1c0781092374132c66e79e2241b | g |
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!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.
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!