In this section

The macOS Forensic Toolstack: mac_apt, APOLLO, ccl-segb, and the Native Commands

Module 0

Introduction

A macOS collection is several different kinds of thing wearing one directory structure, and the format of each store decides which tools can read it at all.

That makes tool selection a reading problem rather than a matter of preference. Some stores have several parsers and the choice is genuinely yours; some have one; and a few have one that nobody has updated since the last release changed the format underneath it.

This section is about matching the tool to the format, and about the specific point where a broad pass stops being evidence and becomes a map.

Scenario

A breadth pass over the collection from NE-VANCE-MBP completes in an afternoon and produces a database of parsed artifacts. The report is written from that database. Nobody opens a raw store, and nobody records which release of the tool produced which table.

01

The format decides the tool

Which is the first move on every artifact

Start with what a collection actually contains. Property lists hold configuration and persistence in a binary encoding you convert before reading. Databases back the behavioral stores, the browsers and the privacy grants, and you query them directly. Unified logging writes a compressed trace that Apple's own tooling reads cleanly and little else does. The newer activity streams use a record format that a single parser handles.

FOUR FORMATS IN ONE COLLECTION

  property list   configuration, persistence      convert, then read
  database        behavior, browsers, privacy     query it directly
  trace store     the unified log                 Apple tooling, or one parser
  record stream   the newer activity streams      a single parser reads it

  the format is the thing you read off the file first

Four formats and four different doors. Reading the format off a file before choosing a tool is the move that prevents most of the wasted effort on this platform, and it takes seconds compared with the hour spent discovering that a suite cannot open something.

The analysis stack here is almost entirely open source, and that is deliberate rather than a budget decision. You can read the code, so when a parser claims an artifact says something you can confirm the claim against the source rather than trusting a result you cannot inspect. In a discipline where the output ends up in a report somebody else will challenge, being able to say why a tool produced a value is worth more than convenience.

There is a practical corollary about mixed stacks. Commercial suites still earn their place in acquisition, where the engineering is genuinely hard and the licensing buys tested code against hardware you cannot replicate. The reading, though, is where inspectability pays, and there is no contradiction in acquiring with one and analyzing with the other. What matters is knowing which half of the work you are in when a result surprises you.

Reading the format off a file is also cheaper than it sounds. The first bytes of most of these stores identify them, the extension is usually honest, and where neither settles it the platform's own conversion tool will either open the file or refuse it in a way that tells you what it is not. Thirty seconds spent there routinely saves an hour spent asking why a suite produced an empty table. The habit generalizes: on a platform that changes stores annually, identifying what you are holding is the one step that never goes out of date.

02

Start with breadth

And know what it costs

The first pass over a collection is breadth, and the workhorse is a plugin framework rather than a suite. It takes a disk image, a mounted volume or a targeted artifact set, runs modular parsers across all of it, and writes one table per plugin into a single database. You point it at the collection and let it map the landscape before committing to a theory about what happened.

Reading across the resulting tables is how you find the store your question points at without opening every file by hand. Each plugin name is a category of evidence rather than a tool feature: one reads download provenance, one parses the trace store, one turns the filesystem changelog into rows, one recovers shell history for every user on the machine.

Decide

What an empty table supports

The breadth pass over the collection completes without errors. Every plugin reports finished. The table for the newer activity stream contains no rows at all, and the machine is running the current release. Nobody recorded which version of the tool was run.

What does the empty table establish?

The decision above is the one the scenario's report got wrong. Commit to an answer before reading the reasoning underneath it, because an examiner who commits first learns considerably more from the explanation than one who scrolls straight past it.

The framework runs against more than a mounted volume, which is worth knowing before you convert anything. It accepts disk images in the common formats, targeted artifact sets, and collections produced by other triage tools, so a set of files somebody else pulled from a live machine can go through the same pass as a full image. That removes an argument about whether the collection was the right shape before analysis can begin.

It also means the breadth pass is available on evidence you did not collect yourself, which is the common case in a handover. Somebody else's triage output can be mapped the same way, and the resulting tables will tell you what that collection actually contained rather than what its operator believed they had taken. That gap between what was collected and what somebody thinks was collected is common enough in handovers to be worth checking rather than assuming.

