ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

iOS IPA资源文件安全加固:防解压、篡改与二次分发

iOS IPA资源文件安全加固:防解压、篡改与二次分发 1. 先说清楚IPA里的资源文件为什么是最好欺负的入口先讲一个我自己经历过的事。前两年帮一个客户做 iOS 应用的安全评审对方产品逻辑和核心算法都做了混淆研发觉得已经够安全了。结果我把 IPA 下载下来后缀改成 zip 解压前后只用了五分钟就拿到了他们完整的引导页大图、全套按钮切图、服务端接口地址、内购商品配置甚至在 Localizable.strings 里翻到了一条注释掉的后台登录接口。那一刻客户的表情非常精彩。这就是 iOS IPA 资源与文件安全加固要解决的问题。很多团队把精力全放在代码混淆、反调试、防注入上却忽略了 IPA 本质是一个公开的 Zip 压缩包。任何人拿到安装包都可以直接解压、查看、替换其中的文件再重新签名安装。资源文件如果没有任何保护就等于把 UI 素材、运营配置、密钥原材料和部分业务逻辑直接摊在桌面上。1.1 拿到一个 IPA 只需要三步后续风险全在这三步里攻击者拿到 IPA 的路径并不神秘常规就三步从下载站、旧版本收集渠道、或者通过正规途径获取到 .ipa 安装包。把后缀名改成 .zip用系统自带的压缩工具或命令行unzip App.ipa -d ipa_out解压。打开Payload/xxx.app目录浏览全部文件。三步走完这个 App 在你面前基本不设防。从那之后攻击者可以做的事情包括但不限于替换启动图插入广告、修改配置文件改变服务器地址、提取资源素材另作他用、在可执行文件里注入代码后再用自己的证书重新打包分发。这中间任何一步都不需要对 iOS 系统做破解级别的操作纯手工就能完成。我们做资源与文件安全加固本质上就是拉高这三步的门槛。不指望让攻击者拿不到内容而是要让他拿到内容之后无法直接用、无法无痕篡改、无法二次分发。这是资源加固和传统代码安全防护的最大区别也是很多人一开始没想清楚的地方。1.2 资源加固和代码混淆解决的是两码事代码混淆解决的是可执行二进制被人读明白的问题资源加固解决的是隐藏静态文件和资源被白嫖、被篡改的问题。两个领域有交集但目标完全不同。举个例子。代码混淆做得再好App 的引导图还是明文躺在.app目录里攻击者用assetutil就能把 Asset Catalog 里的图全部导出来。接口地址写在Info.plist或配置 plist 里不需要逆向任何二进制鼠标点开就能看到。Localizable.strings 包含的所有本地化文案用strings命令直接读很多开发者在里面藏过 Debug 开关和测试账号这些都是真实存在的风险。资源加固的思路很简单就是让这些静态文件不可直接读取、不可无感篡改、不可无脑复用。落地手段通常包含三块加密存储、完整性校验、分发链路保护。前面两块放在客户端第三块要联动服务端。这三块我会在后面的章节里逐一展开。1.3 动手前先列一张加固目标清单在开始改工程之前建议先想清楚你要护住什么。不是所有资源都需要加密也不是所有文件都要做完整性校验盲目加固只会拖慢启动速度、增加包体积还容易把自己搞崩溃。我一般会让团队先列一张表按资产等级分三档。第一档是核心资源比如主视觉素材、含敏感业务参数的配置文件、多语言文案里隐藏的提示信息、支付和内购相关配置第二档是普通资源比如常规图片、音频、字体第三档是可以公开的内容比如引导页文案、帮助文档。第一档必须加密加校验第二档至少做篡改检测第三档可以不做处理。另外还要想清楚一件事你防的是谁。如果你的 App 只上架 App Store那主要防的是普通用户和提取素材做二次开发的团队这部分人不会去深入 patch 你的加密逻辑。如果你的 App 走企业分发或者有大量历史版本散落在第三方下载站那就要按攻击者会认真研究你的加固逻辑来设计客户端加密只能算第一道门服务端必须有联动校验。弄清楚目标后面每一步才不会走偏。2. 拆一个 IPA 看看哪些文件在替你裸奔加固的第一步永远是把敌人看清。我建议你拿自己最近一次构建的 IPA 出来实际解压看一遍哪怕你已经很熟悉工程结构。因为构建产物和源码目录是两回事很多东西在工程里看着正常打包之后就暴露了。2.1 Payload 目录下的家底清单一个典型的.appbundle 长这样Payload/YourApp.app/ ├── YourApp // 可执行文件Mach-O 格式 ├── Info.plist // 应用元数据、权限声明、scheme ├── Assets.car // Asset Catalog 编译后的图片资源包 ├── embedded.mobileprovision // 描述文件包含证书和设备信息 ├── PkgInfo // 固定格式的包类型标记 ├── _CodeSignature/ │ └── CodeResources // 包内所有文件的哈希清单 ├── Frameworks/ // 动态库这里是重灾区 ├── PlugIns/ // App Extension 等扩展 ├── .lproj/ // 多语言资源目录 │ ├── Localizable.strings │ └── InfoPlist.strings ├── .nib / .storyboardc // 编译后的界面描述文件 └── 其他自定义资源目录这份清单每一样都有对应的攻击价值。Assets.car 可以被解析出原始图片Info.plist 不加密的话就是明文的字典Localizable.strings 用文本编辑器就能打开_CodeSignature/CodeResources这个目录很多人不重视但它是做完整性校验的核心依据后面第四章会重点讲。2.2 三类攻击者最爱的资源文件根据我接触过的案例攻击者最感兴趣的资源文件大概是三类。第一类是视觉素材。UI 切图、插画、动态效果用的序列帧、Live2D 模型、音频文件。这属于直接变现的资产很多下载站把这些素材拆出来打包卖给同行甚至有人专门写脚本批量扒资源。这类文件的特点是体积大、数量多全量加密的成本最高但收益也最直接。第二类是业务配置文件。包括接口地址、功能开关、内购商品映射、缓存策略、广告配置。这些文件往往是 plist、JSON、或者自定义格式。它们会直接泄露服务端的接口路径和参数设计攻击者拿着这些内容可以去撞接口、试越权。更危险的是如果这些配置没有被校验攻击者把里面的服务器地址改成一个自己搭的钓鱼服务器再诱导用户安装后果就不是盗素材那么简单了。第三类是本地化文案和隐私相关文本。很多人觉得 strings 文件无所谓但我在多个 App 里翻到过写在注释里的测试后台地址、真实的邮箱账号、甚至临时写死的云存储密钥。这类内容不需要多高深的技术就能挖出来是审计时的低级错误重灾区。2.3 Info.plist 和签名信息泄露了哪些底牌Info.plist 是被低估的信息源。它不只是给系统看的关系表里面包含了大量业务信息Bundle Identifier 让你知道这套代码属于哪个产品线CFBundleURLSchemes 暴露了 App 内注册的所有 URL Scheme很多开发者的唤起协议和通用链接配置都能在这里找到蛛丝马迹NSAppTransportSecurity 里的 NSAllowsArbitraryLoads 是否开启直接告诉攻击者这个 App 对 HTTP 明文流量是什么态度。embedded.mobileprovision也很要命。企业分发包里带的是企业证书签名里面会列出签名证书的 Subject、Team Identifier、以及被授权的设备 UDID。如果这个包是从某个开发群里流出来的攻击者能通过描述文件反推出团队信息、证书类型、甚至证书的有效期和权限范围为后续定向攻击做准备。_CodeSignature/CodeResources则是防篡改的核心突破口。它本身是一份 XML 格式的哈希清单列出了 bundle 内几乎所有文件的 SHA-1 哈希。系统在安装和启动时会拿这份清单校验文件是否被改动。但这里的关键点是当一个攻击者重签 IPA 时他可以重新生成一份 CodeResources把篡改后的文件哈希写进去系统层面无法察觉。所以真要防篡改不能只依靠苹果的签名机制要在代码里再自己维持一份独立的账本。3. 资源加密怎么做才不算白忙活资源加密的核心矛盾在于加密后的资源必须在运行时能被解码否则 App 自己都没法用。可一旦解码逻辑在客户端存在攻击者就有机会绕过它。所以正确的目标不是让攻击者解不开而是让直接读取的成本变得足够高。3.1 先解决密钥存放加密方案的地基密钥存放是资源加密里最重要、也最容易被做崩的一环。我在审计中见过太多加密了个寂寞的案例用 AES 加密了图片然后把密钥以一个 32 字节的字符串明晃晃写在一个KeyConstants.h里。攻击者都不用逆向直接在解压后的资源目录里搜AESKey就找到了。比较务实的做法有三个层次。第一个层次不要明文存密钥要做派生。把密钥拆成几段分散在代码里运行时通过拼接、翻转、甚至 XOR 一段常量来还原。比如把 256 位密钥拆成三份第一份写在资源文件的前 16 字节里第二份从可执行文件的某个固定偏移读取第三份由设备型号和 App 版本号做哈希后参与派生。这样攻击者就算拿到其中一份也无法直接还原完整密钥。第二个层次不要一套密钥用天下。每个发布版本生成一套随机密钥构建时通过环境变量注入工程。这样上一个版本泄露的密钥不会影响新版本。实践中你只需要在 CI 脚本里用openssl rand -base64 32生成密钥然后以编译参数或配置文件的方式传给构建脚本源码仓库里不需要存储任何版本的最终密钥。第三个层次用系统能力做隔离。把核心密钥在 App 第一次启动时写入 Keychain之后从 Keychain 读取参与解密。Keychain 的读取有系统权限管理比明文写在二进制里安全一个量级。缺点是首次启动需要安全地完成一次密钥初始化这块逻辑本身就是被逆向的重点目标所以要配合完整性校验一起做。3.2 图片和音频资源构建后加密、运行时解密对图片和音频这类大文件我强烈建议你放弃在源码里直接处理而是放在构建完成之后、打包 IPA 之前统一做后处理。原因很简单不污染源码不增加开发期心智负担出问题可以随时回退。具体流程是这样在工程里把需要加密的资源放到一个独立目录比如Resources/Protected/。构建出.app之后写一个脚本遍历这个目录用 AES-256-GCM 逐个文件加密把原始文件删除或改名。加密后的文件保留原扩展名的伪装或者统一改成.bin随包分发。运行时在代码里加载这些文件先解密到Data再用UIImage(data:)或AVAudioPlayer(data:)创建对象。在 Swift 里解密逻辑用 CryptoKit 写起来非常干净import CryptoKit func decryptProtectedData(_ cipherData: Data, key: SymmetricKey) throws - Data { let sealedBox try AES.GCM.SealedBox(combined: cipherData) return try AES.GCM.open(sealedBox, using: key) }这里要特别提醒GCM 模式自带认证标签如果文件被篡改解密会直接抛错。这比老的 CBC 模式好很多CBC 只做加密不做认证的话攻击者改掉几个字节你根本发现不了。对于图片资源还有一个取舍要讲。如果你的图片原本放在 Asset Catalog 里被编译进了Assets.car那么加密整个 Assets.car这条路基本走不通因为系统框架UIImage(named:)直接读的是Assets.car内部的专用压缩格式你没法在系统读取前插入解密步骤。所以加密方案只适用于代码自加载的资源。对 Asset Catalog 里的官方资源正确姿势是不加密但做哈希完整性校验防止被替换和二次打包。对真正敏感的运营素材、付费内容相关图片从工程层面直接放到自定义资源目录走自加载路线。3.3 配置文件和多语言文本的内容保护配置文件的保护和图片还不太一样。图片是解密后显示生命周期极短配置文件则可能要被多处代码读取如果每次都解密、解析、释放性能和工程复杂度都受不了。我的建议是重新设计启动链路。敏感配置不要再以明文 plist 躺在 bundle 里而是打成一个加密包里面用 JSON 或 protobuf 存一份完整配置字典在AppDelegate启动早期统一解密一次解析成内存中的模型对象后续所有业务模块都从这个对象读取配置。这样解密只做一次也方便全局统一管理。多语言文本处理方式类似。Localizable.strings默认是明文且系统 APINSLocalizedString直接读取.lproj目录下的文件你没法在系统读取时挂钩子。如果文案里有敏感内容就不要用系统的本地化机制加载公共文本而是把所有文案移动到加密资源里自己实现一个Localized(key:)函数从内存字典中取值。对文件存储的措辞我想强调一句无论配置文件还是文案解出来之后别为了图方便把它写回沙盒的 Documents 或 Caches 目录。一旦明文落盘攻击者只要连上设备直接读文件就能绕过你所有加密逻辑。内存里用、用完释放才是安全的。3.4 几个加密过程中的实操细节加密方案光是能跑远远不够我再分享几个被验证过很关键的实操点。第一IV初始向量必须随机不能复用。AES-GCM 模式下每次加密都生成新的随机 nonce把它拼在密文头部。解密时先读 nonce再解密正文。如果图省事使用固定 IV相同资源在多个版本中会产生相同密文攻击者可以根据文件大小和被加密资源的原始特征做已知明文攻击缩小密钥搜查范围。第二加密后文件体积会被观察到。图片资源的原始大小、颜色分布、缩略图特征其实都能泄露信息。比如 1024×1024 的无损 PNG 压缩前和压缩后体积往往有固定规律攻击者光靠文件名和文件大小就能猜出这是哪张图。所以加密后的文件建议全部改名打乱命名规则或者统一塞进一个自定义容器文件里避免暴露映射关系。第三日志里千万别打印解密后的内容。我自己就踩过这个坑为了排查线上配置加载问题在日志里输出了一段 JSON 配置结果日志文件被收集到崩溃分析平台第三方平台上的运维人员等于直接看到了明文配置。调试日志一定要经过脱敏处理资源解密相关的 log 尤其要克制。4. 完整性校验让篡改和二次打包现出原形加密能让资源没法被直接读取但它拦不住替换文件这一招。攻击者完全可以不管你的加密逻辑直接把整个加密资源文件换成一个自己的版本。这时候你就需要完整性校验用来确认包里的文件和发布时保持一致。4.1 苹果签名机制防住了什么又漏掉了什么很多人以为苹果的签名机制天然防篡改这是一个非常普遍的误解。准确地说苹果签名机制保证的是在系统信任链完整的情况下包里文件的哈希与_CodeSignature/CodeResources记录一致。系统在安装、启动 App 时确实会校验这些哈希但有一个前提攻击者用的是苹果信任的合法证书完成了重签名。真实攻击场景往往是这样攻击者解包替换掉一张图片、改一个配置、塞入一段恶意逻辑然后用自己的开发者证书重新签名。因为证书合法系统信任新签名安装校验照样全部通过。CodeResources里记录的是攻击者篡改后的文件哈希对系统来说这是一份自洽的包。所以苹果的签名管住了非法证书却没有管住合法证书下的内容变更。这就解释了为什么客户端必须自己再做一道校验在代码内部保存一份发布时所有关键文件的哈希期望值运行时重新计算一遍和期望值比对。攻击者可以改文件但要让 App 自己骗过自己就必须再改动校验逻辑本身这等于逼他从改文件升级成改二进制和重签名门槛高了一个数量级。4.2 文件级指纹校验把关键资源哈希钉死在发布包设计校验清单的原则很简单只盯关键文件。把 bundle 里每一个文件都做哈希再全量校验开销太大也没必要。我建议把资源分成两类一类是构建期确定的静态文件比如 Assets.car、关键配置加密包、核心图片另一类是业务运行中可能被外部写入的缓存这类不做哈希校验做了反而误伤。具体实现时你可以发布一个独立的清单文件integrity.json内容包含每个关键文件的路径和 SHA-256 哈希值。这个清单放到一个加密资源里密钥从可执行文件中派生所以攻击者想要伪造清单必须先逆向拿到密钥和校验代码成本非常高。校验代码在 Swift 里很直接import CryptoKit func verifyProtectedFile(at path: String, expectedHash: String) - Bool { guard let data FileManager.default.contents(atPath: path) else { return false } let digest SHA256.hash(data: data) let actual digest.map { String(format: %02x, $0) }.joined() return actual expectedHash }实际操作中校验时机也要讲究。启动时全量校验一次发现不对就直接提示应用数据异常请重新安装这是最稳妥的兜底。但对那些体积大、读频率低的资源比如启动图序列帧建议改成随用随验首次加载某个加密资源时先计算哈希和清单比对一致才做解密。这样把校验成本摊到使用过程中不会让冷启动多出几百毫秒。4.3 客户端校验与服务端校验怎么搭配客户端校验有个天然缺陷攻击者终究可以通过静态分析找到校验代码把判断逻辑改成恒真。要挡住这种绕过最终还是要靠服务端联动。可行的方案是App 启动或执行关键业务时把一组发行证明上报给服务端。这组证明可以包含设备当前安装包的 Build 号、CodeResources中记录的指定文件哈希、本地完整性校验的结果、以及一个由本地密钥对安装包签名盖章过的 token。服务端维护一个合法版本清单。如果上报的哈希与官方发布的版本不一致或者同一套文件哈希在极短时间内对应了异常多的设备服务端就可以判定这个包被篡改过。轻则把结果标记为高危设备、限制部分接口访问重则直接拒绝服务并推送客户端更新提示。需要说明的是服务端校验本身要防滥用。接口要做频率控制和设备指纹绑定否则攻击者可以伪造上报数据直接打爆校验接口。我这里比较常用的做法是把校验接口放在登录接口之前校验失败直接拦截同时在服务端记录校验失败的设备特征方便后续追踪这批问题设备背后的分发路径。4.4 运行时环境与调试状态检查的取舍聊完文件再补一个容易被忽视的检查点运行环境本身。如果一个 App 在调试模式下运行攻击者可以利用调试器直接修改内存前面所有资源加密和文件校验都可以被绕过。基础的环境检查包括检测getppid的父进程是否为调试器、调用sysctl查看当前进程是否被 traced、检查常见调试工具的共享库路径、检查进程启动参数中是否有特殊标记。整套逻辑可以做但我不建议做得太激进误判会导致正版用户在特殊环境下根本无法启动。这里有一个现实教训。我之前帮一个团队加过环境检测测试机没事测试同事一开 Xcode 的 Debug 模式就闪退。后来把逻辑调成了检测到异常环境只拉高加密等级不做硬阻断比如在这种情况下拒绝解密核心配置文件核心功能不可用但 App 不会崩溃。这样既给攻击者设置了路障又不会把自己人误伤到崩溃。峻烈检测的度要在上线前集中测试一轮才能定下来。5. 资源从分发到落地的泄露缺口比你想的多前面讲的都是 IPA 在安装包内的状态。但一个文件和 App 的实际接触过程从 HTTPS 下载、到落沙盒、到缓存存在很多独立的泄露点。这些环节如果不处理前面做的资源加密会白费。5.1 下载接口的 MIME 和 Content-Disposition一个头决定预览还是落地很多开发者在给用户提供 IPA 或资源文件下载时只挂了一个简单的 GET 链接结果在 iOS 的 Safari 里点开页面没有弹出下载提示而是直接把文件当成纯文本或图片预览了。这个热搜词叫h5 在 ios 下载文件变成了预览背后的原因就是服务器返回的Content-Type和Content-Disposition没设置对。正确的响应头应该这样Content-Type: application/octet-stream Content-Disposition: attachment; filenameyourfile.ipaapplication/octet-stream明确告诉系统这是一个二进制流不要尝试按格式解析attachment告诉浏览器这是一个需要保存的附件而不是内联展示。两个头缺一个Safari 和 WKWebView 都容易自作主张去预览。这个看似是下载体验问题实际也是安全问题。相比真正落地到沙盒文件预览意味着文件会以明文内容被 WebView 临时渲染出来临时文件往往会落在我们可以预知的临时目录里完成渲染后系统未必立即清理。对敏感资源能不预览就不预览这是最基本的分发姿态。5.2 沙盒缓存与临时文件解密结果别随手写盘App 运行过程中不可避免地要产生缓存。如果你在解密资源后调用了data.write(to:)写到了Caches目录那么攻击者只要在手机上下载一份 App 的沙盒备份就能直接拿走你加密保护过的明文产物整个过程不需要任何逆向技能。正确的屋檐是这样的解密后的数据只管在内存里用用完立刻置空。对于音频、视频这类必须通过文件路径播放的资源需要对临时文件开启文件保护属性try data.write(to: tempURL, options: [.completeFileProtectionUnlessOpen])打开后加一个使用后立即删除的清理任务并且保证在 App 进入后台时执行一次removeItem(at:)。这样文件在沙盒中存在的时间窗口被压缩到最短。另外NSURLCache和 WKWebView 的网络缓存也很容易被忽略。如果你通过 WebView 下载过资源Library/Caches下会多出一份完整的网络缓存副本。所以涉及敏感资源传输优先走原生代码的下载逻辑并在应用层做加密落地避免经过 WebView 的默认缓存。5.3 历史版本 IPA 的二次分发你不加固别人替你加固现在市面上存在大量历史版本下载站用户搜索 App 旧版本时很容易搜到这些站点。很多历史包根本不是开发者自己发布的而是被用户或灰产从设备里提取出来、再重新打包分发的。这些二次分发渠道最大的危险在于你无法保证下载到的包和当初上架的包完全一致。攻击者在二次分发前可以替换掉里面的配置文件、塞入插屏广告 SDK、甚至加入收集用户信息的代码然后重新签名。因为包是用合法企业证书或私人证书签名的用户安装后系统并不会拒绝。对开发者而言能做的很有限但有一条很实际尽量提高私自提取和重打包再分发的代价。具体来说就是做好版本校验和篡改检测同时在自己的分发渠道页面明确告知用户哪些地址是官方渠道。万一出现了已经泄露的历史版本包服务端要有能力通过版本号、构建号、设备指纹的组合识别出这些非官方包并限制其访问核心服务。这也是服务端联动校验除了防篡改之外另一个非常实际的价值。6. 把加固动作编进 CI 流水线一套可落地的脚本骨架前面讲了很多思路和方法最后落地到工程还要解决一个问题人工去执行这些操作不现实。开发、测试、发布流程那么紧没有人能保证每次发版都手动解包加密一次。所以资源加固必须自动化最好是作为 CI 流水线里一个独立的构建后处理步骤。6.1 加固编排解包、加密、重打包、重签名、自测标准的加固编排大致是下面这个顺序从 CI 拿到原始构建产物可能是App.xcarchive或已经签名好的.ipa。解包unzip App.ipa -d build_ipa/。生成新的随机 AES 密钥注入到工程的密钥配置中通过编译参数或临时文件不进 Git。执行资源加密脚本遍历Payload/App.app/下指定的敏感资源目录逐个加密替换并生成integrity.json哈希清单。将integrity.json和必要的新资源文件放回.app目录。重新打包zip -r App_encrypted.ipa Payload/。重新签名使用发布证书和描述文件执行codesign。验证签名codesign --verify --strict --verbose2 App_encrypted.ipa。启动自动化冒烟测试确认 App 能正常启动、解密逻辑没破坏资源加载。这九个步骤里最容易被忽视的是第 7 步。很多人直接在解包后的目录上重新打包但忘了签名结果安装到真机上直接被系统拒绝。而且重签名一定要用发布包对应的描述文件否则 Bundle ID 和权限对不上启动后各种功能异常。6.2 Python 加固脚本骨架与 Swift 解密端我分享一个精简但能跑的 Python 加密脚本骨架。这里用了cryptography库AES-256-GCM 模式每个文件使用随机 noncenonce 直接放在密文前面。import os import json import hashlib import secrets from cryptography.hazmat.primitives.ciphers.aead import AESGCM def encrypt_file(key, src_path, dst_path): nonce secrets.token_bytes(12) with open(src_path, rb) as f: plain_data f.read() aesgcm AESGCM(key) cipher_data aesgcm.encrypt(nonce, plain_data, None) with open(dst_path, wb) as f: f.write(len(nonce).to_bytes(1, big)) f.write(nonce) f.write(cipher_data) def sha256_file(path): h hashlib.sha256() with open(path, rb) as f: for chunk in iter(lambda: f.read(65536), b): h.update(chunk) return h.hexdigest() key secrets.token_bytes(32) protected_dirs [Protected, Config, Audio] manifest {} app_path build_ipa/Payload/YourApp.app for root, dirs, files in os.walk(app_path): for name in files: src_path os.path.join(root, name) rel_path os.path.relpath(src_path, app_path) if any(rel_path.startswith(d) for d in protected_dirs): dst_path src_path .enc encrypt_file(key, src_path, dst_path) os.remove(src_path) manifest[rel_path] sha256_file(dst_path) with open(os.path.join(app_path, integrity-manifest.json), w) as f: json.dump(manifest, f, indent2)注意几个细节每次构建用的 key 从环境变量里读不进 Git脚本执行完后把key立刻销毁只有配置到客户端代码里的派生逻辑能还原它。如果生成的资源很多建议脚本加上失败重试和中断恢复防止 CI 跑到一半断网导致整个发布卡住。Swift 端对应的解密逻辑看起来是这样的func loadProtectedResource(named fileName: String, key: SymmetricKey) throws - Data { guard let url Bundle.main.url(forResource: fileName, withExtension: enc) else { throw ResourceError.missing } var fileData try Data(contentsOf: url) let nonceLength Int(fileData.removeFirst()) let nonce fileData.prefix(nonceLength) let cipherText fileData.dropFirst(nonceLength) let sealedBox try AES.GCM.SealedBox(nonce: Array(nonce), ciphertext: Array(cipherText), tag: []) // 因为 Python 端没有单独把 tag 取出上面这个写法不对见下方说明 return try AES.GCM.open(sealedBox, using: key) }上面这段 Swift 故意留了一个坑Python 端用aesgcm.encrypt返回的是nonce ciphertext tag拼在一起的数据而 SwiftAES.GCM.SealedBox需要把 tag 从密文尾部拆出来。正确的做法是读取末尾 16 字节作为 tag前面的部分作为 ciphertext。这个边界问题很容易在联调时踩到我当时的教训是花了一个晚上才定位到 Python 端和 Swift 端对combined format的理解差异。自动化测试脚本里一定要加一条加密-解密-比对的循环测试两个端代码都能过才算完。6.3 上线前的破坏性测试清单自动化脚本只保证流程能跑,不能保证加固有效。上线前建议拿出一台测试机按下面的清单做一轮破坏性测试篡改任意一张加密图片的密文文件后App 启动是否会出现加载失败或整体拒绝启动。替换integrity-manifest.json为一个伪造清单App 启动是否检测到哈希不一致。用个人开发者证书重新签名整个压缩包App 运行时校验是否能捕获这个版本与官方版本不一致。解包后直接删除或替换某个Info.plist字段看启动流程是否会因为配置缺失而暴露异常错误信息。在启动过程中用调试模式运行确认环境检测逻辑是温和降级还是硬阻断并根据你的产品定位调整策略。安装后把 App 切到后台检查沙盒Caches和tmp目录是否存在解密后的明文资源。这些测试做完你才算知道自己部署的加固方案在真实攻击下能撑多久。我在实际项目里的感受是加密和校验手段堆得再多也做不到绝对不可破解但当你把门槛抬高到需要逆向分析半天才能动一分的程度大多数只想着顺手把资源拆走的人就会放弃。安全加固本质上比的是成本和耐心能把成本拉上去就已经赢了大半。最后一句话是我个人的体会资源加固这块最容易翻车的不是技术方案而是做一半停一半。加密了图片没管文案管了文案没做校验做了校验又没覆盖二次打包检测任何一环留下缺口前面的投入都会打折扣。把它当成一条完整的链路去设计和维护远比临时补几个洞更可靠。
返回列表