Apple 应用清单投影¶
REA 可以将同一个 IPA、macOS .app 目录、ZIP 或 DMG 的一条或多条经过认证的 inventory_artifact Evidence 记录投影为 Apple 应用清单。该投影是免执行的:它报告精确路径、内容哈希、检测到的格式、包结构、运行时家族提示以及基于路径的桥接假设。它不解析 plist 或 CMS 语义,也不声称观察到运行时调用。
框架提示匹配完整路径段,例如 React.framework、ReactNative.framework、hermes.framework、Flutter.framework 和 UnityFramework.framework。Preact.framework、Community.framework 或 MyApp.framework 中的子字符串不能识别这些家族。通用的 App.framework 本身也不能单独识别 Flutter。运行时家族仍然是源于清单的提示,而非观察到的加载。通用框架和 dylib 路径不能确定 Swift 或 Objective-C;此投影不解码语言元数据。桥接依据是从原生路径推断出来的,并为每个配对的脚本重复,而不是证明特定的绑定。
{
"name": "project_apple_application_graph",
"arguments": {
"inventory_evidence": ["<inventory_artifact Evidence>"]
}
}
结果包含每个清单类别中的所有组件。桥接假设是每个应用根目录内基于路径的 JavaScript/原生配对。为了限制其笛卡尔积展开的规模,投影将序列化的候选数组限制为 2 MiB。它会在创建候选对象之前计算配对数量和精确的 UTF-8 JSON 大小;当字节预算超出时,保留一个确定性前缀。结果的 bridge_candidate_coverage 报告总数、已输出和省略的配对数量;整体覆盖率变为 partial,并由一条限制说明该省略。重复的清单页按工件身份和出现路径合并;其 Evidence ID 仍作为源 Evidence 保留。
内容节点通过 SHA-256 标识字节,并且只报告由内容推导出的格式。每次出现都会报告自己的 artifact_kind、artifact_format 和可执行权限。因此,相同的字节可以在不同路径下作为脚本、资源或原生插件出现,而不会覆盖另一个出现的角色。APK 和 IPA 等包格式属于该出现;它们的 ZIP 字节共享一种内容格式。图和目录身份使用与区域设置无关的 Unicode 排序。
对于 IPA 清单,位于每个应用根目录之外的归档组件仍保留在组件列表中。它们的 JavaScript 和原生候选会与其他未挂根组件分组,从而保留清单投影的路径配对语义,而不将它们归属于某个应用包。
当提供的页面重构出完整的经认证清单时,覆盖率为 complete-within-inventory,否则为 partial。partial 结果表示缺失情况未知。当候选假设被省略时,组件数组仍然完整;组件数组仍包含来自所提供清单页的每个组件,此时整体覆盖率为 partial。
未读取其字节的已清单化文件(例如加密归档成员)会列在 unavailable_components 中,包含原始路径、格式、加密标志、限制和空内容身份。同样的应用根目录和 AppleDouble 归属规则适用。这些文件会使整体覆盖率变为 partial;即使 inventory_complete 为真,组件缺失情况也未知。目录没有文件字节,符号链接保留在 symlinks 中,因此二者都不是不可用的文件组件。该附加字段参与 projection_id(包括为空时),因此 Apple 投影标识符会变化。
应用根目录¶
| 清单根目录 | 应用根目录 |
|---|---|
| IPA | 每个 Payload/<Name>.app |
.app 目录 |
.,即被清单化的包本身 |
| 包含应用的目录、ZIP 或 DMG | 每个最外层的 <Name>.app,其后跟随 Contents/ |
文件和切片出现提供组件端点。目录树身份保留在源清单和包结构中,链接保留在 symlinks 中。
根目录和布局发现使用已知目录以及由更深层清单路径所隐含的祖先目录。名为 *.app、Contents 或 Versions 的叶文件或符号链接不能确立该目录。目录与经认证文件身份会在应用结构大小写偏好之前选定,因此文件无法遮蔽折叠后的目录名,目录也无法遮蔽元数据/签名文件。
使用 ditto -c -k --keepParent 创建的 ZIP 归档会报告 <Name>.app。DMG 会报告 <image>.dmg/<Volume>/<Name>.app;DMG 子清单需要原生 macOS 挂载适配器。当 DMG 只有其根身份时,投影为 partial,并说明包是否存在未知。其他清单格式(如 APK 或 ASAR)会被作为输入错误拒绝。
名称以 .app 结尾的已清单化目录,即使其清单不完整或缺少 Contents/,也是自身的根目录。对于目录、ZIP 和 DMG 清单,位于每个应用根目录之外的组件不会被归属于该应用。这包括安装程序、磁盘映像附加内容和 AppleDouble 侧车文件,因此它们不会进入运行时家族和桥接候选。一条限制会报告有多少已清单化条目被排除在外。IPA 投影仍会列出每个归档组件,包括 SwiftSupport/。
platforms 为带有 Contents/ 的应用根目录列出 macos。为具有 Info.plist 的浅层应用根目录列出 ios。两者都没有的应用根目录(例如空清单)没有平台。
macOS bundle 结构¶
bundles 列出每个应用程序根目录及其下的所有嵌套 bundle。每个 bundle 都会报告其 parent_path、layout、info_plist_path、executable_candidates 和 signing_paths。
| Layout | 含义 | 内容目录 |
|---|---|---|
macos-deep |
包含 Contents/ |
Contents/,可执行文件位于 Contents/MacOS/ |
versioned-framework |
包含 Versions/ |
每个实际的 Versions/<version>/ |
shallow |
两者都不包含 | bundle 目录本身 |
角色是路径约定(role_basis: "path-convention"):
| 角色 | 路径 |
|---|---|
app-extension |
*.appex |
xpc-service |
*.xpc,包括 framework 内的 XPC 服务 |
framework |
*.framework |
system-extension |
*.systemextension |
driver-extension |
*.dext |
plug-in |
PlugIns/*.bundle、*.plugin、*.qlgenerator、*.mdimporter |
resource-bundle |
Resources/*.bundle,例如 SwiftPM 资源 bundle |
bundle |
任何其他 *.bundle |
login-item |
Contents/Library/LoginItems/*.app |
helper-application |
Contents/Helpers/*.app |
nested-application |
任何其他嵌套的 *.app |
这些组件还会列出:
privileged_helpers:直接位于Contents/Library/LaunchServices/中的文件,即 SMJobBless 辅助程序所在位置;launchd_plists:直接位于Contents/Library/LaunchAgents/(domain: "agent")或Contents/Library/LaunchDaemons/(domain: "daemon")中的.plist文件,供 SMAppService 使用;helpers:直接位于Contents/Helpers/中的文件。
符号链接按路径记录在 symlinks 中。这些链接目标不会被登记在清单中,因此版本化 framework 是通过其实际的 Versions/<version>/ 目录来描述的,而不是通过 Versions/Current。当某个 framework 拥有多个实际的 Versions/<version>/ 目录(即使其中只有一个包含 plist,因为 Versions/Current 决定哪一个生效),或者其唯一版本目录没有 Resources/Info.plist 时,该 framework 的 info_plist_path 为 null,并附有限制说明解释原因。
AppleDouble 伴生文件(._* 和 __MACOSX/)属于清单记录项,但它们描述的是相邻文件。它们被排除在根目录、bundle 角色和结构组件之外。
该投影不会读取 plist。要对某个 bundle 的 info_plist_path 或 launchd plist 使用 inspect_plist,以读取 CFBundleExecutable、标识符、SMPrivilegedExecutables、BundleProgram、URL scheme 和用途描述。MCP 调用要求存在已建立的 open_binary 或 open_bundle_resources 会话,即使 inspect_plist.path 是显式指定的也不例外。对于未解析出可执行文件的 bundle,请先打开其资源会话。CLI 命令 rea inspect-plist <path> 会在其单次调用期间打开所选工件。对某个可执行候选者使用 inspect_signature 可获取代码签名声明。
Dylib 加载解析¶
trace_dylib_resolution 用于回答每个 Mach-O dylib 加载最终到达哪个文件。它运行在活动 Mach-O 上,或活动 .app 中的每个可执行文件上。bundle 中的每个可执行文件(主应用、XPC 服务、应用扩展、登录项、特权辅助程序和辅助工具)都是独立的进程根,拥有各自的 @executable_path。
rea trace-dylib-resolution MyApp.app --json
rea trace-dylib-resolution MyApp.app --root Contents/MacOS/MyApp --architecture arm64 --json
{
"name": "trace_dylib_resolution",
"arguments": { "roots": ["Contents/MacOS/MyApp"], "architecture": "arm64" }
}
加载命令是在 TypeScript 中从 Mach-O 和 FAT 头解析的,包括较新链接器为延迟初始化等标志生成的 dylib_use_command 编码,以及 LC_LAZY_LOAD_DYLIB_INFO(0x3a)。该命令将安装名称存储在 linkedit 负载中,而不是存储于加载命令本身。其标志和链起始镜像偏移必须位于 slice 的 VM 范围内;该范围是 dyld 的单页下限,再加上除 __LINKEDIT 之外的每个段,从 __TEXT vmaddr 起算。负载字符串必须从 24 字节头之后开始。加载路径的 NUL 字节必须出现在负载最后一个字节之前,以匹配 dyld 的扫描方式。不同的负载字节和负载范围查找次数保持在 16 MiB 限制内,并且已经读取过的文件范围会被复用。字符串扫描和 UTF-8 检查会对每次引用(包括重复和重叠)计数,并在达到同一总量时停止。每个 slice 最多保留 1024 条这样的命令;更多副本不受支持,而不会各自成为一条被追踪的边。符号引用(包括重复)每 slice 最多 65536 个。对于更大的表,工具会报告不支持,而不会按偏移量缓存单个字符串。不会执行任何代码,也不需要任何 Apple 工具,因此该工具在 macOS 以外的系统上也能运行。
解析遵循 dyld 的规则:
@executable_path是进程根可执行文件所在的目录。当根是库时,它是无法确定的。@loader_path是持有加载命令的镜像的目录,或者是拥有正在展开的LC_RPATH条目的镜像的目录。@rpath会先尝试正在加载的镜像的每个LC_RPATH,再尝试加载了该镜像的镜像的LC_RPATH,一直到进程根为止。- 依赖项按加载命令顺序以 dependents-first 方式加载,并且每个镜像在每个进程中只加载一次。后续 @rpath 请求的安装名称与已加载镜像匹配时,会复用该镜像。
- 在分析根目录内,会跟随诸如
Versions/Current之类的符号链接。通过..或绝对链接逃逸的情况记为escapes-target。 - Dyld 路径组件是 POSIX 名称。在 Windows 主机上,链接文本使用
\作为分隔符;包含\或:的 dyld 组件无法命名分析根目录中的文件,因此其候选为absent。
每条边都会列出所有尝试过的候选路径及其结果:resolved、absent、not-mach-o、malformed、unsupported(解析器覆盖范围之外但可识别的 Mach-O,例如大端序映像)、architecture-missing、not-loadable、platform-mismatch、outside-target、escapes-target 或 undetermined。not-loadable 表示找到了架构匹配的 Mach-O 映像,但它不是 dylib,无法满足 dylib 依赖。platform-mismatch 表示找到了架构匹配的 dylib,但其观察到的平台与进程不兼容。这些结果会拒绝该候选;dyld 搜索可能会继续并解析到更靠后的候选。候选结果描述的是单个搜索路径,而 resolution.status 描述的是最终边。对于每个选定的进程根切片,候选切片按 CPU 类型和 dyld 的子类型偏好顺序进行匹配,与 FAT 表顺序无关。ARMv7s 根切片优先选择 ARMv7s,然后是 ARMv7;其他已知的 ARM32 子类型使用 Apple 的 dyld 源码 中的兼容性行。x86_64h 进程也会加载通用的 x86_64。当存在 ARM32 候选切片时,未知的 ARM32 子类型策略仍保持未确定状态。这是一个以选定根切片为键的静态模型;它不会观察目标设备的主机 CPU,也无法证明设备将执行哪个切片。该边的 resolution 是以下之一:
| 状态 | 含义 |
|---|---|
resolved |
分析根内的某个映像会加载,并且没有任何更早的候选是不可确定的。 |
conditional |
根内的映像可能加载,但更早的外部候选、未建模的环境输入或未知的平台兼容性可能会改变 dyld 实际加载的映像。 |
unresolved |
所有候选在根内都确定不存在或不可用。 |
undetermined |
根内没有候选可解析,并且某个候选位于根之外或依赖于环境。 |
conditional 映像仍会被追踪,因为它是可能加载的根内候选。其下的每条边都有 loader_conditional: true,通过安装名称复用它产生的请求也是 conditional。没有 platform 加载命令的根内 dylib 仍保持未知,除非根是可执行文件且观察到 macOS 平台,并且候选的 CPU 类型是 x86_64、i386 或 arm64。在这种 macOS 上下文中,Apple 的 dyld 会为这些 CPU 类型上的旧版 dylib 提供一个隐式的 macOS 平台。该候选记录 platform_basis: "macos-dyld-implicit";所报告的切片字段 platform: null 和 platforms: [] 仍然是解析得到的事实。对于其他进程上下文,平台仍然未知,覆盖不完整,且该候选不会被当作不兼容处理。如果之后某个已知候选解析成功,它仍是具有条件解析结果的被追踪映像;该追踪不会声称未知候选的依赖图。条件边上的发现项表示:它们仅在条件映像实际加载时才适用。
指向分析根目录下路径的绝对安装名称会在该根目录内进行检查,符号链接和 .. 由同一个受限树解析器解析。根目录之外的绝对路径(如 /usr/lib/libSystem.B.dylib)是 outside-target;在 macOS 11 及更高版本上,大多数系统库位于 dyld 共享缓存中而不是磁盘上,因此 REA 不会针对宿主检查它们。Mach-O 路径字符串不会从 file: URL 转换,也不会通过与无关的宿主路径进行查找。
findings 是推导得出的,不是观察得到的:
required-load-unresolved、weak-load-unresolved和lazy-load-unresolved。LC_LAZY_LOAD_DYLIB和LC_LAZY_LOAD_DYLIB_INFO依赖会被解析但不会被遍历,因为 dyld 会在首次使用时才加载它们;未解析的此类依赖不是启动失败。LC_LAZY_LOAD_DYLIB_INFO没有兼容版本,因此这些字段为 null。其 may-be-missing 标志被报告为weak,带该标志的未解析依赖不是首次使用失败:如果映像不存在,dyld 会继续执行。earlier-rpath-candidate-absent:放在更早的已建模搜索路径上的兼容 Mach-O 可能会优先,具体取决于未建模的搜索输入和代码签名库验证。这些发现项并不能确定 dyld 将加载哪个映像;请使用inspect_signature获取签名元数据。dyld-environment-present:保留观察到的LC_DYLD_ENVIRONMENT条目。只有可执行文件根提供进程设置;库和其他非可执行根上的条目不会改变解析或覆盖。适用的可执行映像选择覆盖未被建模。
空的 dyld 设置有不同的消费方。现代 dyld 会在普通目录项和回退目录项后附加斜杠和映像名称,因此空条目可能会搜索 /child.dylib 或 /Foo.framework/Foo。REA 对这些未经检查的路径保留不确定性;空的回退不会抢占普通搜索的命中。空的版本化目录列表不会扫描目录,空后缀只会重复原始候选,因此它们不会增加搜索不确定性。所有情况下都会保留所报告的命令值。
verify:macos-bundle 会针对每个已追踪映像,用 otool -l 核对解析器的结果。它还会将两个进程根的预测加载顺序与 dyld 实际加载的映像进行比较(DYLD_PRINT_LIBRARIES)。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 rea2/rea