During the 2024 DEADFACE CTF competition, I crafted a series of intriguing steganography challenges designed to test players’ problem-solving skills and creativity. In this blog, I’ll walk you through my intended solutions for each challenge, providing insights into the thought process behind their creation and the techniques used to crack them.

Offsite Targets

Players are be given a JPG image and must use steghide to uncover the hidden flag.

Challenge Description

lamia415 sent an image to daem0n with secret information about who or what they are building a campaign against. Turbo Tactical wants to lean forward on this and prepare any individuals or companies that might be targeted by DEADFACE. Find out what the hidden message is and submit the flag.

Solution

Players can find the relevant conversation in GhostTown. The GhostTown thread will show the image and the password used to extract the hidden data.

Players should use steghide to reveal the hidden file.

steghide extract -sf img20240803.jpg

The players will be prompted for a password. Based on the GhostTown forum thread, the password is d34df4c3.

Once players enter the password, the file secret.txt.gz will be extracted. Players must first unzip this file.

gunzip secret.txt.gz

Once they gunzip the file, they can read its contents:

Hope you're ready for some fun. I'm currently at the company off-site morale event, and it's the perfect opportunity to gather intel. I'll be sending you the details of some of my coworkers soon. With their info, you can craft some wicked social engineering campaigns. Get ready to make them dance to our tune.

flag{S0c14l_3ng1neer1ng_1nt3l_fr0m_0ff-s1t3}

Flag

flag{S0c14l_3ng1neer1ng_1nt3l_fr0m_0ff-s1t3}


Something in the Dark

Players are given an image that has a flag with a layer with a low transparency setting. They should use either Photoshop, GIMP, or stegsolve to uncover the hidden text on the image.

Challenge Description

DEADFACE extracted a sensitive photo from Lytton Labs. As far as we can tell, it’s just a normal photo of a neighborhood at night, but the man who took the photo insists he saw something else. Here is the man’s original tweet. He later added the following image below.

The tweet is shown below:

The tweet from the challenge card.
didyouseeit.png image from the challenge card.

Solution

When players save and open the image, they won’t be able to see the transparent flag in the image. They’ll need to use a tool like stegsolve to navigate through the various color planes of the image.

Install StegSolve:

wget http://caesum.com/handbook/Stegsolve.jar -O stegsolve.jar
chmod +x stegsolve.jar

Run StegSolve

java -jar stegsolve.jar&

Using the GUI, open didyouseeit.png. The twitter (or, X) post included in the challenge gives a hint that players should look at the red, green, and blue planes of the image.

At the bottom of the stegsolve window is a left and right button. Click right until you arrive at “Red plane 0” or “Red plane 1”. Players will see the flag clearly.

stegsolve solution for this challenge.

Flag

flag{ar3_we_410N3??}


Descended from Wolves

Players are given an image and must find the flag by modifying the PNG’s height and CRC in hex to reveal the hidden portion of the image.

Challenge Description

There’s an image circulating on GhostTown of some weird dog. Based on the conversation in the forum, it sounds like the image holds some data that lamia415 extracted from De Monne Financial. Use the context in the conversation to determine what was extracted.

In GhostTown, there is a thread where lamia415 mentions this image. She claims it's her dog standing over his food bowl. This is an indication that there is more we should be seeing in the photo that is not there.

Based on the GhostTown conversation, the image was used to exfiltrate an access token that belongs to a De Monne employee.

Players can try using binwalk or other steg tools, but none will work. The height was modified in the IHDR chunk of the image’s hex value. If player’s load the image into CyberChef and use the To Hexdump recipe, they’ll see the following output.

The yellow-highlighted bytes are the IHDR. The width starts at 00000010 with 00 00 04 00 (1024 pixels). The height is the next 8 bytes (00 00 02 dd). The area highlighted in green is the CRC.

CRC (Cyclic Redundancy Check)

A 4-byte CRC is calculated on the preceding bytes in the chunk, including the chunk type code and chunk data fields, but not including the length field. The CRC is always present, even for chunks containing no data.

CRC provides integrity. If the preceding bytes in the chunk change, so too does the CRC need to change.

Change the Height

Players will need to increase the height. For example, they can change 00 00 02 dd to 00 00 05 00.

Calculate the CRC

Players must next calculate the CRC. This can be done manually; however, the zlib library in Python has a crc32 method that can be used to calculate the CRC. Here is a script that will accomplish this:

import zlib

