DC702 CTF - Write Up

By Omar Essilfie-Quaye

Github Repo Link to github repository Docs Link to source code documentation

The CTF

I was recently able to attend a Capture The Flag (CTF) event run by DC702 and C2Soc. It was a relatively short CTF for beginners and and experts alike. I had 4 hours to capture as many flags as possible or be forever known as a newb. JK the organizers were amazing and were walking around and checking in on everybody to make sure they were having fun and throwing cheeky hints in here and there. These hints were much appreciated as I am definitely a n00b.

One thing that was made clear right at the start was that this was a short CTF and at all times the goal was to stay on task. This was a crucial theme across some of the challenges and if you were not paying attention it could have been easy to fall into a few traps.

The CTFs were split into a few sections, I focused on some of the reverse engineering based ones and my team mates did some of the others. In the interest of education I will share some of my solutions to the puzzles that I found the most fun. I will not be sharing the final flags to respect the integrity of any future DC702 events.

For useful CTF tools online explore the CyberChef website. It has almost everything you can think of in terms of quick little tools to help get things done. I do try and use local tooling where possible at CTF's in case there is unreliable network connections, but if you ever need a cyber Swiss army knife this is the place to look.

- - 80 Points

This CTF has a fairly simple aim to get the flag from the python script summoning_ritual.py. There are a quite a few functions in the script which I gave a quick glance to understand roughly how they work on my way down to the main function. At the time the checksum function stood out so I was mentally preparing to come back to this when needed.

            
def display_banner():

def calculate_checksum(data):
    """Mysterious encryption function for data integrity"""

def validate_incantation(user_input):
    """The sacred cipher - DO NOT MODIFY - DELETE AFTER DEBUG"""

def initialize_ritual_components():
    """Setup the ancient digital components"""
            
          

A more detailed inspection of the main function shown below led me to realise a few things:

  1. The user input is taken in, capitalised and a checksum calculated. But this checksum is not saved in any variable and does not need to be analysed.
  2. The validate_incantation() function is the main function of importance.
  3. The flag is basically the user input wrapped in curly braces as per the standard format in most CTFs.
            
def main():
    ...
    try:
        user_input = input().strip().upper()
    ...
    calculate_checksum(user_input)
    ...
    if validate_incantation(user_input):
        ...
        print(f"The spirits whisper: 'flag{{{user_input}}}'")
        print("\nCongratulations! You have found the magic word!")
    else:
        ...
            
          

A refocus on the validate_incantation() function very quickly reveals that the sacred_word variable is the required input for the python script to get the flag.

            
def validate_incantation(user_input):
    """The sacred cipher - DO NOT MODIFY - DELETE AFTER DEBUG"""
    sacred_word = "CTF-REDACTED-THE-SACRED-WORD-TO-GET-THE-FLAG-REDACTED-CTF"

    if len(user_input) != len(sacred_word):
        return False

    for i in range(len(sacred_word)):
        if user_input[i] != sacred_word[i]:
            return False

    return True
            
          

The first flag and the lesson is already being made clear. Stay on task! Don't waste time looking at things which don't get you to the flag. It would have been very easy to waste a lot of time trying to reverse the calculate_checksum() function. I was lucky that I decided to get an overall understanding of the control flow of the script before getting drawn down a rabbit hole with zero benefit.

The output of the summoning ritual script when the user input is correct

- - 100 Points

This flag is similar to the summoning ritual but has a slightly increased difficulty. In this case the flag isn't directly in the python script and you have to understand how python works a little bit to figure out the puzzle.

            
def validate(c, i):
    if (i == 0 or i == 9) and c == "t":
        return True
    if (i == 1) and (ord(c) == 104):
        return True
    if (i == 2) and (c == "secure"[4]):
        return True
    if (i == 3 or i == 4) and (c == chr(ord("g")-2)):
        return True
    if i in [5,6,7,8,9]:
        if c == "tniop"[::-1][i-5]:
            return True
    if i in range(10,21):
        doot = base64.b64decode("MWZvdXIxZml2ZTk=".encode("ascii")).decode("ascii")
        if c == doot[i - 10]:
            return True
    if i in range(21, 31):
        if c == "GANGALF-TON-"[::-1][i-21]:
            return True
    return False
            
          

The crux of the problem is the validate character function which takes in a character and an index and checks if the character is correct. Manually going through the first few quickly reveals a pattern, the number pi. The flag is the first few digits of pi in an alternating pattern back and forth as numbers and words. I was able to use the expected length of the password to figure out at what point to stop and plugging it in to the script revealed the flag.

