File System Forensics
- Read all three file timestamps with stat and explain what atime, mtime, and ctime each record
- Use the ctime/mtime mismatch to catch an attacker who backdated a file with touch
- Scope an intrusion to a time window with find -mtime, -ctime, and -newer
- Hunt hidden files — dot-names, the '...' directory trick, and trailing-space names — with ls and find
- Know where malware hides on disk: /tmp, /dev/shm (memory-backed), and /var/tmp (survives reboot)
Why build a timeline
Frame file forensics as reconstructing what an intruder touched and when.
In Process & Network Forensics you caught the threat while it was running. But the running process is only the present tense. The harder question after an incident is the past tense: what did the intruder touch — what did they read, drop, edit, or hide — and when?
The answer lives in the filesystem's own clock. Every file on Linux carries timestamps the kernel updates automatically, and a deliberate sweep of them lets you build a timeline: a minute-by-minute reconstruction of the break-in. Once you know when the attacker was active, every file changed in that window becomes a lead.
Think of timestamps as a building's three logs. The front-door badge reader logs every entry (who walked in — that's atime). The document-management system logs every content edit (who changed a file's text — that's mtime). And facilities keeps a separate maintenance log of when a door's lock or a room's nameplate was changed (the record about the room, not its contents — that's ctime). An intruder can forge the entry badge and the edit log, but the maintenance log is kept by a different department — and that's exactly the inconsistency that catches them.
The three timestamps: stat
Read atime, mtime, and ctime and understand what each one actually records.
ls -lshows you one timestamp (mtime) and hides the rest.statshows all three, and the distinction between them is the whole game:- Access (atime) — the last time the file's contents were read.
- Modify (mtime) — the last time the file's contents were changed.
- Change (ctime) — the last time the file's inode metadata changed: permissions, owner, link count, or the contents. Note the trap in the name — ctime is not "created". Linux's standard filesystems don't track creation time at all in the classic API; the
cis for change.
Example — not runnable on your lab box
stat /etc/cron.d/apache-monitorFile: /etc/cron.d/apache-monitor Size: 142 Blocks: 8 IO Block: 4096 regular file Device: fd00h/64768d Inode: 269431 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2024-01-15 09:14:02.000000000 +0000 Modify: 2023-08-02 06:30:11.000000000 +0000 Change: 2026-06-23 02:27:48.114203667 +0000
- (no flag)
- Plain stat prints the full metadata block including all three timestamps.
- -c '%n %z'
- Custom format — e.g. '%z' prints just ctime, '%y' mtime, '%x' atime, for scripting.
- --printf
- Like -c but without a trailing newline; useful when feeding many files into a sort.
Read the three time lines together. Modify says the contents were last written in August 2023 — looks like an old, settled file. But Change says the inode was touched at 02:27 this morning. Contents supposedly untouched for nearly three years, yet the metadata changed minutes ago in the small hours? That gap is the alarm. Something altered this file's permissions, owner, or — far more likely — rewrote its contents and then forged the mtime back to 2023. Check yourself
A file's name suggests it stores a creation date, but stat doesn't show one. Which timestamp do people most often mistake for a creation time?
The tell: ctime betrays touch
Use the one timestamp an attacker can't forge to catch backdating.
Attackers know investigators look at timestamps, so they backdate the files they plant to blend in with old system files. The standard tool is
touch, which can set a file's atime and mtime to any value you like:touch -d '2023-08-02 06:30:11' /etc/cron.d/apache-monitorThat one command made the file look like it was last modified in 2023. But here's the flaw the attacker can't engineer around: the act of calling `touch` itself changes the inode, and changing the inode updates ctime to right now. You cannot set ctime to an arbitrary past value through the normal file API — only the kernel writes it, and it always writes the current time. So backdating mtime necessarily leaves ctime pointing at the moment of the tampering.
Example — not runnable on your lab box
stat -c 'mtime=%y ctime=%z' /etc/cron.d/apache-monitormtime=2023-08-02 06:30:11.000000000 +0000 ctime=2026-06-23 02:27:48.114203667 +0000
- -c
- Use a custom output format string instead of the full block.
- %y
- mtime in human-readable form (use %Y for a raw epoch integer).
- %z
- ctime in human-readable form (use %Z for a raw epoch integer).
Side by side, the contradiction is undeniable: mtime is three years older than ctime. A genuine, untouched 2023 file would have an mtime and ctime that match (or with ctime only slightly later from a permissions tweak). When *ctime is dramatically newer than mtime*, someone reached in and rewrote or backdated this file. The ctime — 02:27:48 today — is your real timestamp for the event. Check yourself
An attacker runs
touch -d '2023-01-01' evil.shto make their script look old. Which timestamp gives them away, and why?Scope the window: find by time
Enumerate every file touched during the intrusion window with find.
One betrayed file gives you a time (ctime 02:27 today). Now flip it around: ask the filesystem to hand you every file changed in that window.
findwalks the tree and filters on timestamps, turning a single lead into the full blast radius.Three time tests do the heavy lifting:
-mtime/-ctime/-atimetake a number of 24-hour days.-1means "less than 1 day ago" (within the last 24h);+1means "more than 1 day ago".-mmin/-cminare the minute versions — far better for a tight intrusion window.-newer FILEcompares against a reference file's mtime: "changed more recently than this file was."
sudo find / -xdev -ctime -1 -type f 2>/dev/null/etc/cron.d/apache-monitor /usr/local/bin/.sysupdate /var/spool/cron/crontabs/www-data /home/www-data/.ssh/authorized_keys /var/log/auth.log
- -xdev
- Stay on one filesystem — don't descend into /proc, /sys, or network mounts that would flood the results.
- -ctime -1
- Inode changed less than 1 day (24h) ago. ctime, not mtime, because ctime can't be forged.
- -type f
- Regular files only — skip directories and device nodes for a cleaner first pass.
- 2>/dev/null
- Discard 'Permission denied' noise so the real hits stand out.
Five files changed in the last 24 hours, and together they tell a story: a planted cron job ( apache-monitor), a hidden binary (.sysupdate), a new crontab for the web user, an added SSH key for persistence (authorized_keys), and a touchedauth.log(likely a cleared login trace). This is the attacker's whole footprint — surfaced by one timestamp query. We searched on ctime deliberately: even files they backdated withtouchstill show up here, because their ctime is recent.Example — not runnable on your lab box
sudo find / -xdev -newer /etc/cron.d/apache-monitor -type f 2>/dev/null | head/usr/local/bin/.sysupdate /home/www-data/.ssh/authorized_keys /var/log/auth.log
- -newer FILE
- Match files whose mtime is more recent than FILE's mtime — anchor the search to a known-bad file.
Once you have one confirmed attacker artifact, -neweranchors everything to it: "show me what was modified after this." It needs no clock math and no guessing at day counts — the known-bad file is your reference point. Pair it with! -newer LATER_FILEto bracket both ends and isolate a precise window.Check yourself
You confirmed one malicious file. Which find test lets you list everything modified more recently than it, without doing any date arithmetic?
Find what's hidden
Surface dot-files and the sneaky names attackers use to stay invisible.
Attackers hide their files in plain sight by exploiting how Linux defines "hidden." There's no hidden attribute — a file is hidden purely by convention: tools like
lsomit any name starting with a dot (.). That convention is the entire hiding place, so your job is to defeat it deliberately.Three tricks to know:
- Dot-names:
.sysupdate,.cache,.(dot-space). Omitted by a plainls. - The `...` directory: a directory literally named
...(three dots). At a glance it blends in with the always-present.and..entries, but it's a real, separate directory an attacker created to stash tools. - Trailing-space and unicode names: a file named
passwd(with a trailing space) or one using a homoglyph looks identical to a legit file in a casual listing.
ls -la /usr/local/bintotal 32 drwxr-xr-x 3 root root 4096 Jun 23 02:27 . drwxr-xr-x 10 root root 4096 Aug 2 2023 .. drwxr-xr-x 2 root root 4096 Jun 23 02:26 ... -rwxr-xr-x 1 root root 8192 Jun 23 02:26 .sysupdate -rwxr-xr-x 1 root root 1840 Aug 2 2023 backup.sh
- -a
- Show ALL entries, including dot-files — without this, .sysupdate and ... are invisible.
- -l
- Long format: permissions, owner, size, and mtime — so anomalies in those columns are visible too.
With -athe hiding place collapses..and..are the normal current/parent entries — but `...` is a third line: a real directory named with three dots, created at 02:26 today, that a plainlswould never show and the eye glosses over. And.sysupdateis a hidden executable dropped at the same minute. Both are invisible tolswithout-a; both scream attacker given the timestamp.Example — not runnable on your lab box
find / -xdev \( -name '.*' -o -name '...' \) -newer /etc/cron.d/apache-monitor 2>/dev/null/usr/local/bin/... /usr/local/bin/.sysupdate /home/www-data/.ssh/authorized_keys
- -name '.*'
- Match any name starting with a dot — every conventionally-hidden file.
- \( ... -o ... \)
- Group alternatives with OR; backslashes protect the parens from the shell.
- -newer FILE
- Combine with the timeline — only hidden files newer than your known-bad artifact.
This is the power move: combine the hidden-file hunt with the timeline. You're not listing every dot-file on the box (there are thousands of legit ones) — only the hidden files that appeared after your confirmed intrusion artifact. The result is a short, high-signal list of exactly the attacker's hidden drops. Check yourself
A plain
lsof a directory looks clean, butls -lashows an entry named...(three dots). Is that a normal filesystem entry?- Dot-names:
Where malware hides on disk
Know the world-writable scratch directories — and which survive a reboot.
Attackers need a place any user can write to, so they gravitate to the world-writable directories that exist on every Linux box. Knowing these by heart tells you where to look first — and one crucial difference between them tells you whether the evidence will still be there after a reboot.
- `/tmp` — world-writable, sticky-bit set, the universal scratch space. On many systems it's cleared on reboot or by a timer; treat its contents as ephemeral.
- `/dev/shm` — world-writable shared memory, backed by RAM (a
tmpfs), not disk. Malware loves it: fast, often overlooked, and it leaves no trace on the physical disk. Because it's memory-backed, everything in `/dev/shm` vanishes on reboot — so capture it before you power-cycle the box, or the evidence is gone for good. - `/var/tmp` — also world-writable, but designed for data that should persist across reboots. Attackers use it for staging that needs to outlive a restart, and it's the one most often forgotten in a quick sweep of
/tmp.
ls -la /dev/shm /var/tmp/dev/shm: total 0 drwxrwxrwt 2 root root 60 Jun 23 02:31 . drwxr-xr-x 18 root root 4020 Jun 23 02:10 .. -rwxr-xr-x 1 www-data www-data 2148 Jun 23 02:31 .x /var/tmp: total 12 drwxrwxrwt 2 root root 4096 Jun 23 02:33 . drwxr-xr-x 14 root root 4096 Aug 2 2023 .. -rw-r--r-- 1 www-data www-data 6021 Jun 23 02:33 .stage.tar
- -l
- Long listing so you can see the rwxrwxrwt permissions and the writing user (www-data here).
- -a
- Show the hidden drops — .x and .stage.tar both start with a dot.
The tat the end ofdrwxrwxrwtis the sticky bit: world-writable, but only a file's owner can delete it. Inside, a hidden executable.xsits in memory-backed/dev/shm(running from RAM, gone on reboot), and a hidden.stage.tar— staged exfil — sits in disk-backed/var/tmp(will survive a restart). Note the owner: `www-data`, the web-server user, writing executables. A web daemon dropping binaries into shared memory is almost never legitimate.Check yourself
You find a suspicious file in /dev/shm during an active incident. Why must you capture it before rebooting?
Try it in your live terminal
Build a real timeline yourself on the sandbox to your right.
You have a real root shell. Run this sequence in order — it's the exact filesystem-timeline workflow you'd use on a live "we think we were breached" call. There may be nothing malicious here; the point is to make the moves reflex so they're ready when it counts. Start by finding the recently-changed files, then interrogate the most suspicious one, then sweep the scratch dirs.
sudo find / -xdev -cmin -120 -type f 2>/dev/null | head -40Everything whose inode changed in the last two hours. ctime ( -cmin) so even backdated files surface. Note anything in /etc, /usr/local/bin, an .ssh dir, or a cron path.stat -c '%n mtime=%y ctime=%z' /etc/cron.d/* 2>/dev/nullCompare mtime to ctime across every cron job. A ctime far newer than its mtime is your backdating tell — on a host whose files were laid down in place by a package manager. Read the callout below before you act on what this prints here: this sandbox is a container, and on a container every file's ctime is the moment the image was unpacked. ls -la /tmp /var/tmp /dev/shmSweep the three world-writable scratch dirs. Look for hidden (dot) names, executables, and an owner that has no business writing there (e.g. a web or db service user). find / -xdev \( -name '...' -o -name '. ' -o -name '..?*' \) -type d 2>/dev/nullHunt directories with deceptive dot-names like .... Any hit is worth a hard look — these are not normal filesystem entries.
- Three timestamps, three meanings: atime = read, mtime = contents changed, ctime = inode changed. ctime is NOT creation time.
touchcan backdate atime and mtime but not ctime — so ctime newer than mtime is the signature of a tampered, backdated file.- Hunt on ctime (
-cmin/-ctime), not mtime, so backdated files still surface; anchor the window to a known-bad file with-newer. - "Hidden" is just a naming convention — defeat it with
ls -aandfind -name '.*', and watch for the...directory and trailing-space names. - Malware lives in world-writable scratch dirs. /dev/shm is RAM-backed and dies on reboot — capture it first; /var/tmp survives a restart.
| Command | Answers | Watch for |
|---|---|---|
stat FILE | All three timestamps + metadata | ctime newer than mtime = backdated/tampered |
ls -lc / ls -lu | ctime / atime (instead of default mtime) | A different story than the ls -l mtime tells |
find / -ctime -1 | Files whose inode changed in the last day | Backdated files still appear — ctime can't be forged |
find / -newer FILE | Everything changed after a known-bad file | The full blast radius around one confirmed artifact |
ls -la / find -name '.*' | Hidden dot-files and dirs | ... directory, .x executables, trailing-space names |
ls -la /dev/shm /var/tmp /tmp | World-writable scratch contents | Service-user-owned executables; /dev/shm wiped on reboot |