03

What the breadth pass costs

In time and in disk

Run carelessly, that first pass takes an afternoon and produces something unwieldy, and two facts about it save most of that time. The default output is a database, while the optional delimited exports are encoded in a way that defeats naive command-line parsing and opens cleanly in a spreadsheet instead. Knowing which you want before the run decides how you will read the result.

# The whole map, when you do not yet know the question
% python mac_apt.py MOUNTED /Volumes/NE-VANCE-MBP /cases/vance/macapt ALL

# Named plugins, when you do: minutes instead of an afternoon
% python mac_apt.py MOUNTED /Volumes/NE-VANCE-MBP /cases/vance/macapt \
    QUARANTINE FSEVENTS TERMSESSIONS

The second form is the one to reach for once a question exists. The unified-log plugin in particular is the heavy one in the set: a forty-gigabyte image can produce a log database over ten gigabytes on its own, which is a meaningful share of an afternoon and of a case drive.

There is a sequencing argument buried in that. The full pass is right when you do not yet know what you are looking for, because its value is the map. Once a question exists, named plugins finish in minutes and produce something small enough to read, and running the full set again out of habit costs hours for tables nobody will open.

The output format decision deserves the same thought. A database is queryable, joinable and awkward to skim; the delimited export is readable in a spreadsheet and awkward to query. Investigations that end in a timeline usually want the first, and a question answered by sorting one table usually wants the second. Choosing after the run means converting, which is a step that exists only because nobody chose before it.

One detail about the delimited exports catches people once. The encoding they use defeats naive command-line parsing, so a pipeline built around them will produce nonsense rather than an error, and the file will open perfectly in a spreadsheet at the same time. An examiner who sees garbage from a script and clean columns in an application is looking at an encoding mismatch rather than a corrupted export. Knowing that saves the re-run, which on a large image is most of an afternoon.

04

Where the pass goes stale

Which is the scenario's error

All of which assumes the tool read everything it was pointed at, and on this platform that assumption has a shelf life. A broad processor is fast precisely because it makes assumptions about formats, the platform reorganizes its stores every year, and the tool predates the newest of them. The pass is a map rather than the territory.

WHAT THE PASS HANDS YOU, AND WHAT IT KEEPS BACK THE TRIAGE PASS one command, many stores GIVES YOU where the evidence lives a parsed view of the stores it knows rows you can read quickly a complete-looking output DOES NOT a finding any view of a store whose format changed this release the raw record behind a row any signal that a store was silently absent The pass tells you where to look. It does not get to tell you what you found.

Four things the pass gives you, and the four it does not.

Read the dashed arrows as the half of the picture the output never mentions, which is what makes a complete-looking result the one to distrust.

A triage pass output, read as evidence

Complete-looking

Stores parsed

the ones it knows

Stores skipped

not reported

Rows

readable, summarized

Raw records

not included

How to read it without over-reading it

  • As a map of where the evidence for your question lives, it is fast and it is the right first move.
  • A store whose format changed in this year's release can be absent from the output with no error and no note.
  • When the case turns on a single row, the raw record behind it is what you open, and the pass does not carry it.

The output that should worry you is the one that looks complete, because completeness is the one property this artifact cannot demonstrate about itself.

Read the rows across rather than down, because both columns describe the same run. The fourth row is the one that catches people: a parser that cannot read a store does not announce the gap, so an output that looks complete is not the same as a run that read everything.

So the rule is narrow and worth memorizing. When the output looks wrong, when the case turns on a single record, or when the machine runs a release the tool predates, open the raw store and confirm it yourself. The pass tells you where to look, and it does not get to tell you what you found.

The same rule has a second edge worth stating. A parsed row that does answer your question is still a claim made by software about a file, and where the case turns on that single row the raw store is where the claim gets confirmed. That is not distrust of the tool; it is the difference between reporting what a parser output and reporting what the evidence says, and only one of those survives somebody else running a different parser.

In practice the confirmation is quick where it matters. Open the store the row came from, find the record, and satisfy yourself that the parsed value is what the record says. For a single decisive row that is minutes, and it converts a finding that rests on tooling into one that rests on evidence.

05

Opening a store yourself

