$ cat writeup.md…
$ cat writeup.md…
sunshinectf2026
Task: a single Discord-screenshot PNG whose bottom appears cut off, with title/flavor hinting at truncation. Solution: the IDAT zlib stream decompresses to more scanlines (418) than the IHDR-declared height (382), revealing 36 hidden rows; patch IHDR height, recompute CRC32, and render the full image to reveal the message input box holding the flag.
hmmmmmmm today i will make a ctf challenge ... uhhh ..... ill type it out just give me a second
English summary: A single PNG file (hereyougo.png, declared 492×382, RGBA) shows a Discord
chat screenshot. A user announces they will make a CTF challenge, posts troll-face emojis and a
photo, then says "ill type it out just give me a second." A Discord message input box with a
blinking text cursor sits at the bottom. The title "you cut me off" and the pause-heavy flavor
text ("uhhh .....", "just give me a second") hint that the typed message is cut off. Goal: recover
the flag (sun{...}) that the user was typing.
Standard recon showed a clean file — this is the misdirection layer:
exiftool / binwalk: valid PNG, single IDAT chunk, no trailing data after IEND.Key insight — compare decompressed IDAT length against the geometry implied by IHDR:
height = 382, width = 492, color type 6 (RGBA) → 4 bytes/pixel.width * bytes_per_pixel = 1 + 492*4 = 1969 bytes.382 * 1969 = 752,158 bytes.823042 / 1969 = 418 scanlines.So the file actually contains 418 rows of pixel data, but IHDR claims only 382 — 36 extra scanlines are hidden below the visible image. This is the classic PNG height-crop hiding technique: the author encoded a taller image, then shrank the IHDR height so a renderer only draws the top portion. The title "you cut me off" points exactly at this cropped/cut-off height rather than at pixel LSB or Morse.
len(4) + "IHDR"(4) + width(4) = byte 20).#!/usr/bin/env python3 import struct, zlib SRC = "hereyougo.png" OUT = "full.png" data = bytearray(open(SRC, "rb").read()) assert data[:8] == b"\x89PNG\r\n\x1a\n" # --- walk chunks: read IHDR, concatenate IDAT payloads --- pos = 8 ihdr_off = None idat = b"" while pos < len(data): length = struct.unpack(">I", data[pos:pos+4])[0] ctype = bytes(data[pos+4:pos+8]) body = data[pos+8:pos+8+length] if ctype == b"IHDR": ihdr_off = pos # start of this chunk (length field) width, height, bitdepth, colortype = struct.unpack(">IIBB", body[:10]) elif ctype == b"IDAT": idat += body elif ctype == b"IEND": break pos += 12 + length # len(4) + type(4) + data + crc(4) # --- geometry check --- channels = {0:1, 2:3, 3:1, 4:2, 6:4}[colortype] # 6 = RGBA stride = 1 + width * channels # +1 filter byte per row raw = zlib.decompress(idat) true_rows = len(raw) // stride print(f"declared height={height}, width={width}, stride={stride}") print(f"decompressed={len(raw)} bytes -> {true_rows} scanlines " f"({len(raw)%stride} remainder)") # --- patch IHDR height + recompute CRC32 --- hdr_data_off = ihdr_off + 8 # start of IHDR data (width field) data[hdr_data_off+4:hdr_data_off+8] = struct.pack(">I", true_rows) # height new_ihdr = bytes(data[ihdr_off+4:hdr_data_off+13]) # "IHDR"+data(13) crc = zlib.crc32(new_ihdr) & 0xffffffff data[hdr_data_off+13:hdr_data_off+17] = struct.pack(">I", crc) open(OUT, "wb").write(data) print(f"wrote {OUT} with height patched {height} -> {true_rows}") # Open full.png: the extra rows reveal the Discord input box with the flag. # Flag format: sun{REDACTED}
Use this technique when:
len(inflate(IDAT))
against height * (1 + width*bytes_per_pixel)).$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar