Skip to main content

Investigative Reversing 3 picoCTF 2019 Solution

The binary hides flag bits in the low bit of eight bytes out of every nine, so find the start offset and invert that cycle.

Published: April 2, 2026Updated: August 25, 2026

Description

Two files: a mystery binary and an encoded.bmp image. The binary hides a flag inside the BMP using a more complex LSB encoding than the earlier challenges in this series.

Download both files.

bash
wget <url>/mystery
bash
wget <url>/encoded.bmp
bash
chmod +x mystery

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Decompile the binary in Ghidra
    Observation
    The challenge gives a compiled binary and an encoded BMP but no source. Decompiling mystery in Ghidra is what reveals the exact start offset and stride pattern the encoder used, before any extraction.
    Load mystery into Ghidra and inspect the decompiled main. The binary opens flag.txt, original.bmp, and encoded.bmp. It reads the flag one byte at a time and encodes each bit into the LSB of a corresponding byte in encoded.bmp, but with a twist: after every 8 encoded bytes it writes one unmodified byte from the original image. That 8-encoded + 1-passthrough pattern repeats 100 times starting at file offset 0x2d3 (decimal 723).
    bash
    ghidra mystery &
    What didn't work first

    Tried: Run strings or strace on mystery to infer the encoding scheme without Ghidra.

    strings shows the filenames (flag.txt, original.bmp, encoded.bmp) and nothing about the 9-byte cycle or the 0x2d3 start. strace shows open and read calls but not the loop arithmetic. Without the decompiled loop body you will guess wrong parameters and extract garbage.

    Tried: Assume the encoding matches the earlier Investigative Reversing challenges and reuse a previous extractor script directly.

    Earlier challenges in this series use a simpler stride, no passthrough bytes, and a different start offset. Reuse that script here and every 9th byte gets consumed as data instead of skipped, so the output is garbled. Read this binary's loop first to learn the 8-plus-1 cycle.

    Learn more

    LSB steganography works by replacing the lowest-order bit of each image byte with one bit of hidden data. A 1-bit change shifts the byte value by at most 1, which is invisible to the eye but detectable by reading that single bit back. The 9-byte cycle here (8 encoded + 1 passthrough) is a deliberate complexity layered on top of basic LSB: the passthrough byte acts as padding that the decoder must skip.

  2. Step 2Confirm the encoding pattern with xxd
    Observation
    Ghidra's decompiled loop points at offset 0x2d3 as the encoding start, with a 9-byte cycle. Worth verifying both against the raw bytes with xxd before building an extractor around them.
    Before writing the extractor, verify the offset. BMP pixel data in this file begins at byte 54 (the standard BMP header size), but the flag encoding starts much later at 0x2d3. Use xxd to inspect that region and confirm the 9-byte cycle boundary by comparing encoded.bmp to the unmodified original. The LSBs of those first eight bytes read 0,0,0,0,1,1,1,0, which is 0x70 little-endian: the 'p' of picoCTF.
    bash
    xxd -s 0x2d3 -l 9 encoded.bmp

    Expected output

    000002d3: 5050 4e4e 5151 5152 50                   PPNNQQQRP
    What didn't work first

    Tried: Start inspecting LSB differences from byte 54, the standard BMP pixel data offset, instead of 0x2d3.

    The standard BMP header ends at byte 54, but the encoder deliberately skips the first 0x2d3 bytes before writing any flag bits. Bytes 54 through 0x2d2 are unmodified image data, so diffing that region against the original shows nothing and suggests the encoding has not started. Seek xxd straight to 0x2d3 to land on the real boundary.

    Tried: Use a visual diff tool or steghide to detect the modified region rather than xxd.

    Visual diff tools compare pixel colors and cannot show a 1-bit LSB change, since the shift is at most one intensity unit, well under perception. steghide expects a passphrase and its own embedding algorithm, so it extracts nothing from a binary that rolled its own scheme. xxd at a specific offset works because it shows raw byte values you can check by hand against the 9-byte cycle.

    Learn more

    BMP files store pixels row by row, starting after a fixed header. The encoding in this challenge deliberately skips the first 0x2d3 bytes so the image header and some initial pixel data remain clean, making a quick visual comparison harder to spot.

  3. Step 3Write the extractor
    Observation
    Ghidra confirms an 8-encoded plus 1-passthrough cycle starting at 0x2d3, and xxd validates the boundary. That is every parameter needed: read 9 bytes per flag character, collect the LSB of the first 8, skip the 9th.
    Open encoded.bmp as raw bytes, seek to offset 0x2d3, then loop 50 times (one iteration per flag character). For each character, read 9 bytes: collect the LSB of each of the first 8 bytes to build a bit string (LSB-first, little-endian), then skip the 9th passthrough byte. Convert the 8-bit string to a character and append it to the flag.
    python
    python3 << 'EOF'
    with open('encoded.bmp', 'rb') as f:
        f.seek(0x2d3)
        data = f.read(50 * 9)  # 50 characters, 9 bytes each
    
    flag = ''
    for i in range(50):
        bits = ''
        for j in range(8):          # 8 LSB-encoded bytes per character
            byte = data[i * 9 + j]
            bits += str(byte & 1)   # collect LSB, LSB-first order
        char_val = int(bits[::-1], 2)  # reverse for big-endian byte value
        if char_val == 0:
            break
        flag += chr(char_val)
    
    print(flag)
    EOF

    Expected output

    picoCTF{4n0th3r_L5b_pr0bl3m_...}
    What didn't work first

    Tried: Collect LSB bits in MSB-first order and convert directly without reversing the bit string.

    The binary writes the least significant bit of each flag byte first, so the raw bit string comes out LSB-first. Call int(bits, 2) without reversing and bit 0 gets treated as the most significant position, giving the wrong character. The [::-1] reversal fixes the ordering, and you can check it: the first decoded character should be 'p' (0x70).

    Tried: Read all 50 * 9 = 450 bytes and then iterate over them with a stride of 8 instead of 9, ignoring the passthrough bytes entirely.

    Striding by 8 instead of 9 means that by the 9th character you are eating the 8th character's passthrough byte as data, and everything after it shifts one position. The output looks plausible for the first few characters (picoCTF{...) and then corrupts from character 9 on. The loop must consume 8 data bytes and skip 1 passthrough byte per iteration.

    Learn more

    Bit-ordering matters: the binary encodes the least-significant bit of each flag byte into the first LSB slot it writes, so the collected bit string is LSB-first and must be reversed before converting to an integer with int(bits[::-1], 2). Always verify bit order by checking known bytes - the 'p' in 'picoCTF' is 0x70 (0111 0000), so the first few extracted bits should spell that out once correctly ordered.

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{4n0th3r_L5b_pr0bl3m_...}

The hash suffix is per-instance and will differ from the value above. Run the extractor against your own encoded.bmp to get the correct flag.

Key takeaway

LSB steganography hides data by overwriting the least significant bit of cover bytes, a change too small to see but perfectly recoverable in software. Real schemes layer structure on top: non-standard start offsets, irregular strides, passthrough bytes, custom bit orderings, all to slow down an analyst without the encoder source. Reversing the encoder first is the reliable move, because it hands you the exact parameters instead of making you guess.

Related reading

Useful tools for Forensics

Where to go next