Description
We found this file. Recover the flag.
Setup
wget <url>/tunn3l_v1s10nSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Identify the file format
ObservationThe 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.bashxxd tunn3l_v1s10n | headbashcp tunn3l_v1s10n tunn3l_v1s10n.bmpExpected 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 4Dis ASCII "BM" (BMP). Other common signatures:FF D8 FF(JPEG),89 50 4E 47(PNG),25 50 44 46(%PDF). Thefilecommand uses a database of these signatures to identify files. See hex dumps for CTF for the broader cheat sheet.Step 2Open the BMP and notice the truncated image
ObservationThe 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.Learn more
A BMP carries its dimensions in the DIB header rather than inferring them from how much pixel data follows. A viewer reads the width and height fields, multiplies them out to work out how many rows to draw, and stops there. Nothing forces those fields to agree with the amount of data actually present in the file, and no checksum covers them, so a deliberately shrunken height simply makes every decoder crop the image in exactly the same way.
That is why the size discrepancy is the tell. The file header records the total file size, and comparing it against width times height times the bytes per pixel shows far more data on disk than the declared dimensions can account for. The extra rows are perfectly ordinary pixels sitting untouched after the ones being rendered; correcting the height field is not recovery of damaged data, it just stops the decoder from cropping. The same trick works on any format that stores dimensions separately from the payload, which is why forensic tools flag a header-versus-body size mismatch as suspicious rather than as corruption.
Step 3Fix the BMP height field in a hex editor
ObservationThe 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.bashxxd tunn3l_v1s10n.bmp | head -2bash# Sample corrupted header (offsets 0x00-0x1F):bash# 00000000: 42 4d 4e 26 0f 00 00 00 00 00 ba d0 00 00 ba d0bash# ^0x0A corrupt ^0x0E corruptbash# 00000010: 00 00 6e 04 00 00 32 01 00 00 01 00 18 00 ...bash# ^0x0E cont. width=0x46e height=0x132 <-- also patch 0x16bash# 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)bashvim -b tunn3l_v1s10n.bmp # then :%!xxd, edit, :%!xxd -r, :wqbash# Or use ghex / hexedit for a graphical editorWhat didn't work first
Tried: Only patch the height field at 0x16 and leave 0x0A and 0x0E as-is.
The bytes at 0x0A read ba d0 00 00, which little-endian is 53434, so the parser hunts for pixel rows starting at byte 53434 rather than byte 54, tens of thousands of bytes into the pixel array. The DIB header size at 0x0E decodes to 53434 too, which matches no BMP header variant. Most viewers then refuse to open the file or render garbage. 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:
BMsignature - 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_offsetbytes (typicallyfile_size - 54for a basic BMP). Each row iswidth * (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_bytesFor this challenge: width 0x46e (1134), bpp 24, so
row_bytes = 1134 * 3 = 3402rounded up 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 fromwc -c tunn3l_v1s10n.bmp(2,893,454 bytes here) 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.
- 0x00-0x01:
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_...}
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. The flag is shown abbreviated on this page; work the steps above to recover the full value.