Overview
This is a straightforward and classic reverse engineering challenge. It is a windows console application that will validate user input and checks the flag entered. I will go into a bit more details since this is a write up for beginners rather than experienced reverse engineers.
Understanding the target
The first step to reverse engineering is figuring out how the target binary is constructed. I don’t want to waste our time reversing a packer or .NET bytecodes in IDA. So, I used Detect It Easy to check the binary.
It’s written in C++ which may have some mangled functions and complicated classes. But it is not that bad to reverse engineer in a classic decompiler like IDA or Ghidra.
Decompile
Loading the binary in IDA, I can see most of the logic is actually written in the main method. That saved me a lot of time as there is not much structures or classes to worry about which tends to be the most time consuming part of reverse engineering a C++ program.
As the main method is fairly long, I will just copy paste the IDA decompilation output here.
int __cdecl main(int argc, const char **argv, const char **envp)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v3 = (char *)operator new(0x33ui64);
v4 = sub_1400026E0(std::cout, (__int64)"Are you ready for [REDACTED]?");
std::ostream::operator<<(v4, sub_1400028B0);
std::istream::getline(std::cin, Str1, 40i64);
if ( !strcmp(Str1, "nimic_interesant") )
{
strcpy_s(v3 + 34, 0x11ui64, Str1); // v3[34]+ nimic_interesant
memset(var_buf, 0, sizeof(var_buf));
sub_140002240((__int64)var_buf); // populate the var_buf
if ( (*((_BYTE *)&var_buf[2] + *(int *)(var_buf[0] + 4)) & 6) != 0 )
sub_1400026E0(std::cout, (__int64)"50 burpees");
*(_OWORD *)Str1 = 0i64;
v13 = 0i64;
v14 = 0i64;
std::istream::read(var_buf, Str1, 34i64);
*(_OWORD *)v3 = *(_OWORD *)Str1;
*((_OWORD *)v3 + 1) = v13;
*((_WORD *)v3 + 16) = v14;
v6 = v3[34];
*v3 ^= v6;
v3[1] ^= v3[35];
v3[2] ^= v3[36];
v3[3] ^= v3[37];
v3[4] ^= v3[38];
v3[5] ^= v3[39];
v3[6] ^= v3[40];
v3[7] ^= v3[41];
v3[8] ^= v3[42];
v3[9] ^= v3[43];
v3[10] ^= v3[44];
v3[11] ^= v3[45];
v3[12] ^= v3[46];
v3[13] ^= v3[47];
v3[14] ^= v3[48];
v3[15] ^= v3[49];
v3[16] ^= v6;
v3[17] ^= v3[35];
v3[18] ^= v3[36];
v3[19] ^= v3[37];
v3[20] ^= v3[38];
v3[21] ^= v3[39];
v3[22] ^= v3[40];
v3[23] ^= v3[41];
v3[24] ^= v3[42];
v3[25] ^= v3[43];
v3[26] ^= v3[44];
v3[27] ^= v3[45];
v3[28] ^= v3[46];
v3[29] ^= v3[47];
v3[30] ^= v3[48];
v3[31] ^= v3[49];
v3[32] ^= v6;
v3[33] ^= v3[35];
v7 = 0i64;
v8 = v3 - byte_140004508;
while ( byte_140004508[v7] == byte_140004508[v7 + v8] )
{
if ( ++v7 >= 34 )
{
v9 = "Nice. Now go get your presents :D";
goto LABEL_10;
}
}
v9 = "Just a few more crunches";
LABEL_10:
sub_1400026E0(std::cout, (__int64)v9);
*(__int64 *)((char *)var_buf + *(int *)(var_buf[0] + 4)) = (__int64)&std::ifstream::`vftable';
*(int *)((char *)&v10 + *(int *)(var_buf[0] + 4)) = *(_DWORD *)(var_buf[0] + 4) - 176;
sub_140002190((__int64)&var_buf[2]);
std::istream::~istream<char,std::char_traits<char>>(&var_buf[3]);
std::ios::~ios<char,std::char_traits<char>>(&var_buf[22]);
result = 0;
}
else
{
sub_1400026E0(std::cout, (__int64)"1000 pushups");
result = -1;
}
return result;
}
Analysis and solution
It is fairly obvious that after "Are you ready for [REDACTED]?"
is printed, it will wait on user to provide an input. It will get the first line of the input and see if it is equal to “nimic_interesant”. Then, it will continue to read 34 bytes from the input stream and store it in Str1. We will call these 34 bytes user_input and “nimic_interesant” the key from here on.
relevant code:
v4 = sub_1400026E0(std::cout, (__int64)"Are you ready for [REDACTED]?"); // cout << "Are you ready for [REDACTED]?"
std::ostream::operator<<(v4, sub_1400028B0);
std::istream::getline(std::cin, Str1, 40i64); // get the first line.
if ( !strcmp(Str1, "nimic_interesant") )
{
strcpy_s(v3 + 34, 0x11ui64, Str1); // v3[34]+ nimic_interesant
memset(var_buf, 0, sizeof(var_buf));
sub_140002240((__int64)var_buf); // populate the var_buf
if ( (*((_BYTE *)&var_buf[2] + *(int *)(var_buf[0] + 4)) & 6) != 0 )
sub_1400026E0(std::cout, (__int64)"50 burpees");
*(_OWORD *)Str1 = 0i64;
v13 = 0i64;
v14 = 0i64;
std::istream::read(var_buf, Str1, 34i64); // get the rest of the input
...
}
This is followed by a large chunk of xor operations:
v6 = v3[34];
*v3 ^= v6;
v3[1] ^= v3[35];
v3[2] ^= v3[36];
v3[3] ^= v3[37];
v3[4] ^= v3[38];
v3[5] ^= v3[39];
v3[6] ^= v3[40];
v3[7] ^= v3[41];
v3[8] ^= v3[42];
v3[9] ^= v3[43];
v3[10] ^= v3[44];
v3[11] ^= v3[45];
v3[12] ^= v3[46];
v3[13] ^= v3[47];
v3[14] ^= v3[48];
v3[15] ^= v3[49];
v3[16] ^= v6;
v3[17] ^= v3[35];
v3[18] ^= v3[36];
v3[19] ^= v3[37];
v3[20] ^= v3[38];
v3[21] ^= v3[39];
v3[22] ^= v3[40];
v3[23] ^= v3[41];
v3[24] ^= v3[42];
v3[25] ^= v3[43];
v3[26] ^= v3[44];
v3[27] ^= v3[45];
v3[28] ^= v3[46];
v3[29] ^= v3[47];
v3[30] ^= v3[48];
v3[31] ^= v3[49];
v3[32] ^= v6;
v3[33] ^= v3[35];
v3 is basically a 50 bytes buffer where the first 34 bytes are the user_input and the next 16 bytes are the key. It is essentially doing the following:
for i in range (34):
user_input[i] = user_input[i] ^ key[i%16]
Then, the while block following the xor operations:
v7 = 0i64;
v8 = v3 - byte_140004508;
while ( byte_140004508[v7] == byte_140004508[v7 + v8] )
{
if ( ++v7 >= 34 )
{
v9 = "Nice. Now go get your presents :D";
goto LABEL_10;
}
}
This is using v7 as an index and v8 is the offset from v3 to byte_140004508. In a way, &v3 == &byte_140004508[v8]
. Understanding that, we can see it is checking if the first 34 bytes in v3 is the same as the first 34 bytes in byte_140004508. The bytes turned out to be:
['0x36', '0x44', '0x20', '0x28', '0x30', '0x24', '0x27', '0x1', '0x3', '0x3a', '0xb', '0xa', '0x6', '0x46', '0x1c', '0x11', '0x31', '0x1b', '0x8', '0x8', '0x7', '0x26', '0x36', '0x8', '0x1b', '0x17', '0x2d', '0xd', '0x1c', '0x9', '0x1', '0x1c', '0x1', '0x14']
So, to get the expected input, we can use the following script
key = "nimic_interesant"
expectedResult = ['0x36', '0x44', '0x20', '0x28', '0x30', '0x24', '0x27', '0x1', '0x3', '0x3a', '0xb', '0xa', '0x6', '0x46', '0x1c', '0x11', '0x31', '0x1b', '0x8', '0x8', '0x7', '0x26', '0x36', '0x8', '0x1b', '0x17', '0x2d', '0xd', '0x1c', '0x9', '0x1', '0x1c', '0x1', '0x14']
expectedInput = ""
for i in range(34):
expectedInput += chr(int(expectedResult[i],16) ^ ord(key[i%len(key)]))
print (expectedInput)
And you will have the flag:
X-MAS{Now_you're_ready_for_hohoho}