Introduction
Steganography is the practice of hiding data inside an ordinary-looking carrier file, such that the very existence of the hidden message is concealed. This is different from encryption, which scrambles data so it cannot be read without a key but makes no attempt to hide that something is being protected. A steganographic image looks like a perfectly normal photograph; only someone who knows where to look will find the flag.
In CTF competitions, steganography challenges appear almost exclusively in the forensics category. The carrier can be any file type: PNG, JPEG, BMP, WAV, MP3, a PDF, or even a plain text file. The five main technique families you will encounter are:
- Image LSB -- data encoded in the least significant bits of pixel color channels, invisible to the naked eye.
- File-within-file -- a second file (ZIP, ELF, PNG) appended to or embedded inside the primary file.
- Audio spectrograms -- images or text drawn in the frequency domain of an audio recording.
- Metadata -- flags or clues stored in EXIF comment fields, GPS coordinates, or thumbnail images.
- Text and whitespace -- data encoded in trailing spaces, Unicode zero-width characters, or homoglyph substitutions inside text files.
This guide focuses on techniques and workflow: what to try, in what order, and why. For detailed installation instructions for each individual tool, see the companion posts linked below.
First steps on any stego challenge
Before reaching for any specialist steganography tool, run through this checklist. It takes under a minute and occasionally reveals the flag outright, without needing anything beyond core Unix utilities.
1. Confirm the actual file type
The file extension can lie. file reads the magic bytes at the start of the file and tells you what it actually is. A PNG that is secretly a ZIP, or a JPEG that is really an ELF binary, will be obvious immediately.
file challenge.png# Example output:# challenge.png: PNG image data, 800 x 600, 8-bit/color RGB, non-interlaced# If it says something unexpected:# challenge.png: Zip archive data, at least v2.0 to extract
2. Inspect magic bytes directly
xxd prints a hex dump of the file. The first few bytes are the magic bytes that identify the format. PNG files start with 89 50 4E 47 (that is.PNG). If the hex dump shows something else at offset 0 or reveals a second magic sequence further in, you have found something interesting.
xxd challenge.png | head -20# Common magic bytes to recognise:# 89 50 4E 47 -> PNG# FF D8 FF -> JPEG# 50 4B 03 04 -> ZIP (PK..)# 7F 45 4C 46 -> ELF binary# 25 50 44 46 -> PDF# 52 49 46 46 -> RIFF (WAV audio)
3. Search for obvious embedded text
strings challenge.png | grep -i pico# Broader search when you don't know the flag prefix:strings challenge.png | grep -E '[A-Za-z0-9+/]{20,}=' # base64 blobsstrings challenge.png | grep -i flagstrings challenge.png | grep -i ctf
4. Read the metadata with exiftool
EXIF metadata is the first place challenge authors hide flags because it requires no pixel manipulation. One command dumps everything.
# Install:sudo apt install libimage-exiftool-perl# Dump all metadata:exiftool challenge.jpg# Focus on comment-type fields that often hide flags:exiftool -Comment -Artist -UserComment -Description challenge.jpg
5. Check file size vs expected size
An 800x600 RGB PNG with no hidden data typically compresses to roughly 300-800 KB depending on the image content. If the file is 3 MB, something extra is in there. Usels -lh challenge.png and compare against a reference or against what the image dimensions would suggest.
strings output.LSB (Least Significant Bit) steganography
LSB steganography hides data by replacing the lowest-order bit of each color channel value in every pixel. A pixel whose red channel value is 11001010 in binary has its LSB changed from 0 to 1 to store a hidden bit, making the channel value 11001011. The visible color difference is just one step on a 256-step scale, completely undetectable to the human eye. Across an entire image, you can store roughly one bit per color channel per pixel: a 1000x1000 RGB PNG can carry around 375 KB of hidden data.
LSB encoding is the most common steganography technique in CTF challenges because it is easy to implement and the results look completely normal.
zsteg: automated LSB scanning for PNG and BMP
zsteg automatically tests dozens of combinations of channels (R, G, B, alpha), bit positions (bit 0 through bit 7), and byte order, then reports anything that decodes as readable text. One command is usually enough.
zsteg challenge.png # scan common channel/bit combinationszsteg -a challenge.png # exhaustive mode (slower, fewer missed cases)# Example output to look for:# b1,rgb,lsb,xy .. text: "picoCTF{hidden_in_lsb_42}"# b1,rgba,lsb,xy .. file: Zip archive data# Extract a specific channel to a file:zsteg -e 'b1,rgb,lsb,xy' challenge.png > extracted.binfile extracted.bin
Pay attention to entries that report file: rather than just text. If zsteg says it found a Zip archive or PNG inside, extract that channel and open the result with the appropriate tool.
Stegsolve: visual LSB inspection
When zsteg finds nothing, Stegsolve lets you visually inspect individual bit planes. Some challenges encode data in a non-standard order (column-major instead of row-major, or MSB instead of LSB) that zsteg misses. In Stegsolve, use the left and right arrow keys to flip through R0, R1... G0, G1... B0, B1... planes. If a plane shows a hidden image or text, you have found the encoding scheme.
# Download and launch:wget http://www.caesum.com/handbook/Stegsolve.jar -O stegsolve.jarjava -jar stegsolve.jar# Then: File > Open, use arrow keys to cycle planes,# or Analyse > Data Extract to combine specific bit planes.
Challenges using LSB techniques
File-within-file and appended data
Many file formats are tolerant of extra data appended after their official end marker. A PNG file ends with an IEND chunk, but parsers that stop reading at that point will simply ignore anything after it. This makes it trivial to stitch a ZIP archive (or any other file) to the end of a valid PNG. Both files remain valid in their respective formats: image viewers display the picture, and ZIP tools extract the archive.
When to suspect this technique: the file is significantly larger than you would expect for its dimensions and color depth, or strings output contains PK(ZIP magic bytes), ELF, or another format signature buried in the middle of what should be image data.
binwalk: detect and extract embedded files
binwalk scans any file for known format signatures and reports the byte offset of each one it finds. The -e flag tells it to extract everything automatically into a new directory.
# Scan for embedded files (report only):binwalk challenge.png# Example output:# DECIMAL HEXADECIMAL DESCRIPTION# 0 0x0 PNG image, 800 x 600# 39804 0x9B7C Zip archive data, at least v1.0# Extract everything found:binwalk -e challenge.png# Recursive extraction (extract files from within extracted files):binwalk -Me challenge.png# Check the output directory:ls _challenge.png.extracted/
Manual extraction with dd
If automatic extraction fails (for example, the extracted ZIP is password-protected and you need to inspect it), you can carve it manually using the byte offset from binwalk.
# Extract ZIP starting at offset 39804:dd if=challenge.png bs=1 skip=39804 of=embedded.zipfile embedded.zipunzip -l embedded.zip# With a password:unzip -P 'password' embedded.zip
foremost: file carving alternative
foremost is a forensic file carver that, like binwalk, scans for magic bytes. It is sometimes better at recovering fragmented or partially overwritten files.
sudo apt install foremostforemost -i challenge.png -o output_dir/ls output_dir/
Challenges using file-within-file
Metadata and EXIF data
EXIF (Exchangeable Image File Format) is a standard for embedding metadata inside image files. A camera stores the shutter speed, aperture, GPS coordinates, and timestamp. Challenge authors store flags. The data sits completely outside the pixel array, so the image looks identical with or without it.
What EXIF can contain
- Camera make and model
- GPS latitude and longitude (exact location where the photo was taken)
- Free-text comment fields:
Comment,UserComment,Artist,Description,Copyright - An embedded thumbnail JPEG (a separate small image inside the main file)
- Software used to create or edit the file
- Creation and modification timestamps
Reading all metadata with exiftool
# Full dump of all metadata tags:exiftool -all challenge.jpg# Quick scan of the fields most often used to hide flags:exiftool -Comment -Artist -UserComment -Description -Copyright challenge.jpg# GPS coordinates (sometimes coordinates encode a flag as numbers):exiftool -GPSLatitude -GPSLongitude challenge.jpg
Extracting the embedded thumbnail
Some challenges hide a flag image inside the EXIF thumbnail, which is a completely separate JPEG stored within the EXIF block. The main image may look blank while the thumbnail contains the flag.
# Extract the thumbnail to a new file:exiftool -b -ThumbnailImage challenge.jpg > thumbnail.jpgfile thumbnail.jpg# Open thumbnail.jpg in an image viewer
The GPS coordinate trick
Occasionally the GPS coordinates themselves encode the flag. The challenge image might have coordinates whose decimal digits, when read in sequence or converted through some simple transformation, spell out the answer. Always check GPS fields when you see them and look for patterns: repeated digit groups, decimal values that correspond to ASCII codes, or coordinates that point to a meaningful location whose name is part of the flag.
exiftool -GPSLatitude -GPSLongitude -GPSAltitude challenge.jpg# Convert decimal degrees to ASCII if the values look like ASCII codes:python3 -c "coords = [112, 105, 99, 111]; print(''.join(chr(c) for c in coords))"
exiftool handles all of these with the same command syntax.Audio steganography
Audio challenges are less common than image challenges but appear regularly in CTF forensics categories. There are three main techniques to look for, and the right tool depends heavily on which one the challenge uses.
Technique 1: Spectrogram analysis
A spectrogram is a visual representation of an audio signal: time runs along the horizontal axis, frequency along the vertical axis, and brightness represents amplitude. Challenge authors draw images or write text by controlling which frequencies are present at each moment. The audio usually sounds like static or noise, but the spectrogram reveals the hidden content.
When to suspect this: the audio sounds like white noise, hiss, or radio static, or it has an unusually sharp frequency cutoff at a specific point.
# Generate a spectrogram PNG from the command line with sox:sudo apt install soxsox audio.wav -n spectrogram -o spectrogram.png# Open spectrogram.png in an image viewer and look for visible text or images.# In Audacity (GUI):# 1. Open the audio file.# 2. Click the track name dropdown > Spectrogram.# 3. Adjust View > Range and Gain settings if the image is dim.# In Sonic Visualizer (often clearer than Audacity):# Layer > Add Spectrogram, then adjust colour scheme to Sunset or Printer.
Technique 2: LSB encoding in WAV audio
Exactly as with images, the least significant bit of each audio sample can carry hidden data without audibly changing the sound quality. This is most common in uncompressed WAV files (MP3 compression destroys LSB data).
# wav-steg-rs: a fast Rust tool for WAV LSB extraction# Install via cargo:cargo install wav-steg-rswav-steg-rs extract -i audio.wav -o extracted.txt# DeepSound: a Windows GUI tool sometimes used in challenges.# If you see DeepSound mentioned in a challenge, it may require a password.# Try the challenge hints or metadata for the passphrase.
Technique 3: Morse code or binary in the waveform
Some challenges encode flags as Morse code (audible beeps) or as binary (short and long tones representing 0 and 1). Open the audio in Audacity, switch to waveform view, and look for a repeating pattern of short and long pulses. Listen to the audio while watching the waveform to decode by hand, or use an online Morse decoder tool.
# View waveform and listen:# In Audacity: View > Zoom In (Ctrl+=) to see individual pulses.# Measure pulse durations using Audacity's selection tool:# Click and drag across one short pulse to see its duration in milliseconds.# Short pulse (dot) vs long pulse (dash) determines Morse code.
Text and whitespace steganography
Text-based steganography does not involve images or audio at all. The challenge provides a text file, source code file, or HTML page that looks normal but has data hidden in the whitespace or in invisible Unicode characters. These challenges require different tools from the image-focused ones.
Trailing whitespace encoding (SNOW)
The SNOW tool encodes messages by appending space and tab characters to the ends of lines. The trailing whitespace is invisible in most text editors and terminals but carries binary data. SNOW is specifically designed for this scheme.
sudo apt install snow# Decode hidden message from a text file:snow -C message.txt# With a passphrase (some challenges use one):snow -C -p 'password' message.txt# Verify trailing whitespace is present:cat -A message.txt # -A shows $ at line end and ^ for tabs# Lines with trailing spaces will show: 'some text $'
Zero-width and invisible Unicode characters
Some tools encode binary data using zero-width Unicode characters that are completely invisible in normal text rendering. Common culprits include:
U+200B(Zero Width Space) -- encoded asE2 80 8Bin UTF-8U+200C(Zero Width Non-Joiner) -- encoded asE2 80 8CU+200D(Zero Width Joiner) -- encoded asE2 80 8DU+FEFF(Zero Width No-Break Space / BOM) -- encoded asEF BB BF
# Detect zero-width characters with xxd and grep:xxd message.txt | grep 'e2 80 8b' # zero-width spacexxd message.txt | grep 'e2 80 8c' # zero-width non-joinerxxd message.txt | grep 'e2 80 8d' # zero-width joiner# Use Python to find and decode a zero-width binary encoding:python3 -c "text = open('message.txt').read()zwsp = '\u200b' # zero-width space = '1'zwnj = '\u200c' # zero-width non-joiner = '0'bits = ''.join('1' if c == zwsp else '0' if c == zwnj else '' for c in text)print(bits)print(bytes(int(bits[i:i+8], 2) for i in range(0, len(bits), 8)))"
Homoglyph substitution
Homoglyphs are characters that look identical (or nearly identical) to common Latin letters but are different Unicode code points. For example, the Cyrillic letterа (U+0430) looks exactly like the Latin a (U+0061). A challenge might replace select letters in a paragraph with their homoglyph equivalents to encode a binary message.
# Check for non-ASCII characters in a text file:python3 -c "text = open('message.txt').read()for i, c in enumerate(text):if ord(c) > 127:print(f'Position {i}: U+{ord(c):04X} ({c!r})')"
Color channel and bit-plane analysis
Sometimes a flag is encoded in a single color channel and invisible in the composite image. The red channel might carry the hidden data while the green and blue channels contain only noise, making the flag undetectable in a normal RGB view. Stegsolve is the primary tool for this kind of visual analysis.
Cycling through bit planes in Stegsolve
Stegsolve displays one bit plane at a time. Press the left and right arrow keys to cycle through all 28 planes (8 bits for each of R, G, B, and alpha). If a plane shows coherent content (readable text, a QR code, another image) rather than random noise, the data is encoded there.
- Open the image: File > Open.
- Use the left/right arrow keys in the main window to step through planes. The plane name appears at the bottom (e.g.
Red plane 0). - Plane 0 for each channel is the LSB. Plane 7 is the MSB. Challenges using LSB encoding will show visible structure in plane 0; MSB encoding shows in plane 7.
- If you find a plane with readable content, note which channel and bit number.
Data Extract: combining bit planes
The Analyse > Data Extract dialog in Stegsolve lets you select which bit planes to combine and extract as a binary stream. This is useful when the flag is spread across multiple planes (e.g. bits 0-2 of the red channel, read in column-major order).
- Go to Analyse > Data Extract.
- Tick the checkboxes for the channels and bit positions you want to include. For standard LSB encoding, tick Red 0, Green 0, Blue 0.
- Choose Row or Column order to match how the data was written.
- Click Preview. Look for readable text at the top of the output. Use Save Text or Save Bin to export it.
- Run
strings saved.bin | grep picoorfile saved.binto identify what was extracted.
Using Python to isolate a single channel
If you know which channel contains the hidden data, you can extract it without Stegsolve using the Python Pillow library. This is useful when you want to automate the extraction or process the channel data further.
python3 -c "from PIL import Imageimg = Image.open('challenge.png').convert('RGB')r, g, b = img.split()# Save just the red channel as a grayscale image:r.save('red_channel.png')# Check if LSBs of red channel form a readable byte stream:pixels = list(img.getdata())bits = ''.join(str(p[0] & 1) for p in pixels) # red channel LSBsmessage = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits)-7, 8))print(message[:200])"
Challenges using bit-plane analysis
Recommended stego triage workflow
When you receive a challenge file, follow this numbered decision tree. It moves from the fastest, most automated checks to progressively more manual investigation. Stop as soon as you find the flag.
- Identify the actual file type. Run
file challenge.*andxxd challenge.* | head. If the format does not match the extension, rename the file and proceed accordingly. - Check metadata first. Run
exiftool challenge.*andstrings challenge.* | grep -i pico. These two commands together find a significant fraction of CTF stego flags because challenge authors frequently use EXIF comments for beginner-level challenges. - Scan for embedded files. Run
binwalk challenge.*. If it reports a second file signature, runbinwalk -e challenge.*and examine the extracted directory. This covers the entire file-within-file category automatically. - For PNG and BMP images: run zsteg.
zsteg challenge.pngcovers LSB encoding in all common channel configurations. If the quick scan finds nothing, runzsteg -a challenge.pngfor exhaustive mode. - For JPEG images: try steghide.
steghide extract -sf challenge.jpg -p ''attempts extraction with a blank passphrase. If it reports data is present but requests a password, check other challenge materials for the passphrase. If none is obvious, trystegcracker challenge.jpg /usr/share/wordlists/rockyou.txt. - For audio files: check the spectrogram. Run
sox audio.wav -n spectrogram -o spec.pngand open the result. If you see text or an image, you are done. If not, open the file in Audacity and look at the waveform for Morse patterns. - Open in Stegsolve and cycle planes manually. This catches non-standard LSB orderings, MSB encoding, single-channel encoding, and anything that automated tools miss. Use the Data Extract dialog to try different channel and bit combinations systematically.
- For text files: look for whitespace and Unicode. Run
cat -A file.txtto see trailing whitespace, thensnow -C file.txt. Runxxd file.txt | grep 'e2 80'to check for zero-width Unicode characters.
Quick reference
Tool matrix covering the main steganography techniques encountered in CTF competitions. For installation details, see the Steganography Tools guide.
exiftool -all file.jpgstrings file.png | grep -i picobinwalk -e file.pngxxd file.png | headzsteg file.pngsteghide extract -sf file.jpg -p ''stegcracker file.jpg rockyou.txtjava -jar stegsolve.jarsox audio.wav -n spectrogram -o s.pngsnow -C file.txtFormat-to-tool decision guide
- Any file, first 60 seconds:
file,exiftool,strings | grep pico,binwalk - PNG image:
zstegfirst, thenStegsolve - JPEG image:
steghidewith blank password, thenstegcrackerif locked - BMP image:
zstegandsteghideboth apply - WAV / audio:
sox spectrogram, then Audacity waveform view for Morse - Text / TXT file:
cat -Afor whitespace,snow, then Python Unicode check