Overview

Sunshine CTF 2023 had two reverse-engineering challenges that my team completed (just before time ran out, too!). This post describes my thought process behind Dill, a challenge worth 100 points.

Dill

‘Dill’ provides players with a .pyc file that is unreadable to users. A .pyc file contains the bytecode of the python file, not the source code. The source code has already been compiled, and we need to decompile it to read it. We can also understand what the .pyc file is doing by running it in a debugger,

Luckily for us, this .pyc file can be easily decompiled by a tool called uncompyle.

  1. It can be downloaded here: https://pypi.org/project/uncompyle6/#files
  2. Choose the latest .whl file and install like: pip install uncompyle6-3.2.3-py27-none-any.whl.
  3. You should now be able to type uncompyle6 --version via commandline and receive a version number.
  4. With this tool, we can decompile dill.pyc like so: uncompyle6 dill.pyc.
Now we have the source code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Dill:
    prefix = 'sun{'
    suffix = '}'
    o = [5, 1, 3, 4, 7, 2, 6, 0]

    def __init__(self) -> None:
        self.encrypted = 'bGVnbGxpaGVwaWNrdD8Ka2V0ZXRpZGls'

    def validate(self, value: str) -> bool:
        return value.startswith(Dill.prefix) and value.endswith(Dill.suffix) or False
        value = value[len(Dill.prefix):-len(Dill.suffix)]
        if len(value) != 32:
            return False
        c = [value[i:i + 4] for i in range(0, len(value), 4)]
        value = ''.join([c[i] for i in Dill.o])
        if value != self.encrypted:
            return False
        return True

Here is what I notice when reading through the class:

  • the prefix and suffix are irrelevant to the encrypted string, since they are just part of the flag for Sunshine CTF.
  • self.encrypted must be the encrypted string. We will have to understand how it was created to understand how to decrypt it.
  • the length of the encrypted and decrypted string is the same (32)
  • there seems to be a strange ordering of numbers in array o declared at the top
  • c and value build the encrypted string. This is where we will be focusing the most, since we will have to undo this process.
I want to run this script locally to test it first. I removed the class and added a main function. I also wanted to understand what `c` and `value` were doing, so I printed them out.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
prefix = 'sun{'
suffix = '}'
o = [5, 1, 3, 4, 7, 2, 6, 0]

encrypted = 'bGVnbGxpaGVwaWNrdD8Ka2V0ZXRpZGls'

def validate(value: str) -> bool:
    print(value)
    if len(value) != 32:
        print("not right length")
    #for every group of 4 letters letter in value
    c = [value[i:i + 4] for i in range(0, len(value), 4)]
    print(c)
    value = ''.join([c[i] for i in o]) # add back the quartets in a strange order
    if value != encrypted:
        return False
    return True

def main():
    string = encrypted
    validate(string)

if __name__ == "__main__":
    main()

Output

challenges screenshot

Ah, so these blocks of 4 letters have been rearranged according to the order listed in o. To put them back, we have to create a new array that organizes them in order. Since 0 should be the first block and is listed last in the array o, we know that the last index 7 should be printed first. Similarly, since 1 is the second block and is printed at index 1. 2 is the third block and is printed at index 5.

o = [5, 1, 3, 4, 7, 2, 6, 0]

To print the blocks in the correct order again, we will use this new array:

a = [7, 1, 5, 2, 3, 0, 6, 4]

By reordering the blocks, we arrive at the solution:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
prefix = 'sun{'
suffix = '}'
o = [5, 1, 3, 4, 7, 2, 6, 0]
a = [7, 1, 5, 2, 3, 0, 6, 4]

encrypted = 'bGVnbGxpaGVwaWNrdD8Ka2V0ZXRpZGls'

def validate(value: str) -> bool:
    print(value)
    if len(value) != 32:
        print("not right length")
    #for every group of 4 letters letter in value
    c = [value[i:i + 4] for i in range(0, len(value), 4)]
    print(c)
    # add back the quartets out of order to 'encrypt' them
    value = ''.join([c[i] for i in o])
    # reorder the quartets to decrypt them
    unencrypt = ''.join([c[i] for i in a])
    print("here", unencrypt)
    if value != encrypted:
        return False
    return True

def main():
    string = encrypted
    validate(string)

if __name__ == "__main__":
    main()
    

Solution output:

solution output

Just add the prefix back to the string, and we have the flag!