Investigative Reversing 1 picoCTF 2019 Solution

Published: April 2, 2026

Description

You are given a binary (mystery) and three PNG images (mystery.png, mystery2.png, mystery3.png). The binary reads a flag from flag.txt and scatters encoded bytes of it across the three PNG files by appending them after each file's IEND marker. Your job is to reverse-engineer the binary's distribution scheme and reassemble the flag from the appended data.

Download all four files.

bash
wget <url>/mystery
bash
wget <url>/mystery.png
bash
wget <url>/mystery2.png
bash
wget <url>/mystery3.png
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 1
    Confirm data is appended after each PNG's IEND marker
    Observation
    I noticed the challenge description says the binary scatters encoded flag bytes across the three PNG files, which suggested the bytes were appended after each file's formal IEND+CRC boundary where image parsers stop reading, making a raw hex dump the right first check.
    PNG files end with an IEND chunk followed by a 4-byte CRC. Any bytes after that CRC are not part of the image and are invisible to standard viewers. Use xxd to dump the tail of each file and confirm extra bytes are present after the IEND+CRC sequence.
    bash
    xxd mystery.png  | tail -20
    bash
    xxd mystery2.png | tail -20
    bash
    xxd mystery3.png | tail -20
    What didn't work first

    Tried: Open each PNG in an image viewer or run pngcheck to look for hidden data

    Image viewers and pngcheck parse only valid PNG chunks and stop at IEND, so appended bytes after the CRC are completely invisible to them. The data exists in the raw file stream beyond the formal PNG structure, which is why a raw hex dumper like xxd is needed to see it.

    Tried: Use steghide or zsteg to extract hidden content from the PNG files

    steghide and zsteg work on pixel-level LSB encoding inside the image data chunks. This challenge appends raw bytes after the IEND+CRC boundary and never touches pixel data at all, so both tools report no detectable payload and return nothing useful.

    Learn more

    The four bytes spelling IEND (hex 49 45 4e 44) mark the last chunk type in a valid PNG. The 4-byte CRC that follows is always ae 42 60 82. Anything beyond that is appended data that image parsers silently ignore, making it a simple hiding spot.

  2. Step 2
    Reverse-engineer the binary in Ghidra
    Observation
    I noticed we needed to know exactly which flag byte goes to which PNG file and whether any arithmetic transform was applied before writing, and that information exists only inside the mystery binary itself, which suggested static analysis with Ghidra as the only reliable way to recover the full index-to-file mapping and the two numeric offsets.
    Open mystery in Ghidra. The decompiled main shows it opens flag.txt and reads 26 characters, then appends bytes to the three PNG files in a fixed order with two byte values modified by numeric offsets before writing. Note the exact index-to-file mapping and the two offsets.
    bash
    ghidra &
    What didn't work first

    Tried: Run strings on the mystery binary to find the flag directly

    strings only surfaces contiguous printable sequences embedded in the binary itself. The flag is read at runtime from flag.txt and written byte-by-byte to the PNG files, so it never exists as a literal string in the binary. strings will show function names and format strings but not the flag or the distribution mapping.

    Tried: Use ltrace or strace to trace the binary's file writes and reconstruct the order

    ltrace and strace can show that writes happen to each PNG, but the output gives raw byte values at the OS syscall level without the higher-level index arithmetic context. The two offset adjustments (0x15 and 4) are computed before the write syscall, so strace shows the already-modified byte value, not the original - making it impossible to know which bytes need the offset subtracted without also reading the Ghidra decompilation.

    Learn more

    The binary writes flag bytes in this order (reconstructed from Ghidra):

    • mystery3.png gets index 1, then 2, then 5, then indices 10-14.
    • mystery2.png gets index 0 with 0x15 (21) added, then index 3 with 4 added.
    • mystery.png gets index 4, then indices 6-9, then indices 15-25.

    To recover the flag you invert those writes: subtract the same offsets before storing each byte back into its correct index position.

  3. Step 3
    Write a Python script to extract and reassemble the flag
    Observation
    I noticed that Ghidra revealed the exact non-sequential index mapping across three files and the two byte-level offsets (0x15 and 4) applied before writing, which suggested writing a script that seeks to each file's post-IEND tail, places each byte at the correct flag index, and subtracts the offsets for the two modified positions before printing the assembled flag.
    The script memory-maps each PNG, seeks past the IEND+CRC boundary to reach the appended bytes, and reads them in the same order the binary wrote them. Two bytes require arithmetic reversal (subtract the offsets that the binary added) before the 26-character flag can be printed.
    python
    python3 << 'EOF'
    import os, mmap
    
    def get_tail(filename):
        size = os.path.getsize(filename)
        fd = os.open(filename, os.O_RDONLY)
        m = mmap.mmap(fd, size, access=mmap.ACCESS_READ)
        # seek past IEND (4 bytes) + CRC (4 bytes)
        start = m.find(b"IEND") + 4 + 4
        tail = m[start:]
        m.close()
        return tail
    
    FLAG_LEN = 26
    flag = [0] * FLAG_LEN
    
    t1 = get_tail("mystery.png")
    t2 = get_tail("mystery2.png")
    t3 = get_tail("mystery3.png")
    
    # mystery3 offsets (in read order): indices 1, 2, 5, 10-14
    flag[1]  = t3[0]
    flag[2]  = t3[1]
    flag[5]  = t3[2]
    for i, idx in enumerate(range(10, 15)):
        flag[idx] = t3[3 + i]
    
    # mystery2 offsets (in read order): indices 0, 3 - with arithmetic
    flag[0] = t2[0] - 0x15     # binary added 0x15 before writing
    flag[3] = t2[1] - 4        # binary added 4 before writing
    
    # mystery offsets (in read order): indices 4, 6-9, 15-25
    flag[4] = t1[0]
    for i, idx in enumerate(range(6, 10)):
        flag[idx] = t1[1 + i]
    for i, idx in enumerate(range(15, 26)):
        flag[idx] = t1[5 + i]
    
    print("".join(chr(b) for b in flag))
    EOF

    Expected output

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

    Tried: Read all appended bytes from each PNG in sequential order and concatenate them directly as the flag

    The binary does not write flag bytes in index order. It scatters them across three files in a specific non-sequential mapping (mystery3 gets indices 1, 2, 5, 10-14; mystery2 gets indices 0 and 3; mystery.png gets index 4, then 6-9, then 15-25). Concatenating the raw tails in file order produces a shuffled, garbled string that is not the flag.

    Tried: Skip the offset subtraction for mystery2's bytes since they look like ASCII already

    The binary added 0x15 (21) to the byte at flag index 0 and 4 to the byte at flag index 3 before writing them to mystery2.png. Without subtracting those offsets, the two recovered characters are shifted to wrong ASCII values, producing a string that fails the picoCTF flag format check even though all other bytes are correct.

    Learn more

    This is not LSB pixel steganography. No image pixel data is touched at all. The flag bytes are simply concatenated onto the end of the PNG binary stream, so no image library is needed - just raw file I/O past the IEND+CRC boundary. The only wrinkle is that two byte values were arithmetically modified before writing (offsets 0x15 and 4), so those must be subtracted to recover the original ASCII characters.

    The Investigative Reversing series (parts 0-4) escalates complexity: part 0 uses a single image with simpler byte appending, while part 1 introduces three images and byte reordering. Later parts add more images or additional obfuscation layers.

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{An0tha_1_...}

The flag is a static value embedded at challenge creation time. Your instance flag may differ if the challenge was regenerated.

Key takeaway

Splitting secret data across multiple carrier files and reordering its bytes is a classic anti-forensics technique that mirrors real-world data-exfiltration methods where attackers fragment stolen files to evade content-inspection tools. Reconstructing the original requires knowing both the distribution mapping and any per-byte transforms, information that lives only in the encoder binary. Static analysis of the encoder in a decompiler is the authoritative way to recover both, which is why reverse engineering is central to binary forensics challenges.

Related reading

Want more picoCTF 2019 writeups?

Useful tools for Forensics

What to try next