Skip to main content

Invisible WORDs picoCTF 2023 Solution

A BMP image hides a ZIP archive across its blue and green colour channels. Extract the channel bytes and unzip the result.

Published: April 26, 2023Updated: August 25, 2026

Description

A BMP image with suspicious noise hides a ZIP archive in its blue and green color channels. Extract the pixel data, skip the red and alpha bytes, and unzip the result to find the flag.

Download the BMP image.

Open it in CyberChef or a hex editor to inspect the file structure.

bash
pip3 install pillow

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Inspect the image and identify the anomaly
    Observation
    The description calls out a BMP with suspicious noise, which points at data embedded whole-channel rather than in the pixel LSBs. Inspect each colour channel on its own to find which ones carry the anomaly.
    Open the image in CyberChef and split into color channels. The blue and green channels show obvious noise patterns at the bottom of the image, while the red channel looks normal. This indicates data hidden in the blue and green channels. The file header reveals a 32-bit bit depth, meaning pixels are stored as BGRA (blue, green, red, alpha).
    python
    python3 -c "from PIL import Image; img = Image.open(\"Invisible WORDs.bmp\"); print(img.mode, img.size)"
    What didn't work first

    Tried: Open the BMP in steghide and attempt to extract with an empty passphrase

    steghide handles JPEG and BMP through a DCT and LSB approach, and expects a passphrase-protected container. It reports no extractable data here, because nothing was embedded that way: the payload occupies whole colour channels rather than least-significant bits. Inspect the channel values directly.

    Tried: Convert the BMP to PNG with ImageMagick and run zsteg on the result

    zsteg reads PNG and BMP but hunts for LSB-plane patterns and known stego signatures. This BMP carries two full bytes per pixel across blue and green rather than single bits, so the plane scans never surface the archive magic. Keep the image in BMP too, so the raw BGRA layout survives extraction.

    Learn more

    BMP files with 32-bit color store pixels as four bytes per pixel: Blue, Green, Red, Alpha (BGRA). The BMP file header at offset 0x0A stores the pixel array start offset. Examining this value reveals where the raw pixel data begins, which helps when working at the byte level without a library.

    In CyberChef, the Split Colour Channels operation separates an image into its component channels. Channels carrying hidden data typically appear as random noise or structured patterns that differ from the rest of the image. Clean channels look smooth and gradual.

  2. Step 2Extract blue and green bytes with Python
    Observation
    Blue and green both show structured noise while red and alpha stay clean. The payload is interleaved across exactly those two channels, two bytes per pixel in the BGRA layout, which a short Python script can walk and collect.
    The pixel array starts at byte 0x8A in this BMP. Each pixel is 4 bytes: BGRA. To extract the hidden data, read two bytes (blue and green) and skip two bytes (red and alpha) for every pixel until the end of the file. Write the extracted bytes to a new file.
    python
    python3 - <<'PY'
    with open("Invisible WORDs.bmp", "rb") as f:
        data = f.read()
    
    # BMP pixel data starts at offset stored at bytes 0x0A..0x0D (little-endian)
    import struct
    pixel_start = struct.unpack_from("<I", data, 0x0A)[0]  # typically 0x8A = 138
    
    out = bytearray()
    i = pixel_start
    while i + 3 < len(data):
        out.append(data[i])      # blue
        out.append(data[i + 1])  # green
        i += 4                   # skip red and alpha
    
    with open("output.bin", "wb") as f:
        f.write(out)
    
    print(f"Extracted {len(out)} bytes to output.bin")
    PY
    What didn't work first

    Tried: Extract only the blue channel (every 4th byte starting from pixel_start) instead of both blue and green

    Reading only the first byte of each pixel captures half the payload, leaving a truncated file binwalk cannot recognize, because the central directory and file headers straddle both channels. Reconstruct by interleaving two bytes per pixel, blue then green, written consecutively into the output buffer.

    Tried: Start reading from byte 0 of the file instead of using the header offset at 0x0A

    The first 138 bytes are BMP file and info headers, not pixel data. Treat them as pixels and garbage lands at the front of the output, corrupting the archive magic bytes so unzip and binwalk both fail. The BMP header itself records where the pixel array starts, so read that offset rather than assuming one.

    Learn more

    The BMP file format stores the pixel array offset at bytes 0x0A through 0x0D as a little-endian 32-bit integer. For this particular file the value is 0x8A (138 decimal), meaning pixel data starts 138 bytes into the file.

    BGRA pixel layout means each group of 4 bytes is one pixel: byte 0 is blue, byte 1 is green, byte 2 is red, byte 3 is alpha. Skipping two bytes and reading two is the pattern: read at i and i+1, then advance i by 4.

  3. Step 3Identify and unzip the embedded archive
    Observation
    The extracted output may begin with garbage before the real file signature. Let binwalk find the archive magic bytes and carve the valid section, rather than pointing unzip at the raw output.
    The extracted binary file is a ZIP archive, but it may be corrupted. Run binwalk to find valid ZIP data inside it, then extract to get a text file. Search the text for the flag.
    bash
    binwalk output.bin
    bash
    binwalk -e output.bin
    bash
    grep -r 'picoCTF' _output.bin.extracted/

    Expected output

    picoCTF{w0rd_d4wg_y0u_f0und_5h3113ys_m4573rp13c3_...}
    What didn't work first

    Tried: Run unzip output.bin directly without using binwalk first

    The extracted stream may carry leading garbage before the first valid archive signature, so unzip reports a missing end-of-central-directory record or extracts nothing. binwalk -e seeks to the right offset and carves out the clean archive. Run binwalk first to confirm the offset, then unzip.

    Tried: Search for the flag inside the extracted text file with strings output.bin instead of grep on the extracted directory

    Running strings on output.bin searches the raw binary before extraction, and the flag text is compressed inside the ZIP, so it will not appear as a readable string in the compressed data. After binwalk -e creates _output.bin.extracted/, the ZIP is decompressed into a plain text file where grep -r 'picoCTF' can match the flag directly.

    Learn more

    binwalk scans a binary file for known magic numbers and reports embedded file formats. ZIP archives start with the bytes PK\x03\x04. Even if the outer file appears broken, binwalk -e can carve out valid ZIP sections. The extracted archive in this challenge contains a text file, which turns out to be the text of Frankenstein, with the flag hidden inside.

    After extraction, use grep -r 'picoCTF' on the extracted directory to find the flag without reading the entire novel.

Interactive tools
  • StegallDrop any file and Stegall runs every applicable steg technique in parallel: LSB sweeps, bit planes, spectrograms, polyglot carving, metadata, whitespace decode, and a 6-layer base/ROT/XOR/zlib cascade. Recursively unpacks results and surfaces flag matches.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.

Flag

Reveal flag

picoCTF{w0rd_d4wg_y0u_f0und_5h3113ys_m4573rp13c3_...}

The flag is embedded in a text file (the novel Frankenstein by Mary Shelley) inside the ZIP archive hidden in the BMP image. The trailing 8-character hex suffix is generated per instance and will differ from the value shown here.

Key takeaway

Steganography can exploit the structural layout of a file format rather than the least significant bits of individual pixels. By assigning an entire color channel to carry a payload, the hidden data is invisible to casual inspection because the visual difference between channels is not apparent at a glance. Understanding a format's byte layout (pixel ordering, channel interleaving, header offsets) is the key skill, and it transfers to any binary format where some fields are less perceptually meaningful than others, including audio samples, video frame padding, and PDF embedded streams.

Related reading

Useful tools for Forensics

Where to go next