MacSecLabs
Phase 01 · Module 1.4 · Beginner

Installer-Based Execution: Preinstall, Postinstall, Receipts, and Privileged Context

A PKG installer is the only macOS delivery format that gives you root-level code execution as a built-in feature of the platform. When a user installs a package, installd executes your preinstall and postinstall scripts with effective UID 0. No exploit. No privilege escalation bug. The installer framework does this by design.

The cost: a PKG leaves a permanent receipt in /var/db/receipts/. It writes to installd logs. It shows the user an installer UI. It may prompt for an admin password. Every artifact is designed to survive reboots, OS upgrades, and application deletion. For the operator, the PKG is simultaneously the highest-privilege and highest-evidence delivery format.

This module dissects the PKG format, the installation lifecycle, and the privileged execution context that installd provides. You will build and inspect a weaponized PKG, read its internals, and understand exactly what a defender sees when they audit a package installation.

The module covers PKG-based execution only. PKG-based persistence (receipt re-entry, LaunchDaemon installation via postinstall) is covered in Phase 07.


Evidence Setup

mkdir -p ~/macseclabs/evidence/1.4-installer-based-execution

The lab builds and inspects PKGs. It does NOT install them. Installation on a production Mac requires explicit authorization and a disposable VM.


PKG Internals: What Is Inside the File

A .pkg file is a xar (eXtensible ARchive) containing four components:

PackageInfo        XML manifest describing the package identity, payload,
                   scripts, and install behavior.

Bom                Bill of Materials. A binary table listing every file
                   the package installs, with permissions, ownership,
                   and checksums. The BOM is what allows pkgutil --files
                   to enumerate installed package contents.

Payload            A gzip-compressed cpio archive of the files to install.
                   Expanded to the target root (/) during installation.

Scripts            A directory containing preinstall and/or postinstall
                   scripts. These are shell scripts executed by installd
                   before and after payload expansion.

Inspect a lab-built PKG to see each component:

mkdir -p /tmp/macsec-pkg-lab/payload /tmp/macsec-pkg-lab/scripts
echo "operator payload placeholder" > /tmp/macsec-pkg-lab/payload/operator-file.txt

cat > /tmp/macsec-pkg-lab/scripts/preinstall <<'PRE'
#!/bin/zsh
# Runs BEFORE payload is written to disk. uid=0 (installd).
echo "preinstall executing as uid=$(id -u) euid=$(id -u)" > /tmp/macsec-pkg-preinstall-proof.txt
PRE

cat > /tmp/macsec-pkg-lab/scripts/postinstall <<'POST'
#!/bin/zsh
# Runs AFTER payload is written to disk. uid=0 (installd).
echo "postinstall executing as uid=$(id -u) euid=$(id -u)" >> /tmp/macsec-pkg-lab-proof.txt
id >> /tmp/macsec-pkg-lab-proof.txt
POST

chmod +x /tmp/macsec-pkg-lab/scripts/preinstall /tmp/macsec-pkg-lab/scripts/postinstall

pkgbuild \
  --root /tmp/macsec-pkg-lab/payload \
  --scripts /tmp/macsec-pkg-lab/scripts \
  --identifier com.macseclabs.lab.installer \
  --version 1.0 \
  /tmp/macsec-pkg-lab/operator.pkg

Now expand it without installing to read the internals:

pkgutil --expand /tmp/macsec-pkg-lab/operator.pkg /tmp/macsec-pkg-expanded

The expanded structure:

/tmp/macsec-pkg-expanded/
  Bom              <- Bill of Materials (binary)
  PackageInfo      <- XML manifest
  Payload          <- gzip'd cpio of files to install
  Scripts/
    preinstall     <- Executed before Payload expands
    postinstall    <- Executed after Payload expands

Read the PackageInfo manifest:

cat /tmp/macsec-pkg-expanded/PackageInfo
<pkg-info identifier="com.macseclabs.lab.installer" version="1.0"
          auth="root" overwrite-permissions="true">
  <payload numberOfFiles="1" installKBytes="0"/>
  <scripts>
    <preinstall file="./preinstall" timeout="600"/>
    <postinstall file="./postinstall" timeout="600"/>
  </scripts>
</pkg-info>

Key fields an operator should understand:

identifier:       The package identifier. Permanent. Appears in receipts
                  and installd logs. Choose one consistent with the
                  engagement pretext. Do not reuse identifiers across
                  engagements.

auth="root":      The package payload files are installed with root
                  ownership. Scripts run as root. This is the default
                  and is required for writing to system paths.