The output of the its pithon script when the user input is correct

There was another lovely clue there with the file name for the python script being "its_pithon.py". That should have been a giveaway but that was a small clue I missed that could have saved me some time. If you want to do the CTF slightly faster than me you can automate checking each password character with a quick python script. I generated a flag-gen.py script after the CTF was over.

- - 125 Points

A binary (changed using hexedit to give a different flag to the original) reversing challenge with no instructions just the binary. Before running the binary I ran the file command against the binary to check what type of binary I was looking at. I had an unstripped 64-bit ELF, this meant I should be expecting to see lots of symbols available and possible code comments when running strings or objdump.

            
$ file re-versing
re-versing: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=0a46ad920eed87ad3f2039fd7539b02d7a3ed487, for GNU/Linux 3.2.0,
not stripped
            
          

My first attempt to obtain the flag involved running the strings command against the binary. I was hoping to find the raw flag in the output of this command but scrolling up and down quickly showed that this was not the case. I decided to grep the output for curly braces to save time looking for possible flag candidates. The output from this command is shown below.

            
$ strings re-versing | grep {
u{UH
^(D|6)(C|1)7\{[R-V]E(g|p)u(l*)ar(z?)_R[T-F]v3Rs(s*)i(n|j)g_6{3}\}
            
          

This output from the grep shows what could have been a flag regular expression. Solving for any valid input for this regular expression creates a valid flag. I was able to test possible match strings on regex101.com. Given that some of the groups of the RegEx had ranges of possible values it was clear that you had to find the correct value within the range. As you solved the RegEx the expected word becomes clear very quickly. Once solved I ran the binary with my guess to confirm the flag.

The output of the re-versing binary when the user input is correct

- - 150 Points

This challenge involves reverse engineering a C source file, cybertruck.c. The instructions tell you about the cyber truck infotainment system and that there is a secret mode. The first step enter the matrix...

            
...

#define HIDDEN_OPTION (4500 * 3)

...

int main() {
    ...
    switch (choice) {
        case 1:
            handle_climate();
            break;
        case 2:
            handle_media();
            break;
        case 3:
            handle_navigation();
            break;
        case 4:
            handle_settings();
            break;
        case 5:
            handle_charging();
            break;
        default:
            if (choice == HIDDEN_OPTION) {
                process_special_mode();
            } else {
                printf("\nInvalid option. Please try again.\n");
            }
            break;
      ...
}
            
          

Having a look at the main function it can be seen that to enter the special mode a secret number needs to be entered. A quick CTRL + F shows that the HIDDEN_OPTION is defined at the top of the source file. The value of the HIDDEN_OPTION is 13500. Entering this mode shows the start of a diagnostic mode which requires authentication. This occurs in the process_special_mode() function.

            
void process_special_mode() {
    char sys_str1[BUFFER_SIZE];
    char sys_str2[BUFFER_SIZE];
    char sys_str3[BUFFER_SIZE];

    get_system_strings(sys_str1, sys_str2, sys_str3);
    ...

    if (calculate_checksum(input) == 2661) {
        ...
        printf("FLAG: %s\n", sys_str1);
        ...
    }
}
            
          

The important sections of the process_special_mode() function are shown above. The value of the flag is stored in sys_str1! I considered spending time reversing the calculate_checksum() function but I realised that the core goal of the challenge is to obtain the flag and that bypassing the checksum would be a quicker way to getting the flag. During the CTF I copied the code within the get_system_strings() function and all the dependencies into the flag-gen.c, and ran it. This printed the flag to the console without wasting time on unnecessary reversing.

The output of the cybertruck binary when the user input is correct

Post CTF I realised there are a few other ways of obtaining the flag:

  1. If C is not your favoured programming language you can copy the essential parts of the algorithm to flag-gen.py.
  2. As you have the source code for the challenge you can patch the if statement to true which completely removes the checksum. As the source prints the flag upon success this will reveal the flag without having to write your own scripts.
  3. If you are feeling particularly technical you can attach a debugger to the binary and set a breakpoint at the if statement. You can then print the value sys_str1 as it is already in memory at that point.
  4. The last option is to try using the angr tool to try and automate getting the correct checksum value. I generated a quick checksum-validation.py script and I do not believe it is possible to obtain a checksum of 2661 using printable ascii characters. I used a simple greedy bin packing approach to try and get the checksum value of 2661 and it never calculated a solution. I might be wrong and using angr to double check is probably a sensible test.
GDB Debugger

I just briefly got bored and thought I would try the debugger variant to make sure i remember how to use GDB. Make sure to compile the cybertruck source code with debug symbols before running in GDB by using the flag "-ggdb".

            
$ g++ -ggdb cybertruck.c -o cybertruck
$ gdb cybertruck
(gdb) break process_special_mode
(gdb) run
            
          

It took me a minute to figure out why there was no useful information in the local symbols when I first reached the breakpoint. Of course the debugger had paused execution before any operations happened in the function so the local variables all had garbage data in them. I stepped forward using next a few times and once I had gone passed the get_system_strings() function I had a look at the local variables and saw the flag hiding in sys_str1 as expected.

obtaining the flag using a debugger

- - 100 Points

A CTF spin on the classic fizz buzz challenge. For all number from 1 to 1024 inclusive do the following:

  1. Print "fizz" if the number is divisible by 3
  2. Print "buzz" if the number is divisible by 7
  3. Print "fizzbuzz" if the number is divisible by 3 and 7

Make a comma separated string of the previous statements. Ensure that there are no spaces and no newline characters. Reverse the string and find the MD5 Hash of the string. The hash is the flag for this challenge and the instructions clear denote that this is a non standard flag format.

This is a rather simple challenge and once the string has been generated it can be hashed with an online tool such as MD5HashGenerator.com . I chose to put everything in a generate_hash.py python script to generate the hash I needed and quickly iterate through any mistakes. The quick mistakes I made included: not checking the inclusivity of the problem statement, leaving a trailing comma in the string and forgetting to reverse the string.

obtaining the fizz buzz flag using a python script

- - 100 Points

The premise if simple, you are given two copies of a file. Use them to find the flag. The two files you are given are copy_1.pdf and copy_2.pdf. These files contain the short story "The Tell Tale Heart" by Edgar Allen Poe. A quick glance shows no visual difference between the files which led me to check if there were any differences in the file data. I ran the diff command as shown below against the two files.

            
$ diff copy_1.pdf copy_2.pdf
            
          

The output of the diff command can be seen in the image below. It looks like some data has been removed from copy_1.pdf. The format of the data looks like a hex representation of characters.

the output of the diff command between copy_1.pdf and copy_2.pdf

I piped the output of diff into a file diff.txt and I cleaned up the excess characters diff-clean.txt. You can then take this clean data and run it through the "from Hex" recipe on cyberChef. This should reveal the flag.

the output of the cyber chef 'from Hex' recipe with the flag

If you want to do the conversion from hex yourself you can use the python "bytes.fromhex()" function. The usage of this can be seen in flag-gen.py. The output of the flag generation python script can be seen below.

the output of the flag-gen.py script

If you want to try a command line only solution you can use xxd to convert the data from hex to ascii.

            
$ xxd -r diff-clean.txt
TPZ{diff_edgar}
            
           

- - Post CTF

This is also a relatively easy python reverse engineering challenge. Take the python file secret_checker.py and see if you can get the flag. The important sections of this script are shown below.

            
ka = ['x120', 'm120', 'a-1', 'a-103', 'x36', ...
...

def check_secret(secret):
    max = 100
    if (len(secret) * 2) + 18 > max:
        return False

    base = f"{0.1 + 0.2:.100f}"[18:18 + len(secret)*2]
    base = [int(base[i:i+2]) for i in range(0, len(base), 2)]

    p = list(map(decode, base, ka))
    return "".join(p) == secret

def main():
    secret = sys.argv[1]
    if check_secret(secret):
        print(f"Correct! Secret is {secret}")
    else:
        print("WRONG!")
              
            

The check for the flag is done in the check_secret() function. Looking at this function may be intimidating however there are a few things of interest. Base and ka are invariant for every program execution so the execution of the decode function will always create the same output. The comparison in the check_secret() function is done on the user input and the decoded ka variable. This means if patch the script to print the p variable just before the function returns you will be given the value of the flag.

the output of the patched secret check script if the input argument is too short

This patch to the script is extremely valuable and saves you from having to reverse the calculations of the base or decoding calculations. The initial output from trying this is shown below. It can be seen that the output is dependent on the length of the user input. The entire flag is only revealed if the input longer than the length of the flag. If the input is too long then the check_secret() function will return early preventing this patch from revealing the secret.

the output of the patched secret check script which shows the full flag

Once again the key is to stay on task. I got drawn into the complexities of the decoding function which was not needed and this prevented me from completing this challenge in the last few minutes of the CTF.

- - Post CTF

CTF Readme: Three witches gathered around their cauldron, each adding a secret ingredient to their magical brew . Each witch added an ingredient to the pot. What are these chefs cooking up?

Hint: Remember use 'Magic' whenever possible.

This one stumped me during the CTF as no matter what I tried to decode the data in python I just kept getting nonsensical data out. At no point did I try more than 2 decoding techniques at once. After the CTF I was playing around with cyber chef when I noticed the Magic recipe under the flow control menu option. I tried this and still got data that didn't make a lot of sense but it was much shorter so it felt promising. I noticed that there was a depth option so I changed this from the default of 3 to 5 to see what would happen and the flag popped out.

the output of cyber chef with the magic recipe depth 5

- - Post CTF

The mystery window challenge is a forensic challenge revolving around extracting data from an image of a mystery-window.jpg. Sadly opening the image didn't reveal the flag.

a mysterious picture of a window with two scary children in it

My first thought was to see if there was any Exif data in the image. Running the exif command against the file shows that there is either no exif data or it is corrupt. Given that I didn't want to write an exif reader to figure out if the possibly corrupt data might have a flag hidden within it I moved on.

the output of the exif command fails as there is no exif data

Next I ran the file command against the image just to confirm it was what I thought it was. I probably should have done this first but I don't think I learned anything from this as I had already opened the file in an image viewer so I knew it was a valid image file.

the output of the file command shows it is just an image

Before expending energy down some more expensive steganography techniques I thought I would check if there were any other files hiding in the image file. I ran the binwalk command on the file to see if there was any hope in this direction. It seems that there is a second image file hiding at 0x43660 after the first image.

the output of the binwalk command shows what the 3 components in the image are
          
$binwalk --dd=".*" mystery-window.jpg
$ls _mystery-window.jpg.extracted/
0*    43660*    4367E*
$file _mystery-window.jpg.extracted/*
0:     JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1,
segment length 16, progressive, precision 8, 1500x1384, components 3

43660: JPEG image data, JFIF standard 1.01, resolution (DPI), density
144x144, segment length 16, Exif Standard: [TIFF image data, big-endian,
direntries=1, orientation=upper-left], baseline, precision 8, 1500x1384,
components 3

4367E: TIFF image data, big-endian, direntries=1, orientation=upper-left
$mv _mystery-window.jpg.extracted/43660 _mystery-window.jpg.extracted/43660.jpg
          
        

I used binwalk again to extract all of the recognised files from the image. This places them all into an extracted image folder. The files are named after their address in the original file but they are not named with the correct file extension. I relabelled the file at 0x43660 as a jpeg and opened it up. At the bottom of the image is the flag.

the extracted image has the flag text at the bottom

If you want to skip all of the effort of trying command line tools you can use cyber chef to expedite some of the work. The image below shows how cyber chef can extract the image file in a similar manner to using binwalk.

using cyber chef to extract the flag

- - Post CTF

This was a nice and easy one. The one line readme is as follows: Just something easy to get started with. "P2{bu-gur-zntvpny-ebgngvba-bs-13}"

This one is pretty obviously a ROT13 cipher. One of the clues that hints at this is that the numbers and special characters are seemingly unchanged. ROT13 only rotates letters by 13, shifting back around to the start of the alphabet if it overflows. This leaves all other characters unchanged. Additionally because each repeating section of plain text is changed to cipher text in the same way each time it is susceptible to "known plain text" and "frequency analysis" attacks.

Known plain text attacks revolve around using known sequences of plain text to decode how a cipher is working. Common words such as "the", "of", "and" are good examples of this. For the ROT13 cipher specifically the word "the" becomes "gur" in the cipher text. Therefore seeing the word "gur" in the cipher text could be a quick clue that you are looking at a ROT13 cipher. Whilst the Enigma machine used by the Germans in World War 2 was not as simple as a ROT13 cipher, it did fall due to relaxations in operational security from the signallers. Usage of common phrases such as "Heil Hitler" or the German word for "Weather" in the same place in every message helped the analysts at Bletchley Park to crack Enigma.

Frequency analysis takes a large portion of cipher text and analyses the number of times each letter occurs. If patterns emerge where some letters are more common than others it may be able to translate that into the expected distribution for the language. For example in English the letters "e", "t" and "a" are the most commonly seen in text. Checkout the hangman project to see a letter frequency graph for the English language. It should be noted that if the length of the cipher text is not long enough (like with this flag) then a frequency analysis attack may not work. Although this can be bypassed with the harvest now decrypt later paradigm. This allows an attacker to obtain enough cipher text to try a frequency analysis attack.

The quick way to get the flag using cyber chef can be seen below. If you want to do it yourself checkout the flag-gen.py which uses the codecs library to perform the ROT13 operation.

using cyber chef to get the flag