-
借助 Sanitizer 查找安全漏洞
探究 Address Sanitizer 和 Thread Sanitizer 如何帮助你发现隐藏的内存损坏和并发问题。了解 Address Sanitizer 如何以字节级精度检测 Swift、C、C++ 和 Objective-C 中的缓冲区溢出和“释放后使用”错误。探索 Thread Sanitizer 如何发现难以察觉的数据争用,以及如何在自动化测试计划中配置这两个工具。
这个讲座最初作为“与 Apple 会面交流”活动“加固你的 App:关键策略助你增强安全性”的一部分讲授。请观看完整视频,以了解更多见解和相关讲座。资源
-
搜索此视频…
大家好 我是丹 是 Apple 的软件工程师 我隶属于安全工具团队 主要负责 sanitizers的相关工作 今天我来这里 是想和大家聊聊 如何利用 sanitizers 在现有代码中发现安全漏洞
在本场演讲中 我将介绍 sanitizers 的概念 然后 我将重点介绍其中两个— — AddressSanitizer 和 ThreadSanitizer 它们是功能最强大的两个 sanitizers
首先 我将介绍其基本概念
Sanitizers 是一种漏洞检测工具 由于其严格的运行时检查机制 它们会在你的代码 中编译额外的记录机制和运行时检查 它们能帮助你在客户受到影响之前 发现那些你此前未曾察觉的漏洞
对于找出那些看似 无法解释的崩溃报告的根本原因 它们也具有不可估量的价值
现在我已经定义了sanitizers的概念 接下来将从宏观层面介绍 AddressSanitizer 当你的程序对内存进行非法操作时 AddressSanitizer 会 检测到并抛出错误
与 guard malloc 等仅 检测堆内存问题的工具不同 AddressSanitizer 还 能检测栈和全局变量区域中的问题 它的检查还具备字节级精度 这意味着即使是最微小的越界访问也会被捕获 接下来 我将介绍地址 sanitizer 能够 检测到的几种典型错误
在这个代码片段中 我计划对这个名为 original 的 NSString 变量进行原始复制 虽然可能不太明显但其中存在一个越界内存访问错误
我来解释一下原因
这里 我获取了 original 底层字节的指针 (如右侧所示)
然后 我为复制操作分配了 original.length 字节的空间
问题就出在这里
original.length 并 没有返回字节数 它实际上返回的是 UTF-16 码单元数 而挥手表情由两个这样的码单元组成
因此 `raw copy` 现在指 向了一块 5 字节的内存区域
接下来 我将 `raw original` 中的字节 复制到 `raw copy` 中 但最后两个字节将超出该内存区域的边界 这正是那种容易被忽略且可能被 利用的棘手错误
幸运的是AddressSanitizer 检测到了此处的缓冲区溢出并向我发出了警告
AddressSanitizer 检测 到的另一种错误类型是“释放后 使用”(use after free) 在这个 C++ 示例中我维护了 一个包含标签页的标准向量 并保存了一个指向当前活动标签页的指针
在右侧 可以看到向量中 为一个元素预留了空间 但该元素尚未被填充
我打开了第一个标签页邮件 并将其设为活动标签页
随后 我又打开了另一个标签页 新闻这迫使向量扩大容量
进而需要进行新的内存 分配并将元素移动到新位置
遗憾的是 此时 active 指针并未随之更新 现在指向了已释放的内存
当我尝试重新加载该标签页时 active 指针 被AddressSanitizer 检测到 并抛出错误提示使用了已释放的内存
如示例所示 Objective-C 支持 C、 C++ 和 Objective-C 三种语言 这包括手动和自动引用计数
最后 Swift 也是如此 尽管纯 Swift 代码不太 可能出现任何内存使用错误
但重要的是 AddressSanitizer 也能跨语言工作 这里我有一个 Swift 数组 numbers
为了从 C 函数中使用这个数组 我将获取一个不安全的缓冲区指针 请注意 指针 `nums` 指向数组的第一个元素
现在 我将调用 `sum` 函数 为此 我需要传入指向数组 起始位置的指针及其长度
`sum` 函数将遍历数 组的每个元素并将其相加 但我这里犯了一个经典错误 我本应使用小于 Len来循环 却误用了小于或等于 因此 最后一次迭代将超出数组边界
AddressSanitizer 捕获了这一错误 并抛出了关于缓冲区溢出的错误 值得注意的是 这正是使用 C 语言的带安全扩展 (band safety extension) 可以解决的那类安全漏洞 以下是你需要了解的关于地址 sanitizer 的信息 首先 它仅适用于开发者阶段 不适用于运行时加固
对于这样一款功能强大的工具 其内存和运行时开销都很低 各自约为 2 到 3 倍
它非常擅长在测试阶段发现缺陷 并在缺陷影响到客户之前 将其消除 因此尽可能多地进行测试非常重要 启用该功能后
请在单元测试和 UI 测试等 自动化测试中启用它 并在开发期间进行手动测试时也启用它 以充分发挥该工具的作用 触发边界情况和罕见路径非常重要
要在开发期间启用AddressSanitizer: 首先 打开产品菜单 然后打开方案 子菜单并选择编辑方案 进入方案编辑器
在此处 导航至运行方案 然后在诊断选项卡下 勾选AddressSanitizer复选框为测试计划启用AddressSanitizer 打开产品菜单 然后选择“测试计划”并编辑测试计划
打开配置选项卡滚动至 “运行时净化”部分
然后选择AddressSanitizer行 并将其设置为“开启”
现在我将演示如何使用AddressSanitizer进行测试
好的 这里是我从“混合语言” 示例幻灯片中提取的代码 我取一个 Swift 数组 numbers 然后 我将通过一个不 安全的缓冲区指针指向该数组 并在其中调用我的 C 函数 sum 我将遍历每个元素 计算它们的和 并将结果返回给 Swift
然后 我将打印出数组和计算结果 现在让我们运行一下
首先 我可以看到程序运行成功了
但显然这里的这个值是不正确的
因此 我现在将在产品方案下的“编辑 方案”中启用AddressSanitizer 并勾选AddressSanitizer 复选框
点击运行按钮后 程序会重新构建 并启用AddressSanitizer我立刻发现它捕获 到了该行代码中的堆缓冲区溢出错误
在左侧 我可以看到导致此问题的调用堆栈 并能发现这发生在 64 字节堆内存分配之后
在这个侧边菜单中我可以看到内存分配的位置 不出所料 正是构建那个 Swift 数组的时候
回到调用栈帧 我正处于实时调试会话中因此可以查看变量的值 目前我看到 `i` 与 `Len` 相等 这表明我的循环边界条件有问题 正如我在幻灯片中讨论的那样 这是因为这里多了一个等号 因此 我将删除它并重新构建 App
现在可以看到程序运行成功了 而且 sum 现在也返回了正确的值
好的 现在我回到幻灯片
接下来要介绍的 sanitizer 是ThreadSanitizer
ThreadSanitizer 用于 检测数据竞争及其他并发问题
当一个线程向某个内存位置写入数据 而另一个线程在未进行同步的情况下 访问同一内存位置时 就会发生数据竞争
在右侧的代码示例中 我从队列 pending 中取出头节点 发送它 释放它 然后将 head 递增 这在没有其他线程执行相同操作时运行正常 现在我有两个工作线程 以下是一种可能的操作交错情况:
线程一读取 head 其值为零
随后 线程 2也读取 head 其值为零
线程 1发送 pending 消息并释放该对象
然后它将 head 的值递增到一
这就是数据竞争 这取决于 线程 2的读取操作和 线程 1的写入操作发生的顺序 线程 2 中的 index 值可能会不同 如果 线程 2 获取的是 head 的过期值 就会导致它读取到已 被 线程 1 释放的 pending 零值 AddressSanitizer 会在发生“释放后 使用”时检测到该问题 而在本次执行中确实发生了释放后使用
这是同一段代码在同一对线程上运行 但这次它们之间没有操作交错 在此执行过程中 地址 sanitizer 无法检测到问题
但有个好消息 尽管我观察到了预期的结果 ThreadSanitizer 仍然 检测到这里存在问题并向我发出了警告 线程二上的这次读取属于数据竞争 ThreadSanitizer 还向 我展示了与其发生竞争的内存访问操作
该操作就在线程一上 这就是为什么ThreadSanitizer对于 查找那些看似无法复现的错误 报告的根本原因非常有用
为了解决这个问题 我使用了一个串行 分发队列 以确保这些代码段 中的每一部分都能原子地运行 并且前面的读写操作在右侧得到同步 我已将这些代码封装 在 `dispatch async` 中 它们将在一个专用的串行队列上运行
使用 Swift 并发机制 可以防止任何数据竞争
ThreadSanitizer 的启用方式 与 AddressSanitizer 相同
请注意 这两者互斥 因此一次只能启用其中一个
对于自动化测试 你可以为测试计划创建两种不同的配置:一种启用 AddressSanitizer 另一种启用 ThreadSanitizer
Sanitizer 在实施内存 完整性检查方面发挥着重要作用
地址检查器可帮助你发现并修复无效的内存访问 当 Emmy 启用时此类访问会导致程序崩溃
线程检查器可帮助你发现数据竞争 这通常会导致释放后使用问题
幸运的是 当发生释放后使用时Emmy 会触发程序崩溃 从而防止漏洞被利用 但这会给你的客户带来困扰
通过ThreadSanitizer查找并 修复该错误 将确保你的客户 安全且满意
接下来你需要采取以下措施 首先 请在测试计划的配置中添加AddressSanitizer 特别是当你的代码库中使用了 C、 C + + 或 Objective-C 时
然后 如果你使用了 C 或 Swift 的预并发功能请在测试计划中添加 ThreadSanitizer 采用 Swift 并发机制以防止 Swift 代码中的数据竞争
最后 下次当你收到无法 解释或无法复现的崩溃报告时 请尝试使用这些 sanitizer 工具看看它们能否检测到任何问题 也许它们能为你提供一些线索说明该异常状态是如何产生的
谢谢 接下来请 Kurt 发言
-