Step 180. CTF Taste Test 5: Forensics & Misc 3 — Dissecting Suspicious Files
Level 3 — Real-World CTF and Advanced Attack Skills | Difficulty ★★★☆☆ | Estimated time: 5 hours
Prerequisites: Steps 176–179 (CTF formats, Crypto intro). You know Python basics for handling files as bytes.
⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. Dreamhack (dreamhack.io) is a legal learning platform built to be solved.
- What you need: WSL Ubuntu (
file,xxd,strings— measured: file 5.45), Python 3 (measured: 3.12.14). - Caution: field tools like binwalk and exiftool aren’t installed in this environment, so they’re introduced as "output examples" only; the core routine is verified hands-on with three suspicious files you build yourself.
CTF’s Forensics category throws you "one suspicious file." An image, a packet dump, a memory dump — fine on the outside, with a flag hidden within. The starting point is always the same suspicion: "what it looks like and what the file actually is are different." Today you drill forensics’ basic routine into your hands and solve three mini case files yourself.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Determine a file’s real type from its magic bytes (file signature)
- Perform the basic forensics routine (
file→strings→xxd→ extraction tools) in order - Read PNG’s chunk structure (IHDR, IDAT, IEND) and extract hidden data after IEND
- See through an extension-disguised file with
file - Organize an evidence file’s analysis process into a "routine document"
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | WSL Ubuntu bash + Python 3 (measured: file 5.45, xxd, strings / Python 3.12.14) |
| Today’s commands | file file, strings file | grep -i flag, xxd file | head, Python bytes.find(b"IEND") |
| Concepts needed | Magic bytes, PNG chunk structure, data after end-of-file (appending), extension disguise |
| Today’s artifact | Solution records for three case files + your own forensics routine document |
2-1. Magic Bytes — A File’s ID Number
A file’s true kind is decided not by its extension but by its first few bytes. These agreed-upon starting bytes are called magic bytes, or the signature.
| File type | Starting bytes (hex) | Appearance |
|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A |
.PNG.... |
| JPEG | FF D8 FF |
... |
| ZIP | 50 4B 03 04 |
PK.. |
| ELF (Linux executable) | 7F 45 4C 46 |
.ELF |
The file command reads these magic bytes and tells you the kind. If the extension says .png but file says "ASCII text" — that file is in disguise.
2-2. PNG’s Chunk Structure — A Chain of Boxes
A PNG file consists of the 8-byte signature followed by boxes called chunks. Each chunk has the structure 4-byte length + 4-byte type + data + 4-byte checksum.
IHDR: the image’s size and color info (always the first chunk)IDAT: the actual pixel dataIEND: the "this is the end" marker
Here’s the key point — bytes that come after IEND mean nothing in the PNG spec. Image viewers ignore them too. Which makes it a favorite hiding spot for authors planting flags.
2-3. The Basic Forensics Routine — Cheapest and Fastest First
When you receive a suspicious file, run the light checks in order:
1. file file → confirm the real type (1 second)
2. strings file | grep -i flag → instantly search visible strings (1 second)
3. xxd file | head/tail → look at the front/back bytes yourself (signature, tail)
4. Specialized tools → binwalk (embedded files), exiftool (metadata), steghide/zsteg (image steganography)
A large share of real challenges end at steps 1–3. When tools can’t catch it, the last resort is looking at the file’s front and back yourself in a hex editor — exactly what we do today.
2-4. A Map of Field Tools — Output Examples
These aren’t in this environment, but here are the field-standard tools as output examples (Screen example — not actually executed):
# Screen example — binwalk: a tool that finds files inside files
$ binwalk image.png
DECIMAL HEXADECIMAL DESCRIPTION
0 0x0 PNG image
69436 0x10F3C Zip archive data, at least v2.0 to extract
# Screen example — exiftool: a tool that reads metadata (shooting info, etc.)
$ exiftool photo.jpg
Camera Model Name : iPhone 13
Comment : DH{m3t4d4t4_1s_4_g1ft}
binwalk catches "a ZIP pasted wholesale inside a file pretending to be a PNG"; exiftool catches "a flag written in a photo’s comment field." For pcap files, the standard flow is opening them in Wireshark and using Follow TCP Stream — reading the flag inside a conversation.
3. Follow Along
3-1. Generating the Case Files — Three Suspicious Files
The script below builds today’s three case files (this chapter’s local output was measured 2026-09-09).
Input (forensics_lab.py)
import struct, zlib
def chunk(typ, data):
c = struct.pack(">I", len(data)) + typ + data
return c + struct.pack(">I", zlib.crc32(typ + data) & 0xFFFFFFFF)
def make_png(path):
sig = b"\x89PNG\r\n\x1a\n"
ihdr = chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0))
idat = chunk(b"IDAT", zlib.compress(b"\x00\xff\x00\x00"))
iend = chunk(b"IEND", b"")
with open(path, "wb") as f:
f.write(sig + ihdr + idat + iend)
make_png("sig.png") # Case 0: a normal PNG (baseline)
make_png("hidden.png") # Case 1: something after IEND
with open("hidden.png", "ab") as f:
f.write(b"DH{1s_th3r3_s0m3th1ng_4ft3r_iend}")
with open("fake.png", "w") as f: # Case 2: extension disguise
f.write("I am not an image. The flag is DH{n0t_4_r34l_png}\n")
Output
(three files created — sig.png 69 bytes, hidden.png 102 bytes, fake.png)
3-2. Case 0: A Normal PNG — Setting the Baseline
First you need to see what a clean file looks like before you can spot a suspicious one. In WSL:
file sig.png
sig.png: PNG image data, 1 x 1, 8-bit/color RGB, non-interlaced
xxd sig.png | head -6
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR
00000010: 0000 0001 0000 0001 0802 0000 0090 7753 ..............wS
00000020: de00 0000 0c49 4441 5478 9c63 f8cf c000 .....IDATx.c....
00000030: 0003 0101 00c9 fe92 ef00 0000 0049 454e .............IEN
00000040: 44ae 4260 82 D.B`.
(Measured 2026-09-09.)
How to read the output: the first 8 bytes 89 50 4e 47 ... are the PNG signature (also visible as .PNG on the right). Then IHDR (length 13 = 0000000d), IDAT (pixel data), and at the very end IEND (49 45 4e 44). A normal PNG ends at IEND. This layout is today’s baseline.
3-3. Case 1: The Uninvited Guest After IEND
Apply the routine to hidden.png. file stays quiet:
file hidden.png
hidden.png: PNG image data, 1 x 1, 8-bit/color RGB, non-interlaced
Notice: the size is 102 bytes, but the normal PNG (sig.png) was 69 bytes. file looks at the signature, answers "PNG," and doesn’t report the 33 bytes tacked on the back. That’s why you trust file but don’t blindly trust it.
Look at the tail yourself:
xxd hidden.png | tail -3
00000040: 44ae 4260 8244 487b 3173 5f74 6833 7233 D.B`.DH{1s_th3r3
00000050: 5f73 306d 3374 6831 6e67 5f34 6674 3372 _s0m3th1ng_4ft3r
00000060: 5f69 656e 647d _iend}
(Measured 2026-09-09. Up to D.B` is IEND and its checksum; the flag shows verbatim after that.)
strings catches it too:
strings hidden.png | grep DH
DH{1s_th3r3_s0m3th1ng_4ft3r_iend}
Proper extraction with Python — compute IEND’s position and cut everything after it:
data = open("hidden.png", "rb").read()
pos = data.find(b"IEND")
end = pos + 8 # IEND 4 bytes + checksum 4 bytes
print(f"file size: {len(data)}, IEND position: {pos}, proper end: {end}")
print("hidden data:", data[end:].decode())
file size: 102, IEND position: 61, proper end: 69
hidden data: DH{1s_th3r3_s0m3th1ng_4ft3r_iend}
How to read the output: "proper end (69) < file size (102)" — this single inequality exposes the disguise. This calculation is a miniature of what binwalk does.
3-4. Case 2: Extension Disguise
fake.png is a PNG in name only. Step 1 of the routine ends it instantly:
file fake.png
fake.png: ASCII text, with CRLF line terminators
strings fake.png | grep -i flag
I am not an image. The flag is DH{n0t_4_r34l_png}
(Measured 2026-09-09.)
How to read it: since the magic bytes aren’t PNG’s, file immediately declared "ASCII text." Extensions are decoration on the user interface; a file’s identity is proven by its front bytes.
3-5. What It Looks Like on the Real Platform — Screen Example
The flow in Dreamhack’s Forensics category (Screen example — this environment did not connect):
# Screen example — a platform challenge page
[Forensics] hidden-in-image Difficulty: 1
Attachment: evidence.zip ← you receive a suspicious file
$ file evidence.png
$ strings evidence.png | grep -i "DH{"
$ binwalk -e evidence.png # extract embedded files
$ steghide extract -sf photo.jpg # JPG steganography (press Enter for no password)
Today’s routine — starting from file and going all the way to looking at the tail yourself — becomes your first two moves as-is. Only the tools change; the order of suspicion stays the same.
4. Missions & Exercises
Mission — Build Your Own Case File and Write the Routine Document
- Build one case file hiding your own flag (
DH{...}) — pick one of three methods: ① append after IEND, ② extension disguise, ③ insert wherestringscan’t catch it (inside chunk data) - Play the role of a colleague: apply the 2-3 routine to your own file from the start, recording each step’s command and result
- Organize that record into "my forensics routine document" — commands, expected results, and the next move when something fails
Exercises
Exercise 1. You opened a file in xxd and the start was 50 4B 03 04. What’s this file’s real type? If the extension is .jpg, what should you suspect?
Exercise 2. A flag can hide even in a file where file answered "PNG image data." Explain why, in terms of the range file inspects.
Exercise 3. A PNG file is 500 bytes, but the IEND chunk ends at byte 450. What do you suspect, and with what Python code do you confirm it?
Exercise 4. Explain what strings does, and guess why challenges where the flag escapes strings are a harder grade.
5. Model Answers & Completion Criteria
Mission Model Answer
An example of a routine document (format is free):
[My Forensics Routine v1]
1. file {file} → check the type. If it differs from the extension, suspect disguise
2. ls -l {file} → record the size (compare with a normal file of the same kind)
3. strings | grep -iE "flag|DH\{" → check for an instant answer
4. xxd | head → check the signature
5. xxd | tail → check the tail (data after the proper end position)
6. If nothing: analyze by chunk/structure in Python → consider specialized tools (binwalk, etc.)
How to verify: ① can you solve your own case file again looking only at the routine document (the document’s completeness)? ② if you chose method ③ (inside chunk data), can you explain why strings can’t catch it — because the data got compressed or the bytes got mangled? Put characters inside IDAT and zlib compression shreds the string, so strings misses it.
Exercise Answers
Answer 1. 50 4B 03 04 (PK..) is ZIP’s signature. If the extension is .jpg, it’s a ZIP disguised as a JPG — rename it and unzip (unzip), or extract the contents with binwalk. CTF’s most common first button.
Answer 2. file decides mainly from the signature at the front and doesn’t verify the entire contents. So as long as the signature is correct, it answers "PNG" no matter what’s attached behind — which is why it stayed silent in Case 1 despite the 102-byte size (69 normal).
Answer 3. Suspect a 50-byte uninvited guest after IEND. Confirmation code:
data = open("mystery.png", "rb").read()
pos = data.find(b"IEND") + 8
print(len(data), pos) # 500 vs 450 — the difference is the evidence
print(data[pos:]) # print the hidden data
Answer 4. strings extracts stretches of "four or more printable characters in a row" from a file. If the flag is hidden in a compressed, encrypted, or XORed state, it isn’t a printable string, so it doesn’t get caught. That’s why such challenges become a harder grade, needing one more layer of decoding.
Completion Criteria Checklist
- [ ] I’ve memorized the magic bytes table for PNG, JPEG, ZIP, and ELF
- [ ] I can perform the
file→strings→xxdroutine in order - [ ] I can explain a normal PNG’s chunk layout (IHDR → IDAT → IEND)
- [ ] I extracted the hidden data after IEND with Python
- [ ] I saw through an extension-disguised file with
file - [ ] I know what binwalk, exiftool, and steghide each find
- [ ] Mission: I built a case file and completed the routine document
6. Common Pitfalls & Fixes
Wall 1. binwalk: command not found
Symptom: bash: line 1: binwalk: command not found (measured 2026-09-09 — this environment has no binwalk).
Cause: the tool isn’t installed. Forensics tools come in too many kinds to install them all up front.
Fix: today’s routine (file, strings, xxd, Python) all works out of the box. When the moment comes that truly needs binwalk (extracting embedded files), consider sudo apt install binwalk then — the golden rule of forensics gear management is not installing tools first, but adding them one by one as problems demand.
Wall 2. Trusting file alone and missing the flag
Symptom: file said "PNG" so you judged it normal, but the answer was at the back.
Cause: file is signature-focused and doesn’t report appended tail data (Case 1 measurement).
Fix: after file, always attach a size comparison and xxd | tail. A routine isn’t one step — it’s a chain.
Wall 3. I can’t find any text in xxd’s output
Symptom: you see only hex and can’t tell what characters there are.
Cause: xxd’s rightmost column is the ASCII translation. You just didn’t look there.
Fix: skim only the right column — . is an unprintable byte, letters show as-is. Case 1’s DH{1s_th3r3... was discovered in that column.
Wall 4. Python’s data.find(b"IEND") returns -1
Symptom: you searched for IEND and got -1 (not found).
Cause: either the file isn’t a PNG, or you opened it in text mode ("r") and the bytes got mangled.
Fix: always open in binary mode — open(path, "rb"). Text mode rewrites line endings and throws off byte-position calculations. And if find returns -1, doubt the signature again from the start.
Wall 5. I extracted it but got garbled characters
Symptom: you cut after IEND but the bytes are unreadable.
Cause: the hidden data isn’t plaintext — it’s encoded or XORed, a challenge mixed with Crypto.
Fix: chain in Step 179’s routine. Two-stage challenges where forensics "pulls it out" and Crypto "reads it" are common in the field.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Magic bytes | Type-marker bytes at a file’s front — identity is proven by these, not the extension |
| PNG chunk | A chain of boxes of length+type+data+checksum — IHDR, IDAT, IEND |
| Data after IEND | A spec-wise meaningless region — a favorite flag-hiding spot |
| Forensics routine | Cheapest and fastest checks first: file → strings → xxd → specialized tools |
| Disguised file | A file whose signature and actual contents differ — file is the first interrogator |
| Steganography | The craft of hiding data in images, etc. — detected by steghide and zsteg |
Today’s Commands
| Command | What it does |
|---|---|
file file |
Judge the real type from magic bytes |
strings file | grep -i flag |
Instantly search visible strings |
xxd file | head / | tail |
Look at the front (signature) and back (tail) yourself |
data.find(b"IEND") + 8 |
Compute a PNG’s proper end position |
binwalk -e file (example) |
Extract embedded files |
exiftool file (example) |
Read metadata |
An Instinct More Important Than Commands
Forensics’ weapon is not a tool list but an order of suspicion. Start from "the outside and the substance differ," and go from cheap checks to expensive ones. And when a tool falls silent (when file says "normal"), don’t mistake silence for evidence — silence only means "the signature is normal."
This eye matches the first move of real incident analysis beyond CTF. Login records, downloaded attachments — everything starts as "one suspicious file." When picking your field in Step 181, if "the process of digging through evidence" was fun today, that’s an important signal too.
Once every box is checked, Step 180 is complete. Click the checkbox in the sidebar to save your progress.