资讯动态

无源码iOS应用安全加固:IPA重签名与动态库注入实战

发布时间:2026/10/19 10:59:45 来源:尧图企业网站定制
1. 没有源码时iOS 应用还能做哪些安全加固很多团队碰到一个很尴尬的情况iOS 应用包IPA在手但源码早就找不到了。外包交付的时候只给了打包产物核心开发离职后只留了一个归档包或者是公司收购来的旧应用原代码仓库已经散落在前任的硬盘里。现在安全审计来了客户要求做一轮安全处理而手里唯一能动的就是一个 IPA 文件。作为长期做移动安全相关工作的人我可以直接告诉你没有源码iOS 应用的安全加固依然可以做而且能做的事情比大多数人想象的多得多。这篇文章不是教你怎么绕过别人的保护机制而是讨论如何对你有权处置的 IPA 做合规加固。默认前提是你对这个应用有合理的处置权。如果你只是拿到一个来路不明的安装包想做“安全处理”来达到某种其他目的那本文的内容并不适合你请停止阅读并把问题交给原始开发者。先说结论。没有源码时iOS 应用的安全加固可以围绕三条线展开运行时防护、二进制加固、分发侧收敛。运行时防护包括防调试、防注入、完整性校验、运行环境检测二进制加固包括符号处理、字符串干扰、关键调用点加固分发侧收敛包括重签名、权限收敛、日志脱敏。这三条线不需要源码就能落地因为它们在二进制和运行机制上做文章。1.1 现实中为什么会有“拿不到源码”的加固需求我遇到最多的场景是 B 端项目交付。甲方要求交付方提供 IPA 的同时必须通过安全检查但交付方的源码仓库由于人员流动和管理不善已经不可用能拿出来的只有 Release 包。这种情况下安全团队接到的任务不是“重新开发”而是“在当前产物上做安全处理”。另一种常见场景是老应用长期无人维护。某公司收购了一个业务线连带收购了一个 iOS 应用但原团队交付的代码库不完整几个关键模块的源码丢失可运行的版本却还躺在发布服务器上。这时候如果接到整改通知比如关键接口需要加密、需要防调试、需要做完整性校验你总不能去给业务方说“源码丢了做不了”。还有一种情况更单纯开发阶段混淆和加固没有在构建流程中做等到 App 已经打好包才发现需要补安全能力。重新拉全源码、改构建脚本的工作量太大不如直接对 IPA 做二进制级加固来得快。在这类项目里我经常说一句话源码是开发的原料IPA 是安全的原料。只要 IPA 能跑起来安全手段就有地方附着。1.2 三大可落地方向运行时防护、二进制加固、分发侧收敛运行时防护的思路是“应用自己保护自己”。在应用启动早期执行一段自检逻辑比如当前进程是否被调试器附加、运行时是否出现可疑的环境特征、关键文件是否被替换过。这段逻辑不需要源码只要能让它先于业务代码执行就行。二进制加固的思路是“让逆向的人难受”。没有源码意味着符号信息可能已经很完整逆向工具可以直接看到类名、方法名、字符串常量。我们可以对 Mach-O 文件本身做符号剥离、字符串扰动、敏感接口的跳转混淆让分析成本大幅提高。分发侧收敛的思路是“从签名和权限上减少暴露面”。拿到 IPA 后可以用自己的证书重新签名可以裁剪不必要的权限声明可以关掉容易被利用的配置项比如不合理的后台模式、不安全的 URL Scheme 回调等。这三条线组合起来即使没有源码也能达到中等强度以上的防护水平。注意我说的是“中等强度以上”也就是防普通脚本小子、防常见自动化分析工具、路径和针对性不强的黑产够用面对顶级逆向工程师二进制级加固永远只是增加成本而不是绝对杜绝。1.3 做不了的部分与常见风险没有源码就意味着有一些事情确实做不了或者代价极高。比如要修改业务逻辑内部的计算规则想在某个按钮点击后插入一段自定义流程没有源码时不建议硬来除非对 Mach-O 的指令级 patch 非常熟悉并且能承担崩溃风险。所谓“硬来”是指直接在汇编指令层把某个函数调用的参数改掉、把某个分支跳转改掉。这种方案在理论上完全可行但有一个很大的隐患每改动一个字节都会破坏代码签名你需要重新签名签名之后如果 iOS 系统版本升级、设备架构变化很可能会出现低级错误。更重要的是指令级 patch 之后的维护成本极高原始包升级一次你的 patch 就要重新做。另外要特别提醒一个风险不能把 App Store 下载的应用拿来处理。通过应用商店分发的包默认带有 FairPlay 加密Mach-O 的 __TEXT 段是加密状态二进制 hash 也与签名不匹配强行修改后的行为不可预期而且这种操作本身也超出了正常安全加固的范畴。如果你检查到手里的 IPA 是带加密的直接停手回到源码方案或者向原始开发者要未加密的企业包。2. 开工第一步先给 IPA 做一次“体检”拿到 IPA 之后最忌讳的一件事就是直接解包开干。我见过有人在公司群里说“我改了一个字节装不上是不是签名坏了”然后项目经理一脸茫然。正确顺序是先把 IPA、可执行文件、签名信息、依赖库状态这四样东西摸清楚再决定往哪个方向发力。2.1 工作环境与基础工具准备处理 IPA 的推荐环境是 macOS原因是签名工具链和系统框架都是现成的。你需要在 Mac 上准备好Xcode 命令行工具、一份可用的开发者证书或企业证书、对应的描述文件以及常用的命令行工具包括codesign、otool、nm、strings、file、lipo。如果你长期做 iOS 安全相关工作还可以准备一个能查看 Mach-O Load Commands 的小工具这类工具不需要集成到 Xcode 工程里单独的命令行即可。我常用的方式是组合使用系统自带命令加开源小工具操作流程可以做到半自动化。在实际项目里我会先建一个独立的干目录比如/tmp/ipa_work所有解包、分析、签名都在这个目录里完成不要把产物直接丢到桌面或者下载文件夹。这样做的原因是后面重签名时要反复修改套壳目录结构一旦和别的文件混在一起很容易引起路径混乱另外安全处理的中间产物应该集中管理避免散落在系统各处带来不必要的困扰。2.2 解包、识别 Mach-O 和加密状态IPA 本质上是一个 zip 压缩包解包命令很简单unzip TargetApp.ipa -d /tmp/ipa_work/app解包后你会看到Payload/TargetApp.app目录。在这个目录里名称和原应用名一致的无后缀文件就是主可执行文件也就是 Mach-O 格式的二进制。注意.app目录下可能还有PlugIns、Frameworks等子目录这些也要看清楚。接下来用file和lipo检查架构file Payload/TargetApp.app/TargetApp lipo -info Payload/TargetApp.app/TargetApp这两条命令会告诉你主二进制支持的架构。如果只有 arm64处理起来最省心如果包含多个架构后面签名时要注意架构匹配且重新签名后需要用lipo或者直接以最终真机架构为准。大多数现代 iOS 应用只需要 arm64。然后查看加密状态otool -l Payload/TargetApp.app/TargetApp | grep -A 4 LC_ENCRYPTION_INFO输出里会有cryptid字段。cryptid 0表示未加密可以直接处理cryptid 1表示加密状态这是应用商店包的特征。如果你的目标是给企业内部应用或合规交付包做加固正常情况下应该只遇到cryptid 0的包。遇到cryptid 1请按上一节说的停下来。依赖库状态也很关键otool -L Payload/TargetApp.app/TargetApp查看主二进制依赖了哪些动态库。此举能帮你判断这个应用有没有已经内置的第三方框架以及后面注入动态库时是否有命名冲突的风险。如果主二进制里依赖的库非常多你还要留意注入后动态库加载顺序问题避免在dyld初始化阶段出现循环依赖。2.3 摸清签名与权限限制签名信息用codesign查看codesign -dvv Payload/TargetApp.app/TargetApp 21这条命令会显示签名者信息、签名算法、Team ID、权限哈希等内容。你要重点关注“Authority”字段它告诉你当前这个包是用什么证书签的。如果是企业证书处理后会相对灵活如果是开发证书你需要确保自己的证书被当前设备的描述文件包含否则装不上。接下来导出当前权限文件codesign -d --entitlements :- Payload/TargetApp.app/TargetApp entitlements.plist导出的权限列表很重要。比如原应用如果申请了location、camera、microphone等敏感权限而你重签名之后并不需要这些能力就可以在这个 plist 里删掉。这属于分发侧收敛能有效减少恶意攻击面。但注意删权限之前要先确认应用在运行时真的不会访问这些资源否则会出现“点击某个功能没反应、系统弹窗也不出现、后台日志全是权限报错”的体验事故。做这一步时还有一个容易被忽略的点要检查.app目录下是否有扩展插件比如 Watch App、Widget Extension、Notification Service Extension。每个扩展都是独立的可执行文件有自己独立的签名和权限需求重签名时一个都不能漏。漏掉任何一个扩展都会导致安装失败而且错误提示往往特别笼统只说“无法安装”。3. 核心实操注入防护代码并重签名体检做完确认手里的 IPA 是可以合法处理的未加密包之后最核心的一步来了把防护代码注入到应用里再重新签名。这一步我用得最多、见效最快。3.1 动态库方案为什么是首选要让自己的代码在应用启动时执行通常只有两个途径修改主可执行文件的入口逻辑或者让系统加载一个额外的动态库。修改入口逻辑属于指令级 patch风险高维护难而注入动态库是在 Mach-O 的 Load Commands 里增加一条LC_LOAD_DYLIB让dyld在启动早期就把我们的代码加载进来。动态库方案的好处是代码可以独立维护、独立更新不需要理解原二进制内部复杂的调用关系即使原应用迭代了版本只要主可执行文件的名字和入口没有本质变化注入流程可以复用。这也是业内做二进制级防护的主流做法。实现动态库注入有两条路一条是手工修改 Mach-O 的 Load Commands这条路比较底层需要用专门的编辑工具存在操作风险另一条是用开源的自动化工具体它可以把动态库路径写入主二进制的加载列表里。我建议普通团队走工具路线越多的人工步骤意味着越多的出错机会。手工方式不是不能做但你要熟悉otool -l的输出格式会解析LC_LOAD_DYLIB结构体还得处理 Load Commands 区域的存储空间不足问题。一旦 Load Commands 区域已经塞满你还要去插入一个新的 load command涉及偏移量的全局调整这种操作一旦算错二进制直接残废。3.2 防护库核心代码示例动态库的工程可以是纯 C 代码可以是 Objective-C也可以是 Swift。我建议用 C 或 Objective-C 写因为这类防护逻辑往往需要直接调用系统调用用 C 写更直白也便于后续把逻辑编译成静态库集成进去。下面是一段反调试逻辑主要思路是让调试器无法成功附加#include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include sys/sysctl.h static void detect_debugger(void) { int mib[4] {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()}; struct kinfo_proc info {0}; size_t size sizeof(info); sysctl(mib, 4, info, size, NULL, 0); if ((info.kp_proc.p_flag P_TRACED) ! 0) { exit(0); } } static void deny_attach(void) { // 主动告知系统拒绝后续调试附加 syscall(SYS_ptrace, 31, 0, 0, 0); // PT_DENY_ATTACH } __attribute__((constructor)) void security_guard_init(void) { deny_attach(); detect_debugger(); }这段代码里的__attribute__((constructor))是关键。它保证函数在动态库被加载时自动执行不需要主程序显式调用。实际上所有基于动态库注入的防护逻辑都要大量依赖这个机制因为你不是在改原应用的源码没法在viewDidLoad里加一行调用。完整性校验的思路类似。常见做法是在启动时读取主可执行文件的二进制内容计算哈希与一份内置的期望值比对。如果哈希对不上说明文件被篡改过直接终止执行。#include CommonCrypto/CommonDigest.h static int verify_main_binary(void) { const char *path [[NSBundle mainBundle] executablePath].UTF8String; FILE *fp fopen(path, rb); if (!fp) return 0; fseek(fp, 0, SEEK_END); long len ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char *buf malloc(len); fread(buf, 1, len, fp); fclose(fp); unsigned char digest[CC_SHA256_DIGEST_LENGTH]; CC_SHA256(buf, (CC_LONG)len, digest); free(buf); // 与内置的 SHA256 比较 const unsigned char expected[] {0x00, 0x01, 0x02}; return memcmp(digest, expected, CC_SHA256_DIGEST_LENGTH) 0; }注意这里为了保持示例简洁省略了内部期望值的完整定义。实际项目中你不应该把完整哈希明文写在二进制里否则攻击者可以搜到哈希值简单修改后就让校验通过建议的做法是拆成多段存到不同位置再用算法拼接或者结合应用的BundleIdentifier、版本号等信息做动态计算。3.3 一次完整的重签名流程动态库编译好之后整个重签名流程可以分为 6 步。以下脚本是我在简化多次后习惯使用的核心流程你可以把它作为基础模板# 1. 进入工作目录 cd /tmp/ipa_work # 2. 把编译好的防护动态库复制进 app 的 Frameworks 目录 mkdir -p app/Payload/TargetApp.app/Frameworks cp SecurityGuard.dylib app/Payload/TargetApp.app/Frameworks/ # 3. 用工具给主二进制增加 LC_LOAD_DYLIB 加载命令 # 这里假设你的 macho_editor 支持 -add-load 参数 macho_editor --binary app/Payload/TargetApp.app/TargetApp \ --load rpath/SecurityGuard.framework/SecurityGuard # 4. 单独签名动态库 codesign -f -s 你的开发者证书名称 \ app/Payload/TargetApp.app/Frameworks/SecurityGuard.dylib # 5. 对整个 app 签名--deep 会把内部插件和框架一起签 codesign -f -s 你的开发者证书名称 \ --entitlements entitlements.plist \ --deep app/Payload/TargetApp.app # 6. 验证签名 codesign -dvv app/Payload/TargetApp.app/TargetApp 21 codesign --verify --deep --strict app/Payload/TargetApp.app重签之后把Payload目录重新压成 IPA 即可cd app zip -ry ../SecuredApp.ipa Payload有几个细节要强调。第一签名必须从内到外先签注入的动态库再签主应用。如果顺序反了外部签名虽然会盖上但系统在做完整校验时会发现动态库和主二进制的签名状态不一致直接判定安装包无效。第二--deep参数不能盲目信任。Xcode 工具链在部分版本中会对--deep给出“已弃用”的警告。更稳妥的做法是显式地对Frameworks下的每个 dylib、PlugIns下的每个扩展以及主二进制分别签名最后再签最外层。自动化脚本要多写几个codesign调用不要嫌麻烦。第三描述文件需要拷贝到.app目录下名字是embedded.mobileprovision。这一步很多人会忘。苹果在签名时会把描述文件嵌进 App 包内安装时系统会读取这个文件来校验设备是否在授权列表里。没有它即使代码签名正确设备也可能拒绝安装。在我做过的项目里最容易出的问题不是签名命令写错而是同一个 app 里存在多个架构签名后没有重新合并。建议在重签前先用lipo -archs确认架构列表用lipo -extract处理多架构的 dylib不要直接拿一个 arm64 的 dylib 去替代原来的 armv7 加 arm64 双架构文件。4. 更进一步完整性校验、符号干扰与部署检查注入动态库只是第一步如果只做这一件事防护强度并不够。因为攻击者发现你的应用多了个动态库之后第一反应就是想办法让这个动态库失效具体手段包括改启动参数、拦截加载、或者直接把这一个文件删掉。要让加固真正起作用需要有“就算有人试图拆掉防护也会触发应用自毁或异常”的设计。4.1 运行时完整性校验怎么落地完整性校验的核心对象有三个主可执行文件、注入的防护动态库、关键资源文件。一个简单的做法是在启动时对这三个文件分别计算 SHA256与内置值比对。更复杂的做法是每隔一段时间重新检测一次或者在收到特定事件时触发校验。这里我说一个真实项目里积累的经验不要把期望哈希放在同一个位置更不要用明文数组。可以先拼接一个字符串再用分隔符切分把每一段放到不同的全局变量里或者利用位运算把哈希拆成两半。这样做的目的不是为了绝对安全而是为了增加攻击者静态分析的难度让他不能一眼看到“哦原来这串字节就是期望值”。完整性校验还有一个“点火”问题。如果校验失败后只是弹个提示框攻击者完全可以用调试器把返回值 patch 成成功。更合理的做法是校验失败时不直接触发可见行为而是在某个业务关键路径上主动触发崩溃或者把敏感数据填充成垃圾值。这个崩溃位置应该随机分布在后续执行流程里而不是固定在校验函数附近。4.2 符号表和字符串的干扰思路没有源码的 IPA其符号表情况取决于当初打包时是否 strip。很多包发布时保留了大量符号一个nm或者反汇编工具就能把类结构、方法名还原得七七八八。即便你在重签名过程中完成了签名符号表依然是明文存在的。对于主二进制你可以用系统自带的strip命令去掉符号表strip -x Payload/TargetApp.app/TargetApp -X Payload/TargetApp.app/TargetApp但要注意对于 Objective-C 应用strip不会去掉 Objective-C 运行时元数据因为 OC 运行时需要方法名做消息派发。如果你的应用是纯 Swift 写的strip 的效果会明显一些。所以符号干扰只能作为辅助手段不要指望它让逆向彻底失效。字符串就更有意思了。没有源码也照样能做字符串加密常见做法是扫描二进制里的可打印字符串把长度较长或者带有明显业务的字符串提取出来生成加密表再写一段解密函数在运行时还原。这个操作需要工具支持市面上有开源方案可以直接遍历 Mach-O 的__cstring段并替换。不过风险在于如果字符串是 Objective-C 方法名或者系统 API 名称替换后要保证动态调用仍然能解析否则会出现运行崩溃。我做这类操作时通常只处理明显是业务关键字、URL、SQL 语句和密钥路径的字符串不对系统类名和 OC 选择器动手。4.3 功能矩阵与落地顺序建议没有源码时每加一种防护手段都有成本。我的建议是分三档来推进。第一档是基础版注入反调试动态库、主二进制完整性校验、重签名。这一档解决了大多数普通攻击者的诉求也能让自动检测工具无法简单抓包修改大约需要半天到一天。第二档是进阶版在基础版之上加环境异常检测对关键资源文件做完整性校验对敏感字符串做加密处理同时把不合理的权限从描述文件里删除。这一档适合安全要求更高的场景新增成本大概一两天。第三档是完整版把源码级别的混淆工具逻辑移植到二进制层面对多个业务函数做控制流隐蔽处理设置动态触发机制配合后端的运行时风控接口上报异常特征。如果没有源码第三档的工程量会明显放大而且每新增一种处理都要重新执行一遍重新签名流程。用一个表把这三种手段放在一起对比防护方向核心动作优点局限性运行时防调试启动早期检测调试状态、主动拒绝附加对抗动态调试最直接对静态分析没效果完整性校验对主二进制和关键资源做哈希比对防篡改效果好期望值本身可被分析符号与字符串处理strip、字符串加密、接口调用点混淆提高逆向分析门槛对 OC 元数据效果有限分发侧收敛重签名、删权限、收紧 URL Scheme减少外部攻击面需要确保功能不受影响有一点值得强调这些手段是组合使用不是单独生效。只做防调试而没有任何完整性校验攻击者可以先把你的防调试函数 patch 掉只做完整性校验而不做符号处理攻击者很容易定位校验函数只做符号处理而不做反调试有经验的逆向工程师照样可以动态调试后绕过。安全加固的方案从来都是“组合拳”。5. 常见问题速查与我的避坑记录重签名和注入这类操作翻车率其实比很多人以为的要高。下面这些问题都是真实项目中遇到过的我把踩过的坑和排查思路整理出来直接按症状对照去看会省很多时间。5.1 闪退类问题安装成功后一打开就闪退最常见原因是代码签名和描述文件不匹配。排查顺序是先用codesign --verify --deep --strict验证签名状态再用security cms -D -i embedded.mobileprovision读取描述文件内容对比 Bundle Identifier、证书 Team ID、设备 UDID。如果签名没问题看崩溃日志。dyld: Library not loaded说明动态库路径不对或者动态库本身没被签进去。Symbol not found说明注入的动态库依赖的某个符号在主程序里不存在通常是版本兼容问题。还有一个很隐蔽的崩溃原因主二进制原本依赖某些rpath指向的动态库你在重签时把整个 Frameworks 目录重新整理了一遍导致部分原依赖路径失效。我建议在注入之前先把otool -L的输出完整保存下来重签后再对照一遍。5.2 防护不生效类问题代码明明注入了防护却不生效十有八九是构造器没有执行。__attribute__((constructor))函数只有在动态库被dyld加载时才会运行如果你只把 dylib 放进了 Frameworks 目录但主二进制的 Load Commands 里没有对应的加载项这个 dylib 就只是静静躺在目录里根本没有被加载。所以排查问题的顺序是先用otool -L查看主二进制确认新增的rpath/SecurityGuard.dylib是否已经出现在加载列表里再用nm -gU查看动态库导出的符号确认构造器符号存在最后如果条件允许在真机上通过 Console 应用观察动态库加载时的日志或用系统工具确认加载是否真的发生了。另外如果应用启用了看门狗机制构造器里执行了太多耗时的初始化逻辑进程会被系统强杀导致误以为防护报错。构造器里的逻辑一定要精简复杂的校验和上报动作放到异步线程里执行。5.3 兼容性类问题iOS 版本更新与架构变化会导致旧的注入方案失效。比较典型的是 arm64e 架构引入后系统对指针签名的要求更严格动态库需要以 arm64e 的子类型构建否则在部分机型上会异常。这个问题在旧项目上不常见但只要是处理新发布的包都要留意。另一个兼容性问题是不完整的多架构处理。如果原 App 目录下的某个动态库是 armv7 加 arm64 的胖二进制你重签时只签了主架构那么在一些特殊场景下校验会报出“二进制文件损坏”之类的错误。签完以后最好对每个 dylib 都执行一遍lipo -info确认架构没有意外变化。如果你在企业内部分发场景下使用还要注意设备描述文件是否包含了每一台测试机的 UDID。企业签名虽然不限制设备数量但描述文件的过期时间是一个大坑过期之后所有打好的包都会失去安装资格即使代码本身没有问题。症状可能原因快速检查方法打开闪退签名或描述文件不匹配codesign --verify检查描述文件权限安装不上扩展插件未签名遍历.app/PlugIns下所有可执行文件再签名防护不生效动态库未被主二进制加载otool -L查看 Load Commands日志上报异常权限被裁剪重签前导出权限文件回滚权限后再试真机异常崩溃arm64e 与 arm64 混用lipo -info查看架构列表做这种活最忌讳的是“全自动黑盒”心态。市面上确实有一些把所有操作都封装起来的工具一键完成注入和重签名但我坚持要在关键步骤留下日志和验证环节。因为一旦目标包的复杂度上来黑盒工具会非常不透明出了问题根本不知道是哪一步错了。我个人的习惯是每次重签名之后手动执行三条命令otool -L、codesign -dvv、codesign --verify全部通过再封装 IPA。最后再分享一个心得当你对没有源码的 IPA 做安全处理时不要只想着一劳永逸。防护代码注入进去只是开始后续每次拿到新版本迭代包都要重新执行一遍注入和重签名流程。最好把整套流程沉淀成脚本把动态库工程单独当作一个模块管理起来。我在几个项目里的做法是把签名、注入、验证的脚本放到内部脚手架仓库里新包进来只要改几个路径和版本号就能续上完全不用看人脸色等源码。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