‹ Back to blog

Fun Fact

This challenge should help you to learn more about reverse engineering Python code. But if not, at least you can learn more about sea creatures!!

The flag format is byuctf{message}, and the attachment is obfuscated.py.

Opening obfuscated.py shows a very large base64 string being decoded and passed straight to exec().

import base64

string = "..."  # one very long base64 value
exec(base64.b64decode(string))

Running unknown decoded code is not a good first move. We only need the text, so I copied the string and decoded it without executing the result.

$ echo "..." | base64 -d > decoded.py
$ less decoded.py

The decoded program is much less intimidating. It presents three menu choices. Option one claims it will print the flag and then says it is not that easy. Option two selects one of 43 ocean facts, which explains the challenge theme. Option three asks for a flag, transforms it and compares the result against a constant string. That is the part worth following.

def option_three():
    user_input = input("\nFlag> ")

    random_array = xor(
        "Snowflake eels have two sets of jaws",
        "pretty crazy, huh?"
    )
    other_random_array = list(string.printable)
    key = other_random_array[random_array[0] + random_array[8]]

    encrypted = "".join([
        chr(ord(x) ^ ord(key)) for x in user_input
    ])
    print("encrypted: ", encrypted)

    if encrypted == 'g%4c$zc%dz4gg;':
        print("Success!")
    else:
        print("\nTry again")
        option_three()

The custom xor() function takes two strings and returns a list of integer XOR results. It repeats the shorter input when necessary, so every element depends only on the two hard-coded sea-creature sentences.

def xor(a, b):
    key = []
    i = 0
    while i < len(a):
        key.append(
            ord(a[i % len(a)]) ^ ord(b[i % len(b)])
        )
        i += 1
    return key

That means random_array is not random at all. The program adds its first and ninth elements and uses the result as an index into string.printable. We can reproduce those few lines in a Python shell instead of tracing the entire menu.

>>> import string
>>> def xor(a, b):
...     return [
...         ord(a[i % len(a)]) ^ ord(b[i % len(b)])
...         for i in range(len(a))
...     ]
...
>>> random_array = xor(
...     "Snowflake eels have two sets of jaws",
...     "pretty crazy, huh?"
... )
>>> random_array[0], random_array[8]
(35, 23)
>>> random_array[0] + random_array[8]
58
>>> string.printable[58]
'W'

The single-byte key is W. For each character supplied by the user, the program XORs its Unicode value with ord('W') and turns the result back into a character. The transformed string must equal g%4c$zc%dz4gg;.

XOR is its own inverse: if plaintext XOR key = ciphertext, then ciphertext XOR key = plaintext. We can therefore apply the exact same loop to the comparison string.

ciphertext = "g%4c$zc%dz4gg;"
key = "W"

plaintext = "".join(
    chr(ord(char) ^ ord(key))
    for char in ciphertext
)
print(plaintext)

The script prints:

0rc4s-4r3-c00l

Putting that message into the format from the challenge description gives us the flag:

byuctf{0rc4s-4r3-c00l}

So the ominous base64 blob eventually collapses to one printable character, W, and a reversible XOR.