April 11, 2026

Steganography Techniques for CTF Competitions

A systematic guide to solving CTF steganography challenges -- from LSB pixel manipulation to file-within-file extraction, audio spectrograms, metadata analysis, and when to use each technique.

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 blobs
strings challenge.png | grep -i flag
strings 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.

Tip: These five checks together take about 60 seconds. Do all of them before launching any specialist tool. Many CTF flags live in the EXIF Comment or are a base64 blob instrings 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 combinations
zsteg -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.bin
file 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.jar
java -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.zip
file embedded.zip
unzip -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 foremost
foremost -i challenge.png -o output_dir/
ls output_dir/

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.jpg
file 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))"
Note: EXIF is most commonly found in JPEG files but also appears in TIFF, PNG (as text chunks rather than EXIF), and some PDF and audio files. 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 sox
sox 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-rs
wav-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.
Tip: Always try the spectrogram first on any audio challenge. It is the most common technique and immediately visible. If the spectrogram looks like normal frequency content with no obvious text or image, move to checking the waveform for Morse patterns, then try LSB extraction on WAV files.

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 as E2 80 8B in UTF-8
  • U+200C (Zero Width Non-Joiner) -- encoded as E2 80 8C
  • U+200D (Zero Width Joiner) -- encoded as E2 80 8D
  • U+FEFF (Zero Width No-Break Space / BOM) -- encoded as EF BB BF
# Detect zero-width characters with xxd and grep:
xxd message.txt | grep 'e2 80 8b' # zero-width space
xxd message.txt | grep 'e2 80 8c' # zero-width non-joiner
xxd 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})')
"
Note: Text steganography challenges often come with a hint that the file "looks normal" or asks you to look more carefully. If you open a text file and it seems unremarkable, paste its contents into a hex editor before giving up. Zero-width characters and unusual Unicode code points will be immediately visible.

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.

  1. Open the image: File > Open.
  2. 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).
  3. 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.
  4. 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).

  1. Go to Analyse > Data Extract.
  2. Tick the checkboxes for the channels and bit positions you want to include. For standard LSB encoding, tick Red 0, Green 0, Blue 0.
  3. Choose Row or Column order to match how the data was written.
  4. Click Preview. Look for readable text at the top of the output. Use Save Text or Save Bin to export it.
  5. Run strings saved.bin | grep pico or file saved.bin to 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 Image
img = 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 LSBs
message = 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.

  1. Identify the actual file type. Run file challenge.* and xxd challenge.* | head. If the format does not match the extension, rename the file and proceed accordingly.
  2. Check metadata first. Run exiftool challenge.* and strings 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.
  3. Scan for embedded files. Run binwalk challenge.*. If it reports a second file signature, run binwalk -e challenge.* and examine the extracted directory. This covers the entire file-within-file category automatically.
  4. For PNG and BMP images: run zsteg. zsteg challenge.png covers LSB encoding in all common channel configurations. If the quick scan finds nothing, run zsteg -a challenge.pngfor exhaustive mode.
  5. 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, try stegcracker challenge.jpg /usr/share/wordlists/rockyou.txt.
  6. For audio files: check the spectrogram. Run sox audio.wav -n spectrogram -o spec.png and 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.
  7. 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.
  8. For text files: look for whitespace and Unicode. Run cat -A file.txt to see trailing whitespace, then snow -C file.txt. Run xxd file.txt | grep 'e2 80' to check for zero-width Unicode characters.
Strategy note: Steps 1 through 3 are format-agnostic and take under two minutes total. Always do them before branching into format-specific tools. Step 4 and 5 are mutually exclusive based on file type. Step 7 (Stegsolve) is the manual fallback when everything automated has failed.

Quick reference

Tool matrix covering the main steganography techniques encountered in CTF competitions. For installation details, see the Steganography Tools guide.

exiftoolEXIF comments, GPS, thumbnailsJPEG, PNG, TIFF, PDFexiftool -all file.jpg
strings + grepPlaintext flags, base64 blobsAnystrings file.png | grep -i pico
binwalkAppended/embedded filesAny binarybinwalk -e file.png
file + xxdTrue file format, magic bytesAnyxxd file.png | head
zstegLSB / MSB pixel encodingPNG, BMPzsteg file.png
steghidePassphrase-protected payloadJPEG, BMPsteghide extract -sf file.jpg -p ''
stegcrackersteghide passphrase via wordlistJPEG, BMPstegcracker file.jpg rockyou.txt
StegsolveAny bit-plane pattern, MSB encodingPNG, BMP, JPEGjava -jar stegsolve.jar
sox spectrogramImages hidden in audio frequencyWAV, AIFFsox audio.wav -n spectrogram -o s.png
snowWhitespace-encoded messagesTXTsnow -C file.txt

Format-to-tool decision guide

  • Any file, first 60 seconds: file, exiftool, strings | grep pico, binwalk
  • PNG image: zsteg first, then Stegsolve
  • JPEG image: steghide with blank password, then stegcracker if locked
  • BMP image: zsteg and steghide both apply
  • WAV / audio: sox spectrogram, then Audacity waveform view for Morse
  • Text / TXT file: cat -A for whitespace, snow, then Python Unicode check