Sunshine CTF 2023 RE Challenge
decompiling .pyc
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 (which is relatively easy).
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.
- It can be downloaded here: https://pypi.org/project/uncompyle6/#files
- Choose the latest .whl file and install like:
pip install uncompyle6-3.2.3-py27-none-any.whl
. - You should now be able to type
uncompyle6 --version
via commandline and receive a version number. - With this tool, we can decompile dill.pyc like so:
uncompyle6 dill.pyc
.
Now we have the source code:
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
andvalue
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.
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
At this point, I’m starting to understand what’s going on. 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:
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:
Just add the prefix back to the string, and we have the flag!