Mark's Cybersecurity Write-Ups

Hack The Boo 2023 - Practice - Crypto - Hexoding

In order to be a successful ghost in the modern society, a ghost must fear nothing. Caspersky always loved scaring people, but he could not reach his maximum potential because he was fearful of cryptography. This is why he wants to join the Applied Cryptography Academy of Ghosts. To gain admission, the professors give you a challenge that you need to solve. They try to spook you with weird functions, but don’t be scared; the challenge can be solved even without the source code. Can you help Caspersky pass the entrance exams? Submit flag & press enter

We’re provided two files:

output.txt

4854427b6b6e3077316e675f6830775f74305f3164336e743166795f336e633064316e675f736368336d33735f31735f6372756331346c5f6630725f615f
Y3J5cHQwZ3I0cGgzcl9fXzRsczBfZDBfbjB0X2MwbmZ1czNfZW5jMGQxbmdfdzF0aF9lbmNyeXA1MTBuIX0=

source.py

from secret import FLAG

HEX_CHARS = '0123456789abcdef'
B64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'


def to_hex(data):
    data = int.from_bytes(data, 'big')
    encoded = ''
    while data:
        i = data % 16
        encoded = HEX_CHARS[i] + encoded
        data >>= 4
    return '0' * (len(encoded) % 2) + encoded


def to_base64(data):
    padding_length = 0

    if len(data) % 3 != 0:
        padding_length = (len(data) + 3 - len(data) % 3) - len(data)

    data += b'\x00' * padding_length
    bits = ''.join([bin(c)[2:].zfill(8) for c in data])
    blocks = [bits[i:i+6] for i in range(0, len(bits), 6)]

    encoded = ''
    for block in blocks:
        encoded += B64_CHARS[int(block, 2)]

    return encoded[:-padding_length] + '=' * padding_length


def main():
    first_half = FLAG[:len(FLAG)//2]
    second_half = FLAG[len(FLAG)//2:]

    hex_encoded = to_hex(first_half)
    base64_encoded = to_base64(second_half)

    with open('output.txt', 'w') as f:
        f.write(f'{hex_encoded}\n{base64_encoded}')

main()

The first thing I did was put the entire output into Cyberchef and try to base64 decode it, which does get us the second half of the flag, but the first half is all garbled.

..._crypt0gr4ph3r___4ls0_d0_n0t_c0nfus3_enc0d1ng_w1th_encryp510n!}

So taking a quick look a the code, we can see that it does in fact split the flag into two halfs:

first_half = FLAG[:len(FLAG)//2]
second_half = FLAG[len(FLAG)//2:]

And it puts each half on it’s own line, which I didn’t notice at first:

f.write(f'{hex_encoded}\n{base64_encoded}')

and its clear by the names of these variables that the first half is “hex encoded” and the second half is “base64 encoded”. This seems obvious now - the first line only uses hex characters (0-9a-f), where the second line includes characters that don’t make sense for hex (and is padded with an equals sign at the end).

So going back to CyberChef, we can hex decode the first half:

4854427b6b6e3077316e675f6830775f74305f3164336e743166795f336e633064316e675f736368336d33735f31735f6372756331346c5f6630725f615f

becomes

HTB{kn0w1ng_h0w_t0_1d3nt1fy_3nc0d1ng_sch3m3s_1s_cruc14l_f0r_a_

and the second half

Y3J5cHQwZ3I0cGgzcl9fXzRsczBfZDBfbjB0X2MwbmZ1czNfZW5jMGQxbmdfdzF0aF9lbmNyeXA1MTBuIX0=

becomes:

crypt0gr4ph3r___4ls0_d0_n0t_c0nfus3_enc0d1ng_w1th_encryp510n!}

And we now have the complete (very long) flag:

HTB{kn0w1ng_h0w_t0_1d3nt1fy_3nc0d1ng_sch3m3s_1s_cruc14l_f0r_a_crypt0gr4ph3r___4ls0_d0_n0t_c0nfus3_enc0d1ng_w1th_encryp510n!}

And the lesson here, as the flag tells us, is that encoding can be reversed without any sort of key or password, so we shouldn’t rely on it to keep our secrets.