乐于分享
好东西不私藏

拆解iOS应用逆向实战中的反调试与反篡改

拆解iOS应用逆向实战中的反调试与反篡改

拆解iOS应用逆向实战中的反调试与反篡改

目录

  1. 应用如何利用iOS侧信道
  2. 应用如何自检
  3. 应用如何在附加调试器时自毁
  4. 应用如何主动破坏自己的崩溃日志
  5. 应用如何借iOS之手终止自身
  6. 应用如何持续复检
  7. 结论

这次探索既出于好奇,也带着一点便利性考量。有人想把某款游戏再“推”一步,刷出更高分;同时在红队工作中,我们也想看清银行类应用在资金处理上的幕后逻辑。目标很简单:挂上调试器、观察行为、弄清实现细节。

但过程并不总是顺利。

有些应用一启动就退出;有些应用先跑一段时间,再在没有明显原因的情况下失败;还有一些场景里,连可用的崩溃信息都没有。虽然每个应用表现不同,但样本看多后,重复出现的模式非常明显。

这些应用开发者早已不再依赖单点检测。他们会叠加多种技术,让分析更难、让修改更不稳定,即便设备并未越狱也是如此。单项技术本身并不新鲜,真正值得关注的是它们如何组合,以及它们在多早的阶段就开始生效。问题逐渐不再是“某个保护点”,而是“整套联动机制”。

下面按实战中常见的几类手法展开,说明它们在iOS应用中如何落地、如何配合。

1 应用如何利用iOS侧信道

我们分析过一款应用,它在任何核心逻辑开始前就会失败。即便没有附加调试器,也没有做任何修改,应用仍会在启动后立即退出。

后来发现,这款应用在非常早期就开始做环境检测,而且主要依赖侧信道信号,而不是显式公开接口。它调用私有系统API,再根据返回行为推断设备上是否安装了某些应用。一旦命中可疑特征,就直接终止后续流程。

一个典型案例来自银行应用:它调用了私有APISBSLaunchApplicationWithIdentifierAndURLAndLaunchOptions。调用目的并非该API的官方用途,而是把返回日志当成侧信道。通过这种方式,它可以根据com.opa334.TrollStoreorg.coolstar.SileoStorecom.tigisoftware.Filza等包标识符(bundle identifier)检测常见改机工具。如果检测命中,就把设备判定为不可信并拒绝继续。

这类具体行为后来在iOS18.5中被苹果修复(CVE-2025-31207),但这种模式本身仍然非常有参考价值。

技术要点:启动前环境检查

  • 通过系统API(含未公开接口)获取间接信号
  • 利用API返回日志等侧信道行为检测已安装应用
  • 依据已知工具的bundle identifier进行命中判定

2 应用如何自检

有些应用会更进一步:在执行任何有效业务前,先校验“自身是否可信”。

一种常见做法(在游戏中尤其常见)是通过csops()查询代码签名状态。特别是读取CS_OPS_ENTITLEMENTS_BLOB,可以拿到自身entitlements。若出现异常entitlements,往往意味着运行环境已被修改或不符合预期。应用据此进一步推断是否处在越狱环境。

部分应用还会先做完整性校验,再决定是否继续运行。常见手段包括对应用数据计算CRC32MD5,以及校验已安装IPA的签名证书。类似LC_ENCRYPTION_INFO_64这样的结构也会被用来判断应用是否被重签名或被篡改。

技术要点:启动前环境检查

  • 结合csops()CS_OPS_ENTITLEMENTS_BLOB检查entitlements并推断越狱状态
  • 通过CRC32MD5进行文件完整性校验
  • 校验证书并借助LC_ENCRYPTION_INFO64识别重签名

3 应用如何在附加调试器时自毁

另一类模式会在你尝试附加调试器时出现:应用会立刻退出。

多数情况下,这与ptrace()PT_DENY_ATTACH有关。设置该标志后,一旦有调试器附加,进程就会终止,常见路径是abort()exit()

绕过时,通常不先处理“检测逻辑”,而是先处理“终止路径”。只要应用无法完成自杀,它就会继续运行。实操里,改写执行流并绕过abort()exit()调用,往往就足够让进程存活,从而继续做运行时分析。

