查看原文
其他

iOS 14.5 如何用 PAC 保护 Objective-C 对象

0xcc 非尝咸鱼贩 2022-08-27

预警:这是一篇对入门极度不友好的文章。本文接跳过 PAC 的概念,而只关心新版的改动。感兴趣的读者请自行参考相关资料


Pointer Authentication Code(指针认证)早在 iOS 12 和 A12 芯片就已经正式推出,不过这两年也经历了不少修修补补。Objective-C 作为 iOS 里的“一等公民”,由于 ABI(应用程序二进制接口)的限制,一些 Runtime 特性直到 iOS 14.5(截止本文发布,仍是最新正式版)才得到保护。


正好 Apple 最近更新了 opensource.apple.com,其中就包含了新版 Objective-C 的部分源码(objc4-824),结合反汇编,我们可以一窥新版的安全机制。




在之前的 GitHub 上,apple/llvm-project 里有一篇非常详细的文档介绍了 PAC 的基本设计、概念和一些潜在攻击的讨论:



PAC 利用 64 位指针高位的比特来添加额外的签名信息,起到对代码指针(如间接跳转、函数返回地址、回调)和数据指针(Class 信息等)的保护。


加密算法用到的密钥,在用户态最常见的三种是 IA、IB 和 DA。A key 在进程内全局都共享一个密钥,而 B key 则是每个线程各自有一个。I 对应 instruction,保护函数指针;D 则是 data,数据指针。


除了 key 之外还可以加盐,以区分不同类别的指针,防止指针替换攻击。


这个文档目前在主线当中已删除,不过提交记录还在


https://github.com/apple/llvm-project/blob/a63a81bd9911f87a0b5dcd5bdd7ccdda7124af87/clang/docs/PointerAuthentication.rst


在这个版本的文档中,涉及到 Objective-C 的部分有 Blocks 和 Objective-C Methods:



Block 即 Objective-C 里的匿名函数,编译器将会生成一个 Block_layout 结构,其中的 invoke 指针即是函数的代码地址,在执行 block 时调用。在 PAC 的设备上这个 invoke 指针被加上了签名,使用的是 IA key 和 block 的地址作为 salt 值,也就是用的 blraa 指令跳转。


在苹果的白皮书里也提到了这点:

https://support.apple.com/zh-cn/guide/security/seca5759bf02/web




Objective-C 方法缓存(method cache)指的是 runtime 用到的一个数据结构。在一个 Objective-C 对象实例当中,第一个成员是 isa 指针,用来标记类信息。而 objc_msgSend 函数也会根据 isa 指向的内存结构来查找 method cache,定位指定的方法的函数指针。


这个特性曾被用作漏洞 exploit 的编写。在 Pwn2Own 历史上有几次沙箱逃逸漏洞,都和 Objective-C 对象的类型混淆或者生命周期有关,最终利用 objc_msgSend 的机制来伪造函数指针,达到控制执行流,跨进程执行任意代码的目的。


phrack 杂志经典的文章 Modern Objective-C Exploitation Techniques 详细介绍了如何伪造 fakecache 和 cacheentry 结构来实现劫持 eip(任意代码执行)。虽然最新的系统细节已经做了大量修改,文章还是很有参考价值的。


换成 iOS 程序员能听懂的人话就是,类比 method swizzling 里的 Method 和 IMP,这篇文章介绍了如何伪造一个指向任意地址的 IMP,在内存里构造一个 self 对象,使得系统用其特定 selector 的时候让 cpu 跳转到任意地址的指令上。


正因为 method cache 的利用技术已经非常成熟,Apple 设计保护方案的时候使用了线程独立的 IB key,加上指针地址本身、class 和 selector 三个因素来生成 salt。




在这里文档有一句非常有意思的话:

Pointer authentication cannot protect against access-path atacks against the Objective-C isa pointer, through which all dispatch occurs, because of compatibility requirements and existing and important usage of high bits in the pointer.


在这个版本上虽然保护了 method cache,但是 isa 还没有!


因为在 PAC 之前,Objective-C 就很喜欢在指针的高位上做文章。有 tagged pointer 用指针本身来表示数字、日期甚至长度较短的字符串的,而 isa 指针的高位还被 ARC 用来做引用计数,判断对象是否需要被释放。


这个 ABI 设计导致 PAC 一时半会不能保护 isa,除非修改 runtime 代码。


Project Zero 在 2019 年实现了一个 iMessage 远程攻击的演示,只知道对方的 iCloud 地址,在 iPhone Xr 上完全不需要受害者任何交互直接远程入侵手机。这个实在是太经典了,我最近写文章起码念叨引用了十几次。


iPhone Xr 用的就是 A12。为了执行代码,这个演示使用了一种独特的 SLOP(SeLector Oriented Programming)技术。


当时的 PAC 还不能保护 isa,因此虽然不能伪造 IMP 来改变程序执行流,但是可以通过伪造 isa 来:1. 构造任意的 Objective-C 对象;2. 调用这个对象上合法的 selector。


iOS 程序员都很熟悉 NSInvocation 类,提供了比 performSelector: 更强大的动态代码执行能力。


SLOP 正是构造了一个由 NSInvocation 组成的数组,通过最外部的 NSInvocation 执行该数组的 makeObjectsPerformSelector:withObject: 方法触发“链式反应”,按顺序执行 NSArray 内所有的 NSInvocation,可以实现一连串的 Objective-C 方法调用。


这个链条可以调用任意方法,但不支持控制流等常见的编程需求。于是他们创建了一个 JSContext,通过 SLOP 将内存读写能力绑定到 js 脚本上,实现一个“图灵完备的代码执行环境,甚至可以把越狱漏洞移植上去。