timeout="600":    Scripts have a 600-second execution window (10 min).
                  Scripts running longer are killed. A payload that
                  sleeps or blocks beyond the timeout is terminated.

overwrite-permissions="true":
                  Installed files get the permissions from the BOM.
                  Without this, permissions are inherited from the
                  target directory.

The Installation Lifecycle

When a user double-clicks a PKG (or installer -pkg runs), installd executes this sequence. Every step is logged:

1. Preflight
   Gatekeeper validates the PKG signature and notarization (if
   quarantined). If the PKG is unsigned and quarantined, Gatekeeper
   blocks installation. If the PKG is not quarantined, Gatekeeper
   skips.

2. User Authorization
   Installer.app shows the package UI. If the package installs to
   system paths (/Library, /Applications), the user is prompted for
   an admin password. The password is cached by the authorization
   framework for the duration of the install session.

3. Preinstall Script Execution
   installd (running as root) executes the preinstall script.
   uid=0, euid=0. Parent process is installd. The script runs in
   the system bootstrap namespace (no GUI, no pasteboard, no
   WindowServer connection). The target filesystem is still in its
   pre-install state: the Payload has NOT been expanded yet.

4. Payload Expansion
   installd extracts the Payload cpio archive to the target root.
   Files are placed with the ownership and permissions from the BOM.
   Existing files are overwritten if overwrite-permissions is true.

5. Postinstall Script Execution
   installd executes the postinstall script. uid=0, euid=0.
   Parent is installd. The Payload files are now on disk. This is
   where the operator writes LaunchDaemons, modifies configuration,
   or stages persistence.

6. Postflight
   installd writes the package receipt to /var/db/receipts/.
   The receipt consists of a BOM (every installed file) and a plist
   (package identifier, version, install time). The receipt is
   permanent.

7. Installer.app closes. The receipt is on disk. The postinstall
   script's child processes have exited. Any files the operator
   wrote during postinstall remain until explicitly removed.

The operator's execution window is steps 3 and 5: the preinstall and postinstall scripts. Both run as root. The postinstall is more useful because the Payload files are already on disk and the operator can reference them.


The Script Execution Context

A postinstall script does not run as a normal user process. The context is fundamentally different from Terminal or SSH execution:

uid:              0 (root)
euid:             0 (root)
gid:              0 (wheel)
Parent process:   /System/Library/CoreServices/installd
Bootstrap:        System (not GUI, not user)
cwd:              / (root of the target volume)
tty:              None (no controlling terminal)
Environment:      Minimal. INSTALLER_TEMP, INSTALL_DIR, PACKAGE_PATH
                  are set by installd. HOME, USER, SHELL are NOT set
                  or set to root defaults.

Available:        Full filesystem write access (except SIP-protected
                  paths). LaunchDaemon installation. Kernel extension
                  loading (if SIP permits).

NOT available:    GUI access (no WindowServer, no pasteboard, no
                  accessibility API). User keychain (requires login
                  password, not root access alone). TCC-protected
                  user data (root does not auto-grant FDA).

This has immediate operational consequences:

An operator who writes a postinstall script that tries to:
  osascript -e 'display dialog "hello"'
  -> FAILS. No WindowServer connection in system bootstrap.

An operator who writes a postinstall script that:
  cp /tmp/operator-launchdaemon.plist /Library/LaunchDaemons/
  launchctl bootstrap system /Library/LaunchDaemons/operator-launchdaemon.plist
  -> SUCCEEDS. Root can write to /Library/LaunchDaemons/.
  -> The LaunchDaemon runs next reboot with root privileges.

An operator who writes a postinstall script that:
  security dump-keychain ~/Library/Keychains/login.keychain-db
  -> FAILS. The user's keychain requires the login password, not
     root access. SIP also protects keychain access without FDA.

Package Receipts: The Forensic Gift to the Defender

Every installed package leaves a receipt at:

/var/db/receipts/<identifier>.bom
/var/db/receipts/<identifier>.plist

The BOM contains the complete file manifest: every path, permission, owner, group, and checksum for every file the package placed. The plist contains the package metadata:

# List installed packages
pkgutil --pkgs | head -20

# Show files installed by a specific package
pkgutil --files com.apple.pkg.CLTools_Executables 2>&1 | head -10

# Show package info
pkgutil --pkg-info com.macseclabs.lab.installer 2>&1

An operator who installs a PKG on a target Mac creates a permanent record that contains:

