RSS Amplifier

Sentinel Den · Engineering blog · May 21, 2026

Auditing an .xcarchive you didn't build: forensic patterns

0
Sign in to vote or save

Muhammad Khan · Sentinel Den

A customer’s iOS app got popped. They tell you the version, the date, the symptom, a coordinated batch of fraudulent transactions, an unexplained spike in token-introspection failures, a credential-stuffing rate that doesn’t match their app’s user base. They hand you a .xcarchive from their build server. The original Xcode environment is gone; the CI pipeline that produced it has been rolled forward; the developer who built it just left. You need a defensible report by end of week.

This is incident response on an artifact, not a codebase. The good news: an .xcarchive is structurally richer than an .ipa. It carries debug symbols, the original Info.plist hierarchy, build provenance metadata in Info.plist, and an unencrypted Mach-O. Used right, you can reconstruct enough of the build to answer the four questions that matter:

  1. What got shipped, exact symbol map, linked libraries, embedded resources
  2. What was supposed to ship, declared entitlements, provisioning chain, SDK versions
  3. Where the gap is, anything in (1) that isn’t in (2), or vice versa
  4. What changed between this version and the last clean one, diffed against a known-good archive

The .xcarchive’s actual structure

An archive is a Bundle directory:

MyApp 2026-05-15, 14.32.xcarchive/
├── Info.plist                    ← archive metadata: scheme, team ID,
│                                    distribution method, signing identity
├── Products/
│   └── Applications/
│       └── MyApp.app/            ← the auditable .app bundle
│           ├── Info.plist        ← the app's own Info.plist (URL schemes,
│           │                       entitlements references, framework load list)
│           ├── MyApp              ← the unencrypted Mach-O (no FairPlay)
│           ├── embedded.mobileprovision   ← the prov profile USED to build
│           ├── PrivacyInfo.xcprivacy      ← declared APIs (iOS 17+)
│           ├── PlugIns/, Frameworks/      ← extensions + dynamic libs
│           └── _CodeSignature/CodeResources
├── dSYMs/                        ← debug symbols, symbolicate crash logs,
│                                    recover function names from optimized code
└── SCMBlueprint/                 ← optional: git ref, branch, repo URLs

Three artifacts in there carry more incident-response signal than the rest combined:

  • SCMBlueprint/ tells you the exact git SHA the build came from. If you have repo access, you can git diff <previous-good-sha>...<this-sha> for the source-level delta.
  • dSYMs/ lets you symbolicate every stack frame in the customer’s bug reports + their crash logs. Without dSYMs, an optimized iOS binary’s stack traces are just hex addresses.
  • embedded.mobileprovision carries the entitlements that were ACTUALLY signed into the build, not what entitlements.plist in the project said, but what shipped.

The audit pipeline

The five-stage pattern is the same regardless of the artifact, but each stage takes the archive’s structure into account.

Stage 1, Provenance reconstruction

Before touching the binary, read the archive’s Info.plist:

plutil -p "MyApp.xcarchive/Info.plist"

You’re looking for:

  • Name, the scheme name (often reveals targets you didn’t know existed)
  • SchemeName, same purpose, set by Xcode-driven builds
  • CFBundleVersion + CFBundleShortVersionString, what the user saw on their device
  • ApplicationProperties.IconPaths, accidentally bundled assets
  • ApplicationProperties.SigningIdentity, who signed ("Apple Distribution: Acme Inc." vs "iPhone Developer:")
  • ArchiveVersion, schema version of the archive itself

This is your “who, what, when” for the report. Cross-reference with the customer’s claim of which build hit prod.

Stage 2, Mach-O dissection on the inner .app

Don’t audit the archive bundle, audit Products/Applications/<App>.app/<binary>. The archive itself is metadata; the audit subject is the binary inside.

For a quick triage:

codesign -dvv --entitlements - "MyApp.app"
otool -L "MyApp.app/MyApp"             # linked libraries
otool -l "MyApp.app/MyApp" | head -100  # load commands
nm -gU "MyApp.app/MyApp" | wc -l       # exported symbols (sanity check)

For real depth: hand it to an audit tool that drives the parsers all at once. SentinelDen Studio treats .xcarchive as a first-class input (drag the archive in from Xcode’s Organizer; it drills to the inner .app automatically) and runs the Mach-O parser, signature inspector, library resolver, privacy-manifest auditor, and symbol browser in one pipeline.