在这里可以发现 Objective-C runtime 的两个急需保护的点,一个是 isa,另一个就是 NSInvocation。


铺垫了这么多,iOS 14.5 针对这两处加入了 PAC 校验。




iOS 14 上 ABI 做了大改,除了 WWDC 2020 上提到的针对内存使用,修改了 method 的结构和 TaggedPointer 的格式之外,其实已经开始对 isa 指针加签名了,只是还没开始要求校验:



这位 Apple 团队的成员也证实了这个说法,没有部署的原因来自于大量的 ABI 设计的挑战和一些边角案例。


到了 iOS 14.5,这个传说中的 signed isa 终于正式上线。


Objective-C 的部分源码可以到 Apple 官方查看和下载:

https://opensource.apple.com/source/objc4/objc4-824/


在这个文件当中定义了一个宏 ExtractISA:


objc4-824/runtime/arm64-asm.h


.macro ExtractISA and $0, $1, #ISA_MASK#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP xpacd $0#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH mov x10, $2 movk x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48 autda $0, x10#endif.endmacro


譬如 objc_msgSend 就用到了 ExtractISA。


在这段代码里定义了一个参数宏 ISA_SIGNING_AUTH_MODE。在 iOS 14 Beta - 14.4,都是使用的 ISA_SIGNING_STRIP 模式。isa 有加签名,但是不做检查,直接用 xpacd 移除签名后使用。


在这之前的版本上仍然可以使用伪造 isa 来构造任意对象执行 Objective-C 代码,只需要一个绕过 ASLR 的漏洞即可获得所有的 isa。


到了 iOS 14.5,ISA_SIGNING_AUTH 模式转正:



这里用的是保护数据指针的 DA key。0x6AE是一个常数 ISA_SIGNING_DISCRIMINATOR属于签名算法当中的 salt 参数指针 x0 本身也会对 sign 的结果有影响


非法构造的 Objective-C 对象将无法通过 AUTDA 指令检查,在指针特定的位设置一个非法标记,让指针本身变成一个非法地址,使用时将产生段错误。


这意味着在 arm64e 上,如果要深拷贝一个 Objective-C 对象,不能简单地当作结构体去 memcpy,而是只能走常规的 NSCopying 协议的 copy 方法。


因为对象的指针本身会对 isa 的计算产生影响,即使是分配两个 Class 相同的对象,它们的 isa 都是不同的:



这样一来直接 memcpy 之后出来的对象显然不能用,调用任何方法都会崩溃。


这样一来,首先没办法像之前一样随心所欲地通过构造结构体来伪造任意 Objective-C 类和对象;其次,即使漏洞本身提供了足够强大的内存读写原语,哪怕是能读取到现有的某个对象的内容,也不能简单复制其 isa。当然,修改现有对象的 ivar 仍然可行。


考虑到对现有对象的篡改的场景,NSInvocation 也做了对应的加固。


CoreFoundation 多了一个私有的方法:


-[NSInvocation _checkSigsForTarget:andSEL:]


在 -[NSInvocation invoke] 和 -[NSInvocation invokeUsingIMP:] 当中都使用到了这个方法。之前可以通过伪造 frame 结构来向 NSInvocation 当中填充任意的参数和 selector 实现任意方法调用。


而现在 NSInvocation 的 target 和 selector 属性都被 DB key 保护,且与 self 指针(NSInvocation 对象地址)绑定:



非法的内容将会抛出异常:Corrupt NSInvocation detected. Pointer auth invalid.


值得一提的是,在此前版本的系统中似乎还很少应用到 DB key:



图来自 bazad 在 BlackHat US 2020 上的主题演讲 iOS Kernel PAC, One Year Later




本文介绍了 iOS 14.5 针对 Objective-C 运行时进一步加强 PAC 保护的两处改动。这些保护仅针对系统自带应用和服务启用,目的是防止跨进程和远程代码执行的漏洞利用。


这些 Objective-C 运行时保护暂时不会应用到第三方 App 上。目前 AppStore 分发的第三方应用不支持启用 arm64e 指令,可能是出于防止跨进程攻击的考虑。此前的一些漏洞研究表明,第三方代码存在滥用 PAC 签名指令来跨进程甚至跨 Exception Level 构造合法指针的能力,可用于沙箱逃逸和或者越狱漏洞的利用上。


对于 App 开发者来说,只要不是在程序里用了依赖运行时结构的奇技淫巧,基本上不需要关心这些改动。对于有兴趣研究程序运行库的实现细节的,分析这些机制可以更深入了解系统在安全攻防上所做的设计和权衡。而对于笔者这样研究漏洞的,日子又更艰难些了。


参考资料


  1. Objective C source code
    https://opensource.apple.com/source/objc4/objc4-824/

  2. Modern Objective-C Exploitation Techniques - nemo
    http://phrack.org/issues/69/9.html#article

  3. Apple 安全白皮书
    https://support.apple.com/zh-cn/guide/security/seca5759bf02/web

  4. Issue 1933: SLOP - A Userspace PAC Workaround
    https://bugs.chromium.org/p/project-zero/issues/detail?id=1933

  5. Advancements in the Objective-C runtime
    https://developer.apple.com/videos/play/wwdc2020/10163/

  6. iOS Kernel PAC, One Year Later - Bazad
    https://bazad.github.io/presentations/BlackHat-USA-2020-iOS_Kernel_PAC_One_Year_Later.pdf



觉得文章有意思 给作者打赏靓靓嘅杯

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存