ihdr_data = bytes.fromhex('4948445200000400000005000803000000')
crc = zlib.crc32(ihdr_data) & 0xffffffff
print(f'{crc:08X}')

Notice the value in the bytes.fromhex function. This takes the entire IHDR (i.e., the yellow highlighted portion of the screenshot). Note that this IHDR includes the new 00 00 05 00 height.

The output from the script will be EEB4D005. This is the new CRC.

Edit the Hex

Back in CyberChef, players should Replace the Input with the output. Make the following changes to the hex dump:

Render the image in CyberChef using the Render Image recipe. Now the full image will be revealed:

Note: Some players originally didn't set the height large enough, so they only saw the access token and never saw the bowl, as was indicated in the GhostTown thread.

Flag

flag{th3_h4ndsom3st_b01_1n_th3_w0rld}


Electric Soldiers

Players are given an image and must extract the MP3 audio file that contains the flag in its LSB.

Challenge Description

We stumbled across this image from d34th that might indicate how DEADFACE plans to sneak stolen information through various networks without detection. According to Ghost Town, d34th has been refining his process for embedding hidden information in various files.

See if you can figure uncover any hidden information in this image.

Solution

Players will receive this image:

electricsoldiers.png

Running file on the image reveals it to be a PNG.

file electricsoldiers.png
electricsoldiers.png: PNG image data, 1024 x 1024, 8-bit/color RGB, non-interlaced

Binwalk

Running binwalk will reveal a separate file header at 0x19F093.

binwalk electricsoldiers.png

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PNG image, 1024 x 1024, 8-bit/color RGB, non-interlaced
2619          0xA3B           Zlib compressed data, default compression
1699987       0x19F093        TIFF image data, big-endian, offset of first image directory: 8

Hexeditor

Players should run hexeditor on the electricsoldiers.png file and skip to 0x19F093 by pressing CTRL+T.

In the file, they should hone in on the IEND chunk and the ID3 header. This indicates that the PNG has ended, but there is still data after the IEND. If players research ID3 headers, it’ll reveal that this header indicates an MP3. Players will get an additional hint regarding this from one of d34th's GhostTown threads.

GhostTown discussion thread

The MP3 starts at 0x19F0B1 (or 1700017 in decimal). Here is a calculator for hex-to-decimal conversion.

Carve with dd

Run dd to carve out the MP3 file:

dd if=electricsoldiers.png of=outfile bs=1 skip=1700017

The file will output as outfile. Running file on outfile will reveal it to be an audio file.

outfile: Audio file with ID3 version 2.4.0

Embed Script (Not Part of Solution)

When I built this challenge, I used the following script to embed the flag in the MP3:

import os

def embed_flag_in_mp3(mp3_file, flag, output_mp3):
    # Convert the flag to binary representation
    flag_bits = ''.join(format(ord(char), '08b') for char in flag)

    # Read the original MP3 file
    with open(mp3_file, 'rb') as f:
        mp3_data = bytearray(f.read())

    # Embed the flag bits in the LSB of the MP3 file
    flag_index = 0
    for i in range(len(mp3_data)):
        if flag_index < len(flag_bits):
            # Modify the LSB
            mp3_data[i] = (mp3_data[i] & 0xFE) | int(flag_bits[flag_index])
            flag_index += 1

    # Write the modified MP3 to the output file
    with open(output_mp3, 'wb') as f:
        f.write(mp3_data)

def concatenate_files(png_file, mp3_file, output_file):
    with open(png_file, 'rb') as f1, open(mp3_file, 'rb') as f2, open(output_file, 'wb') as f_out:
        f_out.write(f1.read())
        f_out.write(f2.read())

# Input files
original_png = 'electricsoldiers-original.png'
original_mp3 = 'mechanical-revolution.mp3'
flag = "   flag{3l3ctr1c_s0ld13rs_4lw4ys_r0ck}"

# Temporary MP3 file with embedded flag
temp_mp3 = 'modified_mechanical-revolution.mp3'

# Output file
output_png = 'electricsoldiers.png'

# Embed the flag in the MP3 file
embed_flag_in_mp3(original_mp3, flag, temp_mp3)

# Concatenate the PNG and modified MP3 files
concatenate_files(original_png, temp_mp3, output_png)

# Clean up the temporary file
os.remove(temp_mp3)

print(f"Flag embedded and files concatenated. Output file: {output_png}")

This script is provided to the players (with the flag redacted).

Solution Script

This script will read the LSB in the MP3 to reveal the flag:

def extract_flag_from_mp3(stego_file, flag_length):
    # Read the stego PNG file
    with open(stego_file, 'rb') as f:
        stego_data = f.read()

    # Find the MP3 data start position (end of PNG data)
    png_signature = b'\x89PNG\r\n\x1a\n'
    png_end = stego_data.find(b'IEND') + 8  # 'IEND' chunk + 4 bytes CRC

    # Extract the MP3 data
    mp3_data = stego_data[png_end:]

    # Extract the flag bits from the LSB of the MP3 data
    flag_bits = ''
    for byte in mp3_data:
        flag_bits += str(byte & 1)

    # Convert binary to ASCII
    flag_chars = [chr(int(flag_bits[i:i+8], 2)) for i in range(0, len(flag_bits), 8)]
    flag = ''.join(flag_chars)

    # Truncate to the actual flag length
    flag = flag[:flag_length]

    return flag

# Input file
stego_file = 'electricsoldiers.png'
flag_length = 100

# Extract the flag
extracted_flag = extract_flag_from_mp3(stego_file, flag_length)
print(f"Extracted Flag: {extracted_flag}")

Solution Script Breakdown

Let's go through the provided solution script line by line to understand what each part is doing in detail.

Solution Script: solution_script.py

def extract_flag_from_mp3(stego_file, flag_length):
  • Function Definition: Defines a function named extract_flag_from_mp3 that takes two parameters: stego_file (the path to the stego PNG file) and flag_length (the length of the hidden flag).
    # Read the stego PNG file
    with open(stego_file, 'rb') as f:
        stego_data = f.read()
  • Open the Stego File: Opens the "stego" PNG file in binary read mode ('rb') as the file object f.
  • Read the File: Reads the entire contents of the "stego" file into the variable stego_data as a byte array. This byte array contains both the PNG data and the appended MP3 data.
    # Find the MP3 data start position (end of PNG data)
    png_signature = b'\x89PNG\r\n\x1a\n'
    png_end = stego_data.find(b'IEND') + 8  # 'IEND' chunk + 4 bytes CRC
  • Define PNG Signature: Defines the PNG file signature as png_signature to identify PNG files. Although it's defined here, it's not directly used for extraction.
  • Find the End of the PNG Data: Searches the stego_data byte array for the occurrence of the PNG end marker b'IEND'.
  • Calculate the End Position: Adds 8 to the index of IEND to account for the 4-byte marker and the subsequent 4-byte CRC (Cyclic Redundancy Check). This gives the position where the PNG data ends and the MP3 data begins.
    # Extract the MP3 data
    mp3_data = stego_data[png_end:]
  • Extract MP3 Data: Slices the stego_data byte array starting from the end of the PNG data (png_end) to the end of the file. This extracts the MP3 data portion into the variable mp3_data.
    # Extract the flag bits from the LSB of the MP3 data
    flag_bits = ''
    for byte in mp3_data:
        flag_bits += str(byte & 1)
  • Initialize Flag Bits String: Initializes an empty string flag_bits to store the binary representation of the hidden flag.
  • Iterate Through MP3 Data: Iterates through each byte in the mp3_data byte array.
  • Extract LSB: For each byte, performs a bitwise AND operation (& 1) to extract the least significant bit (LSB). Converts the LSB to a string (str(byte & 1)) and appends it to flag_bits. This builds a binary string representing the hidden flag.
    # Convert binary to ASCII
    flag_chars = [chr(int(flag_bits[i:i+8], 2)) for i in range(0, len(flag_bits), 8)]
    flag = ''.join(flag_chars)

Convert Binary to ASCII: Uses a list comprehension to convert the binary string flag_bits back into characters.

  • Iterates through flag_bits in chunks of 8 bits (flag_bits[i:i+8]) using a step size of 8 (range(0, len(flag_bits), 8)).
  • For each chunk of 8 bits, converts it from binary to an integer (int(flag_bits[i:i+8], 2)).
  • Converts the integer to its corresponding ASCII character using the chr function.
  • Adds each character to the flag_chars list.
  • Join Characters to Form Flag: Joins the list of characters flag_chars into a single string flag.
    # Truncate to the actual flag length
    flag = flag[:flag_length]
  • Truncate to Actual Flag Length: Truncates the flag string to the known length of the original flag (flag_length). This ensures that any extraneous characters resulting from the LSB extraction process are removed.
    return flag
  • Return Extracted Flag: Returns the final extracted flag string.

Main Execution