- The package identifier (chosen by the operator).
- The package version.
- The install timestamp (UTC, in the receipt).
- Every file the package placed on disk, with full paths, permissions,
  ownership, and SHA-1 checksums.

This receipt survives:

  • App deletion (the receipt is separate from the installed files).
  • Reboots (receipts are on the root filesystem).
  • OS upgrades (receipts are preserved across major macOS updates).
  • rm -rf of the installed files (the receipt is not tied to file existence).

The only way to remove a package receipt is:

sudo pkgutil --forget <identifier>

This requires admin access. It removes the BOM and plist from /var/db/receipts/. It does NOT remove the installed files (those must be cleaned separately). Running --forget is itself a logged action (pkgutil writes to Unified Logging).

For an operator, the receipt is a permanent attribution artifact. If the engagement permits PKG-based execution, the operator must plan to either:

  1. Remove the receipt during cleanup (pkgutil --forget), or
  2. Document the receipt in the engagement report as intentionally left behind to demonstrate the installation path, or
  3. Accept that the receipt is a forensic artifact the defender will find and ensure the package identifier and version are consistent with the engagement's authorized activity.

Reading BOM Files

The Bill of Materials is a binary format. Use lsbom to read it:

# Read an expanded PKG's BOM
lsbom /tmp/macsec-pkg-expanded/Bom 2>&1

# Read an installed receipt's BOM
lsbom /var/db/receipts/com.apple.pkg.CLTools_Executables.bom 2>&1 | head -10

Output columns: path, mode, uid/gid, size, checksum. This is exactly what a defender sees when they audit the package. Every file, its permissions, its owner, its checksum. If your payload writes files during postinstall that are NOT in the BOM, those files are still on disk but the receipt does not list them. A defender who cross-references pkgutil --files against find on the installed prefix finds the discrepancy.


CLI Installation Without the GUI

installer -pkg bypasses Installer.app entirely. No GUI, no user confirmation, no admin password prompt if the calling process already has root:

# GUI-based (what the user sees):
#   User double-clicks PKG -> Installer.app -> admin prompt -> installd

# CLI-based (post-access operator path):
sudo installer -pkg /path/to/operator.pkg -target /

When run with sudo, installer -pkg -target / executes the full installation lifecycle without any UI. The preinstall and postinstall scripts run as root. The receipt is written. No dialog. No user notification beyond the Unified Log entries:

CLI install advantages for the operator:
  - No user interaction required (if admin access is available).
  - No installer UI evidence (no Installer.app in process tree).
  - Installs to / directly. No volume selection, no confirmation.
  - Same receipt, same installed files, same logs as GUI install.

CLI install limitations:
  - Requires admin (sudo). Standard users cannot use -target /.
  - installd still logs the entire installation lifecycle.
  - The receipt is still permanent.
  - ES events still fire for every file write, exec, and script.

Distribution Packages: Beyond pkgbuild

pkgbuild creates component packages. productbuild wraps them into a distribution package (.pkg) with a Distribution XML file that supports conditional logic:

# productbuild with a distribution file
productbuild --distribution dist.xml --package-path ./components \
  --resources ./resources operator.pkg

The Distribution file supports:

JavaScript in <script> tags:
  function myCheck() { return system.run('uname -a'); }
  Runs during the installer's preflight phase. Can execute arbitrary
  commands (as root if the installer runs with admin authorization).

Conditional install:
  <choices-outline> with OS version gating, volume checks, prior
  install detection. The installer only installs components that
  pass the condition.

Volume and target validation:
  <volume-check> and <installation-check> scripts run before
  installation and can block the installer from proceeding.

Custom UI:
  <title>, <welcome>, <conclusion> HTML files control what the
  user sees during installation. Can be used to display a
  pretext-consistent installer experience.

Background image, license agreement, readme:
  Product resources placed in the Resources directory appear in
  the installer UI.

From an operator's perspective, productbuild is the tool for packages that need to:

  • Decide at install time whether to proceed (OS version, target detection, prior infection check).
  • Present a convincing installer UI with pretext-consistent branding.
  • Install different components based on target characteristics.
  • Execute preflight checks that validate the target before the installation commits.

The lab uses pkgbuild because component packages are simpler to build and inspect. Distribution packages are the operator's tool when the engagement requires a convincing installer experience or conditional execution logic.


Weaponizing a PKG: The Postinstall Proof Pattern

