Level 2 - XIPHEREHPIX's Reckless Mistake

Domain(s): Crypto

Our sources told us that one of PALINDROME's lieutenants, XIPHEREHPIX, wrote a special computer program for certain members of PALINDROME. We have somehow managed to get a copy of the source code and the compiled binary. The intention of the program is unclear, but we think encrypted blob inside the program could contain a valuable secret.

We are given 2 files, the source code and the compiled binary.

The source code defines some helper functions:

  • getch() -> to get bytes from the terminal

  • input_password() -> to parse the bytes and output the password string

  • calculate_sha256() -> to calculate the sha256 of a given string

  • verify_password() -> to check if the password hash matches the hardcoded hash

The code also defines the function accumulate_xor, which xors each of the 4 items of 2 arrays, and a gcm_decrypt for decryption with AES GCM.

void accumulate_xor(uint256_t *result, uint256_t *arr_entry) {
    result->a0 ^= arr_entry->a0;
    result->a1 ^= arr_entry->a1;
    result->a2 ^= arr_entry->a2;
    result->a3 ^= arr_entry->a3;
}

The flag is expected to be displayed in the show_welcome_msg function, where AES GCM decryption is executed with some hardcoded arguments.

void show_welcome_msg(unsigned char *key) {
    int plaintext_length;
    unsigned char *iv = "PALINDROME ROCKS";
    
    unsigned char plaintext[128] = { 0 };
    const unsigned char * const header = "welcome_message";
    unsigned char ciphertext[] =
        "\xad\xac\x81\x20\xc6\xd5\xb1\xb8\x3a\x2a\xa8\x54\xe6\x5f\x9a\xad"
        "\xa4\x39\x05\xd9\x21\xae\xab\x50\x98\xbd\xe4\xc8\xe8\x2a\x3c\x63"
        "\x82\xe3\x8e\x5d\x79\xf0\xc6\xf4\xf2\xe7";

    unsigned char tag[] =
        "\xbd\xfc\xc0\xdb\xd9\x09\xed\x66\x37\x34\x75\x11\x75\xa2\x7a\xaf";

    plaintext_length = gcm_decrypt(ciphertext, 
                42,
                (unsigned char *)header,
                strlen(header),
                tag,
                key, 
                iv,
                16,
                plaintext);

    printf("Welcome PALINDROME member. Your secret message is %.*s\n", plaintext_length, plaintext);
}

We can view the execution flow of the code in the main() function.

int main(int argc, char **argv)
{
    char password[MAX_PASSWORD_SIZE + 1] = { 0 };
    int password_length;

    unsigned char key[32];

    printf("Hello PALINDROME member, please enter password sir:");

    password_length = input_password(password);
    if (password_length < 40) {
        printf("The password should be at least 40 characters as per PALINDROME's security policy.\n");
        exit(0);
    }

    if (!verify_password(password, password_length)) {
        initialise_key(key, password, password_length);
        show_welcome_msg(key);
    }
        
    else {
        printf("Failure! \n");
        exit(0);
    }
}

We can see that the program asks for an over 40 byte long password, which it passes into the initialise_key function.

Upon inspection of this function, we see that the password is not actually used in creating the key. This means that we can skip the password verification altogether.

void initialise_key(unsigned char *key, char *password, int password_length) {
    const char *seed = "PALINDROME IS THE BEST!";
    int i, j;
    int counter = 0;

    uint256_t *key256  = (uint256_t *)key;

    key256->a0 = 0;
    key256->a1 = 0;
    key256->a2 = 0;
    key256->a3 = 0;

    uint256_t arr[20] = { 0 };

    calculate_sha256((unsigned char *) arr, (unsigned char *) seed, strlen(seed));

    for (i = 1; i < 20; i++) {
        calculate_sha256((unsigned char *)(arr+i), (unsigned char *) (arr+i-1), 32);
    }

    for (i = 0; i < password_length; i++) {
        int ch = password[i];
        for (j = 0; j < 8; j++) {
            counter = counter % 20;

            if (ch & 0x1) {
                accumulate_xor(key256, arr+counter);
            }

            ch = ch >> 1;
            counter++;
        }
    }
}

The initialise_key function uses a hardcoded seed to generate the array of 20 SHA256 hashes. This means that the array of hashes used in the key generation is constant.

Moving on to the for loop, we can see that the password is only used for determining which of the arrays are XOR-ed to generate the key.

This means that the final key is generated solely from the same 20 SHA256 hashes, no matter what password is entered. Therefore we could bruteforce all combinations of all lengths of the SHA256 hashes until we find the flag.

import hashlib
from Crypto.Cipher import AES
import binascii
import itertools
from Crypto.Util.number import long_to_bytes

def gcm_decrypt(ciphertext, key, iv):
    cipher = AES.new(key, AES.MODE_GCM, iv) 
    plaintext = cipher.decrypt(ciphertext)
    return plaintext

ciphertext = b"\xad\xac\x81\x20\xc6\xd5\xb1\xb8\x3a\x2a\xa8\x54\xe6\x5f\x9a\xad\xa4\x39\x05\xd9\x21\xae\xab\x50\x98\xbd\xe4\xc8\xe8\x2a\x3c\x63\x82\xe3\x8e\x5d\x79\xf0\xc6\xf4\xf2\xe7"
iv = b"PALINDROME ROCKS"
header = b"welcome_message"
tag = b"\xbd\xfc\xc0\xdb\xd9\x09\xed\x66\x37\x34\x75\x11\x75\xa2\x7a\xaf"
seed = "PALINDROME IS THE BEST!"

# generate the constant array
arr = [None for _ in range(20)]
arr[0] = hashlib.sha256(seed.encode()).digest()
for i in range(1, 20):
    arr[i] = hashlib.sha256(arr[i-1]).digest()

# convert to int for XOR
arr = [int.from_bytes(x, "big") for x in arr]

# bruteforce all combinations
def bruteforce():
    for size in range(1, len(arr) + 1):
        for comb in itertools.combinations(arr, size):
            key = 0
            for i in comb:
                key ^= i
            key = long_to_bytes(key)
            key = key.ljust(32, b"\x00")
            plaintext = gcm_decrypt(ciphertext, key, iv)
            if b"TISC" in plaintext:
                print(f"{plaintext=}")
                print(f"{key=}")
                return

Flag: TISC{K3ysP4ce_1s_t00_smol_d2g7d97agsd8yhr}

Last updated