Forensics
Step 248. Five Comprehensive Forensics Challenges — Linking the Chain of Techniques on Your Own
Level 3 — Real-World CTF & Advanced Offensive Skills | Difficulty ★★★★☆ | Estimated time: 4–6 hours
Prerequisites: Step 239 (file signatures), Step 245 (log analysis), Step 246 (EXIF/metadata), Step 247 (encrypted-artifact recovery) complete. Python 3 is used.
⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
- What you need: Python 3 (measured: 3.12.14, with pillow and python-docx), a memo file for notes. No internet connection needed.
- Caution: this is a wargame chapter. Generate the challenge files yourself in section 3, then look only at section 4’s problems and solve them first. Section 5’s model answers are for when you’re stuck or for comparison after solving.
Real-world forensics problems come with techniques mixed together. You open one file and the next clue falls out, and that clue becomes the key to another file — a chain structure. What’s being tested is the thinking that connects what you learned across this track in order: "extract a file from a pcap → that file is encrypted → crack it → inside is a metadata clue." Today, completion means making five mini challenges yourself, solving them pretending you don’t know the answers, and drawing the technique-chain map.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Physically perform the investigative order that starts with a signature check when handed an unknown file
- Connect a clue from one piece of evidence to the key of another (a technique chain)
- Build the habit of falling back on a "tools I haven’t tried yet" checklist when stuck
- Document each solution as a "technique-chain map" so it’s reproducible
- Solve at least four of the five problems independently
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 — pillow, python-docx, zipfile, struct, re |
| Today’s techniques | Signature identification (Step 239), carving, clue extraction from logs (Step 245), EXIF/document metadata (Step 246), XOR key recovery (Step 247) |
| Concepts needed | Technique chains, carving, evidence list → technique-mapping plan, a stuck-response routine |
2-1. Technique Chains — One Piece of Evidence Is Another’s Key
The standard structure of a comprehensive challenge is a chain. For example:
log file ──(read)──→ password clue ──(XOR decrypt)──→ locked file ──(open)──→ flag
Each link corresponds to one of the previous chapters. So skill at comprehensive problems isn’t a new technique — it’s the speed of choosing "which technique to pull out for this evidence right now." That’s why you draw the "evidence list → technique to apply to each" table first when starting a problem.
2-2. Carving — Cutting a File Out of a File
Carving is the technique of finding a file hidden inside another by its signature and cutting it out. Camouflage like a ZIP appended to the end of an image is a staple. The principle is simple: a ZIP starts with PKx03x04, so find that signature’s position in the binary and cut from there to the end — and you have a ZIP. This is exactly why you learned signatures in Step 239.
2-3. The Stuck-Response Routine — Falling Back on the Checklist
The standard routine when stuck is sweeping the "tools I haven’t tried yet" list. For this track:
- Did you check the signature? (Never trust the extension)
- Did you extract strings (the
stringsrole)? - Did you look at the metadata (EXIF, core.xml)?
- Is there another file inside the file? (carving)
- If it’s encrypted — is the key space small? Do you know any plaintext? Is a password clue sitting in another piece of evidence?
The fifth is the core route of today’s challenges — the connecting suspicion: "might the password that opens this file be over there?"
3. Follow Along — Generating the Challenge Bundle
3-1. Building the Five Challenges
Generate the challenge files yourself. build_challenges.py:
from PIL import Image
from pathlib import Path
from zipfile import ZipFile, ZIP_DEFLATED
import io, docx
lab = Path("lab248"); lab.mkdir(exist_ok=True)
# C1: a JPEG disguised as .png — flag in EXIF
img = Image.new("RGB", (200, 120), (120, 30, 30))
ex = Image.Exif()
ex[0x010E] = "FLAG{wr0ng_ext3nsi0n}" # ImageDescription
ex[0x010F] = "LabCam"
buf = io.BytesIO(); img.save(buf, "JPEG", exif=ex)
(lab / "c1_evidence.png").write_bytes(buf.getvalue())
# C2: a ZIP appended to the end of a PNG (carving target)
png = Image.new("RGB", (100, 60), (20, 120, 60))
b2 = io.BytesIO(); png.save(b2, "PNG")
zb = io.BytesIO()
with ZipFile(zb, "w", ZIP_DEFLATED) as z:
z.writestr("flag.txt", "FLAG{th3r3_1s_m0r3_4ft3r_IEND}n")
(lab / "c2_sunset.png").write_bytes(b2.getvalue() + zb.getvalue())
# C3: a password leaked in a web log + an XOR-locked file
log = """192.168.0.5 - - [09/Sep/2026:10:01:02 +0900] "GET /index.html HTTP/1.1" 200 512
203.0.113.77 - - [09/Sep/2026:22:31:10 +0900] "GET /admin/ HTTP/1.1" 403 162
203.0.113.77 - - [09/Sep/2026:22:33:41 +0900] "GET /uploads/shell.php?cmd=zip%20-e%20-P%20bluewave42%20backup.zip%20db.sql HTTP/1.1" 200 88
203.0.113.77 - - [09/Sep/2026:22:34:02 +0900] "GET /files/backup.zip.enc HTTP/1.1" 200 240
"""
(lab / "c3_access.log").write_text(log, encoding="utf-8")
key = b"bluewave42"
plain = b"PKx03x04 fake-zip-body ... FLAG{st0l3n_but_br0k3n} ..."
(lab / "c3_backup.zip.enc").write_bytes(
bytes(b ^ key[i % len(key)] for i, b in enumerate(plain)))
# C4: a memo hidden in docx metadata
d = docx.Document()
d.add_paragraph("Nothing to see here.")
d.core_properties.author = "temp.staff"
d.core_properties.comments = "reminder: FLAG{m3t4d4t4_t3lls_tal3s}"
d.save(lab / "c4_memo.docx")
# C5: a chain — the photo's EXIF Model value is flag.bin's XOR key
img5 = Image.new("RGB", (160, 100), (10, 10, 80))
e5 = Image.Exif(); e5[0x0110] = "horizon"
b5 = io.BytesIO(); img5.save(b5, "JPEG", exif=e5)
(lab / "c5_photo.jpg").write_bytes(b5.getvalue())
flag5 = b"FLAG{ch41n_0f_ev1d3nce}"
(lab / "c5_flag.bin").write_bytes(bytes(f ^ ord("horizon"[i % 7]) for i, f in enumerate(flag5)))
print("created:", sorted(p.name for p in lab.iterdir()))
Run (measured 2026-09-09):
created: ['c1_evidence.png', 'c2_sunset.png', 'c3_access.log', 'c3_backup.zip.enc', 'c4_memo.docx', 'c5_flag.bin', 'c5_photo.jpg']
Rule: you did see the generation code, but when solving, close the code and look only at the files. Solving while knowing the answer and solving while not knowing are different kinds of training. Solving an hour later, once your memory has blurred, is even better.
3-2. The Investigation-Opening Ritual — Evidence List and Technique Mapping
Before solving, fill in this table first in a memo file (plan.md):
| Evidence file | First thing to check | Applicable techniques |
|---|---|---|
| c1_evidence.png | Is the signature really a PNG? | Signature identification, EXIF |
| c2_sunset.png | File size and structure | Carving |
| c3_access.log + c3_backup.zip.enc | Clues leaked in the log | Log analysis, XOR decryption |
| c4_memo.docx | Layers beyond the body text | Document metadata |
| c5_photo.jpg + c5_flag.bin | The relationship between the two files | EXIF, chain thinking |
This table is the real-world object of the "plan first" habit. In actual investigations too, the first 10 minutes go to this table, not to tools.
4. Missions & Exercises
Mission — Solve the Five Challenges Independently
Solve the five problems below and find each FLAG{...}. Rule: without looking at section 5, record the flags you find and your solution process in solutions.md.
- C1. The photo that won’t open — you opened
c1_evidence.pngin a viewer and it won’t open. Why? Reveal the file’s true identity and find the flag. - C2. The sunset photo’s secret —
c2_sunset.pngis an ordinary 100×60 image. But its file size is 356 bytes, somewhat large. Find what’s hidden. - C3. The stolen backup —
c3_access.logis the record of an intruder making off with a backup file. Recover the contents ofc3_backup.zip.enc. (Hint: the attacker left a command in the log) - C4. The document with nothing in it — the body of
c4_memo.docxis a single line: "Nothing to see here." Is there really nothing? - C5. Two pieces of evidence —
c5_photo.jpgandc5_flag.binwere found together. The latter is locked. The former is the key.
Each time you solve a problem, draw its technique-chain map in one line — e.g., "read log → obtain password → XOR decrypt → flag."
Exercises
Problem 1. In C1, facing "a png extension that won’t open," what should the first step of investigation be? Why?
Problem 2. In C3, what did the attacker accidentally leave behind, and why is this kind of mistake common in real incidents too?
Problem 3. In a chain problem like C5, what is the correct investigation order between "the open evidence" and "the locked evidence," and why?
Problem 4. If you solved all five problems but can’t draw section 4’s technique-chain map, what state are you lacking?
Answers & completion criteria · expand/collapse
5. Model Answers & Completion Criteria
Mission Model Answers (all measured 2026-09-09)
C1 solution — the signature beats the extension. Look at the contents, not the extension:
raw = open("lab248/c1_evidence.png", "rb").read()
print(raw[:4].hex(" ")) # ff d8 ff e0 <- JPEG magic!
from PIL import Image
img = Image.open("lab248/c1_evidence.png")
print(img.format) # JPEG
print(img.getexif().get(0x010E)) # FLAG{wr0ng_ext3nsi0n}
Measured output:
ff d8 ff e0
JPEG
FLAG{wr0ng_ext3nsi0n}
Chain map: check signature → identify as JPEG → read EXIF → flag. The file signature FF D8 FF is JPEG’s fingerprint (Step 239). An extension is only a claim; the signature is fact.
C2 solution — the world after IEND. A PNG should end with the IEND chunk, but this file is longer. Find the ZIP signature and cut:
d = open("lab248/c2_sunset.png", "rb").read()
print(d.find(b"IEND"), d.find(b"PKx03x04"), len(d)) # 203 211 356
from zipfile import ZipFile
import io
with ZipFile(io.BytesIO(d[d.find(b"PKx03x04"):])) as z:
print(z.namelist()) # ['flag.txt']
print(z.read("flag.txt").decode().strip())
Measured output:
203 211 356
['flag.txt']
FLAG{th3r3_1s_m0r3_4ft3r_IEND}
Chain map: size anomaly detected → search PK signature → carve → unzip → flag. Viewers show only the PNG portion, so the appended ZIP escapes the eye — "the visible end" and "the file’s end" being different is the point of the disguise.
C3 solution — what the attacker’s fingers left behind. Read the log and the webshell command appears:
import re
log = open("lab248/c3_access.log").read()
m = re.search(r"-P%20(w+)", log)
print(m.group(1)) # bluewave42
key = m.group(1).encode()
enc = open("lab248/c3_backup.zip.enc", "rb").read()
print(bytes(b ^ key[i % len(key)] for i, b in enumerate(enc)).decode())
Measured output:
bluewave42
PK fake-zip-body ... FLAG{st0l3n_but_br0k3n} ...
Chain map: log analysis → obtain the password (-P option) from the webshell command → XOR decrypt → flag. %20 is a URL-encoded space. While encrypting, the attacker left that password in cleartext on the command line, and the web server logs every request. Step 245’s webshell-trace reading became the key exactly as-is.
C4 solution — the body is bait. Look not at the docx’s body but at its metadata:
from zipfile import ZipFile
import re
with ZipFile("lab248/c4_memo.docx") as z:
core = z.read("docProps/core.xml").decode("utf-8")
print(re.search(r"<dc:creator>(.*?)</dc:creator>", core).group(1))
print(re.search(r"<dc:description>(.*?)</dc:description>", core).group(1))
Measured output:
temp.staff
reminder: FLAG{m3t4d4t4_t3lls_tal3s}
Chain map: check body (bait) → dissect as ZIP → core.xml → flag. "Not in the body" is not the end — a document has layers beyond the body (metadata, comments, hidden text) (Step 246).
C5 solution — the open evidence is the locked evidence’s key. Start with the photo’s EXIF:
from PIL import Image
key5 = Image.open("lab248/c5_photo.jpg").getexif().get(0x0110)
print(key5) # horizon
data = open("lab248/c5_flag.bin", "rb").read()
print(bytes(b ^ key5.encode()[i % len(key5)] for i, b in enumerate(data)).decode())
Measured output:
horizon
FLAG{ch41n_0f_ev1d3nce}
Chain map: read EXIF → suspect the Model value is the key → XOR decrypt → flag. The iron rule of chain problems is not charging at the locked file first but reading all the open evidence first.
Exercise Answers
Answer 1. Don’t trust the extension; check the file’s first bytes (the signature). An extension is just a name tag you can swap, but a signature is the fingerprint of the content itself. The measurement also revealed ff d8 ff e0 (JPEG).
Answer 2. The attacker typed the password as a command-line option (-P bluewave42), and that command was recorded in cleartext in the web server’s access log. In real incidents too, attackers forget that the server logs every request, or in their haste put passwords on the command line. That command lines and URLs get left all over the place (web logs, bash_history, process lists) is the attacker’s structural weakness.
Answer 3. Read all the open evidence first, then go to the locked evidence. In a chain structure, the open side often holds the locked side’s key (a password, a key, a hint). C5’s EXIF was like that, and so was C3’s log. Grab the locked thing first and brute force is all you have; look at the open things first and the key may come out for free.
Answer 4. You’re lacking not "solved" but "understood reproducibly." To draw the chain map, you must explain at each link which technique you pulled out and why. If there’s a problem you happened to get right by luck, document that problem’s solution over from the beginning — the moment you can explain it is true completion.
Completion Criteria Checklist
- [ ] I identified a file’s true identity by signature, not extension (C1)
- [ ] I carved the ZIP appended after
IEND(C2) - [ ] I found a password clue in a log and opened the locked file (C3)
- [ ] I investigated a layer beyond the docx’s body (core.xml) (C4)
- [ ] I connected an EXIF value to another file’s key (C5)
- [ ] I drew a one-line technique-chain map for each of the five problems
- [ ] When stuck, I used the "techniques I haven’t tried" checklist
- [ ] I solved at least 4 of the 5 independently, without section 5
6. Common Pitfalls & Fixes
Wall 1. Fooled by the extension, giving up at the start
Symptom: "the png won’t open, so it’s a corrupted file," you concluded.
Cause: you believed the extension was fact.
Fix: the first move of investigation is always check the first 4–16 bytes. One line of raw[:4].hex(" ") does it. JPEG is ff d8 ff, PNG is 89 50 4e 47, ZIP is 50 4b 03 04 (Step 239 review).
Wall 2. The carved ZIP won’t open
Symptom: a BadZipFile error.
Cause: you found PKx03x04 at the wrong position (the same bytes happened to occur in the body data), or the ZIP structure is damaged.
Fix: if the signature appears multiple times, instead of find, pull all positions (re.finditer) and try from the later ones. In measured C2 there was only the one at position 211, but real problems sometimes plant false signatures as traps.
Wall 3. You used the log’s password as-is and decryption fails
Symptom: you took bluewave42 from -P%20bluewave42 as the key and the result is garbled.
Cause: you didn’t URL-decode (%20 etc.), or you changed the key’s case or whitespace.
Fix: use not the log’s string as-is but the actual value after decoding. %20=space, %22=". If it still fails, leave open the possibility it’s not the key but a "hint" to the key.
Wall 4. Concluding "nothing’s there" after seeing only the body
Symptom: the document body looked empty, so you moved to the next problem.
Cause: you judged the file by its "visible content" only.
Fix: all five of today’s problems teach the same lesson — behind the visible layer lies another layer. Metadata (core.xml), appended data after the file, EXIF. Before writing "nothing there," run down the layer checklist.
Wall 5. Clinging to one piece of evidence and burning all your time
Symptom: you’ve been brute-forcing one locked file for an hour.
Cause: you attempted only frontal assault, without chain thinking.
Fix: use a timer rule — stuck on one piece of evidence for 20 minutes, move to another. In comprehensive problems, whichever link loosens first is where the chain starts. A locked file’s key is usually in another piece of evidence.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Technique chain | The comprehensive-problem structure where one piece of evidence’s output becomes the next’s input |
| Carving | The technique of cutting a hidden file out of a file by its signature |
| Evidence list → technique mapping | The opening ritual of drawing the "what, with which technique" table before solving |
| Open evidence first | Open files before locked files — the key is on the open side |
| Chain map | A one-line reproduction document: "technique → discovery → technique → flag" |
Today’s Commands & Code
| Code | What it does |
|---|---|
raw[:4].hex(" ") |
Check the signature — investigation’s first move |
d.find(b"PKx03x04") |
Locate an appended ZIP |
ZipFile(io.BytesIO(carved_bytes)) |
Open the carved ZIP |
re.search(r"-P%20(w+)", log) |
Extract a command-line password from a log |
img.getexif().get(tag) |
Read an EXIF clue |
bytes(b ^ key[i % len(key)] ...) |
Repeating-key XOR decryption |
An Instinct More Important Than Commands
Every technique in today’s five problems was learned in previous chapters. What’s new isn’t the techniques — it’s the connections: signature leading to EXIF, log leading to password, EXIF leading to XOR key. Real breach investigations look exactly like this: clues never sit in one place, and each clue opens the next door.
And this chapter’s real grading criterion isn’t the flags — it’s the chain map. A flag can be gotten right by luck, but only when you can explain "why you pulled out that technique" do you have reproducible skill. Half of the Forensics track has passed — from hex (239) through memory (242), disk (243), Windows (244), logs (245), metadata (246), and password recovery (247) — today, all of it met inside a single set of problems.
Once every box is checked, Step 248 is complete.
ONE STEP FURTHER
Finished this lesson?
Check the completion criteria, then mark your progress.