Signal9Signal9 — home
online
all lessons

File System Forensics

Intermediate40 min7 steps
What you'll learn
  • 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)
  1. 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.

  2. The three timestamps: stat

    Read atime, mtime, and ctime and understand what each one actually records.

    ls -l shows you one timestamp (mtime) and hides the rest. stat shows 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 c is for change.

    Example — not runnable on your lab box

    stat /etc/cron.d/apache-monitor
      File: /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?

  3. 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-monitor

    That 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-monitor
    mtime=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.sh to make their script look old. Which timestamp gives them away, and why?

  4. 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. find walks the tree and filters on timestamps, turning a single lead into the full blast radius.

    Three time tests do the heavy lifting:

    • -mtime / -ctime / -atime take a number of 24-hour days. -1 means "less than 1 day ago" (within the last 24h); +1 means "more than 1 day ago".
    • -mmin / -cmin are the minute versions — far better for a tight intrusion window.
    • -newer FILE compares 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 touched auth.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 with touch still 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, -newer anchors 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_FILE to 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?

  5. 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 ls omit 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 plain ls.
    • 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/bin
    total 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 -a the 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 plain ls would never show and the eye glosses over. And .sysupdate is a hidden executable dropped at the same minute. Both are invisible to ls without -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 ls of a directory looks clean, but ls -la shows an entry named ... (three dots). Is that a normal filesystem entry?

  6. 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 t at the end of drwxrwxrwt is the sticky bit: world-writable, but only a file's owner can delete it. Inside, a hidden executable .x sits 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?

  7. 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 -40
    Everything 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/null
    Compare 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/shm
    Sweep 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/null
    Hunt directories with deceptive dot-names like .... Any hit is worth a hard look — these are not normal filesystem entries.
Recap
  • Three timestamps, three meanings: atime = read, mtime = contents changed, ctime = inode changed. ctime is NOT creation time.
  • touch can 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 -a and find -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.
Tool / artifact → what it reveals
CommandAnswersWatch for
stat FILEAll three timestamps + metadatactime newer than mtime = backdated/tampered
ls -lc / ls -luctime / atime (instead of default mtime)A different story than the ls -l mtime tells
find / -ctime -1Files whose inode changed in the last dayBackdated files still appear — ctime can't be forged
find / -newer FILEEverything changed after a known-bad fileThe 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 /tmpWorld-writable scratch contentsService-user-owned executables; /dev/shm wiped on reboot