经常有同行问我一个问题:客户只给了一个 IPA 包,源码早就不在了,还能不能做 iOS 成品包加固?我第一次接手这种需求时,心里是打鼓的,因为按常规思路,加固不都是从源码工程的编译阶段开始的吗?手里只有一个成品包,似乎连下手的地方都找不到。但真正把一个企业签名的 IPA 拆开、逐层过完之后,我得说:只有 IPA 的情况下,iOS 成品包加固能做的事情远比想象中多,只是解题思路和源码级加固完全不一样。
这篇文章适合三类人看:源码遗失或交接断档、只剩安装包的开发者;负责企业内部应用加固和分发整改的运维或安全同学;以及单纯想搞明白“一个 .app 里到底装了什么”的 iOS 技术爱好者。先说结论:无源码加固的核心不是“改代码”,而是“把能改的层改扎实,把不能改的层看守住”。配置层、资源层可以放心动手,二进制层以审计为主,最后所有改动都要落回重签和真机回归。下面是我实操过的完整记录。
1. 先摸透三层结构:只有 IPA 时你实际上在加固什么
“只有 IPA”这句话最容易被误解的地方,是很多人把 IPA 当成一个拆不开的成品黑盒。其实 IPA 就是改了后缀的 zip 压缩包,拆开后里面是 Payload 目录,再往里走是xxx.app这个 bundle。一个典型的 .app 结构长这样:
unzip App.ipa -d ipa_unpack find ipa_unpack/Payload -maxdepth 2 -type d # 典型输出 # ipa_unpack/Payload/YourApp.app # ipa_unpack/Payload/YourApp.app/Frameworks # ipa_unpack/Payload/YourApp.app/PlugIns把 .app 拆成三个层次看,后面每一步加固都会落到对应层上:
| 层次 | 主要改动能力 | 风险 | 是否需要重签 |
|---|---|---|---|
| 配置层(Info.plist / entitlements) | 收紧 ATS、清理后台模式、调整文件共享开关 | 低,误改会影响具体功能 | 是 |
| 资源层(数据库 / 证书 / 本地化文件等) | 删除冗余文件、检索密钥泄漏、清理脏文件 | 低,删除被依赖文件会引发运行时问题 | 是 |
| 二进制层(Mach-O) | 符号剥离、静态审计 | 高,改动逻辑类内容风险极大 | 是 |
三层的操作空间和风险完全不同,所以实操顺序也基本固定:先配置层,再资源层,最后二进制层。别倒过来,不然很容易在最高风险的层上做低收益的折腾。
1.1 最快判断这个包“能不能动”的两条命令
拿到 IPA 先别急着改,先用五分钟确认这个包的来源和签名类型,这决定了后面所有操作的合法性边界。
# 第一条:查看签名信息 codesign -dv --verbose=2 Payload/YourApp.app # 第二条:检查二进制是否带系统加密标记 otool -l Payload/YourApp.app/YourApp | grep -A5 LC_ENCRYPTION_INFO_64用企业证书、Ad Hoc 证书、Development 证书签出来的包,通常LC_ENCRYPTION_INFO_64里的cryptid为 0,说明没有系统级加密,这类包可以合法地做重签和整改。如果cryptid为 1,说明这个包带有 Apple 的 DRM 保护机制,多半是从 App Store 渠道来的。对于这类包,我的原则是:只做只读分析,绝不修改和重新分发。任何试图移除该保护机制的操作都违反条款,还有法律风险,不在“自己加固”的讨论范围内。
判断完签名类型,还要看一眼描述文件里带的权限。用下面命令可以读出 embedded.mobileprovision 里的关键内容:
security cms -D -i Payload/YourApp.app/embedded.mobileprovision > provision.plist重点读application-identifier、get-task-allow、ProvisionsAllDevices这几个字段。get-task-allow为 true 表示允许调试器附加,生产包如果还是 true,说明当时的构建配置有问题。这是第一个可以立刻修的问题——但它是写在 entitlements 里的,改完同样要重签。
2. 第一道工序:Info.plist 与资源层的“体检式”整改
配置层和资源层的改动安全系数最高,而且很多隐患恰恰藏在这里。我先处理这两层,相当于给包做一次全面体检。
2.1 Info.plist 里最容易出问题的几个开关
先用 plutil 把 Info.plist 转成可读格式:
plutil -p Payload/YourApp.app/Info.plist重点检查这几项。
ATS 配置:如果看到
NSAppTransportSecurity下挂着NSAllowsArbitraryLoads=1,这就是一个典型的“允许所有明文网络”配置。处理方式不是无脑改成 NO,而是先确认业务里还有没有 http 接口。如果应用已经完全走 https,就直接收紧;如果还有个别 http 域名,用NSExceptionDomains只放行已知域名。这个改动不用碰代码逻辑,但能显著缩小网络层的暴露面。文件共享开关:
UIFileSharingEnabled和LSSupportsOpeningDocumentsInPlace如果为 true,用户可以通过系统的“文件”App 直接看到应用沙盒里的文档目录。企业应用如果没有“导出文件”的需求,建议关掉。这是很多人容易忽略的、面向用户的数据泄漏口。注册的 URL Scheme:看
CFBundleURLTypes里注册的 scheme。如果注册了类似yourapp://这种敏感 scheme,意味着任意安装的 App 都能尝试唤起你的应用并传递参数。唤起后的参数解析逻辑在代码里,没法改,但至少要把这个暴露面记录下来,后续在客户端侧加参数校验时有个清单可依。后台模式:
UIBackgroundModes里的每一项都对应一个后台运行能力声明。多余的声明不仅容易被审核盯上,也会成为安全事故排查时的干扰项,能去掉就去掉。
2.2 资源层的“垃圾清理”和密钥排查
资源层最容易犯的错,是把生产包打成开发目录。拿出一个生产包,用 find 把所有文件列出来,经常能发现一堆不该出现在线上应用的资产:
find Payload/YourApp.app -type f | sed 's|.*/||' | sort重点找这几类东西:
- 证书与私钥文件:
.p12、.cer、.key、.pem、.der。一旦被打进包里,等于把凭据送给了每一个拿到 IPA 的人。 - 内部配置与文档:
.md、.txt、config.json、debug.plist这类文件里经常写着内部地址、测试账号、数据库连接串。 - 未加密的数据库:
sqlite、realm文件如果整包带着,说明业务数据要么没加密,要么加密逻辑极弱,至少要评估里面存了什么。 - 本地化字符串文件:
.lproj里的 Localizable.strings 常常藏着 API 域名、内部路径、错误提示里的敏感信息,值得全文搜一遍。
实际操作里,我会顺手在资源目录里搜几类关键词:
grep -rniE "(api[_-]?key|secret|password|BEGIN (RSA |CERTIFICATE))" \ Payload/YourApp.app \ --include="*.plist" --include="*.json" --include="*.strings" -l搜出来的东西不用急着删。我的经验是:先建立一份清单,区分“可以安全移除”和“删除会导致功能异常”两类。比如某张宣传图被错误打进 bundle,移除没有副作用;但某个内置的推送证书如果被业务在启动时读取,移除就会导致推送功能失效。这需要结合崩溃报告或业务方的确认来判断,千万不要为了“干净”一刀切。任何资源层改动都会改变 bundle 内容,所以后续必须重签。
3. 第二道工序:Mach-O 二进制的静态体检与修改边界
配置层和资源层处理完后,重头戏是那个 Mach-O 可执行文件。这里必须先把预期管理好:没有源码,二进制层的“加固”绝大多数是审计,而不是改造。
3.1 给二进制做体检的几条命令
# 查看二进制架构与类型 file Payload/YourApp.app/YourApp # 查看链接了哪些动态库 otool -L Payload/YourApp.app/YourApp # 查看导出的符号 nm -gU Payload/YourApp.app/YourApp | head -50 # 检索硬编码的敏感字符串 strings Payload/YourApp.app/YourApp | grep -iE "(api[_-]?key|secret|token|password)"经验之谈:很多企业包为了排查线上问题,编译时没把符号剥离干净,nm -gU能看到较多全局符号,otool -ov甚至能枚举出 ObjC 的类名和方法列表。这类信息对逆向者来说就是“源代码地图”。如果确认包没剥离符号,在重签之前可以用 strip 做一次剥离:
strip -x Payload/YourApp.app/YourApp # 移除本地符号但一定要清楚代价:strip 会修改二进制,破坏原有签名,所以必须在重签流程里做;而且剥离后崩溃日志的可读性下降,需要和稳定性监控方案做权衡。我个人的建议是:只在“线上稳定性监控完备、团队能接受符号化成本”时才做这步,否则宁可保留符号也不要给自己制造排查障碍。
3.2 二进制层“能改”与“不能改”的分界线
能改的:符号剥离,以及上一节说的 Info.plist 键值调整。这些属于“不碰逻辑”的改动,改动后重签即可。
不能改的:任何涉及指令逻辑的改动。比如想给启动流程加一段防调试代码、想加密某个硬编码字符串,这些操作的本质是“改代码”。没有源码时只能靠注入动态库或直接 patch 机器码来实现,但对一个生产包来说风险极高:注入的代码和原逻辑的兼容性没有编译期保证,一个线程安全问题就能让线上应用频繁崩溃;注入动态库之后,Apple 对签名和审核的合规要求也基本无法满足。这类操作我不建议在生产包里做,更不建议拿它去替代正规的安全工程。
换句话说,二进制层的正确动作是“知道里面有什么”,而不是“强行把它变成别的东西”。硬编码密钥、明文协议这类问题,单靠改一个 IPA 是无解的,正确的处理方式是回源码修、发新版,然后把“禁止硬编码密钥”写进 CI 检查。
4. 把改动落回真机:重签、安装与回归的完整链路
改完任何东西都要重签,这是铁律。重签不是跑一条命令那么简单,顺序错了或者 entitlements 丢了,应用装到真机上可能直接闪退,甚至连启动都进不去。
4.1 重签名的正确顺序
我常用的流程分四步:先签嵌套组件,再签主应用,最后整体验证。
# 第一步:导出原包 entitlements,作为重签基准 codesign -d --entitlements - Payload/YourApp.app > entitlements.plist 2>/dev/null || true # 第二步:逐个签名 Frameworks 和 PlugIns(从内到外) codesign -f -s "你的证书名称" Payload/YourApp.app/Frameworks/*.framework codesign -f -s "你的证书名称" Payload/YourApp.app/PlugIns/*.appex # 第三步:签名主应用,并带上 entitlements codesign -f -s "你的证书名称" --entitlements entitlements.plist Payload/YourApp.app # 第四步:整体验证 codesign --verify --deep --strict Payload/YourApp.app这四步里最容易翻车的点有三个:
- 签名顺序:必须先内后外,最后签主 bundle。顺序反了,嵌套框架的签名校验会失败。
- entitlements 匹配:
application-identifier必须和描述文件里的 App ID 一致,aps-environment决定推送通道,这些值不能手改。我见过有人图省事直接删掉 entitlements 重签,结果 App 启动后 Keychain 访问一片报错,因为 Keychain 访问组的 entitlement 丢了。 - 描述文件:重签用的证书要匹配描述文件,Ad Hoc 类型还要确认目标设备 UUID 在
ProvisionedDevices列表里,不然安装时会被拒绝。
注意:修改 bundle ID 这种事情,在重签阶段千万不要做。bundle ID 一变,Keychain 数据、推送通道、内购记录、Universal Links 全部会失效,代价远超收益。
重签通过后,顺手把整个 .app 的哈希打一份基线,留给日后排查:
find Payload/YourApp.app -type f -exec shasum -a 256 {} \; | shasum -a 2564.2 真机安装与回归清单
重签完成后,把 Payload 目录压回 zip 并改名 .ipa,就可以安装到真机上了。安装方式可以是 Apple Configurator、Xcode 的 Devices 窗口,或者 ideviceinstaller 这类命令行工具。安装前确认两件事:设备已开启开发者模式,并且已信任对应的开发者证书。
回归测试不是随便点几下就完事,至少要覆盖这几条路径:
- 应用能否正常启动,有没有启动即闪退的现象。
- 登录态是否还在,Keychain 数据有没有丢。
- 推送能否正常到达,特别是企业应用常用的静默推送。
- 内购或支付流程是否正常,这类功能对 entitlements 极其敏感。
- 如果有 Widget、Share Extension 这类插件,要在真实场景里各跑一遍。
重签理论上不该改逻辑,但签名和 entitlements 层面的问题只会在真机上暴露,所以这步不能省。
5. 边界地图:无源码做不到的,和绝对不能碰的
“只有 IPA 能做什么”这个问题,一半的答案其实是“不能做什么”。把边界想清楚,能省下大量无效工作。
5.1 没有源码就做不到的加固项
- 编译期代码混淆:字符串加密、控制流平坦化、逻辑混淆这些能力都寄生在编译器流程里,对已有二进制无能为力。
- 业务逻辑层的防篡改:如果攻击者定位到了某个关键判断,想改的是“逻辑”,这已经接近逆向重写,不在加固范畴。
- 全面的代码虚拟化:需要定制编译器工具链,同样依赖源码工程。
- 安全的密钥管理:密钥如果硬编码在代码里,必须从源码侧改用 Keychain、Secure Enclave 或服务端方案,改包解决不了。
说到这里必须承认一个扎心的事实:无源码加固解决的是“暴露面积”,不是“代码强度”。它能帮你收敛配置、清理资源、关掉调试后门,但不能把一个本来就不安全的包变成安全的包。
5.2 合规层面不能碰的红线
- 没有权利和授权,不要对第三方的包做任何修改或重签。
- 不要绕过授权校验、内购流程或软件许可;任何以“加固”为名实为破解的动作都在红线内。
- 不要对来自 App Store 的加密包做移除保护后再分发,这类操作违反条款且有法律风险。
- 不要以为“改了包本地能跑”就等于“没问题”:App Store 上架的完整包必须从源码工程构建,重打包产物过不了审核。
- 企业证书重签后的分发必须在自己公司或客户的授权范围内。证书滥用导致开发者账号被封是真实发生过的事,管理上要认真对待。
6. 实操后的经验清单
最后把这几年的实操心得整理成一份清单,算是给后面接手的人留个底。
- 先做基线再动手:动手改包前,记录原始文件的哈希、文件列表、entitlements。后面无论做 diff 还是出事故往回查,都靠它。
- 最小化改动:每次只改一个层面,改完立刻重签验证。不要同时改 Info.plist、资源、符号再重新压包,否则出了问题你根本定位不到是哪一步导致的。
- 保留重签脚本:把 codesign 流程写成脚本放进版本库。即使下次又只剩一个包,恢复链路也能在两分钟内跑完。
- 善用崩溃日志:重签后的首个版本一定要在真实设备上跑够登录、支付、推送这些核心路径。重签不该改逻辑,但签名和 entitlements 层面的问题只会在真机上暴露。
- 别把无源码加固当终点:如果还能找到源码或 CI 流水线的任何碎片——比如 .dSYM、构建脚本、旧版本仓库——都要尽力找回。真正的加固应该在编译期做,无源码操作只是补救措施。
最后再分享一个我常对团队说的话:无源码加固像给已经装修好的房子加锁,能把门锁、窗户、门禁都检查一遍,但改不了房子的承重结构。把配置层和资源层的洞补好,把签名链路管住,已经能拦住大部分漫无目的的扫描式攻击;至于更高强度的保护,还是得靠恢复源码、把安全能力前置到开发流程里。真到了哪一天你手里的包又能从源码完整构建了,再回头看看这次的补课记录,你会发现它没有白折腾——那些检查和整改项,本身就是一份现成的安全验收清单。