77、iOS逆向(六):iOS 14+新特性、指针认证(PAC)、代码签名

好,咱们继续聊iOS逆向。到了iOS 14之后,苹果在安全上又加了不少硬菜。说实话,我第一次打开iOS 14的设备做逆向时,心里就咯噔一下——这玩意儿跟以前不太一样了。

今天咱们重点聊三个东西:iOS 14+的新特性指针认证(PAC)、还有代码签名。这三者其实是环环相扣的,你搞懂了一个,另外两个也就顺了。

一、iOS 14+ 安全新特性概览

iOS 14 引入了一些让我印象深刻的改动。我个人习惯把新特性分成两类:一类是“看得见的”,比如App Clips、小组件;另一类是“看不见的”,也就是底层安全加固。咱们逆向工程师,当然更关心后者。

特性 影响 逆向难度
指针认证(PAC) 函数指针、返回地址被签名 ★★★★★
代码签名强化 运行时签名验证更严格 ★★★★☆
内核完整性保护 内核代码段只读 ★★★★★
App沙箱增强 文件系统访问更受限 ★★★☆☆

嗯,这里要特别提一下PAC。我在项目中遇到过好几次,明明hook点找对了,但一运行就崩。后来才发现,是PAC在作怪。

二、指针认证(PAC)—— 苹果的“防篡改锁”

PAC,全称Pointer Authentication Code。说白了,就是给指针加了个“签名”。

你想想看,传统的漏洞利用,很多都是通过篡改函数指针或者返回地址来劫持控制流的。PAC的做法是:在指针的高位(通常是高16位或高24位)嵌入一个认证码。这个认证码是用一个密钥和上下文算出来的。如果你改了指针,认证码就对不上了,CPU直接触发异常。

2.1 PAC 的工作原理

PAC 的核心是 ARMv8.3 引入的几条指令:

  • PACIA / PACIB:对指令地址进行签名
  • PACDA / PACDB:对数据地址进行签名
  • AUTIA / AUTIB:验证指令地址签名
  • XPACLRI:清除PAC码

举个例子,一个函数返回时,原本是:

ret     ; 直接返回,返回地址在LR寄存器

有了PAC之后,变成了:

autibsp  ; 验证LR中的PAC码
ret      ; 验证通过才返回

如果LR被篡改了,PAC验证失败,CPU会跳转到异常处理程序。嗯,说白了就是直接崩掉,不给攻击者任何机会。

核心要点:PAC不是加密,而是认证。它不隐藏指针的值,只是确保指针没有被篡改。

2.2 逆向中如何应对PAC

说实话,PAC对逆向的影响很大。以前我们hook函数,直接改GOT表或者Inline Hook就行。现在呢?函数指针被签名了,你改了指针,PAC验证就失败。

我曾经在一个项目中,想hook一个系统库的函数。用Frida试了好几次,一调用就崩。后来发现,那个函数指针被PAC保护了。

那怎么办?有几种思路:

  • 绕过PAC:在PAC验证之前或之后修改指针。比如在函数入口处直接修改寄存器。
  • 伪造PAC:如果你知道密钥(通常只有内核知道),可以自己计算PAC码。但这基本不可能。
  • 禁用PAC:在越狱设备上,可以通过内核补丁禁用PAC。但iOS 14+的越狱越来越难了。

小技巧:在调试时,可以用 lldbpacia 命令手动计算PAC码。但记住,这只是调试用,实际运行时密钥不同。

三、代码签名 —— 苹果的“身份证”

代码签名,说白了就是给每个可执行文件、dylib、framework发一张“身份证”。iOS系统在加载代码时,会检查这张身份证是否有效。

iOS 14+ 对代码签名做了强化:

  • 签名验证更频繁:不仅在加载时验证,运行时也会定期检查。
  • 签名范围更广:连JIT编译的代码也要签名。
  • 签名强度更高:使用SHA256哈希,不再支持SHA1。

3.1 代码签名的结构

一个Mach-O文件的签名信息存储在 LC_CODE_SIGNATURE 加载命令中。结构大致如下:

