Run Every Core Forensics Technique Yourself
Twenty self-contained lab exercises across foundations, disk forensics, memory forensics, and network forensics. Every lab builds its own evidence first (so you're not hunting for sample files), then walks through the exact commands to analyse it, with expected output and a checkpoint to confirm you got the right result.
Forensic Foundations
Four labs covering the procedural rules that make every later lab's results trustworthy: hashing, write-blocking, volatility ordering, and chain-of-custody documentation.
▸
Verify Evidence Integrity with Hashing
Foundations
LAB 1.1
Set up a dedicated case folder so nothing in this lab touches your real files.
mkdir -p ~/dfir/lab1.1 && cd ~/dfir/lab1.1 echo "Suspect access log entry: user=admin login=success ip=10.0.0.15" > evidence.txt cat evidence.txt
Record both — MD5 for legacy compatibility, SHA-256 as the case standard.
md5sum evidence.txt sha256sum evidence.txt
evidence.txt. Your exact hashes will differ from anyone else's — that's expected, since the file content (and any trailing newline) determines the hash.sha256sum evidence.txt > evidence.sha256 cat evidence.sha256
Make a copy, change a single character inside it, then check it against the original manifest.
cp evidence.txt evidence_tampered.txt sed -i 's/admin/ADMIN/' evidence_tampered.txt # Verify the ORIGINAL against its manifest -- should pass sha256sum -c evidence.sha256 # Now hash the tampered copy and compare manually sha256sum evidence_tampered.txt
sha256sum -c on the original prints evidence.txt: OK.
The tampered file's hash is completely different from the
original despite only a 5-character change (admin → ADMIN) — this is the avalanche effect.
▸
Mount Evidence Read-Only (Software Write Blocking)
Foundations
LAB 1.2
This 50MB file will stand in for a "seized drive."
mkdir -p ~/dfir/lab1.2 && cd ~/dfir/lab1.2 dd if=/dev/zero of=evidence.img bs=1M count=50 mkfs.ext4 -F evidence.img sha256sum evidence.img > evidence.img.sha256
This represents the original drive in use, before it becomes evidence.
mkdir -p mnt sudo mount -o loop evidence.img mnt echo "original suspect file" | sudo tee mnt/notes.txt sudo umount mnt
# This is now your "acquired" image -- hash it for the manifest sha256sum evidence.img > acquired.sha256 # Mount with the read-only flag, just like a write-blocked drive sudo mount -o loop,ro evidence.img mnt mount | grep evidence.img
mount line for evidence.img includes ro in its options list, e.g. (ro,relatime).echo "tampering attempt" | sudo tee mnt/notes.txt touch mnt/newfile.txt
Read-only file system. The original notes.txt content is untouched.sudo umount mnt sha256sum -c acquired.sha256
evidence.img: OK — the hash matches even after mounting and attempted writes, proving the read-only mount preserved the evidence.
▸
Capture Evidence in Order of Volatility
Foundations
LAB 1.3
mkdir -p ~/dfir/lab1.3 && cd ~/dfir/lab1.3
Active connections and the routing/ARP tables vanish quickly — capture these before anything else.
ss -tunap > 01_network_connections.txt ip route > 02_routing_table.txt ip neigh > 03_arp_cache.txt
ps auxf > 04_process_tree.txt free -h > 05_memory_usage.txt
This is less volatile — it's backed by log files on disk — but still time-sensitive if an attacker is actively logged in.
w > 06_logged_in_users.txt last -n 20 > 07_recent_logins.txt
Even quick triage captures need integrity hashes and a recorded collection time.
date -u > 00_collection_timestamp_UTC.txt sha256sum *.txt > manifest.sha256 cat manifest.sha256
▸
Build a Chain-of-Custody Log
Foundations
LAB 1.4
mkdir -p ~/dfir/lab1.4 && cd ~/dfir/lab1.4
echo "Item: USB-001 -- 16GB USB drive recovered from suspect desk" > item_USB-001.txt
HASH=$(sha256sum item_USB-001.txt | awk '{print $1}')
echo "Initial hash: $HASH"
echo "timestamp_utc,item_id,action,from,to,sha256" > coc_log.csv echo "$(date -u +%Y-%m-%dT%H:%M:%SZ),USB-001,COLLECTED,Crime Scene,Investigator A,$HASH" >> coc_log.csv cat coc_log.csv
In real casework, you re-hash before every transfer to confirm nothing changed while in the previous handler's custody.
sleep 2
CURRENT_HASH=$(sha256sum item_USB-001.txt | awk '{print $1}')
if [ "$CURRENT_HASH" == "$HASH" ]; then
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ),USB-001,TRANSFERRED,Investigator A,Analyst B,$CURRENT_HASH" >> coc_log.csv
echo "Hash verified -- transfer logged"
else
echo "HASH MISMATCH -- DO NOT LOG TRANSFER, FLAG FOR REVIEW"
fi
# Someone edits the file without updating the chain of custody
echo "unauthorized note added" >> item_USB-001.txt
CURRENT_HASH=$(sha256sum item_USB-001.txt | awk '{print $1}')
if [ "$CURRENT_HASH" == "$HASH" ]; then
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ),USB-001,TRANSFERRED,Analyst B,Reviewer C,$CURRENT_HASH" >> coc_log.csv
echo "Hash verified -- transfer logged"
else
echo "HASH MISMATCH -- DO NOT LOG TRANSFER, FLAG FOR REVIEW"
fi
cat coc_log.csv
HASH MISMATCH -- DO NOT LOG TRANSFER, FLAG FOR REVIEW and is not appended to coc_log.csv. The log now shows only two valid entries (COLLECTED, TRANSFERRED) — exactly the gap a defense attorney would seize on.Disk & File System Forensics
Five labs: image a disk, recover deleted files, carve files by signature from a damaged file system, pull metadata plus hidden steganographic data out of an image file, and use MACB timestamps to reconstruct a ransomware-style mass-encryption timeline.
▸
Create and Verify a Forensic Disk Image
Disk
LAB 2.1
dd, hash both the source and the
image, and confirm the image is forensically identical.
mkdir -p ~/dfir/lab2.1 && cd ~/dfir/lab2.1 # Create a 100MB blank container and format it dd if=/dev/zero of=source_drive.img bs=1M count=100 mkfs.ext4 -F source_drive.img # Attach it as a loop device sudo losetup -fP source_drive.img losetup -a
losetup -a shows an entry like /dev/loop0: ... source_drive.img. Note the device name — you'll use it next.Replace /dev/loop0 below with whatever device name appeared in step 1.
mkdir -p mnt sudo mount /dev/loop0 mnt echo "Quarterly report draft" | sudo tee mnt/report.txt echo "Confidential client list" | sudo tee mnt/clients.txt sudo umount mnt
# Hash the source device BEFORE imaging sudo sha256sum /dev/loop0 | tee source.sha256 # Image it bit-for-bit (note: source and dest must differ!) sudo dd if=/dev/loop0 of=case01_image.dd bs=4M status=progress conv=sync,noerror # Hash the resulting image sha256sum case01_image.dd | tee image.sha256
source.sha256 and image.sha256 should be identical — confirming the image is a perfect bit-for-bit copy of the source device.sudo losetup -d /dev/loop0
# Mount the IMAGE FILE itself, read-only -- never the original again
sudo mount -o loop,ro case01_image.dd mnt
ls -la mnt
cat mnt/report.txt
sudo umount mnt
ls shows report.txt and clients.txt inside the image, and cat prints "Quarterly report draft" — proving the image preserved the full file system, not just raw bytes you can't use.
▸
Recover Deleted Files from Unallocated Space
Disk
LAB 2.2
This lab needs testdisk (which includes PhotoRec): sudo apt install testdisk -y
mkdir -p ~/dfir/lab2.2 && cd ~/dfir/lab2.2 dd if=/dev/zero of=disk.img bs=1M count=64 mkfs.ext4 -F disk.img sudo losetup -fP disk.img DEV=$(losetup -j disk.img | cut -d: -f1) echo "Loop device: $DEV" mkdir -p mnt sudo mount $DEV mnt # Create three files echo "Internal memo: layoffs planned for Q3" | sudo tee mnt/memo.txt printf '\xFF\xD8\xFF\xE0\x00\x10JFIF_fake_jpeg_data_for_lab_purposes_only' | sudo tee mnt/photo.jpg > /dev/null echo "Password list: admin:hunter2" | sudo tee mnt/secrets.txt ls mnt/ # Now delete them all sudo rm mnt/memo.txt mnt/photo.jpg mnt/secrets.txt ls mnt/ sudo umount mnt sudo losetup -d $DEV
ls mnt/ shows three files; the second shows an empty directory. The data is still on disk — only the directory entries are gone.PhotoRec is interactive. Run it, then follow the on-screen prompts: select disk.img, choose the partition (or "Whole disk"), select the file system type closest to ext2/ext3/ext4, and choose a recovery destination directory (create recovered/ first).
mkdir -p recovered sudo photorec disk.img
recup_dir.1/ inside your chosen destination. You should find at least the .jpg (PhotoRec carves by signature) — text files may or may not be recovered depending on whether ext4's journal still references them.# debugfs lets you browse a filesystem image read-only without mounting
sudo debugfs -R "lsdel" disk.img
▸
File Carving by Signature
Disk
LAB 2.3
sudo apt install foremost binwalk -y
mkdir -p ~/dfir/lab2.3 && cd ~/dfir/lab2.3 # Create a tiny real PNG using ImageMagick (or use any PNG you have) sudo apt install -y imagemagick 2>/dev/null convert -size 20x20 xc:blue test.png # View the first 16 bytes in hex xxd test.png | head -1
8950 4e47 0d0a 1a0a — the PNG signature (89 50 4E 47 0D 0A 1A 0A) shared by every PNG file ever created.# A minimal valid PDF (header is enough for signature detection) printf '%%PDF-1.4\n%% Lab test PDF content\n%%%%EOF' > test.pdf # Concatenate PNG + PDF + PNG into one raw blob -- simulating # a damaged file system with no directory structure left cat test.png test.pdf test.png > carved_target.raw ls -la carved_target.raw
mkdir -p carved_output foremost -t png,pdf -i carved_target.raw -o carved_output ls -R carved_output
png/ and pdf/ inside carved_output, each containing recovered files (e.g. 00000000.png, 00000123.pdf) — extracted purely from byte patterns, with no file system involved at all.binwalk carved_target.raw
carved_target.raw, and a description (e.g. PNG image, PDF document) — confirming three entries at three different offsets.
▸
Extract Metadata and Detect Hidden Data
Disk
LAB 2.4
sudo apt install libimage-exiftool-perl steghide imagemagick -y
mkdir -p ~/dfir/lab2.4 && cd ~/dfir/lab2.4 # Create a base JPEG (steghide requires JPEG, not PNG) convert -size 400x400 xc:gray photo.jpg # Write GPS coordinates and a capture timestamp into its EXIF data exiftool -GPSLatitude="51.5072" -GPSLatitudeRef="N" \ -GPSLongitude="0.1276" -GPSLongitudeRef="W" \ -DateTimeOriginal="2026:03:14 09:21:07" \ -overwrite_original photo.jpg
exiftool photo.jpg | grep -Ei "gps|date"
GPS Latitude : 51 deg 30' 25.92" N, GPS Longitude : 0 deg 7' 39.36" W, and Date/Time Original : 2026:03:14 09:21:07 — these coordinates correspond to central London.echo "Wire transfer authorization code: 88421-XQ" > secret.txt # Embed secret.txt into photo.jpg, encrypted with a passphrase steghide embed -cf photo.jpg -ef secret.txt -p "lab12345" # Note the file size barely changes ls -la photo.jpg
# Without a passphrase, steghide can confirm something is embedded steghide info photo.jpg # With the correct passphrase, extract it steghide extract -sf photo.jpg -p "lab12345" cat secret.txt
steghide info confirms an embedded file is present and shows its size. steghide extract recreates secret.txt, and cat prints the wire transfer message — recovered entirely from inside what looks like an ordinary gray JPEG.
▸
MACB Timestamp Analysis & Ransomware Simulation
Disk
LAB 2.5
mkdir -p ~/dfir/lab2.5 && cd ~/dfir/lab2.5 echo "quarterly_budget.xlsx contents (simulated)" > budget.txt stat budget.txt
Access, Modify, and Change — all set to the moment of creation, plus a Birth field if your filesystem supports it.sleep 2 cat budget.txt > /dev/null stat budget.txt
Access may not update (relatime/noatime mount options skip it for performance). If it doesn't change, note this — it's an important caveat: "file was read" is often not provable from Access time alone on modern systems.sleep 2 echo "amended figures" >> budget.txt stat budget.txt
Modify and Change advance to the current time (Modify because content changed; Change because the inode's size/mtime metadata changed too). Birth stays at the original creation time.sleep 2 chmod 600 budget.txt stat budget.txt
Change advances again (permissions are metadata), but Modify stays at its previous value — content didn't change, only metadata. This Modify/Change split is exactly how investigators distinguish "file content changed" from "file metadata changed" (e.g. an attacker touching permissions without editing content).Create a set of "user documents," then simulate ransomware rewriting each one's content in a tight time window.
mkdir -p documents && cd documents for i in 1 2 3 4 5; do echo "Document $i - normal business content" > "file$i.docx" sleep 1 done cd .. # Let some "normal time" pass before the attack sleep 3 # Simulate ransomware: rewrite every file's content rapidly cd documents for f in *.docx; do # Overwrite content (simulating encryption) and rename with .locked extension echo "ENCRYPTED-$(date +%s)-$(head -c 16 /dev/urandom | base64)" > "$f" mv "$f" "$f.locked" done cd ..
# List all files sorted by modification time, with timestamps
find documents -type f -printf "%T@ %Tc %p\n" | sort -n
.docx.locked files show Modify timestamps clustered within roughly one second of each other — a sharp contrast to the original creation times, which were spread out by the 1-second sleep in the loop. This clustering of modification times across many unrelated files in a tiny window is the single clearest indicator of a mass-encryption event, and is exactly the pattern timeline tools like Plaso/Timesketch are built to surface automatically.
# The earliest Modify timestamp among the .locked files marks attack start
find documents -name "*.locked" -printf "%T@ %p\n" | sort -n | head -1
.locked files — in a real investigation, this value (correlated against process-creation event logs or memory artefacts from the same second) is typically where you'd start looking for the malicious process's PID.Memory Forensics
Five labs using Volatility 3 against memory images you capture yourself: acquisition, process/network triage, hunting for code injection, dumping artefacts back out of memory, and recovering an encryption key during its live-memory window.
▸
Acquire a Memory Image with AVML
Memory
LAB 3.1
AVML is a single static binary from Microsoft. Download the release for Linux:
cd ~/dfir && mkdir -p lab3.1 && cd lab3.1 wget -O avml https://github.com/microsoft/avml/releases/latest/download/avml chmod +x avml ./avml --help
This simulates the malicious or notable process you'll later find in the memory dump.
# Run a process with a distinctive command-line argument, in the background
sleep 600 --DFIR-MARKER-c2server.evil.example &
echo "Marker PID: $!"
This may take a few minutes depending on how much RAM your VM has allocated.
sudo ./avml memory.lime ls -lh memory.lime
memory.lime roughly the size of your VM's allocated RAM (e.g. a 2GB VM produces a file close to 2GB).sha256sum memory.lime | tee memory.lime.sha256
Before any framework parses it, the raw bytes already contain the evidence — this is what makes memory forensics possible.
strings memory.lime | grep "DFIR-MARKER"
sleep 600 --DFIR-MARKER-c2server.evil.example appears at least once — proof the command line of a running process is captured verbatim in RAM.
▸
Process and Network Triage with Volatility 3
Memory
LAB 3.2
pstree, cmdline, and
netscan to find your marker process and its
parent.
cd ~/dfir && mkdir -p lab3.2 && cd lab3.2 python3 -m venv volenv source volenv/bin/activate pip install volatility3
Volatility 3 needs to identify the kernel/symbol layout — for a Linux image, generate a symbol table first if prompted, or pass --single-location directly. The first run takes longer as it builds caches.
python3 -m volatility3.cli.vol -f ~/dfir/lab3.1/memory.lime linux.pstree
sleep process from Lab 3.1 and note its PID and PPID (parent process ID).python3 -m volatility3.cli.vol -f ~/dfir/lab3.1/memory.lime linux.psaux | grep -i marker
sleep 600 --DFIR-MARKER-c2server.evil.example command line, with its PID matching what you found in step 1.python3 -m volatility3.cli.vol -f ~/dfir/lab3.1/memory.lime linux.sockstat
echo "PID, PPID, and command line of marker process:" > triage_summary.txt python3 -m volatility3.cli.vol -f ~/dfir/lab3.1/memory.lime linux.psaux | grep -i marker >> triage_summary.txt cat triage_summary.txt
▸
Hunt for Suspicious Memory Regions
Memory
LAB 3.3
/proc on a
live system against what Volatility reports from your memory
image, and identify which mapped regions are backed by files
versus anonymous (potentially injected) memory.
This shows what "normal" memory mappings look like for a legitimate process — your baseline for comparison.
echo $$ cat /proc/$$/maps | head -20
/usr/bin/bash, /usr/lib/x86_64-linux-gnu/libc.so.6) — these are file-backed. A few lines with [heap], [stack], or no path at all are normal anonymous regions every process has.cd ~/dfir/lab3.2
source volenv/bin/activate
# List memory maps for all processes (this can be a large output)
python3 -m volatility3.cli.vol -f ~/dfir/lab3.1/memory.lime linux.proc.Maps --pid <PID_FROM_STEP_1>
/proc/PID/maps showed live, since the dump captured that process's state.Without real malware, document the red flags so you'd recognise them:
| Observation | Why it matters |
|---|---|
| Region with rwx (read+write+execute) permissions | Legitimate code is rarely both writable and executable at once — this combination is a classic injection indicator. |
| Executable region with no file path | Normal executable code maps back to a file on disk (the binary or a library). Anonymous executable memory often means code was written directly into RAM. |
| A well-known system process (e.g. bash, sshd) with an unusually large number of anonymous mappings | Could indicate a loader or injected shellcode allocated extra memory regions. |
cat /proc/$$/maps | grep rwx
▸
Recover File Contents from Memory
Memory
LAB 3.4
mkdir -p ~/dfir/lab3.4 && cd ~/dfir/lab3.4
echo "API_KEY=sk-DFIR-LAB-3f9a8b2c1d4e5f6a7b8c9d0e" > secret_config.env
# Read it so its contents are cached in RAM
cat secret_config.env
sudo ~/dfir/lab3.1/avml memory2.lime sha256sum memory2.lime > memory2.lime.sha256
strings memory2.lime | grep "API_KEY"
API_KEY=sk-DFIR-LAB-3f9a8b2c1d4e5f6a7b8c9d0e — the full secret, recovered from a raw RAM dump with a basic string search, demonstrating why secrets in memory matter even if a disk is encrypted.cd ~/dfir/lab3.2 source volenv/bin/activate python3 -m volatility3.cli.vol -f ~/dfir/lab3.4/memory2.lime linux.lsof --pid $$
$$ refers to your current shell, which may differ from the shell active at capture time. If the PID doesn't match, use linux.pstree first to find the correct PID for the shell that ran step 1.
▸
Recover an Encryption Key from a Memory Dump
Memory
LAB 3.5
The script generates a random 256-bit key, prints it (so we know what to search for), and then sleeps — keeping the key resident in its process memory the whole time.
mkdir -p ~/dfir/lab3.5 && cd ~/dfir/lab3.5
cat > keyholder.py << 'EOF'
import os, time, binascii
key = os.urandom(32) # 256-bit AES key
print("Generated key (hex):", binascii.hexlify(key).decode())
print(f"PID: {os.getpid()}")
print("Holding key in memory for 120 seconds...")
time.sleep(120)
EOF
python3 keyholder.py > keyinfo.txt &
sleep 1
cat keyinfo.txt
Generated key (hex): <64 hex characters> and the process PID. Record the hex key value — this is your "ground truth" for the search.The script sleeps for 120 seconds, giving you a window to capture memory before it exits and the key is freed.
sudo ~/dfir/lab3.1/avml memory_key.lime sha256sum memory_key.lime > memory_key.lime.sha256
Because the script printed the key as hex text to stdout, that text representation is likely cached in memory too — search for it first as a sanity check.
# Extract the hex key value you recorded in step 1, then search for it
KEY_HEX=$(grep "Generated key" keyinfo.txt | awk '{print $NF}')
echo "Searching for: $KEY_HEX"
strings memory_key.lime | grep "$KEY_HEX"
The actual 32 raw key bytes held by the Python key variable are a different — and more realistic — target than the printed text. Convert the hex back to raw bytes and grep the binary dump for that exact byte sequence.
# Convert hex string back to raw binary, write to a small file echo -n "$KEY_HEX" | xxd -r -p > key.bin ls -la key.bin # should be exactly 32 bytes # Search the memory dump for this exact 32-byte sequence grep -aboF "$(cat key.bin)" memory_key.lime | head -5
1483920192:...), confirming the raw 32-byte AES key — the actual key material that would be used to encrypt/decrypt data — is physically present in RAM. If no match is found, the key may have been in a CPU register or already garbage-collected at the moment of capture; note this as a real limitation of the technique.
# Wait for the keyholder script to finish wait # Capture memory again now that the process has exited sudo ~/dfir/lab3.1/avml memory_after.lime # Search again -- this time the exact byte sequence should be much less likely to appear grep -aboF "$(cat key.bin)" memory_after.lime | head -5
Network Forensics
Six labs: capture and filter your own traffic with tcpdump/Wireshark, reconstruct a full TCP conversation, simulate and detect DNS beaconing, analyse email headers for spoofing indicators, generate and correlate flow-level connection logs, and examine TLS certificates for red flags.
▸
Capture and Filter Live Traffic
Network
LAB 4.1
sudo apt install tcpdump tshark curl -y (Wireshark GUI optional: sudo apt install wireshark)
ip -brief link show
eth0 or enp0s3 for the primary network adapter. Use this name in the next step.Run the capture in one terminal (it will run for 20 seconds), and in a second terminal — opened immediately — run the curl commands.
mkdir -p ~/dfir/lab4.1 && cd ~/dfir/lab4.1 sudo timeout 20 tcpdump -i <your_interface> -w capture.pcap
curl -s http://example.com > /dev/null nslookup example.com
tshark -r capture.pcap -q -z io,phs
dns and http (or tls if example.com redirected to https) among the captured frames.# Show only DNS queries and responses tshark -r capture.pcap -Y "dns" # Show only HTTP requests, with method and host tshark -r capture.pcap -Y "http.request" -T fields -e ip.dst -e http.host -e http.request.method
example.com and its A-record response. The HTTP filter shows a GET request with host example.com (if the connection wasn't upgraded to HTTPS).
▸
Reconstruct a Full TCP Conversation
Network
LAB 4.2
mkdir -p ~/dfir/lab4.2 && cd ~/dfir/lab4.2 sudo tcpdump -i lo -w stream.pcap port 9999
nc -l -p 9999
In a third terminal, connect and type a message. Switch to terminal 2 and type a reply. Then close both with Ctrl+C and stop the tcpdump in terminal 1 with Ctrl+C.
nc 127.0.0.1 9999 # Type: USER admin # Type: PASS S3cr3tPass!
# Type: 230 Login successful
# List TCP streams found in the capture tshark -r stream.pcap -q -z conv,tcp # Follow stream 0 and print it as ASCII tshark -r stream.pcap -q -z follow,tcp,ascii,0
USER admin, PASS S3cr3tPass!, and 230 Login successful — all sent in plaintext over individual TCP segments, now readable as one continuous exchange.
▸
Detect DNS Beaconing Patterns
Network
LAB 4.3
mkdir -p ~/dfir/lab4.3 && cd ~/dfir/lab4.3 sudo timeout 35 tcpdump -i any -w beacon.pcap port 53
This simulates malware "phoning home" to a C2 domain every 10 seconds.
for i in 1 2 3; do nslookup beacon-c2.dfir-lab.example sleep 10 done
tshark -r beacon.pcap -Y "dns.qry.name contains \"beacon-c2\"" \ -T fields -e frame.time_relative -e dns.qry.name
0.0, 10.0, and 20.0 — a near-perfect 10-second interval.tshark -r beacon.pcap -Y "dns.qry.name contains \"beacon-c2\"" \
-T fields -e frame.time_relative | awk 'NR>1{print $1-prev} {prev=$1}'
10.0 — the inter-query interval. In a real investigation, intervals this consistent (low variance) across dozens or hundreds of queries to an unfamiliar domain are a strong beaconing indicator.
▸
Analyse Email Headers for Spoofing
Network
LAB 4.4
Received chain, and determine whether SPF/DKIM
results indicate spoofing.
This sample represents a phishing email claiming to be from a finance department.
mkdir -p ~/dfir/lab4.4 && cd ~/dfir/lab4.4
cat > sample_header.txt << 'EOF'
Received: from mx.victimcorp.com (mx.victimcorp.com [198.51.100.10])
by mailgw.victimcorp.com with ESMTP id a1b2c3
for <employee@victimcorp.com>; Tue, 14 Mar 2026 09:14:05 +0000
Received: from smtp-relay.attacker-host.net (203.0.113.77)
by mx.victimcorp.com with ESMTP id d4e5f6
for <employee@victimcorp.com>; Tue, 14 Mar 2026 09:14:02 +0000
Authentication-Results: mx.victimcorp.com;
spf=fail smtp.mailfrom=ceo@victimcorp.com;
dkim=none (no signature found)
From: "CEO - Victim Corp" <ceo@victimcorp.com>
Reply-To: finance.urgent@attacker-host.net
Subject: URGENT: Wire transfer needed before EOD
EOF
cat sample_header.txt
grep "^Received:" sample_header.txt | tac
smtp-relay.attacker-host.net (203.0.113.77) — not from any
victimcorp.com infrastructure — before being relayed to
mx.victimcorp.com.
grep -A2 "Authentication-Results" sample_header.txt
spf=fail for smtp.mailfrom=ceo@victimcorp.com, and dkim=none — both checks failed, meaning the receiving server could not verify this message actually came from victimcorp.com's authorised mail servers.grep -E "^(From|Reply-To):" sample_header.txt
From: ceo@victimcorp.com but
Reply-To: finance.urgent@attacker-host.net —
any reply to this "urgent" request would go straight to the
attacker's domain, not to the real CEO.
Based on steps 2–4, write a short summary (3–4 sentences) stating whether this email is spoofed and what evidence supports that conclusion. Save it as verdict.txt.
nano verdict.txt
▸
Generate and Correlate Flow-Level Connection Logs
Network
LAB 4.5
conn.log would
produce), then correlate those flow records against firewall-style
log lines to spot an anomalous high-volume connection.
mkdir -p ~/dfir/lab4.5 && cd ~/dfir/lab4.5 sudo timeout 25 tcpdump -i any -w flows.pcap
# A few small, normal-looking requests curl -s http://example.com > /dev/null curl -s http://example.org > /dev/null # One "high-volume" transfer -- simulate exfiltration with a larger download curl -s https://speed.hetzner.de/100MB.bin -o /tmp/bigfile --max-time 15 || true
Each line below approximates one NetFlow/conn.log record: source, destination, port, protocol, and total bytes per conversation.
tshark -r flows.pcap -q -z conv,ip > flow_summary.txt cat flow_summary.txt
Bytes total dramatically larger than the others (likely megabytes vs. a few KB).echo "src_ip,dst_ip,dst_port,proto,bytes" > conn.csv
tshark -r flows.pcap -q -z conv,ip | grep -E "^[0-9]" | \
awk '{gsub(/<-+>/,",",$0); print $1","$3","$NF}' >> conn.csv 2>/dev/null
# Simpler, reliable alternative: per-packet byte totals grouped by destination
tshark -r flows.pcap -T fields -e ip.dst -e frame.len | \
awk '{sum[$1]+=$2} END {for (ip in sum) print ip","sum[ip]}' | sort -t, -k2 -rn > dst_bytes.csv
cat dst_bytes.csv
dst_bytes.csv lists destination IPs sorted by total bytes received, descending. The IP associated with the 100MB download should be at the top, with a byte total an order of magnitude larger than the example.com/example.org entries.Real investigations correlate flow data against firewall/proxy logs by matching IPs and timestamps. Simulate that correlation here.
# Grab the top destination IP from your flow analysis TOP_IP=$(head -1 dst_bytes.csv | cut -d, -f1) echo "Top talker by bytes: $TOP_IP" # Synthetic firewall log -- one line per allowed connection cat > firewall.log << EOF 2026-03-14 09:10:01 ALLOW 10.0.0.15 -> 93.184.216.34:80 proto=TCP 2026-03-14 09:10:03 ALLOW 10.0.0.15 -> 96.7.128.175:80 proto=TCP 2026-03-14 09:10:05 ALLOW 10.0.0.15 -> $TOP_IP:443 proto=TCP EOF # Correlate: does the top flow-bytes IP appear in the firewall log? grep "$TOP_IP" firewall.log
▸
TLS Certificate Analysis
Network
LAB 4.6
mkdir -p ~/dfir/lab4.6 && cd ~/dfir/lab4.6 openssl req -x509 -newkey rsa:2048 -keyout c2.key -out c2.crt \ -days 3 -nodes \ -subj "/CN=update-service.dfir-lab.example"
openssl x509 -in c2.crt -noout -text | head -30
Issuer and
Subject are identical (self-signed — no
trusted CA vouches for it), the Validity period
is only 3 days (malware certs are often short-lived to evade
blocklists), and there's no Subject Alternative Name matching a
well-known organisation.
cd ~/dfir/lab4.6 sudo timeout 20 tcpdump -i lo -w tls_handshake.pcap port 8443 & openssl s_server -accept 8443 -cert c2.crt -key c2.key -www & sleep 1
curl -sk https://127.0.0.1:8443/ > /dev/null
sleep 3
# Stop the server
pkill -f "s_server"
tshark -r tls_handshake.pcap -Y "tls.handshake.type == 11" \ -T fields -e x509ce.dNSName -e x509sat.uTF8String -e tls.handshake.certificate
CN value update-service.dfir-lab.example — demonstrating that certificate metadata is visible in plaintext during the handshake even though the application data that follows is encrypted.# Fetch and inspect a real site's certificate for comparison
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
openssl x509 -noout -issuer -subject -dates
| Field | Self-signed C2 cert | Real-world cert |
|---|---|---|
| Issuer | Same as Subject (self-signed) | A recognised CA (e.g. DigiCert, Let's Encrypt) |
| Validity | 3 days | Typically 60–398 days |
| Subject | Arbitrary, attacker-chosen CN | Matches the legitimate domain |