And the one it is worth doing on

Opening a store yourself is a smaller step than it sounds, and the privacy database is the one worth learning it on. It answers a question the Windows platform never asks: which applications hold access to the camera, the microphone, the whole filesystem, or the interface that can read keystrokes from every other application.

# Read the privacy-grant store straight off the mounted image
% cd "/Volumes/NE-VANCE-MBP/Library/Application Support/com.apple.TCC"
% sqlite3 TCC.db \
  "SELECT service, client, auth_value,
   datetime(last_modified,'unixepoch') FROM access"

Read the result like an analyst rather than a list. A grant value meaning enforced tells you the permission was live, and the client column tells you whether the holder is named by bundle identifier or by an absolute path to an executable, which is itself a signal worth noticing.

The modification timestamp is the forensic value. It records when each grant changed, which drops straight onto a timeline, and a grant written minutes before something ran is a different fact from one written a year earlier. There is a system database and one per user, so establishing which you are reading is part of stating the finding rather than a detail.

On a live machine both of those databases are protected and a tool needs explicit access to open them, which is the same constraint the module has met elsewhere. On a mounted image that protection does not apply, because it belonged to the running system rather than to the volume, so reading them from the image is ordinary work. Knowing that in advance stops an examiner weakening their own machine to solve a problem that only exists on a live subject.

The per-user and system split is worth reading as a question about scope rather than a filing detail. A grant in a user's own database speaks to what that account permitted; a grant in the system database applies more broadly. Reporting one as the other overstates or understates the reach of a permission, and the two files sit in different places precisely because they answer different questions. Stating which you read, alongside the grant itself, is what keeps the claim at the width the evidence supports.

06

Stores with exactly one parser

Where the choice disappears

For some stores the question of which tool answers itself, and recognizing that is part of knowing the stack. The behavioral and activity stores that place a person at the keyboard have a standard parser and effectively no alternative, and the newer record-stream format defeats the mainstream suites today.

THE FORMAT DECIDES HOW MUCH CHOICE YOU HAVE For some stores the toolstack question answers itself. property list database trace store record stream MANY TOOLS READ THIS pick on convenience, and confirm against the raw file FEW PARSERS the native tooling is the arbiter ONE PARSER no choice to make and if it lags a format change, nothing reads it When a store has exactly one parser, that parser is the only door rather than a preference. It is also the case where a single unmaintained project decides what the platform can be examined for.

Three tiers of choice, and the bottom one is not a choice.

Read the block as three tiers of choice, and notice that only the top tier is a preference. The middle and bottom are constraints, and constraints belong in the record rather than in somebody's head.

THREE TIERS, AND WHAT EACH ONE ASKS OF YOU

  many parsers   choose on convenience, confirm against the raw file
  few parsers    the native tooling settles any disagreement
  one parser     no choice, and its maintenance is your capability

the bottom tier is the one to state in a report

The bottom row deserves a moment. Where a single project is the only thing that reads a store, that project's maintenance schedule is part of your evidential capability, which is an uncomfortable dependency and one worth stating in a report rather than discovering mid-case. It is also an argument for knowing which of your tools sit in that tier before you need them.

The corollary for planning is unglamorous and cheap. Once a year, or after any release that moves a store, check that the single-parser tools in your stack still read the current format end to end, and note the version that worked. A tier-three dependency discovered mid-case is a problem; the same dependency checked in advance is a known constraint you can plan a collection around.

The same check has a reporting use. If you can say which version of a single-parser tool read a store and when that version was confirmed against the current release, the dependency stops being a weakness in the examination and becomes a documented condition of it. Unstated, it is the thing an opposing examiner raises; stated, it is something you already accounted for. The difference between those two positions costs a single line in the record. It is the cheapest insurance in the toolstack, and the only one that has to be bought before the risk is visible.

07

The native toolchain is the arbiter

When two tools disagree

When two parsers produce different answers from the same file, something has to be the reference. The operating system's own tooling reads its own formats definitively, which makes it the arbiter rather than merely another option in the comparison.

# Convert a binary property list to readable form without altering it
% plutil -p /Volumes/NE-VANCE-MBP/Library/LaunchDaemons/com.example.helper.plist