What you’re hunting in this stage:

  • Linked libraries you didn’t expect, a Frameworks/ entry the customer’s threat model doesn’t account for (a forgotten analytics SDK, an outdated Crashlytics build, a community Frida-detection library)
  • Strings the binary contains, backend API endpoints, hardcoded auth tokens, debug flags that should’ve been compiled out (#if DEBUG blocks that ended up in release because of misconfigured xcconfig)
  • Symbol table anomalies, symbols from extensions the customer didn’t know they had

Stage 3, Entitlement vs declaration cross-check

This is where most incident-response audits find the real bug.

The embedded.mobileprovision carries the signed entitlements, what iOS will let the app actually do. The app’s Info.plist declares what features it expects. The PrivacyInfo.xcprivacy declares the required-reason APIs it touches. Cross-check:

# What the provisioning profile claims:
security cms -D -i "MyApp.app/embedded.mobileprovision" | plutil -p - \
  | grep -A50 Entitlements

# What the app's bundle Info.plist promises:
plutil -p "MyApp.app/Info.plist" | grep -E "URLScheme|Bundle|Background|Network"

# What the privacy manifest declares:
plutil -p "MyApp.app/PrivacyInfo.xcprivacy"

The triple cross-check catches:

  • A keychain-access-groups entitlement that’s no longer used by any production code (left over from a migration)
  • A custom URL scheme registered in Info.plist but not actually handled in code (open redirect surface)
  • A required-reason API in the privacy manifest with no matching call site in the binary, meaning someone removed a code path without updating the manifest, and the App Store reviewer didn’t notice

Stage 4, Diff against the last clean version

If the customer has the previous .xcarchive from before the incident, diff structurally, not by file bytes, but by audit-meaningful properties:

Propertyv1.4.2 (clean)v1.4.3 (incident)Δ
Linked frameworks4751+4
Strings >12 chars matching URL pattern312327+15
Embedded provisioning entitlements1819+1 new: aps-environment=production
Privacy manifest required-reason APIs880
Binary entropy (avg per __TEXT page)6.846.93+0.09

This is where build-to-build diffing in your audit tool earns its keep. The +4 linked frameworks row is the lead. The new aps-environment=production entitlement is the smoking gun if the customer didn’t think they shipped push.

Stage 5, Report

You’re writing for a customer’s CISO or audit lead, not a security researcher peer. Three sections:

  1. Confirmed facts, exact versions, signing identity, what shipped vs declared. No interpretation.
  2. Likely root cause, your hypothesis, with the artifacts that point to it.
  3. Followups, what they should verify in their build environment, what to retest, what to rebuild from a known-clean state.

Sign the report. PDF, embedded signature, your team’s identity. SARIF + CycloneDX SBOM as machine-readable attachments. Studio emits all three from one audit run; the SARIF makes the findings ingestable by the customer’s own GitHub code-scanning or Snyk dashboard.

What .xcarchive can’t tell you

An archive is the build output, not the build process. It won’t tell you:

  • Whether the build environment was compromised (a poisoned Pods/ cache, an SDK pulled from a typosquatted CocoaPods name)
  • Whether the developer signed under duress
  • What CI variables were in scope when the build ran
  • Whether enableTestability or swift_enable_module_lookup was on

You answer those by asking the customer for their CI logs from that build’s run. The archive gives you a starting point; the CI logs give you the chain.

But for incident triage in the first 48 hours, what shipped, what’s anomalous, where the gap is, what changed, the .xcarchive is enough. The audit pipeline runs locally on your machine in minutes, and the report you produce is defensible without the customer’s source.

Drag the archive into Studio. Run the audit. Write the report. Send the customer back to their build pipeline with a specific question. That’s the loop.

Engineering opinion, not advice. This post reflects the author's engineering reasoning at time of publication. It is not professional security, legal, financial, or compliance advice; do not rely on it as a substitute for review by qualified professionals for your specific situation. Posts may become outdated as iOS, Apple frameworks, attacker techniques, and our own SDKs evolve. The canonical source of truth for shipped SDK behavior is /docs and /changelog.

Read the original on sentinelden.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.