Skip to main content

tunn3l v1s10n picoCTF 2021 Solution

A corrupted BMP image hides more than it shows. Analyze the file format to reveal the full picture.

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

Description

We found this file. Recover the flag.

Download the file tunn3l_v1s10n.

bash
wget <url>/tunn3l_v1s10n

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Identify the file format
    Observation
    The file has no extension, so the name says nothing about its type. Inspect the raw bytes to establish the real format before anything else.
    xxd shows the first bytes start with 42 4d (ASCII 'BM'). That's the BMP signature. Rename to .bmp so image viewers will open it.
    bash
    xxd tunn3l_v1s10n | head
    bash
    cp tunn3l_v1s10n tunn3l_v1s10n.bmp

    Expected output

    00000000: 424d 4e26 0f00 0000 0000 bad0 0000 bad0  BMN&............
    00000010: 0000 6e04 0000 3201 0000 0100 1800 0000  ..n...2.........
    What didn't work first

    Tried: Run 'file tunn3l_v1s10n' instead of xxd to identify the format.

    The file command does correctly report a PC bitmap, but it gives you the type and nothing else, not the corrupted field values at 0x0A, 0x0E, and 0x16 that need patching. xxd shows the exact bytes and offsets the fix is computed from, so skipping it means skipping the information the repair depends on.

    Tried: Run 'strings tunn3l_v1s10n' hoping to extract the flag text directly.

    The flag is drawn as pixel art in the image data, not stored as an ASCII string. strings prints a few text fragments from the header or the decoy region and never the flag, because the flag exists only as coloured pixels that appear once the height field is corrected and the image renders.

    Learn more

    Magic bytes (file signatures) are the first bytes of a file that identify its format, independent of the extension. 42 4D is ASCII "BM" (BMP). Other common signatures: FF D8 FF (JPEG), 89 50 4E 47 (PNG), 25 50 44 46 (%PDF). The file command uses a database of these signatures to identify files. See hex dumps for CTF for the broader cheat sheet.

  2. Step 2Open the BMP and notice the truncated image
    Observation
    The BMP magic bytes check out, and the recorded file size is far larger than a small decoy image would need. So there is pixel data beyond the region the header tells viewers to render.
    Open tunn3l_v1s10n.bmp in an image viewer. It shows only a small portion (a decoy message) at the top. The real flag is in the lower portion of the pixel data, hidden because the BMP header lies about the height.
  3. Step 3Fix the BMP height field in a hex editor
    Observation
    The header bytes at 0x0A and 0x0E both read 'ba d0', a repeating pattern that looks deliberately corrupted, and the height at 0x16 is far smaller than the file size implies. All three fields were falsified to hide the flag in the lower pixel rows.
    BMP stores width at file offset 0x12 and height at 0x16, both 4-byte little-endian signed integers. The current value at 0x16 (32 01 00 00 = 306) is too small. Two additional fields are corrupted in this file: the pixel data offset at 0x0A (ba d0 00 00, should be 36 00 00 00 = 54) and the DIB header size at 0x0E (ba d0 00 00, should be 28 00 00 00 = 40). Patch all three fields, then compute the correct height from the file size and width.
    bash
    xxd tunn3l_v1s10n.bmp | head -2
    bash
    # Sample corrupted header (offsets 0x00-0x1F):
    bash
    # 00000000: 42 4d 4e 26 0f 00 00 00 00 00 ba d0 00 00 ba d0
    bash
    #                                           ^0x0A corrupt  ^0x0E corrupt
    bash
    # 00000010: 00 00 6e 04 00 00 32 01 00 00 01 00 18 00  ...
    bash
    #           ^0x0E cont.  width=0x46e  height=0x132 <-- also patch 0x16
    bash
    # Fix 0x0A: ba d0 00 00 -> 36 00 00 00  (pixel data offset = 54)
    bash
    # Fix 0x0E: ba d0 00 00 -> 28 00 00 00  (DIB header size = 40)
    bash
    # Fix 0x16: 32 01 00 00 -> 52 03 00 00  (height = 850)
    bash
    vim -b tunn3l_v1s10n.bmp   # then :%!xxd, edit, :%!xxd -r, :wq
    bash
    # Or use bless / hexedit for a graphical editor
    What didn't work first

    Tried: Only patch the height field at 0x16 and leave 0x0A and 0x0E as-is.

    With the pixel data offset left at 0xBAD0, the parser hunts for pixel rows starting at byte 47824 rather than byte 54, which is past the end of the file. Most viewers then show a blank image or refuse to open it. Correct both 0x0A and 0x0E, to 0x36 and 0x28, before fixing the height has any visible effect.

    Tried: Use a steganography tool like steghide or zsteg to extract the hidden content instead of editing the header.

    Nothing here is hidden by LSB steganography or a steghide passphrase. The concealment is entirely the falsified height, which makes decoders stop rendering before they reach the flag rows. steghide reports no extractable data and zsteg finds nothing meaningful, because there is no embedded payload: the pixel art is already in the raw data and only needs the decoder told the correct dimensions.

    Learn more

    BMP header byte offsets (all little-endian):

    • 0x00-0x01: BM signature
    • 0x02-0x05: total file size in bytes
    • 0x0A-0x0D: pixel data offset (start of pixel array; should be 54 = 0x36 for a basic BMP)
    • 0x0E-0x11: DIB header size (should be 40 = 0x28 for BITMAPINFOHEADER)
    • 0x12-0x15: image width
    • 0x16-0x19: image height
    • 0x1C-0x1D: bits per pixel

    Computing the corrected height. The pixel data occupies file_size - pixel_data_offset bytes (typically file_size - 54 for a basic BMP). Each row is width * (bits_per_pixel / 8) bytes, padded up to a 4-byte boundary. So:

    pixel_data_size = file_size - pixel_data_offset
    row_bytes = ((width * bpp + 31) // 32) * 4   # 4-byte aligned
    height = pixel_data_size // row_bytes

    For this challenge: width 0x46e (1134), bpp 24, so row_bytes = 1134 * 3 = 3402 rounded-lg to 3404. Note that the size field at 0x02-0x05 is also corrupted in this file and does not reflect the real file size; use the actual on-disk size from wc -c tunn3l_v1s10n.bmp (approximately 2,893,454 bytes) rather than the header value. Divide the pixel-data size by row_bytes and write the result (850) at offset 0x16 in little-endian.

    Verification. Save and reopen the image. The top should still show the decoy line, but now the bottom rows render and the flag appears as drawn text in the previously hidden region.

Interactive tools
  • File Magic IdentifierIdentify file types from magic numbers. Paste hex bytes or drop a file to detect PNG, JPEG, ZIP, PDF, ELF, PCAP, SQLite, and dozens of other formats.

Flag

Reveal flag

picoCTF{qu1t3_a_v13w_2020}

Three BMP header fields are corrupted: pixel data offset at 0x0A (ba d0 -> 36 00), DIB header size at 0x0E (ba d0 -> 28 00), and image height at 0x16 (32 01 -> 52 03 = 850). Patching all three reveals the flag hidden in the lower portion of the image.

Key takeaway

Image formats trust their own header metadata completely: a parser renders exactly as many rows as the height field declares, so shrinking that value hides everything below the falsified boundary. Manipulating header fields to conceal or corrupt data recurs in CTFs and in real malware that embeds payloads in images. Knowing a format's on-disk layout well enough to compute valid field values from file size, width, and bits per pixel is the core skill for both recovery and deliberate steganography.

Related reading

Tools used in this challenge

Where to go next