# Read a collected log archive with the platform's own reader
% log show --archive /cases/vance/logs.logarchive --style compact --last 6h

Read the commands as conversion and as reading, both done by the platform itself, and neither altering the store it touches. That property is what lets you use them freely during an examination without adding a change you would have to explain.

There is one more reason to reach for the native tooling first rather than last. It is present on every machine running the operating system, needs no installation, carries no download attribute and requires no signature decision, so it is the one part of the stack that works on a borrowed machine or in an environment where installing software is not an option. That makes it the fallback as well as the arbiter. Both roles come from the same property, which is that it ships with the system rather than alongside it.

The same properties make it the right thing to reach for in the first hour of a live response, where installing anything is a change to the machine and a decision to justify. Working with what is already present keeps the intervention small, and small interventions are easier to describe afterwards than a toolkit somebody deployed under pressure.

There is a practical limit on the graphical side worth knowing before you rely on it. The console application gives comfortable access to the logs, and its output cannot be piped into command-line tools or scripts, which rules it out for anything you need to repeat or evidence. So the native tooling is where you confirm and the open-source stack is where you work at speed, and a report that never touched the first is one where nothing was checked against the format itself.

08

Recording which tool read what

Because versions decide findings

Recording the tool against each store is the step the scenario skipped entirely, and it costs seconds. On a platform that changes formats annually the version is part of the finding, because a table produced by a release that predates the store it claims to have read is not evidence of absence.

THE TOOL LINE, PER STORE

  the store      which artifact was read
  the tool       its name, and the version that produced the table
  confirmed by   the raw store, or the native tooling, or neither
  gaps           stores the tool could not parse, named explicitly

  the last line is the one nobody writes

Read the block as the tool line a report carries per store. The confirmation line records whether anybody checked a row against the raw store, and the last line is the one nobody writes.

Naming the gaps is what separates a parsed collection from an examined one. A store the tool silently skipped is indistinguishable in the output from a store that held nothing, and only the person who ran it can tell the reader which it was. Writing that down while it is fresh is trivial, and reconstructing it afterwards is guesswork presented with the authority of a record.

The version matters more here than in most disciplines because of how quickly the platform moves. A parser that was current when a case opened may be two releases behind by the time the report is challenged, and the question will be what it could read at the time rather than what it can read now. Recording the version answers that question without anybody having to reconstruct which build was installed on which afternoon.

It is also the only practical way to scope the damage if a parser is later found to mishandle a format. With versions recorded per store per case, the affected work is a query; without them, it is a review of everything, which in practice means nobody does it and the question stays open. An open question of that kind does not resolve itself; it simply sits with the case until somebody else asks it.

09

Practice

Read one store two ways
hands on

Reading the same store through a parser and through the native tooling is the habit that makes the rest of this section concrete.

Do this Confirm one parsed result against the raw store
  1. Answer the scenario. Name what the report is entitled to say from a breadth pass alone, and what it is not.
  2. Sort the formats. List the four and say which of them leaves you a genuine choice of tool.
  3. State the stale case. Say what a breadth tool does when it meets a store whose format changed, and why that is dangerous in an output that looks complete.
  4. Now on a Mac you have permission to use, convert a binary property list with the native tool and read it.
  5. Then read the privacy database directly and look at which clients are named by path rather than by identifier. This is the reading that the breadth pass would have flattened into a row.
Step 5 is the one to keep. Seeing the raw store behind a parsed row is what makes you willing to open one under pressure, and that willingness is the whole difference between the pass being a map and the pass being the finding. Steps 1 to 3 need nothing but this page.

The format of a store decides which tools can read it, so reading the format off a file precedes choosing a tool. A breadth pass across a collection maps where the evidence for a question lives, and its output is a map rather than the territory: a broad processor makes format assumptions, the platform reorganizes its stores annually, and a store the tool cannot parse is silently absent from a result that still looks complete. Named plugins finish in minutes where a full pass takes an afternoon, and the unified-log plugin alone can turn a forty-gigabyte image into a log database over ten gigabytes. Some stores have exactly one parser, which makes that project's maintenance part of your evidential capability, and the platform's own tooling is what a disagreement between parsers is measured against.