# Input file
stego_file = 'electricsoldiers.png'
flag_length = 100 # Guess the length of the flag
  • Define Stego File Path: Sets the variable stego_file to the path of the stego PNG file ('electricsoldiers.png').
  • Define Flag Length: Sets the variable flag_length to an estimated length of the flag (100 in this case).
# Extract the flag
extracted_flag = extract_flag_from_mp3(stego_file, flag_length)
  • Call Extraction Function: Calls the extract_flag_from_mp3 function with stego_file and flag_length as arguments. Assigns the returned value (the extracted flag) to the variable extracted_flag.
print(f"Extracted Flag: {extracted_flag}")
  • Print Extracted Flag: Prints the extracted flag to the console with the prefix "Extracted Flag: ".

Flag

flag{3l3ctr1c_s0ld13rs_4lw4ys_r0ck}


Tri Harder

Players are given a text file with an email conversation and must identify the whitespace that is hiding a message. The whitespace uses 3 different character codes for whitespace, indicating ternary (base-3) numbering. Players must convert the ternary to binary, then to plaintext.

Challenge Description

The security team at NexGen Softworks reached out to us regarding a potential tip about DEADFACE. They were alerted to unusual email traffic between one of their employees and a third-party vendor regarding infrastructure upgrades. The email chain looks fairly innocent, but NexGen’s SEIM flagged it as suspicious and they’re not sure why.

Solution

When players inspect the email, they should notice a large amount of whitespace under the signature block in each email.

Find the Whitespace

Email with excessive whitespace

Convert the Whitespace to Charcode

If players copy this whitespace and convert it to hexadecimal, they’ll notice 3 distinct unicode characters for whitespace: U+200A, U+2002, and U+2009.

Convert Unicode to Ternary

The challenge title is a hint that this is a Base-3 numbering system (or ternary). From here, they will have to determine which unicode characters are 0s, 1s, and 2s (requires a small amount of guess work). They should replace these unicode character codes with their respective ternary numbers.

In the below screenshot, we successfully determine the right values and replace them with 0, 1, and 2.

Convert Ternary to Binary

Now that players have the ternary number, they must convert it to binary. Asking ChatGPT to write a ternary-to-binary converter is pretty simple and will generate a code similar to this:

import sys

def ternary_to_binary(ternary_str):
    # Convert the ternary string to a decimal (base-10) integer
    decimal_value = int(ternary_str, 3)

    # Convert the decimal value to a binary string and remove the '0b' prefix
    binary_str = bin(decimal_value)[2:]

    return binary_str

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python script_name.py <ternary_value>")
        sys.exit(1)

    ternary_value = sys.argv[1]
    try:
        binary_value = ternary_to_binary(ternary_value)
        print(f"Ternary: {ternary_value} -> Binary: {binary_value}")
    except ValueError:
        print("Invalid ternary value. Please provide a valid ternary number.")

If players don’t want to code their own converter, this site will convert ternary to binary: Web Conversion Online.

Converting the ternary will result in this binary output:

110010000110011001101000111010001101000001011000010000001001001001000000110001101100001011011100010011101110100001000000110011101100101011101000010000001100001011101110110000101111001001000000110011001110010011011110110110100100000011101000110100001100101001000000110111101100110011001100110100101100011011001010010111000100000010011000110010101110100001001110111001100100000011000110110111101101101011011010111010101101110011010010110001101100001011101000110010100100000011101010111001101101001011011100110011100100000011101000110100001101001011100110010000001110111011010000110100101110100011001010111001101110000011000010110001101100101001000000111001101110100011001010110011100101110

NOTE: Both the script provided AND the online tool seem to leave a zero off at the beginning of the binary result. Add a zero at the front to correct the issue.

Convert Binary to Readable Plaintext

If you don’t add the 0 at the front of the binary, you’ll get the wrong output as seen in the screenshot below.

Add the zero at the front, and the message will be revealed.

Repeat this process for each of the whitespace lines in the email. The final email from Travis will have the flag.

Flag

flag{1n51d3r5_0p3n_7h3_D00r5}

Conclusion

Steganography is a fascinating blend of creativity, problem-solving, and technical skill, and I hope these writeups provided valuable insights into the strategies used to tackle these challenges. Whether you’re a seasoned CTF competitor or just beginning your journey into the world of hidden data, I encourage you to keep exploring and experimenting. The thrill of uncovering concealed information is what makes steganography so rewarding, and I look forward to seeing even more innovative solutions in future competitions. See you in the next DEADFACE CTF!