tunn3l v1s10n picoCTF 2021 Solution

Published: April 2, 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 1
    Identify the file format
    Observation
    I noticed the file tunn3l_v1s10n had no extension, which suggested I could not trust the name alone and needed to inspect the raw bytes to determine the actual format before any other analysis.
    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 'PC bitmap' here, but it only tells you the type - not the specific corrupted field values at 0x0A, 0x0E, and 0x16 that you need to patch. xxd lets you read the exact byte values and offsets needed to compute the fix, so skipping xxd means skipping the information required for the actual repair in the next step.

    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 in the file. 'strings' will print a few embedded text fragments from the header or decoy region but will not output the flag, because the flag exists only as colored pixels that become visible after the height field is corrected and the image is rendered.

    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 2
    Open the BMP and notice the truncated image
    Observation
    I noticed the BMP magic bytes were confirmed and the file size (0x0F264E from bytes 0x02-0x05) was much larger than what a small decoy image would require, which suggested that pixel data existed beyond the region the header told 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 3
    Fix the BMP height field in a hex editor
    Observation
    I noticed the header bytes at offsets 0x0A and 0x0E both read 'ba d0' (a suspicious repeating pattern that looks intentionally corrupted), and the height value at 0x16 was much smaller than the file size implied, which suggested all three fields had been deliberately 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 still at 0xBAD0 (47824), the BMP parser looks for pixel rows starting at byte 47824 instead of byte 54, which is past the end of the file. Most viewers will either show a blank image or refuse to open the file entirely. Both 0x0A and 0x0E must be corrected to 0x36 and 0x28 respectively 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.

    The flag is not hidden via LSB steganography or a steghide passphrase - it is concealed purely by the falsified height value that causes image decoders to stop rendering before they reach the flag rows. Steghide will report 'could not extract any data' and zsteg will find nothing meaningful because there is no embedded payload; the pixel art flag is already in the raw pixel data and just needs the decoder to be told the correct image 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 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 file formats trust their own header metadata completely; parsers render only as many rows as the height field declares, so truncating that value hides anything below the falsified boundary. Manipulating header fields to conceal or corrupt data is a recurring forensics technique seen in CTFs and real malware that embeds payloads in image files. Understanding a format's on-disk layout well enough to compute valid field values from first principles (file size, width, bits-per-pixel) is the core skill for both forensics recovery and intentional steganography.

Related reading

Want more picoCTF 2021 writeups?

Tools used in this challenge

What to try next