rea-reverse

iOS 描述文件与 IPA 查看器

拖入 .mobileprovision、.provisionprofile、.ipa、压缩的 .app 或 .xcarchive、Info.plist 或 .entitlements 文件,就能看清里面写了什么:描述文件类型、过期时间、团队、设备和证书;App 的 Bundle ID、隐私权限说明和内嵌框架;以及可执行文件实际签进去的 entitlements,并逐项和描述文件对比。

描述文件和安装包不会离开你的浏览器. 文件在本地一个没有网络权限的 Web Worker 里读取。IPA 通过 ZIP 目录按需读取:只打开各个 plist、内嵌描述文件,以及可执行文件的头部和签名。 如何自己验证

把描述文件、IPA 或 plist 拖到这里

或

支持 .mobileprovision、.provisionprofile、.ipa、压缩的 .app 或 .xcarchive(含 ZIP64,最大 16 GiB)、Info.plist(XML 或二进制)、.entitlements、PrivacyInfo.xcprivacy,以及单独的 Mach-O 可执行文件。

这个工具不做什么

  • 不校验签名:既不校验 Apple 对描述文件的 CMS 签名,也不校验 App 的代码签名。这里看着没问题的文件,如果被改过,设备照样会拒装。
  • 不解密、不重签、不修改任何东西。App Store 下载的包(cryptid 1)只有头部和签名可读,而本工具也只读这些。
  • 不交叉检查每个 App 扩展自己的内嵌描述文件和 entitlements;需要的话把扩展的描述文件单独拖进来。

iOS 测试包装不上的时候,报错很少直接告诉你签名的哪一环出了问题。“无法安装”“The executable was signed with invalid entitlements”(0xe8008016)“A valid provisioning profile for this executable was not found”(0xe8008015),归根到底都落在两处的几项信息上:描述文件,以及可执行文件的代码签名。在 Mac 上一般这样看:

security cms -D -i embedded.mobileprovision
codesign -d --entitlements :- Payload/MyApp.app

这个页面直接从文件里读出同样的数据,在浏览器里完成,并把两边并排对照。它面向的是检查自己的包和描述文件:测试机为什么装不上、导出的 IPA 里到底塞的是哪个描述文件、推送 entitlement 写的是 development 还是 production。

可以拖进来的文件

  • 描述文件:.mobileprovision(iOS、iPadOS、tvOS、watchOS、visionOS)和 macOS 的 .provisionprofile。描述文件是一个 CMS SignedData,里面包着 XML plist;现在的描述文件用定长编码,早期的描述文件和 openssl cms -stream 生成的是 BER 不定长编码,两种都能读。security cms -D 输出的 XML 也可以直接拖进来。
  • 安装包:.ipa、压缩过的 .app(访达里的“压缩”)或压缩过的 .xcarchive。先只读 ZIP 中央目录,然后只打开 App 的 Info.plist、embedded.mobileprovision、PrivacyInfo.xcprivacy、各扩展的 Info.plist 和主可执行文件。几个 GB 的 IPA 也不会整个读进内存,支持 ZIP64。
  • 单个文件:XML 或二进制(bplist00)格式的 Info.plist、.entitlements、PrivacyInfo.xcprivacy,或者一个单独的 Mach-O 可执行文件(单架构或通用二进制),用来列出签名里的 entitlements。

能看到什么

描述文件:名称、App ID 名称、App ID(application-identifier,带团队前缀)、UUID、团队 ID 和名称、平台、创建和过期时间(以及还剩几天)、是否由 Xcode 管理、完整的 Entitlements 字典、注册设备的 UDID 列表(带搜索框)、开发者证书(通用名、取自 OU 字段的团队、SHA-1、有效期),以及 CMS 签名者的证书。

描述文件类型并不写在文件里,这里按 Xcode 和 fastlane 的判断方式推出来:

描述文件包含 类型
ProvisionsAllDevices = true 企业(In-House);macOS 上是 Developer ID
ProvisionedDevices,且 get-task-allow = true 开发(Development)
ProvisionedDevices,get-task-allow = false Ad Hoc
两者都没有 App Store(macOS 上是 Mac App Store)

App:Bundle ID、版本和 build 号、最低系统版本、SDK 和 Xcode 版本、设备类型、支持的平台、URL Scheme、必需的设备能力、App Transport Security 配置、每一条 NS…UsageDescription 及其文案、隐私清单(追踪开关、追踪域名、收集的数据类型,以及每个需要理由的 API 类别和理由代码,例如 CA92.1)、内嵌的框架和 dylib(标出自带隐私清单的那些),以及 App 扩展的 Bundle ID 和扩展点。

可执行文件:各架构切片(arm64、arm64e 等)及其 LC_BUILD_VERSION 里的平台和最低系统版本、LC_ENCRYPTION_INFO_64 的 cryptid(为 1 表示 App Store 下载的包,代码是加密的)、CodeDirectory 里的签名标识符和团队、是否为 ad hoc 签名、签名证书,以及代码签名里两份 entitlements:XML plist(slot 5)和 iOS 15 起实际校验的 DER 版本(slot 7)。两份不一致时页面会提示。

会做哪些检查

