Tools: pngcheck in CTF: How to Analyze and Repair PNG Files (2026)

Tools: pngcheck in CTF: How to Analyze and Repair PNG Files (2026)

🔍 pngcheck CTF Tutorial: How to Analyze Corrupted PNG Files and Find Hidden Chunks

This Article at a Glance

Introduction: The PNG Challenge Where I Used Every Tool Except the Right One

What is pngcheck? (And What It Isn't)

What pngcheck actually does

What pngcheck cannot do

When to Use pngcheck in CTF

Problem description keywords that should trigger pngcheck

pngcheck vs zsteg vs binwalk — When to Use Which

Basic Usage With Thinking

Step 1 — Basic validation: is it broken at all?

Step 2 — Verbose output: read every chunk

Step 3 — Maximum detail: zlib and compression info

The Three Most Common CTF Scenarios

Scenario 1: Broken CRC — The Most Common Trap

Scenario 2: Hidden Custom Chunks

Scenario 3: Data Appended After IEND

Common Mistakes and Rabbit Holes

Full Trial Process Table

Command Reference

Beginner Tips

My personal pngcheck workflow for every PNG challenge

Installing pngcheck

What You Learn From Using pngcheck

Further Reading Searching for "pngcheck CTF" or "how to fix corrupted PNG forensics" usually returns tool documentation or terse Writeups that skip the thinking. This article is different: it's a walkthrough of how I actually use pngcheck in CTF PNG forensics challenges — including the 30 minutes I wasted on steganography tools before I learned to validate structure first. If you're stuck on a corrupted PNG challenge and wondering what pngcheck is showing you, this guide will get you unstuck. pngcheck is a command-line PNG validation tool that reads a PNG file's internal chunk structure and reports exactly what's wrong — or what's hidden — at the byte level. In CTF forensics, it's the fastest way to diagnose a corrupted PNG, find non-standard chunks, and decide whether you're dealing with a structural fix challenge or a steganography challenge. By the end of this article, you'll know when to run it, what its output means, and — just as importantly — when to put it down and switch tools. CTF forensics challenges love PNG files. They're binary, they have a well-documented structure, and there are a dozen ways to hide data inside them without visually changing the image. The problem for beginners is that a corrupted or manipulated PNG doesn't announce itself — it just fails to open, or opens fine while hiding something in the chunk data you never look at. pngcheck is the tool that makes the invisible visible. It reads the raw PNG chunk stream and validates every piece of the structure: the magic header bytes, the IHDR dimensions, each IDAT chunk's CRC, the IEND terminator, and anything else lurking in between. It won't decrypt anything or extract hidden images — but it will tell you precisely where the file is broken, where extra data is hiding, and what every chunk in the file actually contains. The challenge that taught me this was picoCTF's Corrupted File — a PNG that wouldn't open, a description that said "I tried to open this image but something seems off," and me spending 30 minutes going down the wrong path completely. My first instinct was steganography. I ran zsteg challenge.png, got output I didn't understand, tried every channel combination. Nothing. I tried stegsolve and clicked through every filter. Still nothing. I even tried strings and grepped for picoCTF{. The flag wasn't there because the image wasn't a steganography challenge. It was a broken CRC challenge. One pngcheck -v would have shown me the answer in 2 seconds. The reason I didn't run it first: I associated PNG challenges with hidden pixel data and never considered that the file structure itself was the puzzle. A PNG file is a sequence of chunks. Each chunk has a type (4-byte name like IHDR, IDAT, tEXt), a length, data, and a CRC checksum. pngcheck reads every chunk in order, validates the CRC, checks that required chunks are present in the right order, and reports anything unexpected. The basic output looks like this: And verbose output — which is what you actually want in CTF — looks like this: This is the part beginners miss. pngcheck is a validator and inspector — it is not an extractor or a decoder. It will tell you that a tEXt chunk exists with keyword "Comment," but it won't show you the content of that comment in basic mode. It will tell you there's data after the IEND chunk, but it won't extract it. It validates structure; everything else needs another tool. I've developed a reflex: if a PNG challenge mentions any of these, pngcheck runs first before anything else: The trap I fell into on Corrupted File — and then again on two other challenges before I finally learned — was running steganography tools on a PNG that had a structural problem. My reasoning at the time: "It's a PNG challenge, so it's probably steganography." That assumption is wrong about half the time. Here's the decision logic I've built since then: Use pngcheck when: the file won't open, the challenge mentions "corruption," "chunks," or "structure," or you want to enumerate what chunks exist before doing anything else. pngcheck answers the question: is this file structurally valid? Use zsteg when: pngcheck reports no errors and the image opens normally. zsteg checks for LSB-encoded data hidden in pixel channels — it operates entirely at the pixel level and doesn't care about chunk structure. If pngcheck says the file is clean, zsteg is your next move. Use binwalk when: you suspect an entirely different file is embedded somewhere inside the PNG, or when pngcheck reports extra data after IEND and you want to extract it cleanly. binwalk signature-scans the raw bytes regardless of format. The decision order that works for me: pngcheck -fvp first, always. If it passes → zsteg. If it fails with extra data → binwalk -e. If it fails with CRC → hex editor to patch. This sequence alone has saved me from countless Rabbit Holes. (For a deeper look at binwalk, see CTF Forensics: How to Use binwalk to Extract Hidden Files.) If this returns an error, you have a structural problem. The chunk name tells you where to look. IHDR error = broken header. IDAT error = broken image data. CRC mismatch = someone modified a byte somewhere. The next question is whether it was intentional (challenge design) or accidental (corrupted file). This is the command I run on every PNG challenge now, even before checking if it opens. The verbose output shows chunk names, offsets, lengths, and CRC status. I'm looking for: The -f flag forces pngcheck to continue checking even after errors (useful when multiple chunks are broken). The -p flag prints the contents of non-critical chunks including text. This is how I found a base64-encoded flag sitting in a tEXt chunk with keyword "Author" — the image opened perfectly, the flag was in plain sight in the chunk data, and I'd wasted 20 minutes on pixel-level steg before checking this. A challenge author modifies a chunk's data without recalculating the CRC. pngcheck catches it immediately: The CRC mismatch means the IHDR data was modified after the CRC was set. In CTF, this almost always means the image dimensions were changed — the real dimensions were replaced with smaller values to crop out the hidden content. The flag is in the part of the image that was "hidden" by reducing the reported height or width. To fix it: find the correct CRC value for the real IHDR data, then patch the file. Here's the Python approach I use: After patching, run pngcheck fixed.png to confirm it passes, then open the image. If the dimensions were manipulated, the fixed file will render at the real size and reveal the hidden area. This pattern is so common in picoCTF and beginner CTFs that I now check for dimension mismatches as the first instinct whenever I see an IHDR CRC error. PNG allows custom (ancillary) chunks. They're valid PNG — most image viewers ignore unknown chunks silently. pngcheck lists them: That flAg chunk after IEND is not standard. The data inside it is the flag. pngcheck found it in one command. Without it, I'd be running steganography tools on an image that wasn't hiding anything in its pixels at all. The IEND chunk is supposed to be the last chunk in a PNG. Data after it is technically invalid, but most image viewers load the image anyway and ignore the trailing bytes. pngcheck flags it: From the offset of IEND, you can calculate exactly where the appended data starts and extract it with dd or binwalk. The Corrupted File mistake was the first one. The second was a different challenge where pngcheck reported "No errors detected" — so I assumed I'd checked everything and moved to pixel-level steganography. I spent another 20 minutes on zsteg before going back and running pngcheck -fvp. The -p flag I'd skipped revealed a tEXt chunk with keyword "flag" containing a base64 string. It was sitting there in plain text the whole time. The file was clean structurally — the data was just hidden in a chunk I hadn't looked at. The third mistake: I saw an IHDR CRC error, correctly identified that dimensions had been manipulated, patched the CRC — and then stopped. The image rendered at the new larger size, but I didn't look at what was in the newly revealed area carefully enough. The flag was written in white text on a white background in the bottom 50 pixels that had been hidden. I would have seen it immediately if I'd opened the image in a tool that let me invert colors. Lesson: when you fix a CRC and the image changes size, the newly revealed area is where to look first. brew install pngcheck If you want to go further with the pattern this article teaches — validate structure before running analysis tools — the same mindset applies to disk images with mount and mmls, to ZIP archives with zip2john, and to PDFs with pdfdumper. Every binary format has a structure validator. Find it before running extraction tools. Using pngcheck teaches you to read binary file structure before running tools. The PNG chunk format is one of the clearest examples of how a file format works internally — fixed headers, typed chunks, CRC integrity checks, a defined terminator. Once you understand why pngcheck exists and what it's validating, you start applying the same thinking to every binary challenge: what does the spec say this file should contain, and what does this specific file actually contain? That gap between spec and reality is where CTF flags live. pngcheck is the tool that measures that gap for PNG files. In real-world forensics, the same principle applies. Investigators validate file format integrity to detect tampering — a modified PNG with a recalculated CRC might look valid to an image viewer but will show the modification timestamp inconsistency at the chunk level. CTF PNG challenges are teaching you actual forensic thinking, not just CTF tricks. This article is part of the Forensics Tools series. You can see the other tools covered in the series here: CTF Forensics Tools: The Ultimate Guide for Beginners. Introducing the pngcheck command, how to use it in CTF, and common patterns used in problems. Here are related articles from alsavaudomila.com that complement what you've learned here about pngcheck: Once pngcheck confirms that a PNG file is structurally clean and you still suspect hidden data, the next tool to reach for is zsteg. It operates entirely at the pixel level, scanning LSB-encoded channels that pngcheck cannot see. zsteg in CTF: Detect and Extract Hidden Data from Images walks through exactly when and how to use it, including the patterns that distinguish a clean image from one carrying hidden payloads. When pngcheck reports extra data after the IEND chunk, the right tool to extract it is binwalk. Rather than manually calculating offsets with dd, binwalk signature-scans the raw bytes and pulls out embedded files automatically — regardless of filesystem or format boundaries. binwalk in CTF: How to Analyze Binaries and Extract Hidden Files explains how to read its output and avoid the common mistake of extracting too aggressively. If pngcheck reveals a tEXt or iTXt chunk with suspicious content, exiftool can read the full metadata embedded across all chunk types — including GPS data, creation timestamps, and author fields that pngcheck shows as keywords but doesn't display in full. exiftool in CTF: How to Analyze Metadata and Find Hidden Data covers how metadata fields become hiding places in CTF challenges and what to look for. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ pngcheck challenge.png OK: challenge.png (800x600, 24-bit RGB, non-interlaced, 92.3%). $ pngcheck challenge.png OK: challenge.png (800x600, 24-bit RGB, non-interlaced, 92.3%). $ pngcheck challenge.png OK: challenge.png (800x600, 24-bit RGB, non-interlaced, 92.3%). $ pngcheck -v challenge.png File: challenge.png (153847 bytes) chunk IHDR at offset 0x0000c, length 13 800 x 600 image, 24-bit RGB, non-interlaced chunk tEXt at offset 0x00025, length 36, keyword: Comment chunk IDAT at offset 0x00057, length 8192 (OK) chunk IDAT at offset 0x02065, length 8192 (OK) chunk IEND at offset 0x25819, length 0 No errors detected in challenge.png (5 chunks, 92.3% compression). $ pngcheck -v challenge.png File: challenge.png (153847 bytes) chunk IHDR at offset 0x0000c, length 13 800 x 600 image, 24-bit RGB, non-interlaced chunk tEXt at offset 0x00025, length 36, keyword: Comment chunk IDAT at offset 0x00057, length 8192 (OK) chunk IDAT at offset 0x02065, length 8192 (OK) chunk IEND at offset 0x25819, length 0 No errors detected in challenge.png (5 chunks, 92.3% compression). $ pngcheck -v challenge.png File: challenge.png (153847 bytes) chunk IHDR at offset 0x0000c, length 13 800 x 600 image, 24-bit RGB, non-interlaced chunk tEXt at offset 0x00025, length 36, keyword: Comment chunk IDAT at offset 0x00057, length 8192 (OK) chunk IDAT at offset 0x02065, length 8192 (OK) chunk IEND at offset 0x25819, length 0 No errors detected in challenge.png (5 chunks, 92.3% compression). $ pngcheck challenge.png CRC error in chunk IHDR (computed 4a3f2c1b, expected 00000000) ERRORS DETECTED in challenge.png $ pngcheck challenge.png CRC error in chunk IHDR (computed 4a3f2c1b, expected 00000000) ERRORS DETECTED in challenge.png $ pngcheck challenge.png CRC error in chunk IHDR (computed 4a3f2c1b, expected 00000000) ERRORS DETECTED in challenge.png $ pngcheck -v challenge.png $ pngcheck -v challenge.png $ pngcheck -v challenge.png $ pngcheck -fvp challenge.png $ pngcheck -fvp challenge.png $ pngcheck -fvp challenge.png $ pngcheck -v challenge.png chunk IHDR at offset 0x0000c, length 13 800 x 600 image, 24-bit RGB, non-interlaced CRC error in chunk IHDR (computed 4a3f2c1b, expected 1a2b3c4d) ERRORS DETECTED in challenge.png $ pngcheck -v challenge.png chunk IHDR at offset 0x0000c, length 13 800 x 600 image, 24-bit RGB, non-interlaced CRC error in chunk IHDR (computed 4a3f2c1b, expected 1a2b3c4d) ERRORS DETECTED in challenge.png $ pngcheck -v challenge.png chunk IHDR at offset 0x0000c, length 13 800 x 600 image, 24-bit RGB, non-interlaced CRC error in chunk IHDR (computed 4a3f2c1b, expected 1a2b3c4d) ERRORS DETECTED in challenge.png import struct, zlib with open("challenge.png", "rb") as f: data = f.read() # IHDR chunk data is at bytes 12-28 (after 8-byte signature + 4-byte length + 4-byte type) ihdr_data = data[12:29] # 4 (length) + 4 (IHDR) + 13 (data) = offset 12 to 28 chunk_type_and_data = data[16:29] # just "IHDR" + 13 bytes of data correct_crc = zlib.crc32(chunk_type_and_data) & 0xFFFFFFFF print(f"Correct CRC: {correct_crc:#010x}") # Patch: replace bytes 29-33 with correct CRC patched = data[:29] + struct.pack(">I", correct_crc) + data[33:] with open("fixed.png", "wb") as f: f.write(patched) import struct, zlib with open("challenge.png", "rb") as f: data = f.read() # IHDR chunk data is at bytes 12-28 (after 8-byte signature + 4-byte length + 4-byte type) ihdr_data = data[12:29] # 4 (length) + 4 (IHDR) + 13 (data) = offset 12 to 28 chunk_type_and_data = data[16:29] # just "IHDR" + 13 bytes of data correct_crc = zlib.crc32(chunk_type_and_data) & 0xFFFFFFFF print(f"Correct CRC: {correct_crc:#010x}") # Patch: replace bytes 29-33 with correct CRC patched = data[:29] + struct.pack(">I", correct_crc) + data[33:] with open("fixed.png", "wb") as f: f.write(patched) import struct, zlib with open("challenge.png", "rb") as f: data = f.read() # IHDR chunk data is at bytes 12-28 (after 8-byte signature + 4-byte length + 4-byte type) ihdr_data = data[12:29] # 4 (length) + 4 (IHDR) + 13 (data) = offset 12 to 28 chunk_type_and_data = data[16:29] # just "IHDR" + 13 bytes of data correct_crc = zlib.crc32(chunk_type_and_data) & 0xFFFFFFFF print(f"Correct CRC: {correct_crc:#010x}") # Patch: replace bytes 29-33 with correct CRC patched = data[:29] + struct.pack(">I", correct_crc) + data[33:] with open("fixed.png", "wb") as f: f.write(patched) $ pngcheck -v challenge.png chunk IHDR at offset 0x0000c, length 13 chunk IDAT at offset 0x00025, length 8192 (OK) chunk IEND at offset 0x25801, length 0 chunk flAg at offset 0x25815, length 42 (unknown ancillary chunk) No errors detected in challenge.png (4 chunks). $ pngcheck -v challenge.png chunk IHDR at offset 0x0000c, length 13 chunk IDAT at offset 0x00025, length 8192 (OK) chunk IEND at offset 0x25801, length 0 chunk flAg at offset 0x25815, length 42 (unknown ancillary chunk) No errors detected in challenge.png (4 chunks). $ pngcheck -v challenge.png chunk IHDR at offset 0x0000c, length 13 chunk IDAT at offset 0x00025, length 8192 (OK) chunk IEND at offset 0x25801, length 0 chunk flAg at offset 0x25815, length 42 (unknown ancillary chunk) No errors detected in challenge.png (4 chunks). $ pngcheck challenge.png invalid chunk name "" (00 00 00 00) ERRORS DETECTED in challenge.png $ pngcheck challenge.png invalid chunk name "" (00 00 00 00) ERRORS DETECTED in challenge.png $ pngcheck challenge.png invalid chunk name "" (00 00 00 00) ERRORS DETECTED in challenge.png chunk IEND at offset 0x25801, length 0 additional data after IEND chunk chunk IEND at offset 0x25801, length 0 additional data after IEND chunk chunk IEND at offset 0x25801, length 0 additional data after IEND chunk # Debian/Ubuntu/Kali -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install pngcheck macOS -weight: 500;">brew -weight: 500;">install pngcheck # Debian/Ubuntu/Kali -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install pngcheck macOS -weight: 500;">brew -weight: 500;">install pngcheck # Debian/Ubuntu/Kali -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install pngcheck macOS -weight: 500;">brew -weight: 500;">install pngcheck - "The image is corrupted" or "won't open" - "Something is wrong with the file" - "Check the structure" or "check the chunks" - "The file passes validation but something is off" - Any hint involving CRC, chunk, header, or IHDR - Any chunk with a CRC error - Non-standard chunk names (anything that isn't IHDR/IDAT/IEND/tEXt/zTXt/gAMA/etc.) - Chunks in wrong order (IDAT before IHDR is invalid) - Content after IEND - Run pngcheck -fvp challenge.png immediately — even before trying to open the file - Read every line of output. Non-standard chunk names are red flags - If there's a CRC error in IHDR, check the dimensions with a hex editor before doing anything else - If it passes cleanly, note any tEXt, zTXt, or iTXt chunks — extract their content - Check for data after IEND - Only switch to steganography tools (zsteg, stegsolve) after structural analysis is complete