Forensic recovery from filesystem slack space and swap space — two locations that file-level tools cannot see. Slack lives at the end of every allocated file’s last block; swap holds passively-evicted RAM pages. Both contain forensically valuable artefacts and neither shows up in ls, find, or any stat-based view. Companion to File Carving for the unallocated-region recovery story.
Side: blue
File slack — concept
A file’s data occupies whole filesystem blocks. The last block almost always has unused bytes between the end of file data and the block boundary. Anything written there is invisible to filesystem tools — md5sum reads only 0..size-1 bytes, never reaching slack.
[ block N-1 ][ block N (last) ][ ]
^- file data ^- slack space (zeroed at allocation,
but may contain residual deleted-file data
or deliberately-hidden payloads)
Slack size formula:
slack_size = block_size - (file_size MOD block_size)
slack_byte_offset_in_disk = last_block_number * block_size + (file_size MOD block_size)
Block size is reported by stat as IO Block. Last allocated block comes from debugfs (see Filesystem Metadata).
Calculate slack offset
# 1. file size and FS block size
stat target.jpg
# Size: 58178 Blocks: 120 IO Block: 4096
# Inode: 789983
echo $((58178 % 4096)) # 834 - bytes occupied in last block
echo $((4096 - 58178 % 4096)) # 3262 - bytes of slack available
# 2. find the actual disk blocks
df target.jpg # which device, e.g. /dev/sda1
sudo debugfs -R "stat <789983>" /dev/sda1 | cat
# EXTENTS: (0-14):12156982-12156996
# block 12156996 is the last allocated block; it contains the slack
# 3. compute the slack-start byte offset on the raw device
echo $((12156996 * 4096 + 834))
# = 49775083866 (this is the first byte of slack in /dev/sda1)debugfs is piped through cat because it would otherwise enter interactive mode. Always include the pipe.
Read slack contents
# read 4 KiB starting at slack offset
sudo xxd -s $((12156996 * 4096 + 834)) -l 4096 /dev/sda1 | head
# verify a known footer immediately precedes slack (sanity check before writing)
sudo xxd -s $((12156996 * 4096 + 832)) -l 16 /dev/sda1
# expected: ffd9 0000 0000 ... (JPEG EOI followed by zero-filled slack)Findings:
| Slack content | Meaning |
|---|---|
| All-zero | clean — no residual data, no hiding |
| Non-zero binary that is not file-data continuation | residual from a previously-deleted file that occupied the same block, OR deliberately written payload |
| Recognisable file fragment (PE header, JPEG header, ASCII text) | almost certainly residual or planted |
| Footer-then-zeros-then-content | a payload was deliberately written into slack after the file was created |
Block-level extraction with dd
# extract slack region as a separate file for analysis
sudo dd if=/dev/sda1 of=slack.bin \
bs=1 \
skip=$((12156996 * 4096 + 834)) \
count=$((4096 - 58178 % 4096))
file slack.bin
strings slack.binUse bs=1 for byte-precision skip and count. Slow but accurate. For larger ranges, switch to block-aligned extraction with bigger bs and adjust skip / count accordingly.
Demonstrating slack persistence
# write a known string to slack
echo "marker text" | sudo dd of=/dev/sda1 bs=1 seek=$((12156996 * 4096 + 834))
# host file hash unchanged (slack lives outside the file boundary)
md5sum target.jpg # same hash as before
# slack now holds the marker
sudo xxd -s $((12156996 * 4096 + 834)) -l 32 /dev/sda1
# 6d61 726b 6572 2074 6578 740a ... "marker text\n..."This pattern demonstrates why slack-space hiding works: filesystem reads stop at the recorded size; raw block-device reads do not.
On evidence: never write to evidence. The pattern above is for understanding only — operationally you image the disk first, then read slack from the image.
Swap space analysis
Swap holds RAM pages evicted under memory pressure. A browser with many tabs, a clipboard manager, or any long-running process leaves recoverable artefacts in swap: URLs, page content, JSON, copied passwords, document fragments.
First-pass: strings
strings -n 8 swap.dump > swap.strings # min length 8, save full text dump
grep -i -E '(http|https|ftp)://[a-z0-9.-]+/' swap.strings | sort -u
grep -i -E '@[a-z0-9.-]+\.(com|net|org|io)' swap.strings | sort -u # email addresses
grep -i -E '\b[A-Fa-f0-9]{32,}\b' swap.strings | sort -u # MD5 / SHA candidatesswap_digger
Categorised first-pass swap analysis. Web credentials, email content, wifi credentials, URLs, hashes.
git clone https://github.com/sevagas/swap_digger.git
cd swap_digger
sudo ./swap_digger.sh -a -l -s /full/path/to/swap.dump| Flag | Purpose |
|---|---|
-p | system passwords from /etc/shadow patterns |
-g | guess passwords from observations (high false-positive rate) |
-a | extended tests — web passwords, emails, wifi creds, URLs, hashes. Default for forensic analysis |
-l | write timestamped log of all stdout. Always use — categorised sections only appear in stdout, not in saved files |
-s <path> | swap device or dump file (must be absolute path) |
-r <root> | root path of target filesystem (when cross-referencing /etc/shadow) |
-c | auto-erase working directory after completion (skip during investigation) |
Pitfalls
- swap_digger caches in
/tmp/swap_dig/. Re-runs return cached results unless you delete the cache or use-c. - swap_digger’s URL section uses
sort | uniq -cdwhich only emits URLs seen more than once. One-off URLs are filtered out — also grep the rawswap_dump.txtfor full coverage. - swap_digger fails silently on relative paths. Always use absolute paths.
- Password output has high false-positive rate. Cross-reference with URL/email findings before treating as credentials.
Forensic interpretation
| Finding | What it tells you |
|---|---|
| ASCII text in slack | residual deleted-file content OR deliberate hiding |
| File-format header in slack | a different file once occupied the same block; recover with header/footer carving |
| Multiple URLs in swap from one domain | host fetched that resource recently; correlate with browser history |
| Password-like strings near payload URLs in swap | likely captured during exfil prep; treat as compromised credential |
| Clipboard manager artefacts in swap | user copied that content into a clipboard buffer at some point |
| Process command lines in swap | recovers attacker shell sessions even after the process exited |
Pitfalls
- Slack offset arithmetic uses
last_block * block_size + (size MOD block_size). The most common error is multiplying by sector size (512) instead of block size (typically 4096). debugfsagainst a mounted filesystem can return stale data. Always read against an image.bs=1is byte-precise but slow. For multi-GB extractions, segment the read with block-alignedbsand adjust offsets.stringsdefault minimum length 4 produces enormous output on large swap files. Use-n 8or higher to filter noise.- Swap is overwritten as the system runs. The artefacts you recover are a snapshot — gaps are normal and do not mean nothing happened.
- Encrypted swap (LUKS, dm-crypt) returns garbage. Confirm
swapon --showplaintext before investing analysis time.
links:
Field Manual | Data Recovery | File Carving | Filesystem Metadata