DEADFACE CTF 2024 Steganography Write-Up
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 todaem0n
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:
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.
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 fromd34th
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:
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.
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 markerb'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 variablemp3_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 toflag_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 withstego_file and flag_length
as arguments. Assigns the returned value (the extracted flag) to the variableextracted_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
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!