拖入的是带描述文件的包时,页面会把常见安装失败背后的几项逐一核对:

  • 过期:描述文件已过期,或 30 天内过期。
  • Bundle ID:CFBundleIdentifier 必须匹配描述文件 App ID 去掉团队前缀后的部分,* 是通配符(TEAMID.com.example.*)。
  • Entitlements:可执行文件签入的每一项 entitlement 都必须在描述文件允许的范围内。字符串和数组可以用描述文件里的通配符(钥匙串分组的 TEAMID.*、Associated Domains 的 *);App Store 或 Ad Hoc 包里 get-task-allow 为 true、production 描述文件配上 development 的 aps-environment、别的团队的钥匙串分组、描述文件没有开通的能力(iCloud、App Groups),都会列出键名和两边的值。macOS 上 com.apple.security. 开头的沙盒和 Hardened Runtime 权限不需要描述文件,会单独标出。
  • 团队:签入的 application-identifier(或 CodeDirectory)里的团队与描述文件的团队是否一致。
  • 签名证书:代码签名里的证书必须在描述文件的 DeveloperCertificates 里,按 SHA-1 比对。对应的就是“描述文件不包含签名证书”这种情况。
  • 扩展:每个 .appex 的 Bundle ID 都必须以主 App 的 Bundle ID 加一个点开头。

想确认某台设备在不在描述文件里,把它的 UDID 粘贴到设备列表上方的输入框。比对只在页面里进行,不会发送到任何地方。

我们是怎么验证的

测试样本都是合成的,由 test/ios/build.sh 在 macOS 27.0.1、Xcode 27.0(iPhoneOS 27.0 SDK)和 OpenSSL 4.0.3 上生成:一个一次性的 CA、一张“开发者”证书和一张描述文件签名证书;五个手写的描述文件(开发、BER 不定长编码的 Ad Hoc、已过期的 App Store、App ID 为 * 的企业版,以及一个 macOS 开发描述文件),用 openssl cms -sign -binary -nodetach 签名;再把一个很小的 C 程序编译成 arm64 和 arm64e,用 codesign --entitlements --generate-entitlement-der 签名后打包成 Payload/Fixture.app。另一个“错误”的包故意做错了一堆东西:Bundle ID 不对、对着 App Store 描述文件签了 get-task-allow true 和开发环境推送、用了别的团队的钥匙串分组、多了一个描述文件没有的 iCloud 权限、签名证书不在描述文件里、Widget 扩展的 Bundle ID 前缀不对,还有一个切片的 cryptid 被改成了 1。单元测试把解析结果和 security cms -D、plutil、codesign -d --entitlements(XML 和 --der 两种)、lipo -archs、otool -l 的输出逐项比对。我们还用解析器跑了一遍这台 Mac 上 Xcode 下载的 20 个真实描述文件:名称、UUID、团队、过期时间、设备列表、entitlements 和证书 SHA-1 全部 20 个都与 security cms -D 一致(这些数据不在本站和测试里出现)。

做样本时踩了两个坑。plutil -convert json 会拒绝解码后的描述文件,报 “Invalid object in plist for JSON format”,因为描述文件里有 JSON 表示不了的 <date> 和 <data>,所以构建脚本改用 plutil -p 生成参照输出。另外 codesign -dv 根本不打印 Authority= 行,要用 -dvvv 才能看到是哪张证书签的。如果把 security cms -D -i 直接用在 .ipa 上而不是里面的描述文件,会报 security: failed to add data to decoder: UNKNOWN (-8183(d):先解压,或者直接把 IPA 拖到这里。

你的文件如何保持私密

  • 文件在你设备上的 Web Worker 里读取,不上传,这个页面也没有服务端。
  • UDID、团队 ID、Bundle ID 和证书信息只显示在页面上。粘贴的 UDID 只在页面里比对,不保存。
  • 统计事件只含粗粒度的值:成功或错误码、文件类型(profile、ipa 等)、推出的描述文件类型、未通过的检查项数量区间,以及文件大小和耗时区间。完整列表见隐私政策。
  • 工具页不展示广告。

如何自己验证

  1. 打开浏览器开发者工具的 Network 面板,拖入一个 IPA。所有请求只有页面资源和 Worker 脚本 /_astro/ios-worker-*.js。
  2. 或者加载页面后断网,再拖入一个描述文件,照样能用。
  3. Worker 运行在内容安全策略 default-src 'none'; script-src 'self'; connect-src 'none' 下,浏览器会拦下它发出的任何请求。
  4. 用你自己的工具对照:security cms -D -i file.mobileprovision、codesign -d --entitlements :- MyApp.app 和 otool -l MyApp | grep -A4 LC_ENCRYPTION 会给出同样的值。

局限

  • 不做任何密码学校验。 既不校验 Apple 对描述文件的签名,也不校验 App 的代码签名;签名后被改过的描述文件或二进制在这里看着一切正常,但设备会拒绝。
  • 不解密、不重签、不修改。App Store 下载的包只读取 load command 和签名。
  • App 扩展只列出 Bundle ID,不交叉检查它们自己的描述文件和 entitlements。需要的话把扩展的 embedded.mobileprovision 单独拖进来。
  • Entitlements 的比对规则覆盖常见情况(精确值、* 通配符、数组取子集)。Apple 安装器还有一些特殊规则这里没有模拟,例如只授予 Apple 自己签名代码的权限。
  • 设备列表来自描述文件本身。设备在开发者账号里是否仍处于启用状态,从文件里看不出来。

常见问题

我的 IPA 里用的是哪个描述文件?

就是 Payload/<App>.app/embedded.mobileprovision,页面在 描述文件 一节显示它的名称和 UUID。Xcode 导出时选中的描述文件可能和你以为的不一样,自动签名时尤其如此;看 UUID 就知道到底是哪一个。

描述文件里明明有这个 entitlement,还是装不上。

看 签入的值 那一列:不光键要有,值也必须被允许。用发布描述文件签名的包里 aps-environment 却是 development,是最常见的情况;钥匙串分组的团队前缀写错也很常见。再看签名证书那一行:证书续期后重新生成的描述文件里,已经没有旧证书了。

怎么查设备的 UDID?

连上设备,在访达里点设备名称下方那行文字,直到出现 UDID;或者在 Xcode 的 Window > Devices and Simulators 里查看。

相关工具和教程