Forensic metadata extraction from ext2/3/4 filesystems. stat for the kernel-interpreted view, debugfs for raw inode + EXTENTS + deletion time, find for hidden-file sweeps, fls for image-based filesystem walks. Verified against GNU coreutils stat 9.7, debugfs 1.47.2, sleuthkit fls.

Side: blue


stat — kernel-interpreted inode view

stat /etc/passwd                                          # full inode dump
stat -c '%n %s %i %A %U %y' /etc/passwd                   # selected fields, custom format
find <dir> -exec stat {} \;                               # batch over a directory tree
Output decoded
File: secret-code.txt
Size: 0           Blocks: 0          IO Block: 4096   regular empty file
Device: 801h/2049d  Inode: 789983      Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/    kali)   Gid: ( 1000/    kali)
Access: 2022-09-05 05:20:52.323629828 -0400
Modify: 2022-09-05 05:20:52.323629828 -0400
Change: 2022-09-05 05:20:52.323629828 -0400
Birth:  2022-09-05 05:20:52.323629828 -0400
FieldMeaning
File, Size, Blocks, IO Blockfilename (resolved from directory entry), byte size, number of 512-byte filesystem blocks, FS block size
Devicedevice-major/minor — match against /dev/sda1 etc.
Inodeinode number
Linkshard-link count. 1 for files; for directories, 2 + number of subdirectories (. and .. always present)
Access: (mode)permission bits in octal + rwx form, owning uid + gid
Access: (timestamp)atime — last read
Modify:mtime — last content write
Change:ctime — last metadata or content change
Birth:crtime — file creation. ext4 only. Shows as - on ext2/ext3

The four-timestamp model (a/m/c + birth) is unique to ext4. NTFS exposes its own four-time set (STANDARD_INFORMATION). Compare-and-contrast goes in the report.

touch -a sets atime, touch -m sets mtime, touch -t sets both. ctime cannot be set to a past time without filesystem-level manipulation — anti-forensic timestomping that touches mtime but leaves ctime out of sync is detectable.


debugfs — raw inode + EXTENTS

debugfs is the ext2/3/4 inspector. Reads the raw inode, not the kernel’s interpreted view.

sudo debugfs -R 'stat <file>' /dev/sda1                   # full inode dump for a single file
sudo debugfs -R 'ls /etc' /dev/sda1                       # directory listing including deleted entries (marked)
sudo debugfs -R 'icheck <block>' /dev/sda1                # block -> inode reverse lookup
sudo debugfs -R 'ncheck <inode>' /dev/sda1                # inode -> filename(s)
sudo debugfs -R 'lsdel' /dev/sda1                         # deleted inodes still in the journal

Debugfs shows fields plain stat does not:

  • Generation — inode version counter, increments on each reallocation (used by NFS)
  • EXTENTS: — actual block ranges the file occupies. Required for dd skip=... extraction and slack-space examination
  • dtime — deletion time. Plain stat has no concept of this; debugfs reads it directly from the inode
  • raw mode bits and any extended attributes

Caveat: debugfs targets the raw block device. /dev/sda1 is correct for a host whose root is on that partition; on LVM, use df -T / first to find the actual device (/dev/<vg>/<lv>).

Block-level extraction with EXTENTS
# debugfs reports EXTENTS like:
# (ETB0):2105, (0-7):8388616-8388623
 
# extract block range 8388616 - 8388623 with dd (assuming 4096-byte FS blocks)
sudo dd if=/dev/sda1 bs=4096 skip=8388616 count=8 of=recovered.bin

This is how “I have an interesting file” becomes “I have the bytes of that file” for files that have been deleted but whose inode is still parseable.


find — hidden file sweep

# all hidden entries (files and directories) under /home, excluding . and ..
find /home -name '.*' ! -name '.' ! -name '..'
 
# hidden files only
find /home -type f -name '.*'
 
# hidden directories only
find /home -type d -name '.*' ! -name '.'
 
