View in English

  • Apple 开发者
    • 入门汇总

    探索“入门汇总”

    • 概览
    • 学习
    • Apple Developer Program

    及时了解最新动态

    • 最新动态
    • 开发者你好
    • 平台

    探索“平台”

    • Apple 平台
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    • App Store

    精选

    • 设计
    • 分发
    • 游戏
    • 配件
    • 网页
    • Home
    • CarPlay 车载
    • 技术

    探索“技术”

    • 概览
    • Xcode
    • Swift
    • SwiftUI

    精选

    • 辅助功能
    • AI 与机器学习
    • App Intents
    • Apple 智能
    • 游戏
    • 安全性
    • Xcode Cloud
    • 社区

    探索“社区”

    • 概览
    • “与 Apple 会面交流”活动
    • 社区活动
    • 开发者论坛
    • 开源

    精选

    • WWDC
    • Swift Student Challenge
    • 开发者故事
    • App Store 大奖
    • Apple 设计大奖
    • Apple Developer Centers
    • 文档

    探索“文档”

    • 文档库
    • 技术概述
    • 示例代码
    • 《人机界面指南》
    • 视频

    发布说明

    • 精选更新
    • iPadOS
    • iPadOS
    • macOS
    • watchOS
    • visionos
    • Apple tvOS
    • Xcode
    • 下载

    探索“下载”

    • 所有下载
    • 操作系统
    • 应用程序
    • 设计资源

    精选

    • Xcode
    • TestFlight
    • 字体
    • SF Symbols
    • Icon Composer
    • 支持

    探索“支持”

    • 概览
    • 帮助指南
    • 开发者论坛
    • “反馈助理”
    • 联系我们

    精选

    • 《开发者账户帮助》
    • 《App 审核指南》
    • 《App Store Connect 帮助》
    • 即将实行的要求
    • 协议和准则
    • 系统状态
  • 快速链接

    • 活动
    • 新闻
    • 论坛
    • 示例代码
    • 视频
 

视频