PT_DENY_ATTACH被直接使用时,也有成熟方法可以修改或屏蔽其行为,让调试器能够附加。相关思路已有公开资料,例如Bryce Bostwick的文章:Undebuggable,其中详细说明了在iOS上处理ptrace()的过程。

技术要点:运行时反调试(ptrace()

  • 调用ptrace(PT_DENY_ATTACH)阻断调试器附加
  • 在检测到调试行为后触发进程终止

4 应用如何主动破坏自己的崩溃日志

有些应用不只会退出,还会确保你几乎无法从崩溃里学到任何东西。

我们遇到过一个样本:正常运行时看起来没问题,一旦开始调试,崩溃日志就失去价值。寄存器被写成同一个明显不可能的值,回溯也不再指向有效上下文。

进一步分析后发现,应用会在崩溃前主动向CPU寄存器写入垃圾值。某个案例里,所有寄存器都被设置为0x123456789a00。崩溃仍然会发生,但现场状态已不可信,几乎没有可提取信息。

图注:该iOS应用在macOS上运行时,会在崩溃前污染寄存器。

这种做法会显著增加定位难度。即便你命中了正确代码路径,最终拿到的崩溃现场也已被污染。

它并不能彻底阻止调试,但会拖慢分析效率:你必须在崩溃前找到检测点,而不能再依赖崩溃现场反推。

技术要点:通过寄存器污染提升分析阻力

  • 在崩溃前覆盖寄存器状态
  • 让崩溃日志呈现无意义寄存器值
  • 混淆检测逻辑触发源并破坏回溯可读性

5 应用如何借iOS之手终止自身

有一款游戏样本出现了我们见过最“奇怪”的崩溃形式:应用可以正常运行,但一旦开始调试就会被系统直接终止,而且没有常规崩溃日志。

原因是内存压力策略。它不通过abort()或越界访问直接崩溃,而是把内存占用推高到足以触发jetsam。对iOS来说,jetsam是内核层面的内存回收机制:系统内存紧张或进程超限时,内核会直接终止进程。

由于终止由系统执行,不会产生常规应用崩溃日志。你通常只能看到jetsam记录,而反调试检测逻辑不会出现在可用回溯中。

在该案例中,这一行为还与越狱检测、追踪检测等手段叠加,导致“沿着崩溃回溯定位检测点”这条常规路径几乎失效。

技术要点:通过资源耗尽触发jetsam

  • 申请过量内存以强制触发系统级终止
  • 避免生成应用层崩溃日志
  • 仅留下系统层jetsam记录

6 应用如何持续复检

有些应用能通过初始检测,但仍会在后续阶段失败。

这类样本的共同点是:检测逻辑并未结束,而是在后台持续运行,并通过延迟机制执行惩罚。一次检测失败后,应用可能先记录状态,过一段时间再终止。这个延迟会切断“触发点和崩溃点”的直接关联。

通常还会有周期任务充当心跳。它按固定间隔唤醒并重复执行部分检测逻辑,因此“启动时通过一次”并不代表后续安全。

这种设计会让行为变得不稳定且难以预测。失败可能在很晚阶段才出现,而且很难从表象直接反推出触发原因。

技术要点:持续检测与延迟执行

  • 记录篡改状态并在延迟后触发崩溃
  • 通过定时器解耦检测与执行
  • 使用周期心跳任务重复检查运行状态
  • 即便通过初始检查,也可再次触发终止

结论

把这些样本放在一起看,变化趋势很清晰。过去常见的是单点检测,或一次ptrace()调用;现在更常见的是多层组合:早期做环境判断,运行时做调试器拦截,崩溃日志被主动破坏,甚至通过jetsam让日志彻底消失;再叠加完整性校验与延迟执行,整条保护链路会在应用启动后持续生效。

每个单项技术都不算特别复杂,真正的难点在组合方式。你面对的不是某一个机制,而是一套彼此补位的系统。

如果你熟悉Windows上的保护体系(反作弊、反调试、反篡改等),可能会问:为什么不直接采用更激进的内核驱动或代码注入方案?答案在于iOS的安全模型不同:它不允许内核扩展,也不允许未签名代码执行。

点击阅读原文跳转到原博客