# whole filesystem, exclude pseudo-FS noise
find / -name '.*' -xdev                                    # stay on one mount
find / -name '.*' -not -path '/proc/*' -not -path '/sys/*' # alternative explicit excludes
Useful predicates
find /home -mtime -7                                       # modified in last 7 days
find / -mtime -1 -type f -not -path '/proc/*' -not -path '/sys/*'   # recently changed files
find / -newer /tmp/marker -type f                          # files newer than a reference
find / -size +100M -type f                                 # files larger than 100 MB
find /home -user <username>                                # owned by a specific user
find / -perm -u+s -type f 2>/dev/null                      # SUID binaries (privesc surface)
find / -perm -g+s -type f 2>/dev/null                      # SGID binaries
find / -nogroup -o -nouser 2>/dev/null                     # files with no owning user/group (suspicious)
find / -name '*.sh' -newer /tmp/marker                     # recent shell scripts
Pitfalls
  • find / without -xdev recurses into /proc and /sys which generate enormous synthetic output. Always -xdev or explicit -not -path excludes.
  • ls -a is not a search tool — it shows dotfiles in the current directory only. The activity wants find for tree-wide search.
  • Names like ..test (two leading dots) are hidden — -name '.*' matches them but ! -name '..' does not exclude them. Only the literal name .. is the parent reference.

fls — image-based filesystem walk (sleuthkit)

fls lists files inside a forensic image without mounting it. Includes deleted entries that are still in directory blocks.

fls -r image.dd                                            # recursive listing
fls -r -m / image.dd > body.txt                            # mactime body file (timeline source)
fls -d image.dd                                            # deleted entries only
fls -F image.dd                                            # files only, no directories
fls -o <sector_offset> image.dd                            # offset for multi-partition image
fls -p image.dd                                            # full path on each line
fls image.dd <inode>                                       # contents of one specific directory inode
Building a timeline
fls -r -m / image.dd > body.txt
mactime -b body.txt > timeline.txt                         # mactime turns body file into chronological timeline
mactime -b body.txt -d > timeline.csv                      # CSV output
mactime -b body.txt 2024-04-15..2024-04-20 > window.txt    # restrict to a date window

The body file format is documented at man mactime. Each row records a file’s MAC times, size, and metadata. mactime sorts by timestamp.


df — filesystem identification

df -h                                                      # human-readable, all mounted FS
df -T                                                      # show filesystem type
df -B1 /path                                               # exact byte counts for one path
df /etc/passwd                                             # which mount holds this file
df -T /                                                    # what FS type is the root

df <path> reports the filesystem the path resolves to. Cross-reference with the Device field from stat to confirm the file lives where you expected.


/etc/fstab — mount configuration

cat /etc/fstab                                             # static mount table at boot time
findmnt                                                    # currently-mounted view (nicer than mount(1) for triage)
findmnt -t ext4                                            # only ext4 mounts
mount | column -t                                          # all mounts, aligned columns

/etc/fstab reflects intended mounts. findmnt reflects actual current state. Compare the two — extra mounts or missing ones are findings.


Forensic time interpretation

Single-file analysis
Modify: 2022-05-02 14:30:00 -0400
Change: 2022-05-02 14:30:00 -0400
Access: 2024-04-21 09:15:33 -0400
Birth:  2022-05-02 14:30:00 -0400

Birth = Modify = Change at the same instant: file written once at creation and never modified afterwards. Access much later = someone read it recently.

Mismatch patterns
PatternLikely cause
Modify < Birthtimestomp via touch -m -t <past> after creation. Birth is set at creation and cannot be back-dated by touch
Birth shown as -not an ext4 filesystem (ext2/ext3 / FAT)
Change significantly later than Modify and Accessmetadata-only operation (chmod, chown, link/unlink, xattr write)
Access significantly earlier than Modifyfile modified after the last read — recent write, not yet read since
All four exactly equal at a millisecondfreshly-written file, never read
Timezone

stat displays times in the analyst host’s local timezone. Forensic reports state times in UTC. Either convert manually or set TZ=UTC stat <file> for the command run.


Pitfalls

  • stat shows the kernel’s interpreted view. For deleted files or inode-level detail, use debugfs.
  • debugfs against a mounted filesystem can return stale data. Best practice is to read against an image, not a live mount.
  • find / -name '*' is enormous and slow. Always scope with -type, -xdev, time predicates, or path excludes.
  • Times in stat use the analyst host’s TZ. Convert to UTC before quoting in reports.
  • Birth (crtime) is ext4-only. Do not assume it exists on every Linux filesystem.
  • fls on an unmounted image without -o defaults to offset 0 — wrong for multi-partition images. Use mmls first to find the partition’s start sector, then -o <sector>.

Field Manual | Linux Filesystem | Linux Forensics | Mount Disk Images