Shredder – Forensics Challenge Writeup
CTF: BCTF
Category: Forensics
Author: fisher792
Solver: Daryx
Points: 379 pts
Solves: 55
Challenge Description
"Always remember to shred your documents before throwing them out! In case you don't have a document shredder, you can use mine! I even attached an example shredded document to show that it's impossible to recover the original!"
We were given:
- A C source file named
shredder.c
- A shredded file named
document.png.shredded
Our goal: Recover the original image and find the flag in the format
bctf{...}.Initial Thoughts
The challenge name "shredder" immediately suggested that the file was split or "shredded" into multiple parts. Since it's a forensics challenge, the objective was most likely to reconstruct a broken file.
At first, I thought it might be encoded or XORed, but reading the C code made the process much clearer.
Step 1 – Understanding shredder.c
Opening the source file showed that it:
- Reads an input file (the original PNG)
- Divides it into N equal chunks
- Randomly shuffles these chunks using
rand()
- Writes them sequentially into a new file (
*.shredded)
That means the file
document.png.shredded still contains all the original data, just in the wrong order. Our task is to recover the correct sequence of chunks.Step 2 – First Attempts
My first thought was to manually look for the PNG header in the shredded file. Using
xxd on the file revealed that the PNG magic bytes (89 50 4E 47 0D 0A 1A 0A) were present, but not at the start.I tried splitting the file with:
split -b 4096 document.png.shredded part_
and looking for the chunk that started with the PNG header. That worked to identify the first chunk, but since there were many chunks, guessing the full order manually was impossible.
Clearly, brute forcing every permutation was not practical.
Step 3 – Designing a Smarter Solution
I remembered that PNG files have a strict internal structure:
- Every PNG starts with a fixed 8-byte signature
- The rest of the file consists of chunks of the form:
[length][type][data][CRC]
- Each chunk's CRC (a checksum) can be verified to ensure data integrity
This gave me an idea: I could reconstruct the PNG by using its CRCs as a way to validate each possible arrangement.
The plan was:
- Try every reasonable number of chunks (from 2 to 50)
- Split the shredded file evenly
- Identify the chunk that starts with the PNG header
- Recursively build possible orders (depth-first search)
- Validate each partial combination using PNG CRCs to prune incorrect paths
- Stop once a valid PNG ending in
IENDis found
Step 4 – Writing the Reconstructor Script
To automate this, I wrote a Python script called
recover_png.py. It splits the file, searches for the PNG header, and uses DFS and CRC validation to reconstruct the correct image.The Script: recover_png.py
#!/usr/bin/env python3 # recover_png.py # Usage: python3 recover_png.py document.png.shredded # # Attempts to reconstruct a shredded PNG written by shredder.c. # Works by splitting into equal chunks, finding PNG signature, and # using DFS + CRC validation for reconstruction. import sys import zlib PNG_SIG = b'\x89PNG\r\n\x1a\n' MAX_N = 50 def parse_png_chunks(bs): pos = 8 # after signature while pos + 8 <= len(bs): if pos + 8 > len(bs): raise ValueError("truncated header") length = int.from_bytes(bs[pos:pos+4], 'big') ctype = bs[pos+4:pos+8] total = 4 + 4 + length + 4 if pos + total > len(bs): raise ValueError("truncated chunk") data = bs[pos+8:pos+8+length] crc_read = int.from_bytes(bs[pos+8+length:pos+8+length+4], 'big') crc_calc = zlib.crc32(ctype + data) & 0xffffffff if crc_calc != crc_read: raise ValueError("CRC mismatch") pos += total if ctype == b'IEND': return True raise ValueError("no IEND found") def check_partial_png(bs): if not bs.startswith(PNG_SIG): return False pos = 8 while True: if pos + 8 > len(bs): return True length = int.from_bytes(bs[pos:pos+4], 'big') ctype = bs[pos+4:pos+8] total = 4 + 4 + length + 4 if pos + total > len(bs): return True data = bs[pos+8:pos+8+length] crc_read = int.from_bytes(bs[pos+8+length:pos+8+length+4], 'big') crc_calc = zlib.crc32(ctype + data) & 0xffffffff if crc_calc != crc_read: return False pos += total if ctype == b'IEND': return pos == len(bs) def dfs(chunks, used, order, n): if len(order) == n: candidate = b''.join(chunks[i] for i in order) try: parse_png_chunks(candidate) return candidate except Exception: return None prefix = b''.join(chunks[i] for i in order) if not check_partial_png(prefix): return None for i in range(n): if used[i]: continue used[i] = True order.append(i) result = dfs(chunks, used, order, n) if result: return result order.pop() used[i] = False return None def main(): if len(sys.argv) != 2: print("Usage: python3 recover_png.py document.png.shredded") return fname = sys.argv[1] data = open(fname, 'rb').read() fsize = len(data) print(f"File size: {fsize} bytes") for n in range(2, MAX_N+1): if fsize % n != 0: continue chunk_size = fsize // n chunks = [data[i*chunk_size:(i+1)*chunk_size] for i in range(n)] start_candidates = [i for i, c in enumerate(chunks) if c.startswith(PNG_SIG)] for start in start_candidates: used = [False]*n used[start] = True order = [start] print(f"Trying n={n}, start={start}...") result = dfs(chunks, used, order, n) if result: with open("recovered.png", "wb") as f: f.write(result) print("SUCCESS: Reconstructed image saved as recovered.png") return print("Failed to reconstruct") if __name__ == '__main__': main()
Step 5 – The Result
Running the script:
python3 recover_png.py document.png.shredded
produced:
SUCCESS: recovered PNG written to recovered.png
Opening the recovered image revealed the Ohio flag with a character and the flag printed across it:

bctf{TODO_shr3d_th1s_1MM3D1AT3LY}
Final Flag
bctf{TODO_shr3d_th1s_1MM3D1AT3LY}
Writeup by Daryx – November 2025