The canonical operator pattern for this module is root execution proof, not persistence. The postinstall script should prove its privilege context, write evidence to a disposable location, and stop. LaunchDaemon installation and receipt re-entry are Phase 07 topics and should not be copy-pasted from this module.

# 1. Build a payload that proves root execution without persistence.
cat > /tmp/payload.sh <<'PAYLOAD'
#!/bin/zsh
# Operator objective: gather host info and prove privileged context.
# Runs as root. Parent is installd. No GUI. No user keychain access.

OUT="/tmp/macsec-pkg-lab-proof.txt"
{
  echo "ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  echo "uid=$(id -u)"
  echo "euid=$(id -u)"
  echo "user=$(id -un)"
  echo "parent=$(ps -o comm= -p $PPID 2>/dev/null || echo unknown)"
  hostname
  sw_vers
} >> "$OUT"

# Gather system context
id >> "$OUT"
PAYLOAD

# 2. Place payload in the pkgroot.
mkdir -p /tmp/pkgroot/private/tmp
cp /tmp/payload.sh /tmp/pkgroot/private/tmp/macsec-pkg-proof.sh
chmod +x /tmp/pkgroot/private/tmp/macsec-pkg-proof.sh

# 3. Write the postinstall script (calls the payload).
mkdir -p /tmp/pkgscripts
cat > /tmp/pkgscripts/postinstall <<'POST'
#!/bin/zsh
# Postinstall: runs as root after payload files are on disk.
# Execute the staged payload.
/private/tmp/macsec-pkg-proof.sh
POST
chmod +x /tmp/pkgscripts/postinstall

# 4. Build the PKG.
pkgbuild \
  --root /tmp/pkgroot \
  --scripts /tmp/pkgscripts \
  --identifier com.operator.stage1 \
  --version 1.0 \
  /tmp/stage1.pkg

When the user installs this PKG, the postinstall script runs the payload. The payload runs as root and writes a proof file under /tmp. The receipt is written. The operator has root-level code execution from a single user action, without adding persistence in this module.

Why Preinstall Is Less Useful Than Postinstall

Preinstall:  Payload files are NOT on disk yet. You can run commands as
             root, but anything you create must be self-contained (no
             dependency on payload files). Useful for: environment
             checks, target validation, OS version gating.

Postinstall: Payload files ARE on disk. You can reference them directly.
             Useful for: executing installed tools, installing
             LaunchDaemons/LaunchAgents, modifying configuration,
             cleaning up installation artifacts.

The postinstall script should be idempotent. If the user installs the same package twice, the postinstall runs twice. A script that appends to a file on every install will produce duplicate entries. A script that checks whether the persistence mechanism already exists before installing it is operator-grade.


PKG Signing and Gatekeeper Interaction

Like .app bundles, PKGs are subject to Gatekeeper when quarantined:

Unsigned PKG + quarantine:           Gatekeeper blocks installation.
                                     User sees: "This package will
                                     damage your computer."

Ad-hoc signed PKG + quarantine:      Gatekeeper blocks. Ad-hoc signing
                                     is not trusted for packages.

Developer ID signed PKG + quarantine: Gatekeeper allows (if policy is
                                     "identified developers").

Notarized PKG + quarantine:          Gatekeeper allows silently.

PKG without quarantine:              Gatekeeper skips. installd runs
                                     the scripts regardless of signing
                                     state.

The PKG signing requirements mirror the .app signing requirements covered in Module 1.3. The operator calculus is the same: quarantine triggers Gatekeeper; without quarantine, Gatekeeper skips; without a Developer ID, Gatekeeper blocks a quarantined PKG.


OPSEC: The PKG Leaves the Most Evidence of Any Format

A PKG installation creates a permanent, auditable chain of evidence. The operator who uses PKGs without understanding this chain burns the engagement on first install.

Before install:
  PKG file on disk (quarantine xattr, provenance, creation timestamp).
  If downloaded: browser history, download URL in WhereFroms.

During install:
  installd log entries for preinstall and postinstall execution.
  Kernel ES events for every file created by the payload and scripts.
  Unified Log entries from any command the script runs.

After install:
  Package receipt (BOM + plist) in /var/db/receipts/.
  Installed files on disk (with timestamps, ownership, permissions).
  Any persistence mechanism the postinstall created.
  User's admin password was entered (if prompted).
  Authorization framework cached the credential briefly.

Cleanup required:
  pkgutil --forget <identifier> (requires admin).
  Remove installed files.
  Remove any persistence mechanisms.
  Remove the PKG file itself.
  Verify receipt deletion (pkgutil --pkgs | grep <identifier>).

