MSB picoCTF 2023 Solution

Published: April 26, 2023

Description

Most-significant-bit steganography hides data in the high bit of each pixel channel. Use a Python MSB steganography script or Stegsolve to extract the hidden text, then grep for the flag.

Install Stegsolve (or an equivalent tool) and open the provided PNG.

Use Analyse → Data Extract, set bit order to MSB First, and enable Red 7, Green 7, Blue 7. Scroll to the top of the extracted text.

bash
wget https://artifacts.picoctf.net/c/294/Ninja-and-Prince-Genji-Ukiyoe-Utagawa-Kunisada.flag.png
bash
# Option 1: Python MSB extraction script
bash
pip3 install Pillow
bash
# Option 2: Stegsolve
bash
wget http://www.caesum.com/handbook/Stegsolve.jar -O stegsolve.jar
bash
java -jar stegsolve.jar

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
Stegsolve's bit-plane extraction is explained step-by-step in the Introduction to Steganography Tools post - including the exact settings to use for MSB vs LSB extraction.
  1. Step 1
    Extract MSB data
    Observation
    The challenge title 'MSB' directly named the target bit plane, which suggested extracting bit 7 from each pixel channel rather than the more common LSB (bit 0) used in standard steganography tools.
    Use a Python MSB steganography script to extract bit 7 from each pixel channel. The extracted bytes concatenated form the hidden text. Alternatively, use Stegsolve: toggle Red 7, Green 7, Blue 7 in the data extractor and save the output.
    python
    python3 - <<'PY'
    from PIL import Image
    
    img = Image.open("Ninja-and-Prince-Genji-Ukiyoe-Utagawa-Kunisada.flag.png")
    pixels = list(img.getdata())
    
    bits = []
    for pixel in pixels:
        for channel in pixel[:3]:  # R, G, B
            bits.append((channel >> 7) & 1)  # MSB
    
    # Group bits into bytes
    out = bytearray()
    for i in range(0, len(bits) - 7, 8):
        byte = 0
        for b in bits[i:i+8]:
            byte = (byte << 1) | b
        out.append(byte)
    
    with open("msb_output.bin", "wb") as f:
        f.write(out)
    print("Written msb_output.bin")
    PY
    bash
    strings msb_output.bin | grep pico

    Expected output

    picoCTF{15_y0ur_que57...d55bee}
    What didn't work first

    Tried: Using steghide or zsteg instead of a MSB-aware script, since the file is a PNG with a hidden message.

    steghide only works with password-based LSB embedding in JPEG/BMP and will report 'could not extract any data' on this PNG. zsteg checks LSB planes (bits 0-1 by default) and finds nothing because the flag is in bit 7, not bit 0. Neither tool has a mode for arbitrary bit-plane extraction - the Python script or Stegsolve with 'Red 7, Green 7, Blue 7' checked is required.

    Tried: Extracting bit 0 (LSB) instead of bit 7 (MSB) by changing '>> 7' to '>> 0' or '& 1' in the Python script.

    LSB extraction produces a different bit stream entirely - one that belongs to the low-order noise in the image. The output will be binary garbage with no printable flag. The challenge title 'MSB' directly names bit 7 as the target; the script must use '(channel >> 7) & 1' to isolate the most significant bit of each channel.

    Learn more

    In digital images each pixel's color is encoded as a set of channel values (Red, Green, Blue) typically stored as 8-bit integers (0 to 255). Each byte has 8 bit planes: bit 0 is the least significant bit (LSB) and bit 7 is the most significant bit (MSB). Changing bit 7 shifts a channel value by 128, a very visible change. Changing bit 0 shifts it by 1, imperceptible to the human eye.

    Classic LSB steganography hides data in bit 0, where changes are invisible. This challenge uses MSB steganography(bit 7) instead. Because the image was specifically chosen to have "natural" high-bit patterns (or was specifically crafted), the flag hidden in the MSB is not visually obvious. Stegsolve's data extractor reads the selected bit from every pixel in row-major order and concatenates them into a byte stream, which surfaces the hidden ASCII text.

    Stegsolve is a Java tool that provides bit-plane viewers, color filters, and frame analyzers. It is invaluable for CTF image steganography because it visualizes every bit plane at a glance: a pattern of text in an otherwise noisy bit plane is a dead giveaway that data is hidden there. The full bit-plane workflow is covered in the CTF steganography and steganography tools guides.

  2. Step 2
    Search the dump
    Observation
    I noticed the MSB extraction script writes raw binary output to msb_output.bin, which suggested using 'strings' to filter printable sequences before grepping for 'pico', since the flag would be surrounded by non-printable bytes in the bit stream.
    Stegsolve's data extractor saves the raw bytes (the default save dialog dumps to a file you choose; many people just save it as text in the same directory). Run strings on the saved file and grep for pico. Remove stray spaces from the recovered flag if MSB alignment introduced any.
    bash
    strings text | grep pico

    Expected output

    picoCTF{15_y0ur_que57...d55bee}
    What didn't work first

    Tried: Running 'strings text | grep pico' but using the original PNG file rather than the MSB-extracted output file.

    Running strings on the original PNG will scan the raw image bytes - PNG metadata, IDAT compressed pixel data, and ancillary chunks. The flag is not stored as literal ASCII in those bytes; it is encoded bit-by-bit across pixel channels. Only after MSB extraction and byte reconstruction does the flag appear as printable ASCII that strings can find.

    Tried: Grepping the extracted file directly without strings, using 'grep pico msb_output.bin' or opening it in a text editor.

    grep on a binary file without the -a flag may stop at NUL bytes or misinterpret multi-byte sequences, causing it to silently miss the flag. The extracted MSB stream contains many non-printable bytes surrounding the flag text. Using 'strings' first filters for contiguous printable runs of length >= 4, ensuring the flag's ASCII characters are surfaced cleanly regardless of surrounding binary noise.

    Learn more

    After extraction, the raw byte stream may contain the flag interleaved with garbage bytes, or the flag may be contiguous. strings filters for sequences of printable ASCII characters of a minimum length (default 4), which naturally isolates human-readable content like picoCTF{...} from surrounding binary data.

    The grep pico pipe then narrows the output to only lines containing the flag prefix. This two-step pipeline - strings | grep pattern - is a general-purpose technique applicable to any binary file: ELF binaries, firmware dumps, memory captures, or steg output. It is often the fastest first step in any binary analysis task.

    About those spaces: MSB extraction reads one bit per channel per pixel, then groups the resulting bit stream into 8-bit bytes. If the hidden message is shorter than the image (or padded with zero-bytes), runs of zero bits decode to NULs (0x00) which print as nothing, and runs of 0x20 bytes decode as ASCII space. Strip extra whitespace before submitting.

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{15_y0ur_que57...d55bee}

Only the MSB bits carry useful data, so LSB analysis will pass but yield nothing.

Key takeaway

Image steganography exploits the fact that pixel bit planes contribute unequally to visual fidelity: the least significant bits are invisible changes, while the most significant bit controls roughly half the channel's brightness range. Choosing which bit plane carries the payload is a design decision by the hider, so a complete steg analysis must check every bit plane, not just bit 0. Tools like Stegsolve make this systematic by rendering each plane as a separate image, turning a needle-in-a-haystack search into a quick visual scan.

Related reading

Want more picoCTF 2023 writeups?

Useful tools for Forensics

What to try next