打开菜单 关闭菜单
  • 专题
  • 所有视频
  • 关于
  • 简介
  • 转写文稿
  • 采用 Memory Integrity Enforcement 和指针认证

    了解 Memory Integrity Enforcement 和指针认证如何协同防御 App 内存损坏。探究类型感知型安全分配器如何利用内存标记扩展来防止漏洞被利用,以及如何调整分配器包装器和对象池以维护硬件保护。此外,探索如何在 Xcode 中针对 arm64e 启用指针认证,以维护控制流完整性。

    这个讲座最初作为“与 Apple 会面交流”活动“加固你的 App:关键策略助你增强安全性”的一部分讲授。请观看完整视频,以了解更多见解和相关讲座。

    资源

      • 高清视频
      • 标清视频
  • 搜索此视频…

    欢迎参加今天的第一场深度解析。 我叫Enrico Perla。 我是一名安全工程师,稍后我的同事 Filippo Bigarella 和 Oliver Hunt 也将上台与我一同分享。 我们将探讨我们最令人兴奋的两项安全功能。 内存完整性、强制执行以及指针认证。 这是一场面向高级用户的演讲。 我们将探讨内存分配器、指针以及安全模型。 首先,我将向大家介绍内存完整性强制机制 在保护你的App方面所采取的措施。 随后,Filippo将向大家 展示如何解决某些特定的自 定义内存管理实现问题—— 这些问题可能会阻碍App的安全性, 甚至完全无法正常工作。 请牢记完整性强制机制。 随后我们将转换话题, Oliver将登台讲解 我们安全策略的另一个方面。 我们将探讨如何利用 指针认证来防止攻击者实现任意代码执行。

    介绍就到此为止。 让我们从内存完整性强制执行开始。 我们之前提到过, 我们绝大多数的安全问题都是与内存 完整性强制执行相关的内存损坏漏洞。 我们的使命是让大量 DDS 漏洞变得可利用, 这意义重大, 它们不再是你App的安全隐患。

    我们将以与自身使用完全 相同的方式发布内存完整性强制机制。 这不会对我们造成任何限制。 这是因为我们坚信,无论是第一方 App 还是第三方 App, 对保障用户安全都同样至关重要。 因此,我们整理了一些优质资源, 帮助大家入门内存完整性强制机制。 我们将发布一篇博客文章, 详细阐述内存完整性 强制机制所涵盖的所有安全维度。

    我们还准备了一场技术讲座, 将逐步指导你如何在App 中集成内存完整性强制机制。

    此外,我们还围绕同一主题 整理了全面的端到端文档。 大家无需做笔记。 我们会通过电子邮件后续发送所有相关链接。 如果你是稍后观看本视频, 这些链接也会附在在线讲座中。 所以请务必查看这些资料。 这些资料非常有用。 今天我不会对内存完整性 强制机制进行全面介绍。 相反,我将聚焦于我们的App, 重点探讨其安全核心—— 利用内存标记扩展的类型 感知安全内存分配器(MTV)。

    MTV 是一项硬件技术, 也是内存完整性强制机制 背后的最大投资之一,而且可以说, 它是对App影响最为显著的技术。 那么,让我们快速了解一下它。 这是一个锁与钥匙系统。 锁以 16 字节为粒度分配给内存。 这个粒度比页面要小得多, 对于动态分配来说非常理想。 而钥匙则存储在指针中。 高位、锁和密钥通常统称为标签,因此得名。 内存标记软件负责控制标签的分配。 这就是我们所说的标记策略。 如图所示,黄色缓冲区标记 为 7 是软件做出的决定, 左侧的两个指针 也是如此。而硬件则负责 实现所谓的校验策略, 仅在锁和密钥匹配时才允许访问。 现在,即使指针有了标签,

    这对软件的影响也不大。 事实上,启用内存完整性 强制(MIT)非常简单。 只需在 Xcode 中点击几下即可。 与其他需要相当大采用成本的技术不同, MIT 主要适用于未经修改的软件。 当然,除非——我的意思是—— 这里一定有某种陷阱, 否则我们今天也不会聚在这里。 除非你的App实现了自定义内存管理。

    我之前已经多次绕着这个话题转过, 所以让我们来详细探讨一下这意味着什么。 在App中,有三种模式 会导致自定义内存管理。 我将按照它们出现的可能性 从高到低的顺序进行说明。 第一种是内存

    分配封装器。 内存分配封装器是封装系统分配API的接口, 通常出于可移植性考虑, 或者因为你需要在内存 操作周围实现一些额外逻辑。

    这是目前最常见的实现方式。 因此,与我稍后将要讨论的其他两种情况不同, 我将在此给出一个简短的示例来阐明这一概念。 这里我们定义了一个名为 allocate memory的函数, 它实际上会调用 malloc。

    你们看到的其余代码虽然 直接调用了 malloc, 但实际上总是通过这个接口进行调用。 我们先暂且搁置这个示例。 Filippo稍后会上台对此进行详细说明。

    第二种直接内存管理模式是内存池和缓存策略。 这些策略的目标是 通过回收高频使用的对象来提升性能, 从而避免与系统分配器进行往返通信。 它们也较为常见,但远不及分配器封装器普遍。

    最后,还有一种在App中 极其罕见(所幸如此), 但在框架中却更为普遍的模式, 即完全自定义的内存分配器。 在这种情况下,我们有一个可直接替换的方案, 它完全绕过了系统的默认分配器。

    直观来看,这三种模式都会 损害内存标记完整性强制执行的安全性, 因为它们会干扰或完全绕过系统分配器。 因此,毫不意外,今天的关键建议如下:

    使用默认的系统分配器。 我将在这一张幻灯片上多停留一会儿。 请使用默认的系统分配器, 不要使用任何封装、缓存, 甚至不要进行自定义实现。 我们花费了大量时间, 确保它对绝大多数用户而言 既可扩展又快速,而且它是 过去三年间从零开始实现的。 因此,如果你参考的是三年前的性能数据, 请重新评估一下。 你可能会惊喜地发现。 此外,我们重写了该组件, 使其在完整性强制执行方面表现出色。 现在我们确实理解了这一点。 某些障碍的存在是有正当理由的。 也许你手头有些蹒跚前行的遗留代码, 不得不继续沿用。 这没关系。正因如此,在接下来的演讲中, 我们将探讨如何维护这些代码。 但要做到这一点, 你必须达到我们定位器所遵循的极高安全标准。 而要达到这一标准,你需要理解 我们为何要按这种方式构建它。 这意味着我们应当了解其背后的安全 科学原理。

    因此,让我先从攻击者的行为入手— —即利用系统漏洞。 并分享一些内容, 希望这些内容能引起大家的共鸣, 并实际为各位揭开这个话题的神秘面纱。

    编写漏洞利用程序与调试恰恰相反。

    每当你调试内存损坏问题时, 都是从犯罪现场开始的。 某些内存被错误地修改或使用, 导致程序行为异常。 你会尝试拼凑线索,找出原因、找出漏洞, 以及确定内存损坏的源头。而在漏洞利用中, 情况恰恰相反。

    你从某段被错误 处理的内存开始, 试图找到值得修改的内存区域。

    用更科学的方式来说: 我们采用了一种在业内略显独特的方法, 它实际上是以攻击者为中心的。

    我们有一段内存,称之为攻击者类型。 这就是引发内存损坏的那部分。 这里类型一词的作用非常重要。 它指的是内存位置的固有特性。 它可能是一个数据结构。 也可能是字符串,还可能是记录集合。 然后我们有受害者类型。 这是对攻击者而言值得修改的内存区域。 这就是内存破坏的攻击目标。 该内存可能包含密码、函数指针, 或是其他日后能让攻击 者实现任意代码执行的内容。

    这两种类型位于内存的某个位置。 因此它们之间存在特定的距离。 可能存在患者类型,此时距离为一。 或者它们甚至可能重叠,此时距离为零。

    我们假设攻击者能够监视 系统并执行任意次数的操作。 他们可以与内核通信、浏览文件系统, 只要权限允许,任何操作皆可。 我们并不依赖对攻击者行为的限制。

    如果攻击者能够控制并 预测攻击者与受害者之间的距离,他们就赢了。 就是这样。这就是编写内存 破坏漏洞利用程序的全部精髓。

    这看起来似乎很简单—— 嗯,好的科学通常都是如此—— 但实际上却非常深刻。 我们花了很长时间才得出这一结论, 而它之所以重要,是因为它突显了我们 在防御内存破坏时可采取的策略。 它表明,我们实际上只有 两个可以利用的杠杆:类型和距离。 从防御者的角度来看, 整个博弈的核心在于让攻击者无法预测或 控制类型选择和距离,或者使之变得极其困难。

    这就是为什么我们的安全内存分配器 都实现了四项关键安全特性。

    首先,它们具备类型感知能力。 传统上,通过 malloc 进行的内存分配只是一堆字节。 分配器除了请求的大小之外,几乎一无所知。 它并不了解分配的意图。 相比之下,我们的安全分配器能够理解类型, 这一巨大优势既得益于内核 中主要使用的手动类型声明, 也得益于用户空间中 采用的编译时自动类型推导。 一旦掌握了类型信息,我们就能开始发挥作用。 我们的内存分配器可以开始 让攻击者难以控制它们。 我们可以将不同类型划分到内存中的不同区域; 但由于类型数量庞大, 我们无法为每种类型分配一个独立区域—— 尽管这本是理想状态。 因此,我们的做法是将具有 相似特征的类型归类到一起。 我们在系统启动时对这些 集合进行随机化, 使得不同设备上的分组各不相同, 这迫使攻击者必须针对每种不同的组合来微调 其漏洞利用代码。 这些攻击代码可能在某台设备上运行。

    同理,我们也 会对这些类型区域在内存中的位置进行随机化。 同样,我们也 在系统启动时执行这一操作。 这样,我们就在不同 设备之间引入了更多的变异性。

    这再次挫败了 攻击者预测不同 类型类之间距离的企图。 最后但同样重要的是, 我们利用内存标记扩展来阻止针对各类型的攻击。 我们在每次内存分配和释放循环中分配 不同的标记,并确保标记在整个区域内均匀分布。 我们还强制执行以下规则:两个相邻 对象具有相同标签的情况,从而使利用 相邻小尺寸对象之间的距离成为不可能。

    这就是我们的安全分配器的工作原理。 为了防范内存损坏漏洞,

    现在我将把发言权交给Filippo, 他将介绍如何 防范我之前讨论过的三种常见的自 定义内存管理模式。 这样它们就不会影响你App的安全性。

    请Filippo接手。

    我叫Filippo,是 Caere 的一名安全工程师。 正如恩里科所说, 在大多数情况下,采用内存完整性强制 机制并不需要你进行任何代码修改。

    不过,在某些场景下,我们确实需要格外谨慎。 而这正是我接下来要详细探讨的内容。 我们将逐一探讨它们如何影响内存、完整性、 强制机制及其安全模型,并解释最佳的解决方法。

    那么,让我们先 从所有代码库中可能最常见的抽象概念—— 内存分配器封装器——开始看起。

    为此,我们不妨参考恩里科刚才展示的示例。 这个内存分配函数, 让我们有机会定义一组特征,通过这些特征, 你可以识别代码库中的内存分配器封装器。

    首先,封装器是围绕系统内存分 配器 API 添加逻辑的函数。 它们具有通用性,即允许 请求用于存储不同类型的内存, 并且作为与内存 分配器交互的抽象层,被广泛 应用于整个代码库中。

    所以这一切看起来似乎相当无害,对吧? 那么,让我们来探讨一下类型 隔离在实际中是如何运作的。 以便理解分配器封装器带来的安全影响。

    这里有一段实现数据包处理逻辑的代码。

    别担心,我们不需要逐行阅读。 实际上,让我们专注模式地 关注系统分配器接口的核心部分。 这里正是类型隔离的基础所在。 而实际上,是编译器在为你完成所有工作。

    当你在 Xcode 中启用 类型分配器的支持时, 我们会开启一项名为类型 内存操作的编译器功能。 借助这一功能, 编译器会自动为每个分配 代码点推断出类型, 并将每个调用重写为类型感知的调用, 同时将类型信息作为额外参数传递出去。

    你可以想象编译器为每次 内存分配分配不同的形状, 这些形状正是随后传递给系统分配器的信息, 系统分配器会利用 这些信息,通过实现 类型分桶策略来根据类型隔离内存分配。

    当你实现一个封装器时, 这些调用点对编译器而言就变得不透明了, 编译器只能看到一个单一的形状。 此时,系统分配器会将所有 分配操作归入同一桶中, 因为它只能看到包装器内部 调用点生成的类型信息。

    那么,让我们看看如何解决这个问题。

    当然,如果包装器实现的逻辑对代码 功能并非关键, 最简单的解决方案就是直接移除包装器, 并直接调用系统分配器的接口。

    如果你确实需要保留包装器, 可以实现一个类型感知变体 (type-aware variant) 通过采用类型内存操作 (type memory operations) 将类型信息正确地传递给系统 分配器。这正是操作系统 中广泛使用的编译器技术。

    那么,让我们一步一步地走过这个过程。

    首先,你需要为你的上层类型 声明一个类型感知变体, 该变体在 size 参数之后额外 接受一个 typeID 参数。

    然后,你需要使用下划线 malloc 类型 宏对无类型变体进行注解,指定相应的类型 变体以及 size 参数的位置。 我们正在指示编译器将所有对无类型变体的调用 转换为对类型感知变体的调用, 并在每个调用位置合成一个类型描述符。

    最后,你需要实现该类型感知变体。 但这非常简单。 你可以保留所有额外的逻辑。 唯一需要做的改动是,通过转 发作为参数获得的类型描述符值, 来调用相应的知型 malloc 接口。

    采用这种方法,你无需对实际 使用该封装器的代码进行任何修改。 编译器会自动在每个调用位置为你 提供类型信息。

    因此,这只是一个简要概述, 若想了解更深入的内容,建议你查阅相关文档。

    好的。现在我们已经 了解了如何处理分配器包装器。 接下来让我们转向内存池和缓存。

    所谓池, 是指所有通过某种形式对同 类型对象进行回收利用的方法, 其目的是避免每次此类对象被释放后 再次被使用时, 都需与系统分配器进行往返通信。 缓存策略也属于这一范畴。

    在此背景下, 需要理解的关键概念是:这些抽象机制 会改变被回收对象的生命周期。

    这会对内存标记所提供的保护 机制产生严重的安全影响。 下面我将通过一个示例来详细说明这些影响。

    这里有一个对象池, 其中的每个对象都由经过 内存标记的内存分配支持。

    当我们从池中提取一个对象时, 通常会获得指向该对象的指针, 该指针将与该内存分配所 关联的内存具有相同的标记。

    使用完该对象后,我们会将其放回池中。 现在设想一种情况:我们的代码中存在一个漏洞, 导致我们保留了一个指向该对象的悬空指针。 这正是可能导致释放后 使用漏洞的原因,攻击者可以利用该漏洞。

    如果我们只是简单地 回收该对象,这意味着悬空指针 和新的轻量级指针都将仍然具有有效的标记。 控制了悬空指针的攻击者将能够在对象被回收后 对其进行修改,从而 导致内存损坏,进而改变App的逻辑。

    为了使内存分配免受此类漏洞的侵害, 应在对象被回收之前更新其标记。

    重新标记后,任何使用带有旧过期标记的指针 进行的访问都会安全地失败, 从而终止App并防止漏洞被利用。

    接下来,让我说明如何在实践中实现这一点。

    我们在数据包处理代码中实现了回收策略。 当需要分配一个数据包时, 我们会首先尝试从一个线程局部队列中提取它—— 该队列由我们维护,并在释放数据 包函数中处理对象时进行补充。 如你所见,这种方法 恰恰会面临我们刚才所讨论的问题。 那么,我们该如何维持 MTA 提供的保护机制呢?

    当然,此时你可能已经猜到了。 最佳解决方案仍是直接使用系统分配器。 这能确保每次分配都得到正确的重新标记, 从而为你提供内存完整性强制 机制所应有的保护。

    我们花费了数年时间 设计并实现了一个既安全, 又在所有场景下都优于旧分配器的系统分配器。 事实上,借助线程本地缓存, 我们的系统分配器也针对此类场景进行了优化。

    在极少数情况下,如果这种方法不适合你, 我们建议你对代码进行 性能分析,并探索那些不改变对象 生命周期的不同策略。 例如,将你将要使用的对象分批分配。

    好的,因此这种方法使你 能够极其轻松地调整 池化策略,以保留 MI 所提供的安全特性。

    现在,我想花一点时间谈谈自定义分配器。 这未必会在你的App代码中实现, 但它们可能是你所 使用的任何外部依赖项的一部分。 历史上,库通常出于性能考虑或 跨平台移植性而实现它们。

    一旦存在自定义分配器,一切保障都将不复存在。 必须清楚认识到:使用自定义 分配器的代码无法享受 Ma 带来的好处,特别是, 你的App无法受益于类型隔离和内存 标记提供的保护。

    在这些情况下,你应认真 评估保留此类实现所带来的安全影响。 我想你应该已经猜到我们的建议是什么了。

    请将该代码迁移为直接使用系统分配器。 我们坚信,这是保障App 安全的基础。 不过,我们也意识到,在某些情况下 你无法轻易放弃自定义分配器。

    正因如此,从 26.1 版本开始, 我们在所有平台的 SDK 中都已包含你 在自定义分配器中实现 MT 支持所需的所有构建模块。

    接下来我们将逐一 介绍这些模块,但鉴于每个 分配器实现都有其独特之处, 你需要根据自身用 例来理解如何使用这些构建模块。

    首先,你需要检查运行时是否启用了 MT, 可通过如下所示的操作系统 安全配置 API 进行检查。 启用硬件内存标记后, 你的 App将在支持该功能的设备上以 启用empty指令集的方式运行, 但相同的代码仍需在不支持该功能的设备上运行。 请将所有依赖empty 指令集的操作进行相应处理。 架构设计应仅在确认 进程中empty功能确实已启用后才进行。

    接下来,当你通过分配器分配内存页时, 应要求虚拟机(VM) 提供支持内存标记的内存页, 并绕过调用 VM 分配函数时的empty标志。 此时,内核将为你提供一个关联 标记均为零的内存页。

    原因就在于此。最后,也是最重要的一点, 你需要为分配的内存添加标记。 在选择标记方案时 有多种不同的可能性, 且在实现每种方案时 都需要考虑许多细节。

    为了简要概述可用的 API, 我们仅考虑一个简单的示例: 在释放每个内存位置

    时对其重新标记。回到我们的分配器。 在标记和分配方面, 主要涉及两个部分 :选择目标、将其包含在指针中, 然后将标记存储到标记存储器中。 要选择一个标记,第一步是 生成一个排除掩码, 该掩码会考虑当前与该分配相关的目标。 这样,你就可以要求硬件通过排除之前 使用过的标记来生成一个新的随机标记。

    调用 `empty generate random tag` 会返回一个指向同 一内存区域的指针, 但其高位中包含不同的随机标记。 最后但同样重要的是,你需要使用 `empty store tag` 向硬件请求将新生成的标记存储到标记存储区中,方法 是传入包含新标记的指针以及需要标记的底层内存块的大小。这确实就是你在内存分配器中对内存进行标记所需的一切。

    以上就是你在内存分配器中实现内存 标记支持所需的所有基本工具。

    这实际上也标志着我们在米兰的旅程即将结束。 但在结束之前, 请允许我重申本节的关键要点, 以及你可以采取哪些措施来充分 利用内存完整性强制机制。

    你应启用硬件内存 标记功能并支持 Typekit 分配器, 从而利用业界领先的技术为用户 提供切实有效的保护。 在缓解内存损坏漏洞方面, 这些措施非常容易 采用,尤其是因为在绝大多数场景下, 你无需修改代码即可获得这些 技术所能提供的所有安全优势。

    然而,正如我们在本次 演讲中所见,某些编程模式会降低内存 完整性强制机制所提供保护的有效性。 我们建议你对代码库进行审计并识别这些模式, 从而更好地评估所面临的风险。

    每当你识别出此类 模式时,首选的解决方案应是 直接转为使用系统分配器。

    这确实能让你获得最佳的解决方案。

    不过,如果有时这不可行, 我们已为你提供了 必要的工具和知识,以便你调整此类抽象层, 以支持内存完整 性强制执行,并保留深植于操作 系统中的安全特性。

    现在,我邀请Oliver上台,谈谈基于指针 认证的控制流完整性。

    谢谢,Filippo。大家好。 我是Oliver Hunt。 我在 Apple 公司担任工程师, 主要负责安全与工具、 安全工具以及编译器方面的工作。 在本次讲座的这一阶段, 大家已经了解了如何通过 内存完整性强制机制来保护 代码免受内存安全错误的侵害。 内存完整性(MI)使得利用 内存安全漏洞变得极其困难, 但它无法防范所有可能的攻击。 接下来,我将向大家展示如何 采用基于硬件的指针认证技术, 为你的App提供控制流完整性。 这意味着,即使攻击者能够 绕过该机制并篡改任意内存, 在指针认证机制的保护下, 他们仍然无法控制你的 App 程序将要执行的代码。 硬件、编译器和操作 系统协同工作,确保攻击者 在试图劫持你的App 时所针对的指针的有效性。 其工作原理如下。 在底层,

    通过指针认证, CPU 和操作系统会为指针生成加密签名, 然后将这些签名嵌入到指针本身中。 这些签名使硬件能够在指针 被使用前检查其有效性, 从而为你的代码建立一条信任链, 将指针在使用时的值一直追溯到其原始值; 即使在同时

    使用鼠标的App中, 指针认证也能持续保护这些指针。 内存标记扩展通过透明地调整 签名和认证操作,以适配现有的标记。 在所有这些机制就位的情况下, 当攻击者试图篡改指针时, 签名将失效,信任链随之中断。

    此时,当你的代码随后尝试使用该指针时, 硬件会检测到这一情况并 安全地中止你的App。 攻击者在执行任何恶意代码之前已被阻止。

    在 Apple, 我们开发指针认证 API 已有近十年时间,在此期间一直将其 作为保护我们平台的工具。

    如今,该设计已足够稳定和健壮,你可以采用它, 并在你的代码中应用与我们保护自身 软件时完全相同的防护措施。

    虽然指针认证确实会对代码生成带来显著变化, 但我们已确保你的App仅 在少数几个地方可能会察觉到差异。

    那么,让我们来看看主要的变化。

    返回地址长期以来一直是攻击者的目标, 多年来已部署了各种缓解措施, 通过指针认证来保护它们。 现在,这一保护 措施被提升到了一个新的高度。 每当你进行一次调用时,都会产生一个返回地址。 通过指针认证,我们将返回地址本身以及 当前调用帧的相关信息整合 到嵌入该返回地址的签名中。 随后,在追踪该返回 地址之前,这些信息也会被用于对其进行认证。

    这确保了从返回地址首次记录之时 到实际使用时的信任链完整无缺, 甚至能防止攻击者复用来自 先前调用的有效签名指针。

    所有这些操作都作为基本调用约定的一部分进行, 只有极少数情况下, 用汇编语言编写的功能才能察觉到。

    现在,如果你的代码 与调用栈的交互超出了单纯调用函数的基本范畴, 我们也确保了你用 于实现此目的的所有 API 和编译器 功能仍能无缝运行。

    接下来,让我们详细探讨你为 App 程序的动态控制流而明确使用的功能。

    我们将从函数指针开始, 因为这是App中动态控制流的最基本形式。 在 C 和 C++ 等语言中, 存在各种各样限制最少的间接代码执行形式。 因此,我们确保它们始终是带符号的。

    这一点非常重要, 因为在 C 和 C+ + 中, 将函数指针强制转换为整数或不 透明指针是一种常见的编程惯例。 而且我们知道,你无法始终避免这种情况。

    因此,我们确保指针认证模型 在所有这些操作中都能保持嵌入式签名, 而无需你修改代码。

    这意味着,信任链得以维持, 攻击者无法在任何环节篡改这些指针—— 即使编译器可能已无法识别它们是 函数指针。

    但我们知道,在你的代码中, 你并不希望完全依赖函数指针来处理所有操作。 因此,你会广泛使用语言支持的动态分派。

    这包括在 Swift 中调用 非 final 方法、 在 C+ + 中调用虚拟方法, 以及在所有语言中通过 Objective-C 发送消息。 动态分派建立在多层间接引用之上。

    最简单的形式下, 每个对象实例都拥有一个指向某种类型 信息或方法表的指针。

    然后,该数据结构又包含另一个指针, 指向每个方法的实际实现。

    这种交互机制…… 这种交互机制对攻击者极具吸引力, 因为其中的每一层都可以被独立攻击。

    正因如此,在 Swift、 C + + 和 Objective-C 中, 我们对这条链中的每一步都进行了保护。

    当我们在该链中为每个指针创建签名时, 会包含有关对象类型、对象标识或位置的信息, 甚至包括目标方法的类型。

    随后,当你进行动态调用时, 每个步骤都会使用相同的信息进行认证。

    通过这些措施, 我们确保你的App不仅能抵御内存破坏, 甚至还能防范生命周期和类型混淆攻击。

    但正是这种级别的保护,也是指针认证 可能要求你修改代码的少数原因之一。 要了解原因,让我们聚焦于第一个认证步骤。

    通过将对象位置纳入每个签名中, 我们确保了该签名仅在内 存中的此位置有效。

    这可以防止攻击者利用内存安全错误将一个对象 覆盖到另一个对象上。

    但当你使用 Memcpy 等 低级函数来复制这些对象时, 这在根本上与攻击者的做法并无二致, 结果也将相同。 当你稍后尝试使用该对象时, 将会遇到身份验证失败。

    这是指针身份验证将现有代码 从看似正常但行为未 定义转变为运行时失败且 行为未定义的典型案例之一。

    因此,我们仅介绍了指针身份验证所 能提供的最高级别的保护。 其实还有更深层次的保护机制, 但这些你通常不会遇到。

    我们设计此实现方案时, 已确保其能与你的现有代码兼容。 因此,我相信你一定迫不及待 地想在自己的App中采用它。

    接下来,让我带你了解实现这一目标所需的步骤。

    在你开始将其应用到自己的 App 程序之前,你需要确保所有嵌入或 链接的库都支持指针认证。

    如果你是这些库的作者, 则需要自行完成这项工作。 但如果你使用的是外部开发的库, 则需要联系开发者, 要求他们采用指针认证并 为你提供通用二进制文件。

    完成上述步骤后,你就可以在自己的 App 程序中开始采用该功能了。

    具体操作是在 Xcode 项目的构建 设置中选择启用增强安全选项。 这将启用内存完整性、强制执行和指针认证。

    执行此操作后, Xcode 将自动配置你的项目, 以构建一个包含熟悉的 Arm64 切片以及 额外 Arm64 eslice 的通用二进制文件, 后者由支持指针认证的硬件使用。

    如果你希望一次只专注于采用一项功能, 则只需使用启用指针认证选项, 即可单独关注指针认证。

    我们重新设计了所有基于 指针认证的保护机制, 确保它们能与你的现有代码兼容, 而且在启用 所有这些保护措施后, 你的绝大多数App都能正常构建和运行。 但当然,这并非绝对保证。 因此,下一步是你需要对App进行全面测试。

    此时,你最初遇到的一些错误可能是由于代码 中原本存在的缺陷所致, 而这些缺陷现在被指针认证失败所捕获。 这是预期的结果。 既然你已经发现了这些错误,就能着手修复它们。

    但也有可能你的某些代码 在编写方式上与指针认证不兼容。

    对于这类情况,你需要进行一些修改。

    这些不兼容问题的根本原因在于, 你无意中触发了未定义行为。

    由于这些操作往往 与攻击者所利用的漏洞重叠, 因此会被相同的保护机制拦截。

    实际上,大多数代码都不会遇到这些问题。 但请允许我简要介绍我们遇到过的几 种最常见的兼容性错误来源。

    我们观察到的最常见模式,源于 在复制多态对象时不安全地 使用 memcpy 等函数。

    我们之前已经讨论过, 为何在此类操作中使用 memcpy 或类似函数是不安全的。 但即使你没有直接调用这些函数, 你的容器类型中的代码也可能执行了这些操作。 而这正是我们观察到这些故障发生的地方。 解决这些错误的方法是:要么采用更高 层次的语言特性和数据结构, 要么使用库函数来复制和初始化对象, 因为这些功能已内置于你的语言中。 它们了解你对象的类型, 并将尽可能高效地完成所有必要的工作, 以确保正确的语义。

    更不常见的情况是将额外 数据存储在函数指针的高位中。 如果你的代码存在这种情况, 该存储操作将导致指针签名失效。

    要解决此问题,你需要将起始 标记移至指针的低位, 或者干脆将该数据完全移出指针范围。

    这些是我们曾在采用指针认证的代码 中见过的最常见模式, 但即使在极其庞大的代码库中, 它们依然非常罕见。

    但如果你在App中确实发现了这些模式, 就需要在实施认证的过程中予以解决。

    现在,在完成所需的任何更改, 并且测试表明App运行如预期后, 你将把它部署给用户。

    与任何版本发布一样, 你可能会收到关于新崩溃的报告。 而你需要确定这些是否是认证失败导致的。

    为了帮助你诊断此问题,当程序终止时, 生成的崩溃日志中会包含一条诊断信息—— 如果故障可能是由身份验证失败引起的。

    不过,如果你正在执行自己的崩溃日志记录, 你可以检查故障地址的高位来提供 类似的诊断信息。

    现在,仅仅知道崩溃是 由于身份验证失败引起的还不够。 因此,让我们来看一个身份验证失败的示例, 以便你了解其表现形式以及可能的修复方法。

    这里有一个非常简单的程序, 它遇到了身份验证失败。

    当你在 Xcode 的调试器中捕获到该故障时, 起初它看起来与其他崩溃无异, 但由身份验证失败导致的异常 总是会设置故障地址的高位。 这就是你

    在这里看到的 情况。 现在,请不要 过分专注具体被设置的位, 因为这在不同硬件代际之间可能会有所不同。

    在这个小测试程序中, 错误发生在调用复制对象函数之后 立即。 我们之前已经了解到,现有的代码可能以 某种方式复制数据,从而导致生成无效对象。 那么,让我们来看看这个函数。

    或许不足为奇——毕竟这是个演示错误的示例—— 该函数正使用 `Memcpy` 来复制多态对象, 因为这种做法过去是可行的。

    虽然这个示例能非常直观地展示 这种情况在你的代码中是否发生, 但它通常只会在自定义或特殊情况下的容器 及数据结构中出现。

    值得庆幸的是,即使未启用指针验证, 编译器也会针对这些不安全的操作发出警告, 从而帮助你尽早发现这些问题。

    如果你的代码库允许的话。 你应该将这些警告配置为错误。

    现在你已经找到了问题所在。 你需要决定如何修复它。 你有多种可选方案。 那么,让我们来看看其中几种。

    最简单的修复方法是放弃无类型内存访问函数。 即将你的 `mem_copy` 和 `mem_move` 替换为标准库中的函数。 这些函数用于移动、复制和初始化对象。

    这些函数将确保完成所有必要的工作, 以确保你的对象被正确初始化, 并且在安全的情况下, 它们会使用 Memcpy 等函数。

    但鉴于你已经开始摒弃这些低级函数, 你应该考虑直接采用更高级的语言特性。

    例如,这些复制操作的安全等效方案,就是直接 在代码中使用类似 placement的功能。

    我们遇到过最棘手、且难以修复的情况, 就是当你使用 `memcpy` 时, 因为你试图保留对象的动态类型。

    要解决这个问题,可能需要你重构代码, 转而使用语言级多态等机制。 例如,在这个示例中, 我们用虚拟克隆方法替换了复制操作, 或者采用手动类型感知逻辑来执行这些复制。 也就是说,你需要检查类型。

    显然,这仍然是一个非常基础的演示程序, 仅用于向大家展示认证失败会是什么样子, 以及可能的修复方法。

    但几乎所有编程语言中的认证失败问题, 修复方式都是一样的:你需要摒弃低级操作, 转而利用所用语言及其运行 时和库所提供的支持。

    我已经详细讨论过指针认证如何保护你的代码。 我们甚至还看了一个用 C+ + 编写的示例。

    但你可能认为自己不需要采用指针认证, 因为你已经采用了(或正 在采用)像 Swift 这样安全的语言。

    但这还不足以保护你的代码。

    要理解原因,我们需要看看攻击者将如何 针对你的代码发起攻击。

    从根本上说,攻击者需要两样 东西才能控制你的App。 首先,他们需要一个漏洞, 用来破坏App的状态。

    在此示例中,使用不 安全的 `scanf` 函数会让攻击 者能够导致结果缓冲区溢出。

    其次,他们需要一段会处理 该受损状态结果的代码。

    此时,他们可以利用 该缓冲区溢出,覆盖 调用函数中错误处理函数的内容。 当该错误处理函数被调用时, 它将执行攻击者预先选定的指令。 至此,攻击者已接管了App的控制流, 并能够执行任意代码。

    因此,在思考软件漏洞利用时, 你不能仅关注原始漏洞本身。 你还需要思考攻击者试图 利用该漏洞达成什么目的。 现在,让我们来看看这个调用者。 这段不安全的 C 或 C+ + 代码, 想必你已经见过无数次了。 但在安全语言中,它会是什么样子呢? 我已经用 Swift 重写了这个函数, 它与 C 版本几乎完全相同。 事实上,之前用于覆盖 C 函数中错误 处理程序的那个漏洞利用代码, 同样可以替换 Swift 版本中的错误处理程序。

    再次强调,我们使用的是 非常简单的示例以便大家理解, 但无论你使用哪种语言,无论原始漏洞是什么, 无论利用起来有多复杂, 结果都是一样的。 这是因为当用户运行你的 App时, 他们不仅在运行你自己的代码, 还在运行与你代码 并行运行的所有库中的代码。 因此,无论你使用哪种语言, 都需要保护你的代码免受进程中同时 运行的任何不安全代码中可能存在的漏洞的侵害。

    通过采用指针认证,你就能做到这一点。 这使得攻击者更难入侵你的App。 即使是那些设法绕过 内存完整性、强制执行等其他 保护措施的攻击者也不例外。

    而且,你几乎无需修改现有代码 (甚至完全无需修改)就能获得这种保护。

    这就是你如何利用指针认证来防止 攻击者劫持你的App。

    最后,我要感谢大家与恩里科、 Filippo以及我一同参与本次概述, 了解内存、完整性、 强制机制和指针认证如何通过硬件、编译器、 操作系统以及你的 App 程序的协同工作来保护你的用户。

    我相信各位会发现, 采用并运用这些工具既切实可行, 又几乎无需修改代码, 同时能为App的完整性和整体 安全性带来显著益处。

    非常感谢。现在将话筒交还给柯特。

开发者页脚

  • 视频
  • Meet with Apple
  • 采用 Memory Integrity Enforcement 和指针认证
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    • App Store
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • Icon Composer
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • AI 与机器学习
    • Apple 智能
    • 音频与视频
    • 增强现实
    • 商务
    • 设计
    • 分发
    • 教育
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 安全性
    • Safari 浏览器与网页
    打开菜单 关闭菜单
    • 文档
    • 下载
    • 示例代码
    • 视频
    • 文档归档
    打开菜单 关闭菜单
    • 帮助指南与文章
    • 联系我们
    • 论坛
    • 反馈与错误报告
    • 系统状态
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件
    • “反馈助理”
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • Mini Apps Partner Program
    • News Partner Program
    • Video Partner Program
    • 安全赏金计划
    • Security Research Device Program
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖
    • Apple 设计大奖
    • Apple Developer Academy
    • WWDC
    阅读最近新闻。
    获取 Apple Developer App,并在 bilibili 和微信上关注我们。
    版权所有 © 2026 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则