The operator who uses a PKG must plan cleanup at engagement design time, not at execution time. A receipt left behind after the engagement is a permanent record that somebody installed a package with a specific identifier at a specific timestamp.


Detection Engineering

PKG installation is high-signal telemetry. Defenders monitor:

1. Unsigned package installation:
   Event:  installd processes a package with no valid code signature.
   Source: ES events from installd, syspolicyd assessment.
   Alert:  Medium. Legitimate unsigned packages exist (internal tools,
           homebrew, open-source installers), but a defender with a
           software inventory correlates the package identifier against
           known-good identifiers.

2. Package from ~/Downloads/:
   Event:  installd processes a package from a user's Downloads
           directory.
   Source: ES exec event on installd with argv containing
           ~/Downloads/ path.
   Alert:  High. Normal software arrives via App Store, MDM, or
           vendor websites in ~/Downloads/. But the combination of
           ~/Downloads/ + execution without prior user confirmation
           is anomalous.

3. Postinstall script spawns a shell or interpreter:
   Event:  installd creates a child process (zsh, bash, python, perl).
   Source: ES exec event with parent=installd.
   Alert:  Critical. Postinstall scripts that spawn shells for
           non-install tasks are strong indicators of malicious
           package behavior. Many legitimate postinstall scripts
           use shell, but the command line and subsequent child
           processes distinguish install tasks from operator
           activity.

4. New LaunchDaemon after package install:
   Event:  File created in /Library/LaunchDaemons/ within N seconds
           of a package installation.
   Source: ES create events correlated with installd activity.
   Alert:  High. Legitimate software installs LaunchDaemons during
           installation. The alert fires when the LaunchDaemon's
           Label or ProgramArguments do not match known software.

The strongest detection is the correlation chain: package downloaded from browser -> installed by installd -> postinstall spawns shell -> shell writes to /Library/LaunchDaemons/ -> new daemon launches. Five events. Each individually may be benign. The sequence is not.


MITRE ATT&CK Mapping

AspectATT&CK
PKG installer deliveryT1204.002 (Malicious File)
Postinstall script execution as rootT1059.004 (Unix Shell), T1548.004 (Elevated Execution)
LaunchDaemon installation via postinstallT1543.004 (LaunchDaemon), T1546.016 (Installer Packages)
Package receipt analysis/removalT1070.009 (Indicator Removal on Host)
installd abuse for privileged contextT1548.004

What You Should Learn

After completing this module you should be able to:

  • Describe the four components inside a .pkg file (PackageInfo, Bom, Payload, Scripts) and what each contains.
  • Read the PackageInfo XML and identify the package identifier, authorization level, script paths, and script timeouts.
  • List the seven steps in the installation lifecycle and identify which steps give the operator code execution.
  • Describe the execution context of a postinstall script (uid=0, parent=installd, system bootstrap, no GUI, no user keychain).
  • Explain why root access from a postinstall script does NOT grant Full Disk Access, TCC exemption, or keychain access.
  • Build a PKG with pkgbuild --root --scripts that places files and executes a postinstall payload.
  • Expand a PKG with pkgutil --expand and inspect its scripts and PackageInfo without installing.
  • Read package receipts with pkgutil --pkgs, pkgutil --files, and pkgutil --pkg-info.
  • Explain why a package receipt is a permanent forensic artifact and what cleanup requires (pkgutil --forget).
  • Describe the five-event EDR correlation chain that detects malicious package installation.

Researcher Checkpoint

Before moving to module 1.5:

  1. You built a PKG with pkgbuild containing a preinstall and postinstall script.
  2. You expanded the PKG with pkgutil --expand and read the PackageInfo manifest, the preinstall script, and the postinstall script.
  3. You can explain why the postinstall script runs as uid=0 and what capabilities root has versus what root is still blocked from (GUI, keychain, SIP paths, TCC without FDA).
  4. You listed installed packages on your Mac with pkgutil --pkgs and inspected a receipt with pkgutil --pkg-info.
  5. You understand that pkgutil --forget removes the receipt but NOT the installed files.
  6. You can describe the five-event EDR correlation chain and identify which event in the chain is the strongest detection signal.
  7. You saved evidence to ~/macseclabs/evidence/1.4-installer-based-execution/.
  8. You did NOT install the lab PKG on your production Mac.

This is one free module of many.

Get every module across all ten phases, with working code and the detection engineering to catch each technique.

Get full access