1. 安全地编程
1.1. 在一个完整的软件设计过程中,我们要在创建和审查时就将安全性放在心中,但这只是产品开发过程的开始,接下来是实现、测试、部署、运行、监控、维护,并最终在生命周期结束时将其淘汰
1.2. 开发人员不仅必须忠实地实现一个优良设计中明确的安全规定,还必须避免无意中通过有缺陷的代码引入额外的漏洞
1.3. 深思熟虑的设计师可以预测编程中遇到的问题,并就注重安全性的领域提供建议等
- 1.3.1. 知道是什么让代码变得易受攻击,以及如何让它更安全
1.4. 在理想情况下,在设计中应该指定主动的安全措施,即为了保护系统、资产和用户而构建的软件功能
- 1.4.1. 注重开发中的安全性是为了避免软件轻易地掉入陷阱,比如组件和工具中的缺陷
2. 挑战
2.1. 安全编码的挑战主要在于避免引入缺陷,因为缺陷有可能成为可被利用的漏洞
- 2.1.1. 避免以不安全的方式进行编码
2.2. 制作出能够真正运行的软件会带来更高的复杂性,并且需要在设计之外对细节进行填充,所有这些都不可避免地会带来安全风险
2.3. 完美并不是我们的目标,大多数会导致常见漏洞的编码失败模式都很好理解,而且不难纠正
2.4. 恶意影响
-
2.4.1. 在考虑安全编码时,需要考虑的一个重要因素是了解攻击者可能会怎样对正在运行的代码施加影响
-
2.4.2. 不受信任的输入可以通过直接或间接两种方式对代码产生影响
-
2.4.3. 有时不受信任的数据与代码的交互会触发错误,或者会触发具有副作用的功能
-
2.4.4. 通过数据对代码产生影响的技术被称为污染
-
2.4.5. 还有一些其他方法,可以在不存储数据的情况下,让输入数据对代码产生间接影响
-
2.4.6. 在大型系统中,当你从攻击面开始考虑传递闭包(即全部路径的集合)时,你可以体会到渗透到大量代码的潜力
-
2.4.6.1. 能够通过多层进行扩展的能力很重要,因为这意味着攻击者可以访问比你预期更多的代码,从而使其能够控制代码所提供的功能
2.5. 漏洞是bug
-
2.5.1. 大家都已经接受了所有软件都有bug这件事
-
2.5.1.1. 例外也总是存在的:简单代码、可证明是正确的代码,以及运行在航空、医疗或其他关键设备上的高度工程化的软件
-
2.5.2. 能够意识到bug的普遍性是走向安全编码的一个很好的起点
-
2.5.3. 漏洞是对于攻击者来说有用的一部分软件bug,攻击者可以利用漏洞来造成伤害
-
2.5.4. 几乎无法准确地将漏洞与其他bug区分开,因此一开始可以先识别明显不是漏洞的bug,也就是完全无害的bug
-
2.5.4.1. 网页布局未能按照设计工作,就是一个无害的bug
-
2.5.4.2. 搞乱布局的bug也可以是有害的
> 2.5.4.2.1. 遮盖了用户必须了解才能做出准确安全决策的重要信息,因此漏洞的发现是很复杂的
-
2.5.5. 一般来说我们所有人都在编写大量的已知错误,更不用说未知错误了
-
2.5.5.1. 如果还没有修复全部bug,请考虑那些已知的bug,标记其可能造成的漏洞,并进行修复
-
2.5.5.2. 修复一个bug几乎总是比调查并证明它是无害的更容易
-
2.5.6. 有些漏洞是难缠的bug,因为它们不符合任何模式,它们不知怎么躲过了测试,并最终被释放出来
-
2.5.6.1. 代码在正常用途中往往会表现正常,只有在故意进行的攻击下才会展现出有害的行为
-
2.5.7. 从过去的失败中吸取教训是很重要的,因为这些漏洞类别中的很多已经存在了几十年
2.6. 漏洞链
-
2.6.1. 漏洞链的意思是看似无害的多个bug可以结合起来,并形成严重危害安全的bug
-
2.6.1.1. bug加成效应
-
2.6.2. “垫脚石”bug组合在一起,形成了可被攻击者利用的漏洞
-
2.6.2.1. 在Pwn2Own黑客大赛中,有一个团队曾设法将6个bug连接在一起,以实现一次高难度的利用
-
2.6.3. 识别出bug是何时形成漏洞链的,通常非常具有挑战性
-
2.6.3.1. 很容易看出尽可能主动地修复bug是多么有远见
-
2.6.3.2. 应该积极修复那些会引入脆弱性的bug,尤其是关键资产周围的bug
-
2.6.3.3. 认为“会没事的”的观点就只是一个观点,而不是证据
-
2.6.3.4. 认为“它永远不会发生”而将bug留在那里是有风险的
-
2.6.3.5. 充其量只是一种临时措施,而不是一个好的最终分流决定
-
2.6.4. 在实践中,通常很难说服其他人花时间针对那些看似模糊的假设来实施修复,尤其是当修复可疑bug需要大量工作时
-
2.6.5. 很可能大多数大型系统中都充满了未被发现的漏洞链,从而导致我们的系统变脆弱
2.7. bug和熵
-
2.7.1. 一些bug往往会以不可预测的方式对软件造成破坏,这使得我们很难分析它们的可利用性
-
2.7.2. 由执行线程之间的意外交互所引起的bug往往是容易产生这种问题的一类bug,因为它们通常以各种方式出现,并且看起来似乎是随机的
-
2.7.2.1. 内存损坏bug是这类bug中的另一种,因为堆栈的内容在不断变化
-
2.7.3. 自动化降低了重复尝试的成本,直到他们的攻击成功为止
-
2.7.4. 即使你无法清晰地找出明确的因果链,熵诱导出的bug也可能很危险,并值得修复
2.8. 警觉
-
2.8.1. 有了意识,就可以应对困难的挑战
-
2.8.2. 注意力不集中很容易失败,即使事情并不困难
-
2.8.3. 如果没有意识到潜在的安全隐患,并持续关注它,就很容易在不知不觉中掉入它的陷阱
-
2.8.4. 为了能够交付安全的代码,一定要保持警惕并预测所有可能的输入和事件组合
-
2.8.5. 警觉首先需要的是纪律,但随着练习的不断深入,当你知道要注意什么的时候,它就会成为一种习惯
-
2.8.6. 每一次的修复都避免了未来有可能发生的攻击
3. 案例
3.1. 2014年,苹果公司为其大部分产品悄悄地发布了一系列关键安全补丁,并且出于“保护我们的客户”的考虑,拒绝解释问题的原因
-
3.1.1. 一个明显的编辑失误造成的,这个失误破坏了安全保护
-
3.1.2. GotoFail
3.2. 教训
-
3.2.1. 关键代码中的小错误会给安全性带来毁灭性影响
-
3.2.2. 在预见到的用例中,易受攻击的代码仍能正常工作
-
3.2.3. 对安全性测试来说,测试代码拒绝无效用例的能力,比测试代码在合法用例中的正常运行更为重要
-
3.2.4. 代码审查非常重要,它能够找出无意间引入的bug
3.3. 对策
-
3.3.1. 更好的测试
-
3.3.1.1. 至少应该为每个if条件都设置一个测试用例,以确保所有必要的检查都有效
-
3.3.2. 注意那些无法访问的代码
-
3.3.2.1. 很多编译器都提供了选项以对此进行标记
-
3.3.3. 让代码尽可能明确,比如多用圆括号和花括号,即使可以略去它们
-
3.3.4. 使用诸如linter之类的源代码分析工具,可以提高代码质量,并且在此过程中可能会标记出一些潜在的漏洞,以提前进行修复
-
3.3.5. 考虑使用ad hoc源代码过滤器来检测可疑模式
-
3.3.6. 测量并要求全面的测试覆盖率,尤其是对于安全关键代码
4. 编码漏洞
4.1. 新类别的bug是无穷无尽的,不要徒劳地试图编写一份涵盖所有潜在软件漏洞的完整列表
4.2. 原子性
-
4.2.1. 将任务作为一个单一的步骤并保证有效地完成
-
4.2.2. 原子性是一个重要的防御武器,它可以用来预防可能会导致漏洞的意外情况
4.3. 时序攻击
-
4.3.1. 时序攻击是一种旁路攻击,它会从操作执行的时间上推断出一些信息,以此间接地了解系统中本应是私密的一些状态
-
4.3.2. 时间差有时可以提供一些暗示,也就是说会有少量的受保护信息被泄露,从而使攻击者受益
-
4.3.3. Meltdown(熔断)和Spectre(幽灵)是针对现代处理器的时序攻击,它们运行在软件层面之下,但原理是相同的
-
4.3.3.1. 利用了预测执行(speculative execution)的行为特征,即处理器会加速得到预计算结果,同时为了速度而暂时放松各种检查
-
4.3.3.2. 攻击代码可以通过检查缓存的状态,来推测被取消的预测执行期间发生的事情
-
4.3.3.3. 内存缓存技术加快了执行速度,但缓存不会直接披露给软件
-
4.3.3.4. 代码可以判断特定内存位置的内容是否曾经存在于缓存中,这是通过测量内存访问时间做出的判断,因为被缓存的内存处理速度快得多
-
4.3.4. 当软件中存在一系列缓慢的操作(想想if…if…if…if…)时,软件的运行就会出现时间差
-
4.3.4.1. 当我们对执行中的事件顺序有所了解时,就可以通过时间差来推断有价值的信息
-
4.3.4.2. 当使用同一台机器上运行的代码来利用Meltdown和Spectre时,亚毫秒级的时间差都是可以被注意到的,并且这也是很重要的
-
4.3.5. 最好的缓解措施是将时间差缩小到可接受的水平,也就是难以察觉的水平
-
4.3.6. 当软件中存在固有的时间差,并且这个时序旁路会导致严重的泄露时,你可以用来缓解风险的做法就是人为引入时延,以模糊时序信号
4.4. 序列化
-
4.4.1. 序列化是指将数据对象转换为字节流的通用技术
-
4.4.2. 攻击者不仅能够篡改关键数据值,而且通过构造无效的字节序列,甚至能够使反序列化后的代码执行有害的操作
-
4.4.2.1. 需要信任输入的数据,才可以构建出相应的对象以完成相应的功能
-
4.4.3. 只有对受信任的序列化数据执行反序列化才是安全的