Crackme Exploration
With this crackme, we’re supposed to give it a username and a password. We need to find the correct username and the corresponding password to solve this challenge.
Let’s disassemble this executable!
Decompilation Overview
Main function decompilation
uint64_t main()
{
__main();
int32_t rbx;
if (!isDebuggerPresent())
{
timingCheck();
void var_1a9;
void* var_20_1 = &var_1a9;
char var_1d8[0x2f];
std::string::string<std::allocator<char> >(&var_1d8, "Username: ");
void var_1f8;
encryptString(&var_1f8);
std::string::~string();
void var_179;
void* var_28_1 = &var_179;
char var_1a8[0x2f];
std::string::string<std::allocator<char> >(&var_1a8, "Password: ");
void var_218;
encryptString(&var_218);
std::string::~string();
void var_149;
void* var_30_1 = &var_149;
char var_178[0x2f];
std::string::string<std::allocator<char> >(&var_178, "Correct! Access Granted.");
void var_238;
encryptString(&var_238);
std::string::~string();
void var_119;
void* var_38_1 = &var_119;
char var_148[0x2f];
std::string::string<std::allocator<char> >(&var_148, "Incorrect. Access Denied.");
void var_258;
encryptString(&var_258);
std::string::~string();
void var_e9;
void* var_40_1 = &var_e9;
char var_118[0x2f];
std::string::string<std::allocator<char> >(&var_118, "Debugger huh? Impressive.");
void var_278;
encryptString(&var_278);
std::string::~string();
std::string::string();
std::string::string();
void var_e8;
decryptString(&var_e8);
std::operator<<<char>(&_.data$_ZSt4cout, &var_e8);
std::string::~string();
std::string var_298;
std::operator>><char>(&_.data$_ZSt3cin, &var_298);
void var_c8;
decryptString(&var_c8);
std::operator<<<char>(&_.data$_ZSt4cout, &var_c8);
std::string::~string();
void var_2b8;
std::operator>><char>(&_.data$_ZSt3cin, &var_2b8);
if (!isDebuggerPresent())
{
if (!validate(&var_298, &var_2b8))
{
void var_68;
decryptString(&var_68);
std::ostream::operator<<(std::operator<<<char>(&_.data$_ZSt4cout, &var_68));
std::string::~string();
}
else
{
void var_88;
decryptString(&var_88);
std::ostream::operator<<(std::operator<<<char>(&_.data$_ZSt4cout, &var_88));
std::string::~string();
}
rbx = 0;
}
else
{
void var_a8;
decryptString(&var_a8);
std::ostream::operator<<(std::operator<<<char>(&_.data$_ZSt4cout, &var_a8));
std::string::~string();
rbx = 1;
}
std::string::~string();
std::string::~string();
std::vector<char>::~vector();
std::vector<char>::~vector();
std::vector<char>::~vector();
std::vector<char>::~vector();
std::vector<char>::~vector();
}
else
rbx = 1;
return (uint64_t)rbx;
}
Looking at the main()
function, there are some interesting things:
From the start to 140001af7
, what the program did was just encrypting plain-text strings, which gets decrypted later for printing using C++ std::cout
, so those encryption/decryption functions are definitely unrelated to the validation logic.
140001af7
, what the program did was just encrypting plain-text strings, which gets decrypted later for printing using C++ std::cout
, so those encryption/decryption functions are definitely unrelated to the validation logic.14000195d char var_1d8[0x2f];
14000195d std::string::string<std::allocator<char> >(&var_1d8, "Username: ");
140001970 void var_1f8;
140001970 encryptString(&var_1f8);
14000197f std::string::~string();
14000199b void var_179;
14000199b void* var_28_1 = &var_179;
1400019bf char var_1a8[0x2f];
1400019bf std::string::string<std::allocator<char> >(&var_1a8, "Password: ");
1400019d2 void var_218;
1400019d2 encryptString(&var_218);
1400019e1 std::string::~string();
1400019fd void var_149;
1400019fd void* var_30_1 = &var_149;
140001a21 char var_178[0x2f];
140001a21 std::string::string<std::allocator<char> >(&var_178,
140001a21 "Correct! Access Granted.");
140001a34 void var_238;
140001a34 encryptString(&var_238);
140001a43 std::string::~string();
140001a5f void var_119;
140001a5f void* var_38_1 = &var_119;
140001a83 char var_148[0x2f];
140001a83 std::string::string<std::allocator<char> >(&var_148,
140001a83 "Incorrect. Access Denied.");
140001a95 void var_258;
140001a95 encryptString(&var_258);
140001aa4 std::string::~string();
140001ac0 void var_e9;
140001ac0 void* var_40_1 = &var_e9;
140001ae4 char var_118[0x2f];
140001ae4 std::string::string<std::allocator<char> >(&var_118,
140001ae4 "Debugger huh? Impressive.");
140001af7 void var_278;
140001af7 encryptString(&var_278);
140001b06 std::string::~string();
140001b22 std::string::string();
140001b2e std::string::string();
140001b41 void var_e8;
140001b41 decryptString(&var_e8);
140001b57 std::operator<<<char>(&_.data$_ZSt4cout, &var_e8);
140001b66 std::string::~string();
At 140001b79
and 140001bc4
, the program reads the username and password into var_298
and var_2b8
, respectively. Since the program uses C++ std::cin
, which reads until whitespace, so both the username and password cannot contain spaces.
140001b79
and 140001bc4
, the program reads the username and password into var_298
and var_2b8
, respectively. Since the program uses C++ std::cin
, which reads until whitespace, so both the username and password cannot contain spaces.140001b57 std::operator<<<char>(&_.data$_ZSt4cout, &var_e8);
140001b66 std::string::~string();
140001b79 std::string var_298;
140001b79 std::operator>><char>(&_.data$_ZSt3cin, &var_298);
140001b8c void var_c8;
140001b8c decryptString(&var_c8);
140001ba2 std::operator<<<char>(&_.data$_ZSt4cout, &var_c8);
140001bb1 std::string::~string();
140001bc4 void var_2b8;
140001bc4 std::operator>><char>(&_.data$_ZSt3cin, &var_2b8);
- There’s a
validate()
function at140001c31
, which has the username and password in plain-text as its arguments. - The author implemented a timing check and a debugger check as well, which are both unrelated.
Time to look at the validate()
function to understand the credentials check.
The Validation Function
Since we’re pretty sure that the validate()
function accepts the username and password as the first and second argument, respectively, so I’m going to rename arg1
, arg2
for the pseudo-code to be more readable.
Key validation function decompilation
uint64_t validate(std::string const& username, std::string const& password)
{
int64_t rax_1;
(uint8_t)rax_1 = std::string::length() != 0xf;
uint64_t rbx_1;
if (!(uint8_t)rax_1)
{
std::string::length();
std::string::length();
std::string::string();
std::string::length();
void var_58;
std::string::resize(&var_58);
int64_t var_20_1 = 0;
while (true)
{
int64_t rax_24;
(uint8_t)rax_24 = var_20_1 < std::string::length();
if (!(uint8_t)rax_24)
break;
char rax_8 = *(uint8_t*)std::string::operator[](username);
*(uint8_t*)std::string::operator[](&var_58) = ((
(char)((int32_t)rax_8 >> (8 - ((uint8_t)var_20_1 & 3)))
| (char)((int32_t)rax_8 << ((uint8_t)var_20_1 & 3)))
+ (uint8_t)var_20_1 * 3) ^ 0x55;
var_20_1 += 1;
}
int64_t rax_27;
(uint8_t)rax_27 = std::string::length() != std::string::length();
if (!(uint8_t)rax_27)
{
int32_t var_24_1 = 0;
int64_t var_30_1 = 0;
while (true)
{
int64_t rax_37;
(uint8_t)rax_37 = var_30_1 < std::string::length();
if (!(uint8_t)rax_37)
break;
int32_t var_5c_1 = 0;
for (int32_t i = 0; i <= 4; i += 1)
var_5c_1 += 1;
rbx_1 = (uint64_t)*(uint8_t*)std::string::operator[](password);
var_24_1 |= (int32_t)(*(uint8_t*)std::string::operator[](&var_58)
^ (uint8_t)rbx_1);
var_30_1 += 1;
}
(uint8_t)rbx_1 = !var_24_1;
}
else
rbx_1 = 0;
std::string::~string();
}
else
rbx_1 = 0;
return (uint64_t)(uint32_t)rbx_1;
}
In short, the program requires both the username and password to be characters long, and the password have to exactly matches the byte-wise transformation of that username.
Also here’s the decompilation from IDA (which I found easier to understand):
Key validation function decompilation using IDA
__int64 __fastcall validate(__int64 usernameStrPtr, __int64 passwdStrPtr) {
__int64 v2; // rbx
__int64 v3; // rax
char v4; // bl
unsigned __int64 v5; // rax
_BYTE* v6; // rax
unsigned __int64 v7; // rax
int v9; // [rsp+2Ch] [rbp-44h]
_BYTE v10[35]; // [rsp+30h] [rbp-40h] BYREF
char v11; // [rsp+53h] [rbp-1Dh]
int k; // [rsp+54h] [rbp-1Ch]
unsigned __int64 j; // [rsp+58h] [rbp-18h]
int v14; // [rsp+64h] [rbp-Ch]
unsigned __int64 i; // [rsp+68h] [rbp-8h]
if (std::string::length(usernameStrPtr) == 15) {
std::string::length(usernameStrPtr);
std::string::length(usernameStrPtr);
std::string::basic_string(v10);
v3 = std::string::length(usernameStrPtr);
std::string::resize(v10, v3);
for (i = 0;; ++i) {
v5 = std::string::length(usernameStrPtr);
if (i >= v5)
break;
v11 = *(_BYTE*)std::string::operator[](usernameStrPtr, i);
v11 = (v11 << (i & 3)) | (v11 >> (8 - (i & 3)));
v11 += 3 * i;
v11 ^= 0x55u;
v4 = v11;
*(_BYTE*)std::string::operator[](v10, i) = v4;
}
v2 = std::string::length(passwdStrPtr);
if (v2 == std::string::length(v10)) {
v14 = 0;
for (j = 0;; ++j) {
v7 = std::string::length(passwdStrPtr);
if (j >= v7)
break;
v9 = 0;
for (k = 0; k <= 4; ++k)
++v9;
LODWORD(v2) =
*(unsigned __int8*)std::string::operator[](passwdStrPtr, j);
v6 = (_BYTE*)std::string::operator[](v10, j);
v14 |= (char)(v2 ^ *v6);
}
LOBYTE(v2) = v14 == 0;
} else {
LODWORD(v2) = 0;
}
std::string::~string(v10);
} else {
LODWORD(v2) = 0;
}
return (unsigned int)v2;
}
The magic actually lies in this for
loop:
for (i = 0;; ++i) {
v5 = std::string::length(usernameStrPtr);
if (i >= v5)
break;
v11 = *(_BYTE*)std::string::operator[](usernameStrPtr, i);
v11 = (v11 << (i & 3)) | (v11 >> (8 - (i & 3)));
v11 += 3 * i;
v11 ^= 0x55u;
v4 = v11;
*(_BYTE*)std::string::operator[](v10, i) = v4;
}
This piece of code basically encrypts character-by-character using bit rotations, arithmetic, and XOR operations to produce an encoded version stored in v10
.
And… the next for
loop is just a “fancy” way of comparing the transformed username against the password. By the way just ignore the unused inner for
loop there!
if (v2 == std::string::length(v10)) {
v14 = 0;
for (j = 0;; ++j) {
v7 = std::string::length(passwdStrPtr);
if (j >= v7)
break;
v9 = 0;
for (k = 0; k <= 4; ++k)
++v9;
LODWORD(v2) = *(unsigned __int8*)std::string::operator[](passwdStrPtr, j);
v6 = (_BYTE*)std::string::operator[](v10, j);
v14 |= (char)(v2 ^ *v6);
}
LOBYTE(v2) = v14 == 0;
Knowing this, we can definitely recreate the transformation algorithm using Python, to produce the password using a given username!
Making A Keygen
Okay, let’s recreate the algorithm in Python!
import string
def some_transformations(oc, i):
result = ((oc << (i & 3)) | (oc >> (8 - (i & 3)))) & 0xFF
result = (result + 3 * i) & 0xFF
result ^= 0x55
return result
def transform(username):
password = []
for i, c in enumerate(username):
oc = ord(c)
result = some_transformations(oc, i)
password.append(chr(result))
return ''.join(password)
>>> username = "absdgfhjuytrweq"
>>> password = transform(username)
>>> print(password)
4y&æ=ØXºáΤº
If we use a random username, the password would contain several unreadable characters, which… doesn’t seem right. What if we try to find a username that transforms into a printable password?
Looking at the ASCII table, the printable range is from to (excluding since we know that the username cannot contain spaces).
An approach would be to iterate through every printable characters and do some transformations on each character. If we encounter a character where the transformed result falls within the range of , that character is added to the username. We can build the username by doing that times!
We can modify the Python script a little bit to include that:
import string
chars = [chr(i) for i in range(33, 127)]
def some_transformations(oc, i):
result = ((oc << (i & 3)) | (oc >> (8 - (i & 3)))) & 0xFF
result = (result + 3 * i) & 0xFF
result ^= 0x55
return result
def transform(username):
password = []
for i, c in enumerate(username):
oc = ord(c)
result = some_transformations(oc, i)
password.append(chr(result))
return ''.join(password)
username = ''
for i in range(15):
found = False
for cc in chars:
oc = ord(cc)
result = some_transformations(oc, i)
if 33 <= result <= 126:
username += cc
found = True
break
password = transform(username)
print("Username:", username)
print("Password:", password)
And this is the output of this Python script:
Username: !/?!!)<!!#9"<!6
Password: t4WGx4WKl4Wg5<W
After giving the program our username and password, it prints Correct! Access Granted.
!
Surely we can continue modifying the script so that it prints out every valid usernames and their corresponding passwords, but hey at least we’ve cracked the crackme!