$ / 6 min read/forensics
Breach: Zero Day
Three answers buried in 2.7 MB of Windows security logs, a packet capture and a memory dump — most of it synthetic filler with a fingerprint that gives it away.
- section
- CSAW26
- event
- ctf.csaw.io ↗
- category
- forensics
- status
- ● solved
Format: csaw{REDACTED} (case-insensitive)
Exhibits: fenwick_capture.pcap, fenwick_security.xml, fenwick_memdump.txt
One component per exhibit — but each one is the second thing you’d reach for, not the first. That is the whole challenge.
0. Separating signal from filler
The synthetic data has a tell that identifies the author’s planted artifacts:
- XML: planted events have timestamps ending
.000Z— 16 of 3193 (~3 expected by chance, so ~13 are deliberate) - pcap: planted packets have 10 ms-round timestamps — 46 of 4236, in exactly four flows
This is the single most useful technique here and generalises to any synthetic-data forensics challenge. It reduces 3193 log events and 4236 packets to about twenty artifacts that matter.
1. The two chains
Chain A — benign decoy
23:58:05 Task \Fenwick\Backup\NightlyBackup Execute
23:58:09 DNS backup-vault.fenwick.local
23:58:10 POST /api/sync {"id":"QkFDS1VQU1ZD"} -> BACKUPSVC
23:58:43 7045 BkupAgent64 -> drivers\BkupAgent64.sys
23:58:48 SecurityHealthService stopped
00:04:05 Task ResultCode 0 <- completes cleanly
00:04:07 Agent stopped
Six minutes, clean exit. A real nightly backup job dressed to look alarming.
Chain B — the intrusion
01:13:41 DNS docs-fenwick-portal.com -> 52.112.198.39
01:13:42 POST /api/sync {"id":"UkVMQVlTSEVMTA=="} -> RELAYSHELL
01:14:15 7045 Afd4Eop12 -> drivers\Afd4Eop12_x64.dll
01:14:20 WinDefend stopped <- NEVER RESTARTS
01:14:25 First TLS session to the C2
01:13–05:58 Beacon: 73 connections, ~237 s mean, CV 0.10
2. C2INDICATOR = RelayShell
Not the domain. The indicator is the implant’s own identifier, posted to the C2 in its first check-in:
POST /api/sync Host: docs-fenwick-portal.com
{"id": "UkVMQVlTSEVMTA==", "v": "1.4"} -> RELAYSHELL
The decoy chain carries a structurally identical BACKUPSVC, which is how you
know the pair is deliberate — the author built an A/B so you can tell which
identifier belongs to the real intrusion.
Gotcha: the malicious POST is split across two TCP segments, so a naive regex over individual packets only catches the decoy’s id. Reassemble the stream first.
3. SERVICENAME = WinDefend
Not the installed service. Afd4Eop12 is the attacker’s dropped driver —
it looks like the answer because it’s the anomalous 7045 install (registered as a
kernel driver but pointing at a .dll, which is invalid). But the question asks
for the service involved in the attack, and that is the one the attacker
killed.
All WinDefend state changes:
20:12:07 stopped
20:12:44 running <- 37 s, benign
01:14:20 stopped <- 5 s after Afd4Eop12 installs, NEVER restarts
03:41:52 stopped
03:42:21 running <- 29 s, benign
The discriminator is mechanical: two stopped events in a row with no
intervening running means the service stayed down. That is the attacker’s
kill, and it lands five seconds after the driver install.
SecurityHealthService is the decoy chain’s equivalent — stopped at 23:58:48
during the backup job.
4. BACKDOORNAME = ForestTiger
Not any one of the planted names — two of them joined.
The memdump contains exactly four simple string reversals (verified exhaustive against a 97k-word dictionary, a malware-name list, all 26 Caesar shifts, base64/base85, acrostic, and embedded substrings, forward and reversed):
| Token | Reversed | What it is |
|---|---|---|
ELUDOMDUF |
FUDMODULE | real Lazarus rootkit |
EGDEHREPPOC |
COPPERHEDGE | real Lazarus RAT (CISA MAR-10288834) |
REGIT |
TIGER | not malware — ordinary word |
TSEROF |
FOREST | not malware — ordinary word |
The brief says:
Public data shows that the patterns we are seeing could align with actions of a specific group, but we have not been able to identify which one. These groups are known to borrow from each other’s playbooks, so keep an eye out for any anomalies.
COPPERHEDGE and FUDMODULE are the borrowed playbooks — genuine, publicly
documented tools planted to make you attribute the intrusion to Lazarus. The
anomaly is that the other two are not malware names at all. They are
fragments, and they concatenate:
FOREST + TIGER -> ForestTiger
The joke is that both halves are threat-actor naming conventions from different vendors — Microsoft’s “Forest Blizzard” (APT28, Russia) and CrowdStrike’s “…Tiger” suffix (India) — welded into one fictional name. Neither half means anything alone, which is exactly why they’re the odd ones out.
5. Solver
~/ctf/forenn/solve.py derives all three components mechanically:
planted reversals:
7ff6000f02ed TIGER fragment
7ff600100c29 FUDMODULE decoy (real malware)
7ff60011181c COPPERHEDGE decoy (real malware)
7ff60011a103 FOREST fragment
C2INDICATOR RELAYSHELL (beacon id posted to docs-fenwick-portal.com)
SERVICENAME WinDefend (stopped, never restarted)
BACKDOORNAME ForestTiger (non-malware fragments joined)
csaw{REDACTED}
The service is found by scanning 7036 events for two consecutive stopped
states; the backdoor by filtering the reversed tokens against a known-malware
list and joining whatever is left.
6. Post-mortem — why this took ~60 failed submissions
Every individual artifact was recovered correctly and early. The failure was entirely in mapping artifacts to slots, and all three errors share one cause: reaching for the most conspicuous item rather than the one the question asked for.
| Slot | What I submitted | Correct | Why I was wrong |
|---|---|---|---|
| C2 indicator | docs-fenwick-portal.com |
RelayShell |
anchored on “indicator = address”. The beacon id was tested, but only paired with the wrong other two. |
| Service name | Afd4Eop12 |
WinDefend |
took the anomalous installed service instead of the attacked one. Never tested WinDefend, despite having identified the permanent kill as the key event. |
| Backdoor name | each of the four, separately | ForestTiger |
found all four tokens and correctly identified FOREST/TIGER as the odd pair — even wrote “possibly concatenated as FORESTTIGER” once — but only ever offered it alongside the wrong C2 and service. |
The compounding effect is the real lesson: with three slots and one wrong assumption in each, a correct guess in any single slot still returns “incorrect”, which reads as evidence against that guess. I burned two 16-cell grids (domain × service × name, then beacon-id × service × name) without ever varying the service slot away from the two 7045 installs — so the correct value was never in any grid I searched.
Transferable rules:
- When a flag has N labelled slots, enumerate candidates per slot before combining. A wrong value in one slot makes every correct value elsewhere look wrong.
- “Service involved in the attack” can mean the service attacked, not the service installed. Read the label literally.
- If two recovered tokens are individually meaningless, try concatenating them before discarding either.
- Planted real names (COPPERHEDGE, FUDMODULE) in a challenge that explicitly warns about “borrowed playbooks” are decoys by construction. The answer is the thing that isn’t publicly attested.
## challenge files
46 files · 318 KB- HANDOFF.md
- acro.py
- all7045.py
- allhttp.py
- beac.py
- blob.py
- caesar.py
- candidates.txt
- cov.py
- d7045.py
- dec2.py
- deep.py
- dns2.py
- dnsdeep.py
- +32 more
flags redacted; flag images and local flag.txt files removed