大多数浏览器和
Developer App 均支持流媒体播放。
-
保护你的 app 威胁建模和反面模式
考虑漏洞和潜在威胁并清楚应在 app 中的哪些位置应用保护措施非常重要。了解如何通过威胁建模来识别潜在风险以及如何避免常见的反面模式。了解编码技术以及如何利用平台提供的保护来帮助你减少风险并在用户使用你的 app 为他们提供保护。
资源
相关视频
WWDC20
-
下载
(你好 WWDC 2020)
大家好 欢迎参加 WWDC (保护你的 APP 威胁建模和反面模式) 我叫 Richard Cooper 这次讲座的内容全部涉及 如何使你的 app 更加安全 从而保护用户委托给你的数据和资源 在 Apple 我们对于确保 app 能够正确处理数据 尊重用户隐私 设计出稳健的可以抵御攻击的软件 保证我们的所有努力不付之东流充满热情 我的团队与 Apple 的软硬件团队合作 帮助他们提高我们正在构建产品的安全性 并发现我们已构建产品中存在的问题
今天要给大家分享的是 一些我们认为有用的技术 本讲座中 我们将介绍一些关于如何 在你的 app 环境中考虑安全性的概念
为此 我们将讨论威胁建模 如何识别不可信数据 并仔细检查一些人们常犯的错误 为使问题更直观 我们将使用一个简单的 叫做 Loose Leaf 的假设 app 我们对 Loose Leaf 寄予厚望 希望它能成为一个重要的社交平台 可以满足 热茶、冷茶和珍珠奶茶品茶人的各种口味 或许还包括咖啡
为了实现这个目标 我们将支持下列强大的功能: 我们需要社交网络 这样你就可以与世界分享你的活动 使用相机拍照发布的能力 还有创建现场直播视频 我们还想支持私人茶艺会所 让你只和你的朋友聊天 最后 我们将拥有支持更长的表单回顾 配对等能力 虽然这是一个相当简单的例子 但却有许多 与市面上流行的 app 相同的功能 当你看到你的 app 时 你要做的第一件事 就是退后一步想想它的威胁模型 威胁建模是一种思考 在安全环境中可能出现错误的过程 这很重要 因为它允许你考虑你所面临的风险 以及可以采取什么措施来减轻风险 我们不打算描述一个具体的正式流程 因为对于许多开发者来说 学会并遵循一个流程并不实际
相反 我们计划描述一些 你可用来应对威胁建模的策略 这些策略将帮助你思考安全与风险 那么 我们怎么做呢? 你的 app 所拥有的 关键内容之一就是资产
它们并非 你会在 Xcode 资产目录中找到的资产 而是用户可能认为 有价值、敏感或隐私的东西 相机和麦克风、定位数据 联系信息、照片和文件 都是很好的资产例子 这也是为什么我们在过去几年里 一直在竭尽全力地 为这些东西提供平台级保护的原因 一般来说 如果用户被要求授予访问权限 这就是资产
在这一过程中 他们把资产的控制权委托给了你 所以你就需要关心如何保护这个资产 但还有其他东西也表现为资产
用户输入到你的 app 中的意见和观点 都是非常重要的资产 虽然平台保护可能有所帮助 但这是需要特别小心 并确保采取一切保护措施的一类资产 现在 没有攻击者的 威胁模型是不完整的
以下是一些明面上的攻击者: 想要偷窃你的用户的罪犯 或者想破坏你的平台的竞争对手 但我们还应当考虑其他类型的攻击者 他们可能拥有不同级别的访问权限 浪漫的伴侣和家庭成员 就是这类人很好的例子
所以你应该问问自己 “我该如何防止这些攻击者?”
这很重要 因为通常对一台设备 人们的访问权限等级差异很大 无论是物理上还是通过共享帐户都是这样 当你可能认为这些攻击者 对 Loose Leaf 并不感兴趣的时候 如果我们对有用的资产拥有访问权限 并如我们所愿非常成功 我们就可能成为目标 攻击者 经常攻击社交媒体、银行业、生产能力 约会软件 甚至游戏 所以没人能够幸免
一般来说 攻击者是谁 并不重要 重要的是他们有什么能力 以及他们如何利用这些能力来影响我们 为了帮助我们看清 攻击者可能控制哪些数据 以及它在 app 中的位置 我们建议绘制一个数据流图 首先 画出系统中数据存储的位置
在我们这个案例中 我们的网站上被托管了一些信息 我们将把这些信息 用于公开发帖、帐户信息等等
一些信息也存储在 CloudKit 允许我们拥有分散管理的茶艺会所…
还有一个缓存 用来存储帖子和图像的本地版本 最终 这些将会成为保存用户敏感信息的地方
接下来 我们应该添加进去我们所利用的系统资源 我们还应该考虑 数据可能进入我们的 app 的其他方法
本例中 我们安装了一个 URL 处理程序 允许将链接传递给我们
最后 我们需要决定 我们系统中的安全边界应该在哪里 这些内容的布局非常重要 因为它是我们将要做的所有决定的基础 关乎我们该相信什么 不该相信什么 这里要问你自己的关键问题是 攻击者会在什么地方影响我的 app 什么数据是攻击者控制的 他们如何利用这些来获得我们的资产? 本例中 我们将在这里设置一个边界 在系统资源和我们的 app 之间
虽然我们信任这个系统 但内容却是不受控制的 例如 用户选择的文件 有可能来自不可信的来源 所以如果 处理其中的内容 我们就需要小心
自定义文档格式就是一个很好的例子
其他本地 app 我们也不能信任 所以 我们将 在 URL 处理程序上设置安全边界 因为这里提供的数据可能是恶意的
我们还要设置另一个边界 在我们的 app 和它的所有远程资产之间 尽管应该是我们的代码 把数据写入这些远程存储器 但情况可能并非如此 攻击者可能 已经编写了软件来编造恶意帖子 了解了攻击者可能 在哪里影响我们的系统之后 我们就应该 考虑体系结构方面的方法来保护它了 这在我们的 app 和 网站之间的连接方面关系重大
我们 Apple 公司提供了一种通用机制 来加强这些连接 我们称之为 App 传输安全(ATS)
ATS 能够确保 你的 app 通过 URL 系统进行的所有连接 都能对用户数据在 互联网上传输时提供足够的保护 ATS 默认情况下是开启的 但是要使用它 需要确保使用更高级的 API 如 NSURLSession 和朋友
因此 你应该制定各种开发策略 确保在较低级别的 API 上使用它们
你还需要确保 Info.plist 中没有禁用任何 ATS 特性
同样 你也需要考虑 在服务器和本地存储数据所使用的格式
通常 你需要 选择一种易于解析和验证的格式
plist 和 JSON 都是很好的选择 因为它们平衡了灵活性和安全性 可以执行模式验证的格式很理想 因为这些格式 提供了强大的检查和集中化逻辑
我们还需要考虑 如何保护这些存储器中的用户数据 CloudKit 的保护 对于茶艺会所来说足够了吗? 或者我们也应该自己手动加密数据? 我们设置安全边界时 没有在本地缓存 和 app 之间设置安全边界 我们这样做的原因是我们 将在数据进入 app 的过程中进行验证 并且任何本地存储的副本都是可信的 在 iOS 上 这是一个大力实施的保障 因为其他 app 不会更改我们的本地文件 但在 macOS 上 情况就不是这样了 所以 如果我们计划支持 macOS 我们可能需要重新考虑如何保护这些数据 我们还应考虑对第三方的依赖关系 我们使用什么库 他们的安全方法和我们的一致吗 并且测试是否 足以允许这些依赖关系的快速更新 既然我们已经找到了 从架构上降低风险的方法 那我们就来看看 app 内部的实现 我们的 app中主要有三类对象: 负责从外部源中检索数据的对象 负责解析数据并将其转换为模型的对象 以及负责向用户显示数据的对象
我们需要做的是确定哪个是哪类 哪儿是风险最高的地方 因为这些将是最直接地 与攻击者控制的数据打交道的部分 所以 也是我们最需要小心的地方
存储在远程服务器上的 数据是我们最大的风险之一 存储在我们网站上的信息 可能是由攻击者创建的 所以在处理的时我们要非常小心 但是 茶艺会所的数据只能由会员修改 CloudKit 提供保证 在这里 攻击者的集合受到更大的限制 因此风险级别 取决于我们 把哪些攻击者视为威胁模型的一部分 我们现在可以 通过我们的系统跟踪这个数据流 了解相应的风险概况 负责传输数据的组件有一定的风险 在与不可信的实体 进行交互连接时尤其如此 但那些处理数据和负责验证的组件 风险最高 注意 我们在这里排除了服务器 因为它们不在本讲座的讨论范围
我们可以 对我们的整个 app 执行这个过程 了解哪里存在风险 在哪里我们最需要当心 以及在哪里我们最可能犯错误 从而导致出现安全问题 总结一下: 确定你的系统拥有哪些资产 考虑如何设计来保护这些资产 特别是如何保护传输中和静止的数据
确定攻击者可能在哪里影响你的 app 通过系统跟踪该数据 这样你就能明白高风险组件的位置 并且可以计划减轻 在实现过程中可能发生的问题
现在 请 Ryan 来谈谈不可信数据 谢谢 Coops 在我们开始保护我们的 app 之前 我们必须能够识别 不可信数据是如何进入的 那么 什么是不可信数据呢?这很简单 我们将始终假设外部数据是不可信的 除非你能控制这些数据 当你无法确定自己能否控制时 就请往最坏的方面想 我们现在来看一些简单的例子 看看不可信数据 是如何进入我们的 app 的 你可能没有意识到 但是如果你的 app 使用自定义 URL 处理 那么 这个 URL 是完全不可信的 它可能来自任何地方 甚至是你不希望 通过 iMessage 把数据发送给用户的人 与此类似 如果你采用的是自定义的文档格式 那么解析它时可能存在漏洞 可能危及你的 app
我们第一个 需要执行的操作是找出所有数据入口点 这样我们就可以开始了解 app 是如何处理不可信数据的了 既然我们已经完成了数据流图 我们就应该可以得到这些信息了 这只是一个示例 并非 iOS 和 macOS 向 app 暴露的每个入口点的详尽列表 最常见的情况是 app 可能要处理一定量的联网内容 这代表的是 处理不可信数据的最高相对风险 因为攻击者可能会以用户为攻击目标 而无需与用户的设备近距离接触 存在方式 可能为通过 HTTPS 与服务器通信 点对点通信 如实时视频呼叫 甚至蓝牙通信 在我们的示例中 Loose Leaf 有几个功能 可以处理来自网络的不可信数据 因为我们关心茶艺热爱者的隐私与安全 我们为 所有设备到设备的信息实现了端到端加密 现在 我们不会 详细讨论如何实现加密信息 但至关重要的是 在开发安全 app 时应理解 这些类型的安全技术的内在属性 加密并不意味着信息的可信和有效 简单地说 我可以通过加密 保证知道信息来自我的朋友 但这并不意味着 我的朋友没有试图攻击我 加密信息的内容仍然是不可信的数据 我们知道我们的 app 通过网络获取某些信息 然后我们通常会做一些信息处理 举例来说 可以是一些解密或验证加密签名 然后会到达 我们的 app 需要读取该信息内容的位置 现在 我们的工作是查看这些信息 找出哪些可信 哪些不可信 然后我们就能看到如何使用不可信的数据 所以我们可能会看看我们的协议 搜索此发送人的电子邮件地址 是否为经过验证的发送人地址 在我们的示例中 这是经过加密验证的 所以 不可能被假冒 也不可能是我们未料到的地址 所以对此我们可以相信
接下来我们来看一看信息标识符 发觉它是部分可控的 当我们说某物是部分可控时 我们通常是指这个东西 受某个范围的值或某个特定类型的束缚 例如 要求必须是 UUID 但还是有人 可能发送给我们任何有效的 UUID
然后 我们将查看信息有效负载 发现它完全是由发送人控制的 这意味着他们可以 向我们发送任意的信息有效负载 甚至可能不是我们的 app 所期望的格式 所以 对于任何由发送人控制的数据 必须确保在使用之前对其进行验证和净化 至此 我们已经了解了 如何识别和分类不可信数据 现在,我们看看不可信数据 是如何被误处理的一些常见方法 这样 你就可以学会 如何在你自己的 app 中查找这些漏洞了
我们会在这里看一看路径遍历漏洞 这是一种非常常见的安全漏洞 你可能会在 你自己的 app 中发现这种错误 现在 在 Loose Leaf 中 我们能够 将你的茶艺拉花的照片发送给你的朋友 看起来这就是处理它的代码
在本例中 我们正在收取已下载照片 目标是将其移动到一个临时目录中 这样我们就可以 在系统删除已下载文件之前使用它 我们首先要做的事是 看看这个方法会涉及些什么内容 我们的目标是找出 什么值得信任 什么不值得信任 并查看我们将如何处理不可信数据
我们看了一下我们这里使用的方法 发现这个传入的资源 URL 是一个随机文件名 它是在从互联网下载文件时 在设备上本地生成的 它不会受到发送者影响 所以对此我们可以信任
然后我们看一下这里的 fromID 这是经过验证的发送者地址 在我们的示例中 这是经过加密验证的 因此 不可能被假冒 也不可能是我们未料到的地址 所以对此我们可以信任
然后我们来看一下这个名字 这是完全由发送者控制的 这就是它们告诉我们的文件的名称 但它们可以发送给我们任意字符串 看看我们是怎么使用它的 我们接收到这个文件名 并将其传递到附加路径组件 尝试在临时目录中构建此目标路径 如果它们发送给我们的这个字符串 在开始部分有很多回溯组件会发生什么? 现在它们已经遍历到了 我们的临时目录之外 当我们复制文件时 我们将把它复制到 发送者提供给我们的任意位置 显然 这很糟糕 因为它可能会覆盖 app 容器中 可访问的潜在敏感文件 所以你可能会看一看就走 “好吧 我接受这里的 lastPathComponent。” 本例中 这是可行的 它会为你提供安全的文件名 但如果最后的组件是回溯的 也会返回这个值 所以 我们仍然可能覆盖 我们的 app 的敏感文件
使用 lastPathComponent 可能造成的另一个错误 是如果你使用 URL-with-string 手工构建 URL 因为如果斜杠是用百分比编码的 lastPathComponent 仍会返回这个值 当我们去使用 URL 时 它会解码百分比编码 所以 它们仍能让我们 遍历到我们的目标目录之外
那么 正确的方法是什么呢? 我们将在文件名上 使用 lastPathComponent 然后确保这里还有一个文件名 我们要确保它不等同于 这些特殊的路径组件 否则 我们就会退出并拒绝处理文件 我们还必须 确保使用 fileURLWithPath 这将需要添加额外的转义百分号 这样我们就不易 受到这些百分比编码的攻击了 那么我们从中学到了点儿什么呢? 不要在文件路径中使用远程可控的参数 这很容易出错 如果别无选择 就如示例所示 使用 lastPathComponent 和其他验证和净化
一定要使用 fileURLWithPath 这将为文件路径添加额外的转义百分号 这样你就不易受到 这些相同百分比编码的攻击了 但一般来说 如有可能 尽量避免这种模式 只需使用一个带有本地生成的 随机文件名的临时文件路径即可 现在你可以采取的一些措施 是检查传入以下方法的 不可信字符串的所有使用情况 错误处理不可信数据的另一个例子 是不受控制的格式字符串 我们的 app 中可能有一些这样的方法 我们先从设备获得请求信息 然后从 app 中生成响应 并将响应发回设备 我们将在这里进行同样的练习 我们需要查看一下我们的方法 找出可信与不可信的数据 然后考虑如何使用这些不可信数据 于是 我们进行调查 发现这个名字是设备上的本地用户名 是本地生成的 根本不可能受到发送方的影响 对此我们可以相信
但是它们发送给我们的请求 又是由发送者控制的有效负载 它们可以向我们发送任意的请求信息 这样我们就不能信了 那么 我们看看如何使用这个参数 我们先收到这个请求 然后从请求中抓取这个本地化格式字符串 并传递给 NSLocalizedString 因此 这里的目标是在设备上 以用户的本地语言生成正确的响应
但这又只是一个任意字符串 所以 它们可以发送给我们任何东西 即使是我们无法本地化的数据 当我们把它传递给带有格式的字符串时 这里就没有足够的参数 来正确格式化字符串了 于是 最后的结果是 我们实际上将泄漏堆栈的内容 并把它发回给这个人 这可能会泄露 你的 app 内存中的敏感信息 或者在某些情况下 甚至会允许攻击者 控制你的 app 的执行 所以我们一定要避免这种情况的发生 那么我们从中学到了点儿什么呢? 切勿使用或从不信任的数据 生成格式字符串说明符 实际上 做到这一点没有什么正确的方法 所以尽量避免吧 也不要尝试验证或筛选格式说明符 你可能会弄错
请确保没有禁用格式安全编译器标志 它在默认情况下是开启的 但请确保不会在某段代码中禁用它 但我们必须意识到 有些情况下这是行不通的 例如 我们刚刚看到的那个例子 就不会给你发出编译器警告 因为某些情况下 如果不是字符串文字 编译器就不能推断格式字符串
你还需对 Swift 中的 格式字符串格外小心 因为 Swift 没有像 C 或 Objective-C 那样的编译器对格式字符串的验证级别 所以 在 Swift 中 你需要尽可能地使用字符串插值 有时你必须使用格式字符串 比如当构建这些本地化的字符串时 我们必须格外警惕不要引入这样的漏洞
现在你可以采取的一些措施 是检查传入以下方法的作为格式说明符的 不可信字符串的所有使用情况 至此 我们已经讨论了 两个常见的安全问题 当你知道要搜索什么时 这些问题通常都很容易找到 但并不总是那么简单 很多时候 你会 发现一些不可信数据可能利用的逻辑问题 使你的代码流朝着意想不到的方向流动 我们要看的 第一个此类问题是状态讹误问题
在 Loose Leaf 中 我们添加了你的 茶艺会馆会员参加直播流冲泡会话的功能 为此 我们在这里 使用一个基于邀请的会话状态机 在这里我可以邀请某人加入冲泡会话 如果他们接受邀请 我们就会进入一种连接状态 在这种状态中 我可以分享来自我的设备的相机直播流 (等待、邀请、受邀、连接) (断开连接、断开连接) 让我们再深入一点 看看是什么 会在这些状态之间让我们转换
我可以向某人发出邀请 然后我将进入这个邀请状态 而他们则进入受邀状态 如果他们接受了邀请 我们都将进入这种连接状态 现在有可能在我们的设备之间 共享这个连接套接字
但如果我给某人发送了一个邀请同时 也给他们发送了一个邀请接受信息呢? 如果他们未真正接受我们的邀请 我能强迫他们进入这种连接状态吗? 让我们看看这一过程的代码
既然我们了解了 可信数据和不可信数据之间的区别 我们在此应该能够快速浏览并看到 这是经过验证的发送方地址 我可以信任 而这是由发送方控制的信息 所以我不能相信 这里发生的情况是 我们从信息中取出这种类型 然后根据信息类型 将其分派给其他处理程序 但同样 这是来自于发送者控制的有效负载 所以他们仍然可以 发送给我们任何信息类型 现在 在本例中 我们想看看邀请接受信息 所以让我们跳转到这里的句柄
看起来我们是在根据发送者提供给我们的 会话标识符来检查会话是否存在的 然后我们将状态设置为“连接” 看起来在这种情况下 发送方实际上可以强迫我们进入连接状态 我们可能漏掉了一次检查 我们可能需要检查一下 我们是否实际上处于邀请状态 并期望在转换之前收到“邀请接受”信息 那么 这就足够了吗? 也许不是 可能我们还需要做一些其他的检查 一些状态不变量在继续之前必须为真 所以 在本例中 我们需要确保 将转换我们的邀请接受信息 来自我们实际邀请的人
那么我们从中学到了点儿什么呢? 在继续之前 我们需要定义明确的状态不变量 但正如我们看到的那样 这可能非常微妙 很容易出错 在我们的示例中 并不像检查我们 是否处于正确的状态那么简单 在继续之前还需要验证其他属性 我们希望尽早退出 如果这些检查失败 就拒绝继续进行 因为我们不希望 代码进入可能修改状态的意外方向 你可能发现的 另一个常见逻辑漏洞是搭载不可信的数据 在我们的 app 中 我们可能会做很多信息处理 我们从网络上获取一些 发送者提供给我们的 JSON 数据 然后依据这些数据创建一个 其上有这个远程条目的代码字典
然后 在我们完成处理之后 我们将这个另一条目附加到代码字典中 现在将该代码字典 传递送给你的 app 的另一部分 可能用来修改用户界面或数据库 当它到达 app 的那个部分时 就是用它的时候了 现在我们必须在使用它之前 弄清楚上面什么什么可信 所以你可能会看上一眼然后离去 “这是远程条目 我知道这是发送者控制的条目 因为我知道这是通过互联网发送的 但这个另一条目 我能相信吗?” 你可能认为你可以 因为你知道它是由你的 app 本地生成的 所以它不可能被发送者控制 但结果是 我们当然不能相信它 因为它来自于发送者控制的信息 他们也可能为我们提供这个条目 如果他们能够迫使我们的 app 进入一些意想不到的路径 它也不会再添加这个其他条目 那么当它有时间使用这条信息时 我们不知道它是否来自远程发送者 或者它是否是本地生成的 所以我们必须假设我们不能相信它
让我们看一个这种情况的例子 我们添加了 对某人的直播泡茶过程做出反应的功能 你甚至可以附上反应的图像 向他们展示你的想法 看起来我们 已从 JSON 数据创建了这个反应对象
如果有一些随附图像数据 我们会在信息处理期间 将其写入本地文件的 URL 然后将其设置为反应对象上的 imageURL 然后传递给 ViewController 来显示 让我们看看这里发生的情况 如果 imageURL 是非空值 我们会显示...
然后一旦它完成了显示 删除我们写入磁盘的图像 但是 由于我们从使用不可信数据 创建的同一个对象中删除了 imageURL 因此我们为攻击者提供了从我们的 app 容器中删除可访问的任意文件的能力 因为如果有人发送给我们 这个已经设置了 imageURL 的信息 我们就会跳过这段代码 因为没有设置图像数据 并直接在这里显示 而是因为 imageURL 已经设置好了 我们将尝试显示文件路径上的内容 但实际上 我们最终会 删除 JSON 中指定的任何文件 那么我们从中学到了点儿什么呢? 你需要将可信和不可信数据分离 到完全不同的对象中 不要尝试将它们合并成单个对象 因为你要做的是 在可信参数和不可信参数之间引入模糊性 我们总是希望非常清楚什么是不可信数据 这样我们就可以 审计我们对不可信数据的使用 并确保在使用之前 这些数据总是经过验证和净化的 现在我请 Coops 来讲讲 NSCoding 谢谢 Ryan NSCoding 及其更现代的伙伴 NSSecureCoding 已经存在了很长时间 并提供了宝贵的功能 来序列化和反序列化复杂的对象图 它可以用于 将解析后的数据存储到本地缓存中 或者用于自定义文档格式 我们将介绍一些我们经常看到的错误 以及一些尤其危险的错误 如果档案来自不可信数据的话
首先 请停止使用 NSUnarchiver 它已经被弃用了很长时间 不适合用于处理来自不可信来源的数据 你需要使用的是 NSKeyedUnarchiver 与 NSUnarchiver 非常相似 它允许打包和解包对象图 但是它能被安全使用
NSKeyedUnarchiver 也已出现一段时间了 所以使用它的方法已经过时 许多人可能正以这些方式使用它 而没有意识到 你的软件和用户正暴露在攻击之下 这里最上面的一组方法是你应该使用的 底部的都是遗留方法 如果还在使用 应该尽快迁移开 (弃用) 这两个集合的命名非常接近 但是所有被弃用的集合的命名 都采用了“带有”而不是“来自”的字眼 我们弃用这些方法的原因 是它们与安全编码不兼容 你可能并没有完全理解什么是安全编码 所以让我们看看它所解决的问题
没有安全编码 当您接受存档 并调用 unarchiveObjectWithData 时 为你解包的是一个对象图 在本例中 我们期望取回一个字符串数组
这里的问题是创建的对象类型 是在存档中指定的 如果它们来自不可信来源 或可能被攻击者修改 可以返回任何 NSCodingcompliant 的对象
这意味着攻击者可能会修改存档 并导致我们解包一个完全出乎意料的类 比如 WKWebView
尽管这个对象可能 不符合我们所期望的界面 而且你甚至可能执行类型检查 但安全性问题仍然存在 因为攻击者 在不归档类期间可访问到的任何漏洞 都已经触发了 安全编码解决了这些问题 它将兼容的类限制为 实现安全编码协议的类 并要求你 在每个调用站点指定你所期待的类
这些信息在 KeyedUnarchiver 中使用 在尝试解码之前执行验证
这样做可以大大减少 可被攻击者利用的类的数量 减少攻击面
但即便如此 我们还是会看到一些常见的错误 让我们来看看其中的一部分
在这个简单的示例中 我们使用安全编码来检索对象数组
这里的问题是我们已在 allowedClasses 集中指定了 NSObject
安全编码将解码那些符合其协议的对象 这些对象要么在 allowedClasses 集中 要么在一个指定的子类中 因为所有的对象 都是从 NSObject 派生而来的 这就允许 对任何安全编码的对象进行解码 从而破坏了安全编码 为我们提供的降低攻击面的能力 这是一个很容易发现的简单示例 但我们也看到 在更复杂的情况下发生这种情况 其中类列表通过框架的多个层级传递 并最终提供给 KeyedUnarchiver 方法
另一个类似的模式是这样的 从存档中解码一个字符串 然后使用这个字符串 通过 NSClassFromString 查找类 生成的类用作提取对象的 allowedClass
但愿这里的问题对人们来说是显而易见的
className 和由此产生的 allowedClass 都在攻击者的控制之下
因此 它们 可以很容易地传递特定类的名称 甚至 NSObject 从而绕过安全编码的好处 那么我们如何解决这些问题呢? 如果可能 只需使用一组受限制的静态类 确保避免使用 作为大型层次结构基础的类 以减少攻击者可用的类数量 如果你确实想支持动态类呢? 最常见的地方 就是需要支持插件的地方
在这种情况下 我们建议 动态地创建 allowedClass 列表 但是要使用可信数据 要做到这一点 最简单的方法是你的插件支持一个协议 这样 你可以 简单地列举运行所知道的所有类 并创建一组符合协议的类
然后 就可以通过安全解码安全地使用它 因为类列表不在攻击者的控制范围之内 总而言之 将代码 从所有被弃用的方法和类中迁移出来 避免使用 NSObject 或过于泛型的基类 并尽可能使用 静态的 allowedClass 列表 如果做不到 可从可信的数据中生成代码
另一个不错的选择是完全避免它 使用诸如 Swift 可编码 或不太容易扩展的 编码格式 如 plist、JSON 或 protobuf 你可以根据这些信息马上采取行动 搜索你的代码库 在处理不可信数据的区域中使用这些项 另一个困扰高风险代码的问题是内存损坏
无论何时使用 Objective-C 或 Swift 的 Unsafe Pointer 时 都有可能引起内存安全问题 这在解析不可信数据时尤其普遍 这些数据可能来自 缓冲区、字符串或结构化存档
让我们来看一些例子
我们已经讲过 当不可信数据 被误认为可信数据时就会出现安全问题
在本例中 我们将为 一个私人茶艺会所打开 CloudKit 记录 我们做的第一件事是 将 UUID 的字节复制到 iVar 中
如果我们查看一下每个元素 可以发现 CKRecord 是可信的 因为它是由 CloudKit 生成的 但是从中检索到的 数据字段却在攻击者的控制之下 所以我们无法相信 它们包含固定数量的数据
这里的问题是 数据的长度是由攻击者控制的 但是 UUID iVar 的大小是固定的
因此 如果提供的 NSData 包含的 UUID 不能包含的数据 攻击者控制的数据将要被越界写入
要知道为什么这会成为一个问题 我们需要看看会发生什么情况
在包含 UUID 的对象中 我们可以看到下一个 iVar 是通往本地缓存的路径 现在 这个字段 不是从 CloudKit 记录中设置的 因此 溢出 UUID 允许攻击者 获得他们不应该拥有的字段的控制权 memcpy 将导致攻击者控制的数据 按预期写入 UUID 但是由于太大 它将溢出到 localCachePath 中
这样 他们就可以重新配置组缓存 从而导致文件被下载 并覆盖用户的本地使用偏好 通过使用各种技术 攻击者可以利用这些类型的漏洞 在你的 app 中执行代码 这使他们能够访问所有用户的数据 和授予你的所有资源 解决这个问题的明显解决方案是 使用 sizeofUUID 但这会造成另一个问题 因为如果攻击者提供的数据比预期的少 memcpy 将读取“数据”的末尾 从而导致内存中的 其他用户秘密泄露到 UUID 字段中 当 app 将该信息同步回服务器时 它将对用户数据进行编码并返回给攻击者
正确的修复方法是 验证我们检索的对象是 NSData 并检查其长度是否精确匹配 这可以既防止覆盖目标 又防止读取过界
虽然这是一个简单的示例 但在解析不可信数据时应该格外小心 特别是在格式复杂或嵌套的情况下 始终要验证长度 因为即使将 C 结构转换到缓冲区上 如果没有足够的数据 也会暴露用户信息
另一个需要注意的情况是处理整数时 因为整数有溢出的习惯
本例中 在相同的方法下 我们将从一个单独的字段提取 带有 UNICHAR 字符和数量的 NSData 与前面一样 我们应该查看哪些数据是可信的 在本例中 名称和数量都不可信 因为它们均来自攻击者控制的数据
现在 开发者已经预见到 盲目相信数量的危险 因此他们会根据数据的长度进行验证 但有一个问题 攻击者实际上控制了这种比较的两端 并且存在一组值 使得开发者所做的假设不再正确
在本例中 如果攻击者 传递了一个0长度的字符串来表示“name” 传递了8000万 十六进制来表示 “nameCount” 那么整数将会溢出“if”语句将失败
这将导致读取非常大的“name”末尾 可能会泄露用户数据 解决方法是添加一个全面的验证检查 检查类型和长度 但最重要的是 使用 os_overflow 方法来进行运算
本例中 我们使用了 os_mul_overflow 对于那些不知道它们的人 os_overflow 是一组非常有用的函数 它允许安全地处理数字 和稳健检测下溢和溢出情况 使用很简单 只需用相关的 os_overflow 方法替换你的算术运算即可 我们已经使用两个和三个参数 提供了加法、减法和乘法运算
在溢出时 它们的返回值非零 使用这些可以很容易地确保 使用攻击者控制的值可以安全地执行计算 总结一下 一定要验证长度并考虑所有情况 太大、太小 应该正好合适
在处理数字时 要小心运算 就像你没有使用 os_overflow 函数一样 你很有可能会犯错误
最后 要认真考虑 对这些高风险代码路径使用 Swift 但要记住避免使用 Unsafe Pointer 和其他原始内存操作特性 最后 希望本次讲座为你提供了一些 关于如何看待安全的想法 以及一些需要尝试避免的常见陷阱的示例 安全性是一个大话题 而且很难 因为正如我们所见 最小的错误就足以危及你的整个 app
但是 通过了解风险在哪里 进行设计来减少风险 以及在处理不可信数据时的小心谨慎 我们可以极大地降低出错的几率 谢谢大家
-
-
16:34 - Path traversal
func handleIncomingFile(_ incomingResourceURL: URL, with name: String, from fromID: String) { guard case let safeFileName = name.lastPathComponent, safeFileName.count > 0, safeFileName != "..", safeFileName != "." else { return } let destinationFileURL = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent(safeFileName) // Copy the file into a temporary directory try! FileManager.default.copyItem(at: incomingResourceURL, to: destinationFileURL) }
-
22:26 - State management
func handleSessionInviteAccepted(with message: RemoteMessage, from fromID: String) { guard session = sessionsByIdentifier[message.sessionIdentifier], session.state == .inviting, session.invitedFromIdentifiers.contains(fromID) else { return } session.state = .connected session.setupSocket(to: fromID) { socket in cameraController.send(to: socket) } }
-
30:56 - Safe dynamic allowedClasses
NSSet *classesWhichConformToProtocol(Protocol *protocol) { NSMutableSet *conformingClasses = [NSMutableSet set]; unsigned int classesCount = 0; Class *classes = objc_copyClassList(&classesCount); if (classes != NULL) { for (unsigned int i = 0; i < classesCount; i++) { if (class_conformsToProtocol(classes[i], protocol)) { [conformingClasses addObject: classes[i]]; } } free(classes); } return conformingClasses; }
-
34:23 - Buffer overflows
@implementation - (BOOL)unpackTeaClubRecord:(CKRecord *)record { ... NSData *data = [record objectForKey:@"uuid"]; if (data == nil || ![data isKindOfClass:[NSData class]] || data.length != sizeof(_uuid)) { return NO; } memcpy(&_uuid, data.bytes, data.length); ...
-
36:06 - Integer overflows
@implementation - (BOOL)unpackTeaClubRecord:(CKRecord *)record { ... NSData *name = [record objectForKey:@"name"]; int32_t count = [[record objectForKey:@"nameCount"] unsignedIntegerValue]; int32_t byteCount = 0; if (name == nil || ![name isKindOfClass:[NSData class]] || os_mul_overflow(count, sizeof(unichar), &byteCount) || name.length != byteCount) { return NO; } _name = [[NSString alloc] initWithCharacters:name.bytes length:count]; ...
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。