> Where you are: Part III (Digital Forensics), Chapter 17 of 40. Chapter 16 walked the Windows artifact landscape — the registry, event logs, Prefetch, the $Recycle.Bin. This chapter crosses to the other two operating systems you will meet in the...
In This Chapter
- The world is not only Windows
- macOS: the file system is the evidence
- macOS metadata and activity artifacts
- Linux: text logs, binary logins, and the journal
- ext4 under the hood: inodes, extents, and the journal
- SQLite: the common artifact format
- Tool demonstration: from image to artifacts
- Worked example: the engineer's Mac and the build server
- Common mistakes
- Limitations: knowing when to stop
- Progressive project: adding macOS and Linux artifacts to the case file
- Summary
Chapter 17: macOS and Linux Forensics — APFS Snapshots, Unified Logs, and Unix Artifacts
Where you are: Part III (Digital Forensics), Chapter 17 of 40. Chapter 16 walked the Windows artifact landscape — the registry, event logs, Prefetch, the
$Recycle.Bin. This chapter crosses to the other two operating systems you will meet in the field: Apple's macOS, built on a copy-on-write file system (APFS) that quietly preserves prior states, and Linux, the Unix-descended operating system that runs most of the world's servers and leaves its tracks in plain-text logs, binary login ledgers, and a structured journal. The departing-engineer case from Chapter 1 returns here in its natural habitat — a senior engineer's MacBook Pro and the Linux build server they SSH'd into — and the theme deleted is not destroyed takes on a new form: on APFS a deleted file may still live, whole and mountable, inside a snapshot, and on ext4 the file system's own journal can hand back a block map the live inode threw away.Learning paths: 🔍 Forensic Examiner and 🛡️ Incident Response should read every section — macOS endpoints and Linux servers are now unavoidable, and the artifacts here decide cases. 💾 Data Recovery wants the APFS-snapshot, ext4-journal, and SQLite-freelist material, which is where lost data on these systems actually comes back. 📜 Legal/eDiscovery should absorb what these artifacts prove — download provenance, app-usage timelines, login history — and the encryption limits at the end, so you can scope collection and question an examiner intelligently.
The world is not only Windows
Almost every introductory forensics course is, in practice, a Windows course. That made sense for a decade when the corporate endpoint was a Dell running Windows 7. It does not match the bench anymore. The design team uses MacBooks. The developers push to Linux build servers and Kubernetes nodes. The CEO's laptop is a MacBook Air, the point-of-sale runs a Linux variant, the NAS in the closet is Linux underneath, and the phone in the suspect's pocket is — but that is Chapter 24. If your skills stop at the Windows registry, half of modern evidence is invisible to you.
Return to the second of the two phone calls that opened this book: the senior engineer who gave two weeks' notice and walked to a competitor, suspected of taking proprietary designs. In Chapter 1 we sketched that case on a Windows workstation. The reality, for a senior engineer at a hardware company, is more likely this: a company-issued MacBook Pro for daily work, and an account on a Linux build server where the source code actually lives. The Windows-centric story — USB history in the registry, $STANDARD_INFORMATION` versus `$FILE_NAME timestamps, Prefetch proving a "cleaner" ran — has macOS and Linux equivalents, and they are, if anything, richer. This chapter teaches them, and it returns to that engineer for the worked example.
Here is the orienting idea, and it is theme #4 — technology changes, principles don't — wearing new clothes. macOS and Linux are different from Windows and from each other, but the method is identical: understand the technology, image the evidence, analyze systematically, document everything, report accurately. And the two share more than they appear to. macOS is a certified UNIX under the hood — its kernel is BSD-derived (XNU/Darwin), its user-land speaks bash/zsh, ls, cron, and POSIX permissions, and it carries /var/log, /etc, and /private directories that a Linux examiner will recognize on sight. Both lean astonishingly hard on one common artifact format — SQLite — for everything from download history to app usage. Learn that shared substrate and the two systems stop being two problems.
Why This Matters. A forensic examiner who can only work Windows is not a forensic examiner; they are a Windows technician who does some forensics. The evidence does not wait for you on a platform you find comfortable. The departing engineer chose a Mac and a Linux server precisely because that is where the work was — not to evade you, but the effect is the same if you cannot follow. Cross-platform fluency is not advanced specialization in this field. It is the baseline.
macOS: the file system is the evidence
Since macOS 10.13 "High Sierra" (2017), the default file system on every Mac is APFS — the Apple File System. It replaced HFS+, which had served since 1998, and it changed the forensic game in one decisive way: APFS is copy-on-write. We met the concept in Chapter 4; here it pays off. Under copy-on-write, when a file's contents change, the file system does not overwrite the old blocks in place. It writes the new data to fresh blocks and then updates the metadata to point at them. The old blocks are simply no longer referenced — which means, until they are reused, the old version is still physically present. That single design choice, made for crash-safety and performance, is a gift to anyone who recovers data or investigates it.
APFS in brief: containers, volumes, and superblocks
Before the artifacts, the structure. An APFS layout is not "a partition holding a file system." It is a container (Apple calls the underlying GPT partition an APFS container) that holds one or more volumes which all share the container's free space. Your Mac's single internal drive typically presents one container with several volumes inside it: Macintosh HD (the read-only sealed system volume since macOS Catalina), Macintosh HD - Data (your actual files), plus hidden Preboot, Recovery, and VM volumes. Space is pooled — none of those volumes has a fixed size — which is why df on a Mac reports the same "available" figure for several volumes at once.
The container is described by a container superblock (nx_superblock_t), and each volume by a volume superblock (apfs_superblock_t). Each begins with a 32-byte object header, then a magic value you can spot in a hex editor. The container superblock lives at block 0 (with backup copies), and its magic is the ASCII string NXSB:
APFS container superblock (block 0), 4 KiB block size
Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII
0x00000000 2E 9A 41 7C 6B 03 D5 11 01 00 00 00 00 00 00 00 ..A|k...........
0x00000010 A3 04 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ................
0x00000020 4E 58 53 42 00 10 00 00 E0 C8 6E 07 00 00 00 00 NXSB......n.....
^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^
nx_magic = "NXSB" block_size block_count
(offset 0x20) 0x1000=4096 0x076EC8E0
[bytes 0x00–0x07 = Fletcher-64 checksum; 0x08 = object id 1; 0x10 = transaction id (xid)]
The volume superblock follows the same pattern but its magic, at the same offset past its object header, is APSB (41 50 53 42). The block size is almost always 4096 bytes (0x1000), and the transaction id (xid) at offset 0x10 matters more than it looks: APFS is transactional, every change bumps the container-wide xid, and a higher xid means a more recent state. When you find multiple superblocks with different xids, the highest is "now" and the lower ones are windows into the past — the structural reason snapshots work at all. The full APFS on-disk structures are catalogued in Appendix G; the magic values join the master table in Appendix A.
APFS CONTAINER (the GPT partition)
┌───────────────────────────────────────────────────────┐
│ Container superblock (NXSB) + checkpoint descriptors │
│ Space manager · object map · free-space bitmaps │
│ │
│ ┌───────────┐ ┌──────────────┐ ┌─────────┐ ┌───────┐ │
│ │ Volume │ │ Volume │ │ Volume │ │ Volume│ │
│ │ APSB: │ │ APSB: │ │ APSB: │ │ APSB: │ │
│ │ "Mac HD" │ │ "Mac HD-Data"│ │ Preboot │ │ VM │ │
│ │ (sealed) │ │ (your files) │ │ │ │ │ │
│ └───────────┘ └──────────────┘ └─────────┘ └───────┘ │
│ all volumes SHARE the container's free space │
└───────────────────────────────────────────────────────┘
APFS snapshots: prior file-system states, preserved
An APFS snapshot is a named, read-only, point-in-time image of an entire volume, created almost instantly and costing almost no space at the moment of creation. Because the file system is copy-on-write, a snapshot is just a pinned root object at a particular transaction id: the snapshot says "do not free the blocks this tree references," and from then on every change writes new blocks while the old ones stay alive for the snapshot. The result is a fully navigable copy of the volume as it was — every file, every folder, every timestamp — frozen.
Who makes snapshots? Time Machine does, constantly. Since APFS, Time Machine creates local snapshots on your internal drive (not just backups to an external disk) roughly hourly and keeps them for about 24 hours, plus more whenever a backup destination is unavailable. macOS itself makes snapshots before major updates. Software installers make them. This means a typical working Mac is carrying several recent prior states of its file system at any moment — and the user usually has no idea.
You enumerate them with tmutil or diskutil:
# Local Time Machine snapshots (run on a live Mac, or note them during triage)
$ tmutil listlocalsnapshots /
com.apple.TimeMachine.2025-01-15-090112.local
com.apple.TimeMachine.2025-01-15-100143.local
com.apple.TimeMachine.2025-01-15-130210.local
com.apple.TimeMachine.2025-01-15-140251.local
# Lower-level view, including non-Time-Machine snapshots, with transaction ids
$ diskutil apfs listSnapshots disk3s5
Snapshot for disk3s5 (2 found)
+-- 7B2C... (xid 1184) com.apple.TimeMachine.2025-01-15-130210.local
+-- 9F4A... (xid 1201) com.apple.TimeMachine.2025-01-15-140251.local
Read the timestamps in those names against the case. The engineer "cleaned up" his Downloads and Projects folders at 14:50 before returning the laptop. But the 2025-01-15-140251.local snapshot captured the volume at 14:02 — before the cleanup. Mount that snapshot read-only and the deleted proprietary project folder is simply there, intact, with original timestamps. You did not carve it. You did not recover it. You walked into a preserved past and read it like the present.
Recovery vs. Forensics. Same artifact, two purposes. A snapshot to a 💾 recovery engineer is the fastest possible win — a client who emptied the Trash this morning may have an hourly snapshot from before they did it, and restoring is a copy operation, not a carve. To a 🔍 forensic examiner the same snapshot is evidence of a prior state: you mount it read-only from your verified image, record the snapshot name, its transaction id, and the source, and you can now testify that on 2025-01-15 at 14:02 these files existed on this volume — and that someone removed them between then and acquisition. Recovery uses the snapshot to restore. Forensics uses it to prove the timeline. The blocks are identical; the discipline lives in the documentation around them.
Mounting and reading APFS off your image
You will rarely analyze a Mac on the Mac. You image it (the acquisition specifics, including Apple's hard cases, are in Chapter 14 and revisited under Limitations) and analyze the image on your workstation. Two open tools dominate:
- APFS-Fuse — an open-source FUSE driver that mounts an APFS image read-only on Linux, including individual snapshots and (with the password or recovery key) FileVault-encrypted volumes.
- mac_apt (macOS Artifact Parsing Tool) — a Python framework that ingests an E01/DD/DMG image and runs dozens of plugins (APFS, Spotlight, FSEvents, Unified Log, knowledgeC, Quarantine, plist, Safari, and more), writing results to SQLite/Excel.
# Mount the whole container read-only, then list and mount a specific snapshot
$ apfs-fuse -o ro,allow_other macbook_evidence.dd /mnt/apfs
$ apfs-fuse -o ro,vol=1 -s list macbook_evidence.dd # show snapshots in volume 1
$ apfs-fuse -o ro,vol=1,snap=9F4A... macbook_evidence.dd /mnt/snap_1402
# The Sleuth Kit also understands the APFS "pool"; useful for offset-precise extraction
$ mmls macbook_evidence.dd # find the APFS container partition
$ fls -r -o 40 macbook_evidence.dd | grep -i 'Projects/falcon'
Try This. On any Mac you own, run
tmutil listlocalsnapshots /. You will almost certainly see several snapshots you never knowingly created. Now rundiskutil apfs listSnapshots /to see their transaction ids. You have just located several preserved prior states of your own file system — the exact thing that, on a suspect's drive, can place a deleted file at a moment in time. (Do not delete them; just look.)
macOS metadata and activity artifacts
APFS holds the files. The story of what the user did lives in a constellation of metadata stores and activity databases layered on top. These are where macOS forensics gets its reputation for being almost too informative.
Spotlight: the metadata store
Spotlight is the macOS search index, and it records far more than filenames. At each volume's root sits a hidden .Spotlight-V100/ directory; inside, under Store-V2/<store-UUID>/, are the index databases — chiefly store.db and .store.db. For every file Spotlight has seen, it caches dozens of metadata attributes: content type, creation and modification dates, the application that created it, pixel dimensions, duration, author — and, for our purposes, two attributes worth their weight in gold:
kMDItemWhereFroms— the URL(s) a downloaded file came from, and often the page it was linked from.kMDItemDownloadedDate/kMDItemDateAdded/kMDItemLastUsedDate/kMDItemUseCount— when it arrived, when it was last opened, how many times.
On a live system you query these with mdls (metadata list) and mdfind:
$ mdls /Volumes/EVID/Users/jchen/Downloads/diskmaker.dmg
kMDItemContentCreationDate = 2025-01-15 14:42:31 +0000
kMDItemWhereFroms = (
"https://diskmaker.example.com/dl/diskmaker-4.2.dmg",
"https://diskmaker.example.com/download"
)
kMDItemLastUsedDate = 2025-01-15 14:47:55 +0000
kMDItemUseCount = 1
That kMDItemWhereFroms block records that the engineer fetched a disk-imaging utility from a specific URL on the afternoon in question — independent corroboration of the Quarantine and FSEvents evidence we will gather below. Offline, you parse store.db directly with mac_apt's Spotlight plugin or Yogesh Khatri's standalone spotlight_parser, because mdls reads the live index and you must work from the image.
FSEvents: the file-system change log
macOS keeps a rolling log of file-system changes so that applications (Spotlight, Time Machine, backup software) can ask "what changed since I last looked?" That log is FSEvents, stored in the hidden /.fseventsd/ directory at each volume's root as a series of gzip-compressed files named with hexadecimal event identifiers. Decompress one and the stream begins with a four-byte magic — 1SLD, 2SLD, or 3SLD (the "DLS" page versions) — followed by records, each pairing a path, a 64-bit event ID, and a flags mask describing what happened (created, removed, renamed, modified, is-file, is-directory, and so on):
$ FSEParser_V4.1 -c parse -s /Volumes/EVID/.fseventsd -o ./fse_out
Decoded log page 0x0000000000a3f2c1 (gzip → DLS2 stream)
Event ID Full path Reason mask
0x00a3f2c4 Users/jchen/Downloads/diskmaker.dmg Created;IsFile
0x00a3f2c7 Users/jchen/Downloads/diskmaker.dmg Modified;IsFile
0x00a3f3b1 Users/jchen/Projects/falcon-x Created;IsDir
0x00a3f3b9 Volumes/FIELDKIT/falcon-x.zip Created;IsFile;Renamed
0x00a3f4d0 Users/jchen/Projects/falcon-x Removed;IsDir
Two cautions make FSEvents a place beginners stumble. First, FSEvents records do not carry per-event wall-clock timestamps — the event IDs are a strictly increasing counter, not a clock. You establish order directly from the IDs, and approximate time from the gzip file's own modification time plus correlation with timestamped sources (the Unified Log, Spotlight, knowledgeC). Second, FSEvents logs at directory granularity in some cases and coalesces rapid changes, so it tells you a directory changed without always enumerating every file. Used carefully, though, the sequence above is a narrative in five lines: a tool was downloaded, a project folder existed, a zip of it appeared on a removable volume named FIELDKIT, and then the project folder was deleted. Parse it with mac_apt or David Cowen's FSEventsParser.
The Unified Log: macOS's firehose
Through macOS 10.11, Macs logged to plain-text files (/var/log/system.log and the Apple System Log, ASL). Since macOS 10.12 "Sierra," nearly all of that moved into the Unified Logging System — a high-volume, binary, structured log shared with iOS. This is simultaneously a goldmine and a headache.
The data lives in /var/db/diagnostics/ as .tracev3 files, organized into subfolders: Persist/ (the durable log you most want), Special/, Signpost/, HighVolume/, and timesync/. Crucially, to save space the log stores format strings and static text separately in /var/db/uuidtext/ (indexed by UUID). A .tracev3 entry might say "process X logged message template Y with arguments A, B" — and you cannot render the human-readable line without the matching uuidtext entry. So you must collect both /var/db/diagnostics/ and /var/db/uuidtext/ or you will hold logs you cannot fully read.
On a live or mounted system, Apple's native log tool reads it; for offline work you point a parser at the collected folders:
# Live: show a time-bounded slice with a predicate (also: log collect --output case.logarchive)
$ log show --start "2025-01-15 14:00:00" --end "2025-01-15 15:30:00" \
--predicate 'subsystem == "com.apple.iokit.IOUSBHostFamily"' --info
# Offline: parse collected diagnostics + uuidtext from your image with an open tool
$ python UnifiedLogReader.py ./var/db/uuidtext ./var/db/diagnostics ./ul_out
The Unified Log captures USB attach/detach events, process launches, network changes, screen lock/unlock, software installs, and much more. It is enormous and verbose, retention is days-to-weeks (not months), and you will live or die by good --predicate filters. The native log toolchain, Mandiant's UnifiedLogReader, and mac_apt are the workhorses; the artifact paths are listed in Appendix D.
KnowledgeC and biome: the activity timeline in a database
If the Unified Log is the firehose, knowledgeC.db is the curated highlight reel — and for reconstructing what a human was doing and when, it is one of the most valuable artifacts on the platform. It is a SQLite database recording device-usage "events": which app was in focus, for how long, when the device was locked and unlocked, Now-Playing media, even Siri and notification activity. There are two: a system database at /private/var/db/CoreDuet/Knowledge/knowledgeC.db and a per-user one at ~/Library/Application Support/Knowledge/knowledgeC.db.
The central table is ZOBJECT. Each row is tagged with a ZSTREAMNAME (the event type, e.g. /app/inFocus, /app/usage, /device/isLocked, /display/isBacklit) and carries start/end timestamps. The catch — and you will meet it again — is that the timestamps are Mac absolute time: seconds since the Cocoa epoch of 2001-01-01 00:00:00 UTC, not the Unix epoch of 1970. To convert to something human, add 978307200 (the Unix timestamp of that 2001 epoch):
-- App focus timeline from knowledgeC.db (Mac absolute time -> local time)
SELECT
datetime(ZOBJECT.ZSTARTDATE + 978307200, 'unixepoch', 'localtime') AS focus_start,
datetime(ZOBJECT.ZENDDATE + 978307200, 'unixepoch', 'localtime') AS focus_end,
ZOBJECT.ZVALUESTRING AS app_bundle_id
FROM ZOBJECT
WHERE ZOBJECT.ZSTREAMNAME = '/app/inFocus'
ORDER BY focus_start;
focus_start focus_end app_bundle_id
2025-01-15 14:18:02 2025-01-15 14:41:55 com.apple.dt.Xcode
2025-01-15 14:42:10 2025-01-15 14:47:30 com.apple.Safari
2025-01-15 14:47:42 2025-01-15 14:52:09 com.example.DiskMaker
2025-01-15 14:52:30 2025-01-15 14:55:01 com.apple.finder
Read against the case, that is a confession in four rows: the engineer worked in Xcode, switched to Safari (where Spotlight and Quarantine show the download), then ran a third-party disk-imaging tool for five minutes, then opened Finder — exactly the window in which FSEvents shows the project zip landing on the FIELDKIT volume. On newer macOS (Monterey and later), Apple is migrating some of this behavioral telemetry to biome — segmented, protobuf-encoded streams under ~/Library/Biome/ and /private/var/db/biome/ — which mac_apt and emerging tools are learning to parse. The lesson of theme #4 again: the container changes (ASL → Unified Log; knowledgeC → biome), the intelligence persists, and your job is to follow it to its new home.
Gatekeeper and Quarantine: where did this file come from?
When you download a file with Safari, Mail, Messages, or most well-behaved apps, macOS tags it with an extended attribute named com.apple.quarantine. That tag is what makes Gatekeeper pop the "this was downloaded from the internet, are you sure?" dialog the first time you open it. For an examiner it is download provenance written directly onto the file.
$ xattr -p com.apple.quarantine \
/Volumes/EVID/Users/jchen/Downloads/diskmaker.dmg
0083;67a8b3c4;Safari;9F2A1C7E-3B4D-4E5F-8A6B-1C2D3E4F5A6B
| | | |
| | | └─ event UUID → key into the QuarantineEventsV2 database
| | └──────── agent: the app that performed the download (Safari)
| └───────────────── hex timestamp (cross-check against the DB row below)
└────────────────────── flags
The xattr's UUID is a foreign key into a SQLite database in the user's profile: ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2. That database (table LSQuarantineEvent) holds the full record — the download URL, the originating page, the agent, and an authoritative timestamp (again Mac absolute time):
SELECT
datetime(LSQuarantineTimeStamp + 978307200, 'unixepoch','localtime') AS dl_time,
LSQuarantineAgentName AS agent,
LSQuarantineDataURLString AS file_url,
LSQuarantineOriginURLString AS came_from
FROM LSQuarantineEvent
WHERE LSQuarantineEventIdentifier = '9F2A1C7E-3B4D-4E5F-8A6B-1C2D3E4F5A6B';
dl_time agent file_url came_from
2025-01-15 14:42:31 Safari https://diskmaker.example.com/dl/...dmg https://diskmaker.example.com/download
Two forensic subtleties. First, the com.apple.quarantine xattr is removable — xattr -d com.apple.quarantine file strips it, a known anti-forensic move — but the QuarantineEventsV2 database row usually survives even after the xattr is gone, so absence of the tag plus presence of the DB row is itself a signal (theme #3: erasing the trace leaves a trace). Second, a related artifact, TCC.db (Transparency, Consent, and Control), at ~/Library/Application Support/com.apple.TCC/TCC.db and /Library/Application Support/com.apple.TCC/TCC.db, records which apps were granted access to the camera, microphone, screen recording, and Full Disk Access — invaluable when you need to know whether a given tool could have read the data it is accused of exfiltrating.
plist files: the configuration substrate
Configuration and state on macOS live in property lists — "plists." A plist is a structured key/value document, and it comes in two on-disk formats you must recognize. XML plists are human-readable text beginning <?xml ... and <plist version="1.0">. Binary plists are compact and begin with the magic bplist00:
Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII
0x00000000 62 70 6C 69 73 74 30 30 D1 01 02 5F 10 0F 6C 61 bplist00..._..la
^^^^^^^^^^^^^^^^^^^^^^^
"bplist00" -> binary property list, version 00
You cannot read a binary plist in a text viewer; convert it first with Apple's plutil (or Python's plistlib, or ccl_bplist):
$ plutil -convert xml1 -o - com.apple.loginwindow.plist # binary -> XML on stdout
$ plutil -p com.apple.dock.plist # pretty-print either format
Plists answer recurring investigative questions: persistence (~/Library/LaunchAgents/, /Library/LaunchDaemons/, /Library/LaunchAgents/ hold .plist files defining what auto-starts — a favorite of malware, see Chapter 32); recent activity (per-app *.LSSharedFileList and "recent items" plists); login items; network configuration (/Library/Preferences/SystemConfiguration/). Treat every .plist as potential evidence and always check its format before reading.
Keychain and Time Machine
Two more macOS artifacts round out the picture. The Keychain is Apple's encrypted credential store: the user's login.keychain-db in ~/Library/Keychains/ holds saved passwords, certificates, and keys, encrypted with the account password; the /Library/Keychains/System.keychain holds system secrets. You cannot read it without the password (and you should not try without authorization — Chapter 25), but its existence and metadata — which credentials, created when — can matter, and with lawful access to the password the security tool unlocks it.
Time Machine is both a backup system and an evidence multiplier. To an external or network destination it stores dated backups (on APFS destinations as snapshots; on older HFS+ destinations using a clever directory-hard-link scheme). Combined with the local snapshots discussed above, Time Machine means a single Mac can yield not one file-system state but a dozen, stretching back months on the backup drive. When you seize a Mac, ask where it backs up — the Time Capsule, NAS, or USB drive in the next room may hold the version of a file that was "deleted" from the laptop a year ago.
Chain of Custody. macOS artifacts multiply your evidence sources, and every source needs its own provenance line. When you mount snapshot
2025-01-15-140251.local, extractknowledgeC.db, and pull a Time Machine backup off a separate USB drive, you now have three evidentiary objects, each requiring its own hash, its own acquisition note, and its own entry in the custody log (Appendix F). A finding that braids together a snapshot, a Quarantine row, and an FSEvents sequence is powerful — and only as defensible as the documentation proving where each thread came from and that none was altered after collection.
Linux: text logs, binary logins, and the journal
Cross to Linux and the texture changes. Where macOS hides its state in opaque binary databases, classic Linux wears much of its history in plain text under /var/log/ — readable in any editor, which is a blessing for analysis and a curse for integrity (text logs are trivially edited by an attacker with root). Modern Linux adds a structured binary log (the systemd journal) and keeps a few stubborn binary files (wtmp, btmp, utmp) for login history. Learn these and you can work a breached web server, a developer's workstation, or the engineer's build server.
/var/log and the text logs
The heart of Linux logging is /var/log/. The exact filenames split along distribution lines:
| Purpose | Debian/Ubuntu | RHEL/CentOS/Fedora |
|---|---|---|
| Authentication (logins, sudo, ssh) | /var/log/auth.log |
/var/log/secure |
| General system messages | /var/log/syslog |
/var/log/messages |
| Kernel ring buffer | /var/log/kern.log |
/var/log/messages |
| Package management | /var/log/dpkg.log, apt/ |
/var/log/yum.log, dnf.log |
| Scheduled tasks | /var/log/cron.log or in syslog |
/var/log/cron |
| Boot | /var/log/boot.log |
/var/log/boot.log |
The single most useful file for an intrusion or insider case is the authentication log. SSH logins, sudo invocations, su, and failed attempts all land here:
$ grep -E 'sshd|sudo' var/log/auth.log
Jan 15 14:32:07 buildsrv sshd[20451]: Accepted publickey for deploy from 10.0.4.18 \
port 49222 ssh2: RSA SHA256:vK7p...3Qd
Jan 15 14:32:07 buildsrv sshd[20451]: pam_unix(sshd:session): session opened for \
user deploy by (uid=0)
Jan 15 14:35:11 buildsrv sudo: deploy : TTY=pts/1 ; PWD=/srv/repo/falcon-x ; \
USER=root ; COMMAND=/bin/tar -czf /tmp/repo.tgz .
Jan 15 14:51:48 buildsrv sshd[20451]: pam_unix(sshd:session): session closed for user deploy
That fragment establishes that the account deploy logged in by SSH key from 10.0.4.18 (we will tie that IP to the engineer's MacBook), tar'd the entire falcon-x repository to /tmp/repo.tgz via sudo, and logged out nineteen minutes later. Log rotation matters: logrotate periodically renames auth.log to auth.log.1, then compresses older generations to auth.log.2.gz, auth.log.3.gz, and so on, eventually deleting the oldest. So your evidence may be spread across several files, some gzip-compressed — always check the rotated and .gz siblings, and remember that a deleted-but-not-yet-overwritten rotated log can be carved back (Chapter 7).
wtmp, btmp, utmp: the login ledger
Login history on Linux lives in three binary files, not text, and you read them with dedicated tools rather than a text editor:
/var/run/utmp— who is logged in right now (volatile; relevant in live response)./var/log/wtmp— the historical record of logins, logouts, reboots, and shutdowns./var/log/btmp— failed login attempts (brute-force evidence).
Each is an array of fixed-size utmp records (384 bytes on 64-bit glibc), holding a type, a PID, the terminal line, the username (32 bytes), the remote host (256 bytes), and a timestamp. You decode them with last (wtmp), lastb (btmp), who/w (live utmp), and lastlog (the per-user last-login file /var/log/lastlog). On your image you point them at the extracted file:
$ last -f var/log/wtmp -F
deploy pts/1 10.0.4.18 Wed Jan 15 14:32:07 2025 - Wed Jan 15 14:51:48 2025 (00:19)
deploy pts/0 10.0.4.18 Wed Jan 15 09:14:55 2025 - Wed Jan 15 12:02:31 2025 (02:47)
reboot system boot 5.15.0-89 Wed Jan 15 09:01:10 2025 still running
$ lastb -f var/log/btmp -F # failed logins -- a wall of these = brute force
admin ssh:notty 203.0.113.7 Wed Jan 15 03:22:19 2025 - 03:22:19 (00:00)
root ssh:notty 203.0.113.7 Wed Jan 15 03:22:20 2025 - 03:22:20 (00:00)
Because these are binary and structured, they are harder to edit cleanly than text logs — an attacker who scrubs auth.log often forgets wtmp, and a wtmp that disagrees with auth.log is a tampering indicator (theme #3 again). The utmp record layout is in Appendix G.
Shell history: what they actually typed
Few artifacts are as immediately damning as a user's shell history, and few are as misunderstood. The default interactive shell determines the file: ~/.bash_history for bash, ~/.zsh_history for zsh (the default on modern macOS too — note the cross-platform overlap), plus tool-specific siblings like ~/.python_history, ~/.mysql_history, ~/.psql_history, and ~/.lesshst.
$ cat home/deploy/.bash_history
cd /srv/repo/falcon-x
git log --oneline -5
sudo tar -czf /tmp/repo.tgz .
scp /tmp/repo.tgz jchen@10.0.4.18:/Users/jchen/Downloads/
rm /tmp/repo.tgz
history -c
unset HISTFILE
Read that last block carefully. The user tar'd the repo, scp'd it to a Mac at 10.0.4.18, deleted the tarball, then ran history -c (clear the in-memory history) and unset HISTFILE (so the session would not save on exit). Here is the elegant part, and theme #3 in its purest form: the very commands meant to erase the history are recorded in the history, because the file is written from memory and these lines were already in the buffer. Anti-forensics that runs inside the thing it is trying to clean leaves its fingerprints on the way out.
Two technical truths to internalize. First, by default .bash_history records no timestamps — just commands, in order. Timestamps appear only if HISTTIMEFORMAT was set, in which case the file interleaves #<unix-epoch> lines before each command. zsh's extended history format does carry time: : <epoch>:<elapsed>;<command>. Second, history is written on shell exit (or via history -a), so a still-open session's most recent commands may live only in memory — a reason live response (Chapter 15) and memory forensics (Chapter 22) can recover commands that never reached disk.
cron and the systemd journal
Two more Linux sources close the loop. Scheduled tasks are classic persistence and lateral-movement infrastructure: examine /etc/crontab, the drop-in directories /etc/cron.d/, /etc/cron.{daily,hourly,weekly}/, and per-user crontabs in /var/spool/cron/crontabs/ (Debian) or /var/spool/cron/ (RHEL). On systemd systems, the modern equivalent is timer units (*.timer in /etc/systemd/system/ and /lib/systemd/system/), so check both. A cron entry or timer that runs an unfamiliar script at 3 a.m. is exactly the kind of thing Chapter 32 chases.
The systemd journal is Linux's answer to the Unified Log: a structured, indexed, binary log that on many modern distributions supplements or replaces the text logs. It lives in /var/log/journal/<machine-id>/*.journal (persistent) or /run/log/journal/ (volatile, RAM-only — lost on reboot, so capture it live). Journal files begin with the eight-byte signature LPKSHHRH:
Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII
0x00000000 4C 50 4B 53 48 48 52 48 ED FE ... (header fields) LPKSHHRH........
^^^^^^^^^^^^^^^^^^^^^^^
systemd journal magic = "LPKSHHRH"
The killer feature for examiners is that journalctl can read journal files offline, straight from your mounted image, with rich structured filtering on fields like _PID, _UID, _COMM, _SYSTEMD_UNIT, and _BOOT_ID:
# Read the suspect's journal from the IMAGE, filtered by program and time window
$ journalctl --file ./var/log/journal/3f8c.../system.journal \
--since "2025-01-15 14:00" --until "2025-01-15 15:30" _COMM=sshd -o short-iso
# Everything from one boot session; or one unit (e.g., the SSH daemon)
$ journalctl -D ./var/log/journal/3f8c... --list-boots
$ journalctl -D ./var/log/journal/3f8c... -u ssh.service -o verbose
The journal records process executions, service starts/stops, authentication (it ingests the same PAM events as auth.log), and kernel messages, all with structured metadata and microsecond timestamps. Optional Forward Secure Sealing (FSS) can make the journal tamper-evident; even without it, the binary structure resists the casual sed edits that defeat text logs.
Why This Matters. Linux gives you redundant records of the same event on purpose. The engineer's SSH session shows up in
auth.log, inwtmp(vialast), and in the systemd journal — three independent sources. When they agree, your finding is bulletproof. When one is missing or edited and the others are not, the disagreement is your evidence of tampering. Never rely on a single Linux log when three are watching.
ext4 under the hood: inodes, extents, and the journal
Now the hard part, and the place where Linux forensics diverges most sharply from Windows: deleted-file recovery on ext4. We established in Chapter 4 that on NTFS, a deleted file's MFT record typically retains its data-run pointers — the file system remembers which clusters held the file, which is why NTFS undelete is often easy. ext4 is, frustratingly, the opposite. Understanding why is the difference between a competent recovery and a false "it's gone."
The inode and the extent tree
In ext4, a file's metadata lives in an inode — a fixed-size structure (typically 256 bytes) in the inode table holding the mode/permissions, owner UID/GID, size, link count, four timestamps, and the map to the file's data blocks. ext4 keeps four timestamps where older Unix kept three: atime (last access), mtime (last content modification), ctime (last inode/metadata change), and — new in ext4 — crtime (creation/birth time), a genuine forensic bonus that ext3 lacked. There is also dtime (deletion time), set when the inode is freed.
Unlike ext2/ext3, which mapped data with direct/indirect block pointers, ext4 uses extents — a far more efficient scheme that describes contiguous block ranges. The inode's 60-byte block area holds an extent tree, beginning with a 12-byte header whose magic is 0xF30A (stored little-endian as 0A F3):
ext4 inode i_block[ ] region — an inline extent tree (no separate index blocks)
Offset 00 01 02 03 04 05 06 07 08 09 0A 0B
i_block 0A F3 01 00 04 00 00 00 00 00 00 00 <- extent HEADER
│ │ │ │
│ │ │ └─ eh_depth = 0 (extents are inline, here in the inode)
│ │ └─────── eh_max = 4 (room for 4 entries)
│ └───────────── eh_entries = 1
└─────────────────── eh_magic = 0xF30A
00 00 00 00 00 0A 00 00 00 88 00 00 <- one EXTENT entry
decoded: logical blocks 0–2559 → physical block 34816, length 2560
(2560 blocks × 4096 bytes = 10,485,760 bytes = the 10 MB tarball)
ext4 INODE (256 bytes) EXTENT → DATA BLOCKS
┌──────────────────────────────┐
│ mode / uid / gid │
│ size = 10,485,760 │
│ atime mtime ctime crtime │
│ dtime (0 = live) │ ┌──────────────────────┐
│ i_blocks_lo │ extent │ phys 34816 .. 37375 │
│ ┌──────────────────────────┐ │ ───────▶ │ (2560 contiguous 4 KiB │
│ │ extent header 0xF30A │ │ │ blocks of file data) │
│ │ extent: 0–2559 → 34816 │ │ └──────────────────────┘
│ └──────────────────────────┘ │
└──────────────────────────────┘
Why ext4 deletion is harder than NTFS
Here is the painful divergence. When you delete a file on ext4, the file system does several things at once, and the combination is hostile to recovery: it marks the inode free in the inode bitmap, marks the data blocks free in the block bitmap, sets dtime, decrements the link count to zero — and, critically, it invalidates the extent tree, zeroing the block map in the inode. Watch the same inode before and after, via debugfs:
debugfs: stat <524291> (BEFORE deletion — live file)
Inode: 524291 Type: regular Mode: 0644 Flags: 0x80000 [EXTENTS_FL]
User: 1001 Group: 1001 Size: 10485760
ctime: 0x678c7a3f -- Wed Jan 15 14:35:11 2025
crtime: 0x678c7a3f -- Wed Jan 15 14:35:11 2025
EXTENTS:
(0-2559): 34816-37375
debugfs: stat <524291> (AFTER rm — deleted)
Inode: 524291 Type: 0 Mode: 0000
User: 0 Group: 0 Size: 0
dtime: 0x678c7d04 -- Wed Jan 15 14:51:48 2025
BLOCKS:
(none) <- the block map is GONE. Which blocks held the file?
That (none) is the whole problem. On NTFS you would still have the data runs. On ext4 the inode no longer knows where its data was. The data blocks 34816–37375 probably still hold the tarball's bytes (theme #1: deleted is not destroyed) — but the map that would let you find them has been erased from the live inode. This is why ext4 recovery has a reputation for difficulty that NTFS recovery does not deserve.
There is one consolation in the directory itself. When a file is deleted, ext4 does not immediately scrub the directory entry; instead it extends the previous entry's rec_len to "jump over" the deleted one, leaving the deleted name and its inode number sitting in the directory block's slack. So you can often recover the filename and its inode number from directory slack even when the inode's block map is gone — telling you what was deleted and which inode to chase, which sets up the real rescue.
The journal: a second chance at the block map
ext4's saving grace is its journal (managed by jbd2), the same crash-recovery feature that makes the file system robust. The journal is a circular log — by default stored in reserved inode 8 — that records metadata changes before they are committed to their final location, so an interrupted write can be replayed or rolled back. Its blocks begin with the big-endian magic 0xC03B3998. The forensic payoff: the journal frequently contains older copies of the inode table and directory blocks — including copies of inode 524291 from before it was deleted, when the extent tree was still intact. Replay or scan the journal and you can recover the very block map the live inode discarded.
This is the engine behind the ext4 recovery tools:
# 1) Identify recently deleted inodes (those with dtime set)
$ debugfs -R 'lsdel' /dev/sdb2
Inode Owner Mode Size Blocks Time deleted
524291 1001 100644 10485760 2561/2561 Wed Jan 15 14:51:48 2025
# 2) Recover via the JOURNAL (best for ext4 extents). ext4magic mines old inode copies:
$ ext4magic /dev/sdb2 -f srv/repo/falcon-x -j /dev/sdb2 \
-a "2025-01-15 14:00:00" -b "2025-01-15 15:00:00" -r -d /recovery
# 3) extundelete also uses the journal, though it predates extents and is less reliable on them:
$ extundelete /dev/sdb2 --restore-inode 524291
# 4) When metadata is truly gone, fall back to signature carving (no filenames):
$ photorec /dev/sdb2 # or scalpel/foremost — see Chapter 7
ext4magic is usually the strongest first move on ext4 because it leans on the journal to reconstruct extents; extundelete was written in the indirect-block era and struggles with extents; photorec and the carvers from Chapter 7 are the metadata-free fallback that recovers content but not names, paths, or timestamps. The Sleuth Kit's fls/istat/icat work across ext4 too and are your offset-precise, scriptable option for extracting a known inode from an image.
Recovery vs. Forensics. Same journal, two questions. A 💾 recovery engineer mines the ext4 journal to get the file back — replay the old inode, follow its extents to blocks 34816–37375, write out the 10 MB tarball, hand it over, done. A 🔍 forensic examiner asks the journal a different question: not only "what were the bytes?" but "when did this exist, and when was it deleted?" The journal's transaction sequence, the live inode's
dtimeof 14:51:48, and the directory-slack filename together prove the filerepo.tgzexisted, held 10 MB, and was deleted at a specific minute — a timeline fact, not just a recovered blob. Recovery wants the file; forensics wants the history of the file. The journal serves both.Limitation. The ext4 journal is circular and small — typically 128 MB regardless of disk size — so it overwrites itself constantly. On a busy server, the inode copy you need may have been recycled within minutes of the deletion. ext4-journal recovery is therefore a fresh-deletion technique: brilliant for "I rm'd it an hour ago," far weaker for "it was deleted last month." And on an ext4 volume mounted with
data=journalversus the defaultdata=ordered, what the journal retains differs. Know what the journal can and cannot still be holding before you promise a client a clean recovery.
SQLite: the common artifact format
Step back and notice a pattern that has run through this entire chapter. macOS knowledgeC.db — SQLite. QuarantineEventsV2 — SQLite. TCC.db — SQLite. Browser history on both platforms (Chapter 18) — SQLite. Messages, Mail indexes, Photos libraries, countless app caches, and most of iOS and Android — SQLite. It is the single most important artifact format in modern forensics, and learning to read it (and read what was deleted from it) pays off on every platform.
A SQLite database is a single file beginning with a fixed 16-byte magic — the ASCII string SQLite format 3 followed by a null byte:
Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII
0x00000000 53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00 SQLite format 3.
└────────────── 16-byte magic ──────────────┘
0x00000010 10 00 01 01 00 40 20 20 ... page size = 0x1000
The two forensic facts that make SQLite a deleted-data treasure chest:
- Deleted rows persist. When SQLite deletes a row, it does not wipe the bytes; it marks the cell as a "freeblock" within its page and adds the space to a free list. The old row's content — the deleted Quarantine event, the cleared browser entry, the removed message — typically remains readable in the page's unallocated area until that space is reused or the database is
VACUUM'd. Whole freed pages join a freelist and likewise retain their old contents. - The WAL holds recent history. Many SQLite databases run in Write-Ahead Logging mode, producing sidecar files
<db>-waland<db>-shm. The-walfile holds page versions not yet checkpointed back into the main database — which means it can contain both the newest changes and superseded older versions of pages. Always collect the-waland-shmalongside the main.db; analyzing the.dbalone can miss the most recent — and the most recently deleted — activity.
A live query gets you the current rows; recovering deleted rows means parsing the raw pages, freeblocks, freelist, and WAL. The standard sqlite3 client reads live data; tools like undark, walitean, and forensic SQLite parsers (and the script in Appendix B) recover the deleted remnants:
#!/usr/bin/env python3
"""Confirm a file is SQLite, report page size, and dump app-focus rows from knowledgeC.
Illustrative — recovering *deleted* rows requires raw-page parsing (see Appendix B)."""
import sqlite3, struct
MAC_EPOCH = 978307200 # 2001-01-01 UTC, added to Mac absolute time -> Unix time
def is_sqlite(path):
with open(path, "rb") as f:
header = f.read(16)
page_size = struct.unpack(">H", f.read(2))[0] # bytes 16-17, big-endian
return header == b"SQLite format 3\x00", page_size
ok, page_size = is_sqlite("knowledgeC.db")
print(f"valid SQLite={ok} page_size={page_size}") # -> valid SQLite=True page_size=4096
con = sqlite3.connect("file:knowledgeC.db?mode=ro", uri=True) # read-only: never alter evidence
for start, end, app in con.execute("""
SELECT ZSTARTDATE + ?, ZENDDATE + ?, ZVALUESTRING
FROM ZOBJECT WHERE ZSTREAMNAME = '/app/inFocus' ORDER BY ZSTARTDATE
""", (MAC_EPOCH, MAC_EPOCH)):
print(time_fmt(start), "→", time_fmt(end), app) # time_fmt: see Appendix B
con.close()
Recovery vs. Forensics. Same database, two recoveries. When a user "clears history" or an app "deletes" a record, the row usually lingers as a freeblock. The 💾 angle: a client who accidentally deleted contacts from an app may have them recoverable straight out of the SQLite freelist. The 🔍 angle: a suspect who cleared their download history left those rows in the freelist and the
-wal, and you can recover the deleted QuarantineEventsV2 or browser rows and prove what they tried to erase — deleted is not destroyed, applied to a database instead of a disk. Same freeblocks, two purposes; both demand you work read-only on a hashed copy.
Tool demonstration: from image to artifacts
Pull the workflow together on the engineer's MacBook image. You have already imaged and hash-verified it (Chapter 14); now you parse it. The fastest route to broad coverage is mac_apt, which ingests the image and runs its plugin suite in one pass:
# Broad macOS sweep: APFS, Spotlight, FSEvents, Unified Log, knowledgeC, Quarantine, plists...
$ python mac_apt.py -o ./MACOUT -x SQLITE,XLSX DD macbook_evidence.dd
[+] Mounting APFS container ... 4 volumes, 6 snapshots detected
[+] Plugin SPOTLIGHT ... 41,233 metadata records -> Spotlight.db
[+] Plugin FSEVENTS ... 188,402 events -> FSEvents.db
[+] Plugin UNIFIEDLOGS ... parsing diagnostics + uuidtext ... 2.1M entries
[+] Plugin KNOWLEDGEC ... 9,514 ZOBJECT rows -> KnowledgeC.db
[+] Plugin QUARANTINE ... 37 download events -> Quarantine.db
[+] Plugin BASHSESSIONS ... zsh history for 2 users
[+] Done. Consolidated output: ./MACOUT/mac_apt.db (SQLite)
Everything lands in a single SQLite database you can then query and cross-reference — which is why mastering SQLite earlier in the chapter pays off here. For the file-system layer and snapshot work you drop to APFS-Fuse and the Sleuth Kit, and on a Windows analysis workstation you keep a defensible inventory of every artifact you export. PowerShell is excellent for that custody-friendly bookkeeping:
# On the Windows examiner workstation: hash and inventory every exported macOS/Linux artifact
# so each object entering analysis has a recorded SHA-256 (chain-of-custody hygiene).
Get-ChildItem -Recurse -File 'E:\Case2025-014\extracted_artifacts' |
ForEach-Object {
[pscustomobject]@{
Artifact = $_.FullName.Replace('E:\Case2025-014\extracted_artifacts\','')
SizeBytes = $_.Length
SHA256 = (Get-FileHash -Algorithm SHA256 $_.FullName).Hash
Collected = (Get-Date).ToString('s')
}
} | Export-Csv 'E:\Case2025-014\artifact_manifest.csv' -NoTypeInformation
Tool Tip. Resist the urge to let one big tool be your whole methodology.
mac_aptis superb for breadth and triage, but on the stand "the tool found it" is not an answer — you must be able to say how. Validate the tool's key findings against the primary artifact by hand: ifmac_aptreports a Quarantine download, openQuarantineEventsV2yourself in a read-only SQLite client and confirm the row. The polished commercial suites — Cellebrite Inspector (formerly BlackLight), Magnet AXIOM, FTK, X-Ways — automate macOS and Linux parsing well and are surveyed in Chapter 36, but the open tools (mac_apt, APFS-Fuse, the Sleuth Kit,plutil,log,journalctl,ext4magic) teach you the structures the GUI hides, and they are free. The command reference is Appendix H.
When you need everything on one timeline across both machines, you graduate to plaso / log2timeline, the supertimeline engine that parses APFS metadata, Unified Logs, FSEvents, Spotlight, plists, SQLite artifacts, Linux text logs, the systemd journal, and wtmp/btmp — from a single image, into a single normalized timeline. That is the subject of Chapter 21; here, simply know it exists and that the artifacts in this chapter are its raw material.
Worked example: the engineer's Mac and the build server
Assemble the pieces into the case. The departing engineer, "J. Chen," returned a company MacBook Pro and held an account (deploy) on the Linux build server buildsrv. Both were imaged and hash-verified. The question: did Chen exfiltrate the falcon-x project, and can you prove it to a standard that survives cross-examination?
On the MacBook (APFS). Acquisition revealed six snapshots. The com.apple.TimeMachine.2025-01-15-140251.local snapshot, mounted read-only via APFS-Fuse, contained Users/jchen/Projects/falcon-x/ and Users/jchen/Downloads/diskmaker.dmg — both absent from the live volume, confirming deletion between 14:02 and acquisition. The artifact chain, each from the verified image:
EVENT TIMELINE — MacBook Pro (Case 2025-014, all times America/New_York)
14:18:02 knowledgeC /app/inFocus com.apple.dt.Xcode (working in the project)
14:42:31 Quarantine + Spotkit diskmaker.dmg downloaded via Safari from
https://diskmaker.example.com/dl/diskmaker-4.2.dmg
(kMDItemWhereFroms corroborates the same URL)
14:47:42 knowledgeC /app/inFocus com.example.DiskMaker (ran the imaging tool)
14:49:10 Unified Log (IOUSBHost) USB mass-storage "FIELDKIT" mounted at /Volumes/FIELDKIT
14:49:55 FSEvents (event 0xa3f3b9) Volumes/FIELDKIT/falcon-x.zip Created;IsFile
14:50:33 FSEvents (event 0xa3f4d0) Users/jchen/Projects/falcon-x Removed;IsDir
14:51:20 Unified Log (IOUSBHost) "FIELDKIT" ejected
~14:52 Snapshot 140251.local still holds falcon-x/ as of 14:02 (proves prior existence)
Six independent macOS artifacts — knowledgeC, Quarantine, Spotlight, the Unified Log, FSEvents, and the APFS snapshot — interlock into one narrative: download a disk-imaging tool, run it, attach a USB volume, copy a zip of the proprietary project to it, delete the original. No single artifact carries the case; their agreement does.
On the build server (ext4 + Linux logs). The MacBook's 10.0.4.18 address (from its network plist and the Unified Log) matched the source IP in buildsrv's auth.log:
EVENT TIMELINE — buildsrv (Linux, ext4)
14:32:07 auth.log + wtmp + journal SSH publickey login: user 'deploy' from 10.0.4.18
14:35:11 auth.log (sudo) deploy: COMMAND=/bin/tar -czf /tmp/repo.tgz . (10 MB)
14:38:44 .bash_history scp /tmp/repo.tgz jchen@10.0.4.18:/Users/jchen/Downloads/
14:51:48 ext4 inode 524291 dtime /tmp/repo.tgz deleted (rm)
14:51:55 .bash_history history -c ; unset HISTFILE (self-incriminating cleanup)
14:51:48 wtmp / journal session closed for 'deploy'
[recovery] ext4magic via jbd2 journal -> restored inode 524291's extents (34816-37375)
-> recovered repo.tgz, 10,485,760 bytes, SHA-256 matches the zip on FIELDKIT
The kill shot is that last line. The repo.tgz recovered from the build server's ext4 journal hashes identically to the falcon-x.zip that FSEvents recorded landing on the FIELDKIT USB volume — cryptographic proof that the file Chen scp'd from the server is the same file copied to the removable drive. The attempt to cover tracks (rm, history -c, unset HISTFILE) is itself in the recovered history and contradicted by wtmp and the journal. The anti-forensics did not beat a thorough examiner; it added evidence. That is anchor case #2's lesson, proved on a Mac and a Linux box instead of a Windows desktop — and the timeline assembly is exactly what Chapter 21 formalizes.
War Story. I once worked a Linux intrusion where the attacker was meticulous:
auth.logandsyslogwere surgically edited to remove their session,.bash_historywas empty, and the timestamps on the web shell were stomped. They made one mistake. They never touched/var/log/wtmp, because it is binary andcat-ing it returns garbage, so it does not look like a log to someone in a hurry.last -f wtmpprinted their session — username, source IP, login and logout to the second — in a tidy line that the carefully scrubbed text logs had been edited to deny. The discrepancy between the clean text logs and the untouched binary ledger was not just a recovery of the session; it was affirmative proof of tampering, which is often worth more than the original entry. Check every log, especially the ones that do not look like logs.Ethics Note. A personal-feeling device cuts both ways, and macOS makes this acute. A MacBook's knowledgeC, Spotlight, Photos library, Messages, and browser history expose an extraordinarily intimate portrait of a person's life — health, relationships, location, beliefs — almost all of it irrelevant to whether they took
falcon-x. Your authorization (the engagement letter, consent, or warrant — Chapter 25) defines your scope, and scope discipline is an ethical obligation, not a suggestion. Run targeted queries for the project files, the USB activity, and the relevant window; do not go scrolling through someone's private photographs or text threads because the tool put them in front of you. And on these systems above all — where you will sometimes encounter material you did not seek, including the category that triggers the mandatory-reporting duty of Chapter 28 — know your obligations before, not after, you find it.
Common mistakes
- Treating macOS like Windows. There is no registry on a Mac; the equivalents are plists and SQLite databases scattered across
~/Library/. Looking for "the macOS registry" wastes time and signals inexperience. Learn the macOS artifact map (Appendix D) on its own terms. - Ignoring APFS snapshots. The single biggest macOS rookie error. A "deleted" file is frequently sitting whole inside an hourly Time Machine local snapshot. Always enumerate snapshots (
tmutil listlocalsnapshots,diskutil apfs listSnapshots) before you carve — why reconstruct from fragments what a snapshot holds intact? - Collecting Unified Logs without
uuidtext. Grabbing/var/db/diagnostics/but not/var/db/uuidtext/leaves you with.tracev3entries you cannot fully render, because the format strings live inuuidtext. Collect both, or uselog collectto produce a complete.logarchive. - Forgetting Mac absolute time. knowledgeC, QuarantineEventsV2, and many Apple SQLite timestamps count seconds from 2001-01-01, not 1970. Forget the
+978307200correction and your timeline is off by 31 years — an error opposing counsel will gleefully exhibit. (Some columns use Unix time, some use Mac absolute time; verify per column.) - Expecting ext4 to undelete like NTFS. On ext4 the live inode's block map is zeroed at deletion, so the NTFS reflex ("read the data runs from the deleted record") fails. Reach for the journal (
ext4magic), recover the filename from directory slack, and fall back to carving. Promising easy ext4 undelete is how you over-promise a client. - Editing or
cat-ing binary logs.wtmp,btmp, the systemd journal, and APFS/SQLite stores are binary; opening them in a text editor or piping through naive tools can mangle them. Uselast/lastb,journalctl --file,plutil, and read-only SQLite. And always work from a copy — opening a SQLite DB read-write silently checkpoints the WAL and alters your evidence. - Trusting plain-text Linux logs at face value in an intrusion. Text logs under
/var/log/are trivially editable by root. Corroborate them against the binarywtmp/journal, and treat gaps and inconsistencies as evidence in their own right (theme #3). - Skipping the WAL. Analyzing a
.dbwithout its-wal/-shmsiblings can miss the most recent — and most recently deleted — records. Collect and analyze them together.
Limitations: knowing when to stop
Theme #5 lands hard on these platforms, and honesty about it is part of competence.
Encryption is the wall. Modern Macs ship with FileVault (APFS native encryption), and Apple Silicon and T2 Macs encrypt the internal SSD by hardware default even before FileVault is "on." Without the user's password, the institutional recovery key, or lawful compelled access, an encrypted APFS volume is a wall this book teaches you to recognize, not pretend to breach (Chapter 29). On Linux, LUKS full-disk encryption is the equivalent. With the key, APFS-Fuse and cryptsetup open the volume; without it, you have noise.
Legal Note. Encryption turns a technical wall into a legal question. When the data is locked and the key lives only in a suspect's head, the issue is no longer "can I break it?" but "can the suspect be compelled to provide it?" — and in U.S. courts that drags in the Fifth Amendment privilege against self-incrimination, with a genuinely unsettled split over whether compelling a passcode is "testimonial" (often protected) versus compelling a fingerprint or face (frequently not). Accessing a Keychain, a FileVault volume, or a LUKS disk also requires that your authority covers it: a consent form, an engagement letter, or a warrant defines what you may unlock, and a build server physically hosted in another country can pull in cross-border regimes (the CLOUD Act, MLAT) before you touch it. Resolve the legal authority before the technical attempt — the full framework is Chapter 25 and Appendix E.
Acquisition itself is harder on Apple. Apple Silicon and T2 Macs cannot simply have their soldered SSD removed and write-blocked the old way; the storage is encrypted to the Secure Enclave and bound to the board. Imaging often requires booting to recovery and using a target-disk-style or live approach with the user's credentials — which means the comfortable "pull the drive, write-block, image" workflow of Chapter 14 may be impossible, and you document a different, defensible method instead.
Macs are all SSD, so TRIM applies. Every modern Mac uses flash with TRIM enabled, so the SSD recoverability limits of Chapter 9 are in full force: a file deleted from the live volume and not preserved in any snapshot may be physically gone in seconds. The snapshot is often your only shot at deleted Mac data — another reason to check them first.
Logs age out. The Unified Log retains days to a few weeks, not months. The ext4 journal is ~128 MB and circular, holding only recent transactions. auth.log rotates and the oldest generations are deleted. KnowledgeC prunes older rows. None of these is a permanent archive; if the relevant window predates what the artifact still holds, the honest finding is "the record no longer exists," not a guess.
Time zones and clock trust. Mixed UTC and local timestamps, a misconfigured system clock, NTP drift, and daylight-saving boundaries all corrupt timelines. Record the system time zone (from the relevant plist on macOS, /etc/timezone or /etc/localtime on Linux), note it in your report, and standardize on UTC internally. A timeline built on an unstated or wrong time zone is worse than none, because it looks authoritative while being wrong.
Limitation. The defining hard case of this chapter: a current Apple Silicon MacBook with FileVault on, no password, no recovery key, no backups, and no snapshots that predate the relevant deletion. You can prove the device exists and is encrypted; you can prove essentially nothing about its contents. "The data is inaccessible and the evidence insufficient to reach a conclusion" is the correct, professional finding — and saying it plainly is the mark of an examiner who can be trusted with the cases that do break.
Progressive project: adding macOS and Linux artifacts to the case file
Your Forensic Case File so far holds an acquisition record, hashes, recovered files, and Windows artifacts. Now extend it across platforms. If your case image includes a macOS or Linux system (most realistic multi-device cases do), complete the following; if it is Windows-only, perform these steps against a practice macOS image and a practice Linux image from Appendix J so the skills are in hand for the capstone.
- macOS file system. Enumerate APFS snapshots (
diskutil apfs listSnapshots/tmutil listlocalsnapshots, ormac_apt/APFS-Fuse on the image). Record each snapshot's name and transaction id in your case file, mount the most relevant one read-only, and note any files present in the snapshot that are absent from the live volume. - macOS provenance and activity. Extract and query, read-only:
QuarantineEventsV2(download URLs/agents/times),knowledgeC.db(app-focus timeline — remember+978307200), SpotlightkMDItemWhereFromsfor key files, and the relevantcom.apple.quarantinexattrs. Add the rendered Unified Log slice for your time window (collectdiagnosticsanduuidtext). - Linux logs and logins. Carve out
auth.log/secure(plus rotated/.gzgenerations), decodewtmp/btmpwithlast/lastb, pull the relevant.bash_history/.zsh_history, and read the systemd journal offline withjournalctl --file. Note any gaps or inter-log disagreements as findings. - ext4 recovery. For one deleted file of interest, run
debugfs -R 'lsdel', recover via the journal withext4magic, recover the filename from directory slack, and hash the recovered file. If it matches a copy found elsewhere (e.g., on a USB volume), document the match — that is your strongest correlation. - Normalize and correlate. Convert every timestamp to UTC, label its source artifact, and add the events to your growing master timeline. Write two or three sentences stating what the cross-platform, cross-artifact correlation establishes — and, in the spirit of theme #5, exactly where the evidence stops.
Everything you add here flows into the supertimeline of Chapter 21 and, ultimately, the capstone report of Chapter 38. Keep each artifact's hash and acquisition note with it.
Summary
macOS and Linux are not exotic detours from "real" (Windows) forensics; they are half of the modern caseload, and they reward the examiner who learns their artifacts on their own terms. macOS is built on APFS, a copy-on-write file system whose snapshots — created constantly by Time Machine — preserve entire prior states of the volume, so a "deleted" file is often sitting intact in a snapshot from an hour ago. Around the file system, macOS layers an unusually rich set of activity stores: Spotlight metadata (including kMDItemWhereFroms download provenance), FSEvents (a file-system change log keyed by monotonic event IDs), the Unified Log (binary .tracev3 plus uuidtext strings), knowledgeC/biome (an app-usage and device-state timeline in Mac absolute time), Gatekeeper/Quarantine (com.apple.quarantine xattrs and the QuarantineEventsV2 database), plists (binary bplist00 or XML), and the Keychain and Time Machine. Linux keeps its history in plain-text /var/log/ files (auth.log/secure, syslog/messages), binary login ledgers (wtmp/btmp/utmp, read with last/lastb), shell history (.bash_history/.zsh_history — where even the cleanup commands incriminate), cron/systemd timers, and the structured systemd journal (LPKSHHRH, readable offline with journalctl --file). ext4 recovery is genuinely harder than NTFS because deletion zeroes the inode's extent map — but the jbd2 journal often holds an older copy of the inode with its extents intact (recovered via ext4magic/debugfs), the directory slack preserves the filename, and carving is the fallback. Binding both platforms together is SQLite, whose deleted rows linger in freeblocks and freelist pages and whose -wal sidecar holds recent and superseded history — deleted is not destroyed, applied to a database. The open toolkit — mac_apt, APFS-Fuse, plutil, the log command, journalctl, ext4magic, and log2timeline/plaso — turns these structures into evidence, and the departing-engineer case proved on a Mac and a Linux server what it proved on Windows: anti-forensics rarely beats a thorough, cross-platform examiner. And the limits are real and worth stating plainly — FileVault/LUKS encryption, Apple Silicon acquisition hurdles, all-SSD TRIM, short log retention, and time-zone traps can make data genuinely unrecoverable or a conclusion genuinely unreachable.
You can now: - Identify APFS containers, volumes, and superblocks (
NXSB/APSB), enumerate and mount APFS snapshots read-only, and explain why copy-on-write makes them forensic gold for both recovery and proof of a prior state. - Extract and interpret the core macOS activity artifacts — Spotlight (kMDItemWhereFroms), FSEvents, the Unified Log (withuuidtext), knowledgeC (converting Mac absolute time with+978307200), Quarantine/Gatekeeper, plists, Keychain, and Time Machine. - Work the Linux artifact set —auth.log/secure,syslog/messages,wtmp/btmp/utmpvialast/lastb, shell history, cron/timers, and the systemd journal read offline withjournalctl --file. - Explain why ext4 deletion is harder than NTFS, and recover deleted files using the jbd2 journal (ext4magic/debugfs), directory-slack filenames, and carving as a fallback — and hash the result. - Recognize SQLite as the cross-platform artifact format, collect its-wal/-shmsiblings, and explain how deleted rows persist in freeblocks and the freelist. - State the honest limits — FileVault/LUKS, Apple Silicon acquisition, TRIM, log retention, and time-zone discipline — and render a defensible "insufficient evidence" finding when they apply.
What's next. Chapter 18 — Browser and Internet Forensics — the SQLite skills you just built pay off immediately: browser history, cache, cookies, downloads, and session data live in SQLite databases (places.sqlite, History, Web Data) on every one of these platforms, and they are where intent and online activity are written down. We follow the user from the file system onto the open web.
Practice in exercises.md, test yourself with the quiz, apply it in the case studies (one · two), review the key takeaways, and go deeper with further reading.