247 lines
14 KiB
Markdown
247 lines
14 KiB
Markdown
# Apple Application Inventory Projection
|
|
|
|
REA can project one or more authenticated `inventory_artifact` Evidence records
|
|
for the same IPA, macOS `.app` directory, ZIP, or DMG into an Apple application
|
|
inventory. The projection is execution-free: it reports exact paths, content
|
|
hashes, detected formats, bundle anatomy, runtime-family hints, and path-based
|
|
bridge hypotheses. It does not parse plist or CMS semantics or claim observed
|
|
runtime calls.
|
|
|
|
```sh
|
|
rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
|
|
```
|
|
|
|
```json
|
|
{
|
|
"name": "project_apple_application_graph",
|
|
"arguments": {
|
|
"inventory_evidence": ["<inventory_artifact Evidence>"]
|
|
}
|
|
}
|
|
```
|
|
|
|
The result includes every component in each inventory category and every
|
|
JavaScript-to-native bridge candidate pair. It has no caller-selected component
|
|
budget, prefix truncation, or omitted-count fields. Duplicate inventory pages
|
|
are merged by artifact identity and occurrence path; their Evidence IDs remain
|
|
attached as source Evidence.
|
|
|
|
Coverage is `complete-within-inventory` when the supplied pages reconstruct the
|
|
complete authenticated inventory, and `partial` otherwise. A partial result
|
|
states that absence is unknown. Component and bridge-candidate arrays are still
|
|
the complete projection of the inventory that was supplied.
|
|
|
|
## Application roots
|
|
|
|
| Inventory root | Application root |
|
|
| -------------------------------------- | --------------------------------------------------- |
|
|
| IPA | Each `Payload/<Name>.app` |
|
|
| `.app` directory | `.`, the inventoried bundle itself |
|
|
| Directory, ZIP, or DMG containing apps | Each outermost `<Name>.app` followed by `Contents/` |
|
|
|
|
ZIP archives made with `ditto -c -k --keepParent` report `<Name>.app`. A DMG
|
|
reports `<image>.dmg/<Volume>/<Name>.app`; DMG child inventory needs the native
|
|
macOS mounting adapter. When a DMG has only its root identity, the projection is
|
|
`partial` and says that bundle absence is unknown. Other inventory formats, such
|
|
as APK or ASAR, are rejected as input errors.
|
|
|
|
An inventoried directory whose name ends in `.app` is its own root even when its
|
|
inventory is partial or lacks `Contents/`. For directory, ZIP, and DMG
|
|
inventories, components outside every application root are not attributed to
|
|
the application. This covers installers, disk-image extras, and AppleDouble
|
|
sidecars, which therefore stay out of runtime families and bridge candidates. A
|
|
limitation reports how many inventoried entries were left out. IPA projection
|
|
still lists every archive component, including `SwiftSupport/`.
|
|
|
|
`platforms` lists `macos` for application roots with `Contents/`. It lists `ios`
|
|
for shallow application roots that have an `Info.plist`. An application root
|
|
with neither, such as an empty inventory, has no platform.
|
|
|
|
## macOS bundle anatomy
|
|
|
|
`bundles` lists each application root and every nested bundle under it. Each
|
|
bundle reports its `parent_path`, `layout`, `info_plist_path`,
|
|
`executable_candidates`, and `signing_paths`.
|
|
|
|
| Layout | Meaning | Content directory |
|
|
| --------------------- | --------------- | --------------------------------------------- |
|
|
| `macos-deep` | Has `Contents/` | `Contents/`, executables in `Contents/MacOS/` |
|
|
| `versioned-framework` | Has `Versions/` | Each real `Versions/<version>/` |
|
|
| `shallow` | Neither | The bundle directory itself |
|
|
|
|
Roles are path conventions (`role_basis: "path-convention"`):
|
|
|
|
| Role | Path |
|
|
| -------------------- | --------------------------------------------------------------- |
|
|
| `app-extension` | `*.appex` |
|
|
| `xpc-service` | `*.xpc`, including XPC services inside frameworks |
|
|
| `framework` | `*.framework` |
|
|
| `system-extension` | `*.systemextension` |
|
|
| `driver-extension` | `*.dext` |
|
|
| `plug-in` | `PlugIns/*.bundle`, `*.plugin`, `*.qlgenerator`, `*.mdimporter` |
|
|
| `resource-bundle` | `Resources/*.bundle`, such as SwiftPM resource bundles |
|
|
| `bundle` | Any other `*.bundle` |
|
|
| `login-item` | `Contents/Library/LoginItems/*.app` |
|
|
| `helper-application` | `Contents/Helpers/*.app` |
|
|
| `nested-application` | Any other nested `*.app` |
|
|
|
|
The components also list:
|
|
|
|
- `privileged_helpers`: files directly in `Contents/Library/LaunchServices/`,
|
|
where SMJobBless helpers live;
|
|
- `launchd_plists`: `.plist` files directly in `Contents/Library/LaunchAgents/`
|
|
(`domain: "agent"`) or `Contents/Library/LaunchDaemons/` (`domain: "daemon"`),
|
|
as used by SMAppService;
|
|
- `helpers`: files directly in `Contents/Helpers/`.
|
|
|
|
Symlinks are reported by path in `symlinks`. Their targets are not inventoried,
|
|
so a versioned framework is described through its real `Versions/<version>/`
|
|
directories rather than `Versions/Current`. A framework's `info_plist_path` is
|
|
`null`, with a limitation explaining why, when it has more than one real
|
|
`Versions/<version>/` directory (even if only one of them holds a plist,
|
|
because `Versions/Current` decides which one applies) or when its single
|
|
version directory has no `Resources/Info.plist`.
|
|
|
|
AppleDouble sidecar files (`._*` and `__MACOSX/`) are inventory facts, but they
|
|
describe neighbouring files. They are excluded from roots, bundle roles, and the
|
|
anatomy components.
|
|
|
|
The projection does not read plists. Use `inspect_plist` on a bundle's
|
|
`info_plist_path` or a launchd plist to read `CFBundleExecutable`, identifiers,
|
|
`SMPrivilegedExecutables`, `BundleProgram`, URL schemes, and usage descriptions.
|
|
Use `inspect_signature` on an executable candidate for code-signing claims.
|
|
|
|
## Dylib load resolution
|
|
|
|
`trace_dylib_resolution` answers which file each Mach-O dylib load reaches. It
|
|
runs on the active Mach-O, or on every executable in the active `.app`. Each
|
|
executable in a bundle (the main app, XPC services, app extensions, login items,
|
|
privileged helpers, and helper tools) is a separate process root with its own
|
|
`@executable_path`.
|
|
|
|
```sh
|
|
rea trace-dylib-resolution MyApp.app --json
|
|
rea trace-dylib-resolution MyApp.app --root Contents/MacOS/MyApp --architecture arm64 --json
|
|
```
|
|
|
|
```json
|
|
{
|
|
"name": "trace_dylib_resolution",
|
|
"arguments": { "roots": ["Contents/MacOS/MyApp"], "architecture": "arm64" }
|
|
}
|
|
```
|
|
|
|
Load commands are parsed in TypeScript from Mach-O and FAT headers, including
|
|
the `dylib_use_command` encoding newer linkers emit for flags such as delayed
|
|
initialization. Nothing is executed and no Apple tool is required, so the tool
|
|
also works off macOS.
|
|
|
|
Resolution follows dyld:
|
|
|
|
- `@executable_path` is the directory of the process root's executable. It is
|
|
undetermined when the root is a library.
|
|
- `@loader_path` is the directory of the image that holds the load command, or
|
|
of the image that owns the `LC_RPATH` entry being expanded.
|
|
- `@rpath` tries each `LC_RPATH` of the loading image first, then those of the
|
|
image that loaded it, up to the process root.
|
|
- Dependencies load dependents-first in load-command order, and each image
|
|
loads once per process. A later request whose install name matches an
|
|
already loaded image reuses it.
|
|
- Symlinks such as `Versions/Current` are followed inside the analyzed root.
|
|
An escape through `..` or an absolute link is `escapes-target`.
|
|
|
|
Each edge lists every candidate path tried, with its outcome: `resolved`,
|
|
`absent`, `not-mach-o`, `malformed`, `unsupported` (recognized Mach-O outside
|
|
parser coverage, such as big-endian images), `architecture-missing`,
|
|
`outside-target`, `shared-cache`, `escapes-target`, or `undetermined`. Slices follow dyld's
|
|
graded architectures, so an `x86_64h` process also loads `x86_64`. The edge's `resolution` is one of:
|
|
|
|
| Status | Meaning |
|
|
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `resolved` | An image inside the analyzed root loads, and no earlier candidate was undeterminable. |
|
|
| `conditional` | An image inside the root, or in the supplied shared cache, resolves, but an earlier candidate that REA cannot decide (for example `/opt/vendor/lib` or `/usr/lib/swift` without a cache) would win if it exists on the running system. |
|
|
| `unresolved` | Every candidate is definitively absent or unusable inside the root. |
|
|
| `undetermined` | No candidate resolves inside the root, and some candidate lies outside it or depends on the environment. |
|
|
| `shared-cache` | An absolute install path, or an `@rpath` candidate outside the root, was found in the dyld shared cache passed as `shared_cache` (CLI `--shared-cache`). dyld loads the cached image, so later in-root candidates are unused. |
|
|
|
|
A `conditional` image is still traced, because it is the image the analyzed
|
|
root supplies. Every edge below it has `loader_conditional: true`, and a
|
|
request that reuses it by install name is `conditional` too. Findings on those
|
|
edges say that they apply only if the conditional image loads.
|
|
|
|
Absolute install names such as `/usr/lib/libSystem.B.dylib` are
|
|
`outside-target`. On macOS 11 and later most of them live in the dyld shared
|
|
cache rather than on disk, so REA does not check them against the host.
|
|
|
|
`findings` are derived, not observed:
|
|
|
|
- `required-load-unresolved`, `weak-load-unresolved`, and `lazy-load-unresolved`.
|
|
`LC_LAZY_LOAD_DYLIB` dependencies are resolved but not traversed, because dyld
|
|
loads them on first use; an unresolved one is not a launch failure.
|
|
- `earlier-rpath-candidate-absent`: a Mach-O placed at an earlier search path
|
|
would load first. Whether code-signing library validation would reject it is
|
|
not evaluated; use `inspect_signature`.
|
|
- `dyld-environment-present`: `LC_DYLD_ENVIRONMENT` search paths are not modeled.
|
|
|
|
`verify:macos-bundle` checks the parser against `otool -l` for every traced
|
|
image. It also compares the predicted load order of two process roots with the
|
|
images dyld actually loads (`DYLD_PRINT_LIBRARIES`).
|
|
|
|
With `shared_cache`, absolute candidates are looked up in that cache's image
|
|
list. A process uses the cache only when both of these hold:
|
|
|
|
- the cache serves its CPU family (an arm64e cache serves arm64 and arm64e);
|
|
- one of its `LC_BUILD_VERSION` or `LC_VERSION_MIN_*` platforms (reported per
|
|
slice as `platforms`) matches the cache's platform or alternate platform.
|
|
|
|
An iOS cache extracted from an IPSW is therefore not used for a macOS
|
|
executable. A limitation names each process root the cache was not used for,
|
|
and why. The result's `shared_cache` names the cache path, UUID, architecture
|
|
and OS version. Cached images are not traversed.
|
|
|
|
An image listed in the cache but mapped in a missing or UUID-mismatched
|
|
subcache is `undetermined`. It is listed in
|
|
`coverage.unverified_shared_cache_images` and makes coverage partial. Without a
|
|
cache, system paths stay `undetermined`.
|
|
|
|
## dyld shared cache
|
|
|
|
`inspect_dyld_shared_cache` reads a dyld shared cache file set selected by path.
|
|
On current macOS this is
|
|
`/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e`;
|
|
a cache extracted from an IPSW also works. It runs without an open target.
|
|
|
|
```sh
|
|
rea inspect-dyld-shared-cache /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e --image /usr/lib/libSystem.B.dylib --json
|
|
```
|
|
|
|
```json
|
|
{
|
|
"name": "inspect_dyld_shared_cache",
|
|
"arguments": {
|
|
"cache_path": "/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e",
|
|
"images": ["/usr/lib/libSystem.B.dylib"]
|
|
}
|
|
}
|
|
```
|
|
|
|
The result reports:
|
|
|
|
- the magic, architecture, UUID, platform and OS version, any alternate platform
|
|
(Mac Catalyst), cache type, shared region and maximum slide;
|
|
- every VM mapping, with the file that holds it;
|
|
- every subcache by file suffix, with its UUID checked against the main cache.
|
|
A missing or mismatched subcache makes coverage `partial`, and images mapped
|
|
there are `unmapped`;
|
|
- the complete image list with addresses.
|
|
|
|
For each requested install path, the tool reads that image's load commands
|
|
(install name, dependencies with their re-export, weak, upward and delayed-init
|
|
flags, and rpaths) through the cache mappings. A path the cache does not
|
|
contain is `absent`. Older single-file caches are read from their legacy header
|
|
fields.
|
|
|
|
Nothing is executed, slid or extracted. Slide info, local symbols and the
|
|
cache's code signature are not parsed. The Evidence subject is the main cache
|
|
file, identified by its digest.
|