skip to content
Site header image Minh Ton

crackmesone — duckzzy’s keygenme

A nice keygenme challenge with a difficulty of 2, written in C/C++ for Windows. This is my attempt at creating a keygen through static analysis of the challenge’s executable.


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.
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.
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 at 140001c31, 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 1515 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)
4’†y&Žæ=Ø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 3232 to 126126 (excluding 3232 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 [33,126][33, 126], that character is added to the username. We can build the username by doing that 1515 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!