SuperBlob {
    magic: "fairsign" 或 "fairplay"
    count: 2
    blobs: [
        CodeDirectory {  // 代码目录
            version: 0x20400
            hashType: SHA256
            hashSize: 32
            hashes: [...]  // 每个页面的哈希
        },
        Entitlements {   // 权限
            data: "..."
        }
    ]
}

嗯,这里要注意,CodeDirectory 里包含了每个代码页面的哈希值。系统在加载时,会逐页验证这些哈希。如果你改了代码,哈希就对不上了。

3.2 逆向中的代码签名绕过

在越狱设备上,我们通常用 ldidjtool 来重新签名。但iOS 14+ 之后,有些签名验证是在内核里做的,光重新签名可能不够。

我记得有一次,我修改了一个系统dylib,重新签名后还是加载失败。后来发现,iOS 14 引入了 amfi(Apple Mobile File Integrity)的新策略,连已经签名的dylib也要验证完整性。

解决办法?要么打内核补丁禁用amfi,要么用 CoreTrust 漏洞(如果有的话)。

警告:不要在生产设备上尝试禁用代码签名。这会导致设备变砖,而且无法恢复。

四、PAC与代码签名的关系

这两个东西其实是配合使用的。代码签名保证了代码的静态完整性,PAC保证了运行时的控制流完整性。

我画了一张图,帮你理清它们的关系:

iOS 14+ 安全机制:PAC 与代码签名 代码签名(静态) 验证 Mach-O 文件完整性 逐页哈希校验 防止代码被篡改 指针认证(动态) 验证指针完整性 防止控制流劫持 运行时签名验证 加载时验证 运行时验证 共同目标:防止代码执行被篡改 静态 + 动态 = 双重保护 攻击者:需要同时绕过两者 逆向工程师:需要理解两者

从图上可以看得很清楚:代码签名是“静态锁”,PAC是“动态锁”。你只解开一个,另一个还是会把你锁住。

五、实战:在iOS 14+上调试PAC保护的代码

好了,理论说完了,咱们来点实际的。假设你有一个iOS 14+的设备,想调试一个系统库函数,但被PAC保护了。怎么办?

我个人习惯用 lldb 配合 debugserver 来做。步骤如下:

  1. 找到目标函数地址:用 image lookup -nnm 命令。
  2. 检查是否有PAC保护:看反汇编中是否有 autibsppacibsp 等指令。
  3. 在PAC验证前打断点:在 pacibsp 之前打断点,此时指针还未被签名。
  4. 修改指针:在断点处修改寄存器值,然后跳过PAC验证指令。

举个例子:

(lldb) disassemble -n _some_function
...
0x1a2b3c00: pacibsp          ; 对LR签名
0x1a2b3c04: stp x29, x30, [sp, #-0x10]!
0x1a2b3c08: ...
...
0x1a2b3c50: autibsp          ; 验证LR签名
0x1a2b3c54: ret              ; 返回

(lldb) breakpoint set -a 0x1a2b3c00
(lldb) breakpoint set -a 0x1a2b3c50

; 在第一个断点处,修改x30(LR)
(lldb) register write x30 0xdeadbeef
; 然后跳过pacibsp指令
(lldb) jump -a 0x1a2b3c04

注意:跳过PAC验证指令可能会导致后续的 autibsp 验证失败。所以最好在 autibsp 之前也做处理,或者直接跳过整个验证流程。

嗯,说实话,这种方法只适用于调试场景。在实际的越狱工具中,通常会用更底层的方法,比如内核补丁来禁用PAC。

六、总结

iOS 14+ 的安全机制,说白了就是“层层加码”。代码签名管静态,PAC管动态,两者配合,让逆向的难度上了一个台阶。

但别怕。理解原理之后,总有办法绕过去。我见过太多人一看到PAC就放弃了,其实只要你静下心来分析,找到PAC验证的时机和位置,还是可以hook的。

最后送大家一句话:逆向的本质不是对抗,而是理解。你理解了苹果的设计思路,自然就知道怎么应对了。


公众号:蓝海资料掘金营,微信deep3321