the art of software security assessment Chap1. translate

软件安全审计的艺术——识别以及预防软件的漏洞

Translator: 山海(Chenyu ZHU)

注:翻译从原书 6.软件安全审计概述(Introduction to Software Security Assessment)开始,前面的并不是正文内容。 ——By 译者

第一部分:软件安全审计概述

6.1 第一章:关于软件漏洞的基础知识

“任何足够高级的技术都与魔法难以区分”

—— 亚瑟 C. 克拉克

6.1.1 概论

对软件技术缺乏理解的普通人倾向于将软件简单地看作一种像魔法一样的技术。一些软件在复杂度上决不输于任何硬件,但绝大多数人不会看到软件转动的轮子,听到它引擎的轰鸣声,或者拆开螺母和螺栓看看它是如何工作的。计算机软件早已经集成到我们的社会中,影响着几乎所有人日常生活的每一方面。人们对于软件的依赖性使得我们不得不面对它的安全问题。你无法忍住去了解什么样的软件能够保证你在使用它时是是安全的。你能怎样确认安全问题?软件安全失效又代表什么?

在这本书涵盖的课程中,你将会学习到如何去理解和评估软件安全性。你将会看到怎样结合理论和代码实践来进行安全审计。这个过程包括了怎样剖析应用程序,发现安全漏洞,评估每个漏洞表现出来的危险性。你还会学到怎样最大化利用你的时间,专注于一个程序中最与安全问题相关的部分然后优先找出最关键的漏洞。这些都是你理解一个程序安全审计所需要掌握的基础知识。

本章内容介绍软件漏洞的原理,并阐述违反软件系统安全原则的含义。你还能学到软件审计的基础,包括动机(即为什么这么做 ——By 译者),审计的种类,以及审计如何配合软件开发进程。最后,一些能帮助你分类安全漏洞以及发现这些安全问题一般情况下是由什么原因造成的知识也会列出。

6.1.2 漏洞

现代软件就像魔法一样。一群专业团队花费超过十年的时间开发的复杂的软件可以立刻被黑客抬走。从第一眼看下来,这好像是不太可能的,就像魔法一样。然而, 当你透过帘子看它是如何运作的时候,这种奇妙的感觉就会消失。 总之,软件漏洞能让攻击者能轻松利用这些弱点以打成他们目的。在软件安全的语境下,漏洞的定义是一些软件的缺点与疏忽,这些缺点与疏忽能被攻击者恶意利用来干坏事,或者获取敏感信息,破坏以及摧毁一个系统,以及控制计算机系统以及程序。

这个定义和软件 bug 有点类似,bug 是造成程序产生非预期结果的错误,过失与疏忽。很多程序员都吃过软件 bug 的亏。广义地来说,软件漏洞也是 bug 地一种。软件漏洞是能给你隐藏惊喜的 bug(surprise mother fucker? ——by 译者):恶意用户能够利用它们对软件与系统实施攻击,或者控制你的系统与程序。绝大多数软件漏洞都是软件 bug,但仅仅有一部分 bug 能被算作安全漏洞。一个 bug 在被称为安全漏洞时,它必须造成与安全有关的影响。也就是说,它会允许攻击者做一些用户通常不会做的事。(这个主体会在接下来的章节继续提到,人们经常犯这种错误,将疏忽造成的安全问题当作 bug)。

人们常说,安全是可靠性的一个子集。这可能不是一个宇宙级真理,但我们可以拿这句话做一个类比。一个可靠的程序相对来说 bug 会少很多:它很少在用户使用下崩溃,它能很好地处理异常条件。它通过“防御式”的方法开发出来所以能处理不确定的执行环境和不正确的输入。(关于 ”防御式“ ,参见代码大全的防御式编程——by 译者)安全程序具有鲁棒性(robust):它可以击退入侵者的集中攻击,这些入侵者试图操纵它的环境和输入,以便利用它达到某种邪恶的目的。软件安全性和可靠性也有相似的目标,因为它们都需要在开发策略上关注于消除软件缺陷。

尽管将安全漏洞与软件 bug 进行比较是有用的,但有些漏洞并不那么清晰。例如,允许你编辑你不应该访问的关键系统文件的程序可能会根据其规范和设计完全正确地运行。所以它可能不属于大多数人对软件 bug 的定义,但它确实是一个安全漏洞。

攻击程序中的漏洞的过程称为利用(exploiting)。攻击者可能会利用漏洞,以一种聪明的方式运行程序,在程序运行时改变或监视程序的环境,或者如果程序本身就不安全,那就就简单地用这个程序达到预期目的。当攻击者使用外部程序或脚本执行攻击时,这个攻击程序通常被称为攻击脚本。

如前所述,攻击者可以利用漏洞来破坏系统的安全性。将“系统的安全性”概念化的一个有用方法是将系统的安全性看作是由安全策略定义的。从这个角度来看,当系统的安全策略被违反时,软件系统的安全性就会被破坏。

加州大学戴维斯分校的计算机科学教授 Matt Bishop 在计算机漏洞领域已经有了很多年的研究。 他从正式的学术角度和技术角度对计算机安全进行了大量的思考。如果你对这些话题感兴趣,可以看看他的书《计算机安全:艺术与科学》 ( Bishop, M. Computer Security: Art & Science. Addison-Wesley, 2003 ), 他的主页也奉上: http://nob.cs.ucdavis.edu/~bishop/

对于由软件组成的系统,用户对应有安全政策,它简要指出了哪些能做哪些不能做。例如,这个政策可能会这样说:”未经身份验证的用户禁止使用日历服务。”如果未经身份验证的用户拥有访问日历服务的权限,那么这毫无疑问违反了安全政策。

每个软件系统都应该考虑自己的安全政策。它的形式可能是编写的文档,也可能是一个非正式的松散的期望集合,用户对在这个系统中怎么做是合理的有这样的期望。对于绝大多数软件系统,人们通常能懂得什么样的行为违反了安全规则,即使它没有在安全政策中被提及。因此,“安全政策”通常表示用户和社区对于什么行为是允许的,什么行为是禁止的所达成的共识。安全政策可以有如下所示的几种形式:

  • 对于特别敏感和具有严格限定作用域的系统,安全政策可以是通过数学学证明然后通过程序代码验证的规范约束。这种方法通常很昂贵,并且只适用受到严格控制的软件环境。比如信号灯,电梯,飞机等性命攸关的设备中所嵌入的系统能够通过这种验证。然而这种方法对于很多这样的应用程序也是非常昂贵且笨拙的。
  • 安全政策可以是一个正式的书面条款。就像附录 Q 中阐述的“C.2 信用卡信息应当永远不泄露给第三方或者未经充分加密后通过媒介传输。”这个条款可能来自于在开发过程中创建的关于软件的政策,也可能来自于与软件使用的资源相关的政策,例如网站的安全政策,操作系统政策或者数据库安全政策。
  • 安全政策可以仅仅由非正式的,模糊的人们对合理程序安全行为所期望的集合构成。例如“让犯罪组织访问我们的信用卡数据库可能是不好的”。

Java 虚拟机(Java Virtual Machine, JVM) 以及.Net 公用语言运行库(Common Language Runtime, CLR)有不同程度的代码访问安全性(Code Access Security, CAS)。代码访问安全性提供了加载与运行时广泛的验证方法。这些验证包括字节码的完整性,软件的始发者以及代码访问限制的应用程序。这些技术最明显的应用包括 Java applet 和.NET 管理的浏览器控件的沙箱环境。

尽管代码访问安全性可以用作严格的,形式化的安全模型的平台,但有些与之相关的注意事项必须要题。第一个问题就是大多数开发人员不完全了解它的应用和功能,因此商业软件中很少使用。第二个问题是代码访问安全性完全依赖于底层组件的安全性,JVM 和.NET CLR 都是漏洞的受害者,这些漏洞允许应用程序走出虚拟机沙箱并运行任意代码。

在实践中,软件系统的安全政策可能大部分都是由人们非正式的期望组成。然而,它经常来自开发过程和参考站点资源的正式文档的安全政策。系统安全政策这个定义有助于澄清“系统安全”这个概念。总之安全就是终端用户的需求和期望。

安全期望

考虑人们对软件安全可能有的期望有助于确定他们认为哪些行为是违反安全的。安全通常用三个部分来描述:保密性(avalability),完整性(integrity)以及实用性(avalability).

保密性

保密性要求信息私有(private). 在任何情况下,软件都应当做到隐藏信息或者隐藏信息的存在。软件系统经常要处理机密数据,比如国家级机密,公司的商业机密,甚至敏感的个人机密信息。

商业以及其他组织在软件中存储了很多机密信息。比如财务信息通常就是保密的。关于商业计划与业绩的信息更是战略级别的,这些信息对于非法竞争或者内幕交易等犯罪活动都可能有很大的用处。因此商业数据通常是要求保密性的。这些保密的商业数据包括商业关系,联系人,法律诉讼,或者其他需要保密的敏感信息。

一个软件系统处理信息时,出于隐私的考量,人们对保密的要求通常是很高的。组织以及个人用户都希望小心地把控哪些人能够访问这些信息。如果这些信息还包括财务数据或者医疗记录,不当的数据纰漏就可能涉及到责任问题。软件通常都想要对个人以及用户的信息保密,例如个人文件,邮箱,活动记录,账号和密码等。

在不同类型的软件中,机密是由软件代码构造出来的。比如一段用来评估市场上的潜在交易或者新的 3D 图形引擎的代码,尽管它不是交易机密,但它仍然是敏感的.比如评估贷款申请人信用风险的代码,或者在线视频游戏战斗系统背后的算法.

软件通常会将信息做一个划分,确保只有通过验证的用户允许访问授权过的信息.这个要求意味着软件通常需要使用访问权限控制技术来对用户在访问数据时进行验证.加密方法在数据传输与存储时也通常用于数据保密.

完整性

完整性亦即数据的可信度与正确性.人们对软件有这样的要求,即拥有防止自己的数据被更改的能力.完整性不仅仅时对于数据的内容来讲,而且要包括数据的来源.有的软件可以通过记录一段数据的更改或者数据来源的更改来检测数据完整性的变化.

数据完整系通常包括了信息的划分,也就是软件使用访问权限控制技术验证用户是否拥有权限对数据进行更改.验证过程也是软件的一个重要组成部分,它能保护数据来源的完整性,因为它严格地告知了软件这个用户是谁.(例如你通过了 QQ 密码验证, 就等于告诉了系统你是这个 QQ 的拥有者,你可以随便更改你的 QQ 信息 — By 译者)

特别地,相比于保密性,用户对完整系也有类似的需求.任何允许攻击者修改他们本不允许修改的数据都的问题应该被视为一个安全漏洞.任何允许用户为章程其他用户并操纵数据的问题也应当被认定为对数据完整性的破坏.

软件在完整性方面的漏洞可以是毁灭性的.攻击者能够利用数据更改权限访问软件系统,并且得到它的控制权.

实用性

实用性也就是使用信息以及资源的能力.一般来说, 它指的是用户对系统的可用性及其抵御拒绝服务攻击(denial-of-service DoS)的能力的期望。

允许用户摧毁一个软件可以被认定为违反了实用性原则的漏洞.攻击者可以利用这个漏洞,使用一些特殊的输入或者环境破坏来通过消耗软件系统资源,例如 CPU,硬盘存储,网络带宽等方式使一个程序无法运行.

6.1.3 审计的必要性

绝大多数人都希望供应商能够给他们的软件提供一定程度的对于完整性的保证.然而供应商在真实情况下几乎不给自己的软件提供质量保证.如果你对此表示怀疑,只要阅读下每个商业软件附带的几乎所有商业软件都适用的最终用户许可协议即可(end user license agreement ,EULA).当然,为了讨好用户,绝大多数供应商都会说自己对软件质量有自己的保障评估指标.这些评估指标往往出于市场考量,例如功能,实用性,一般的安全性.从历史上看,这使得安全性被随意地应用或者偶尔被完全忽略.

一些产业确实出台了他们自己的安全要求和标准,但他们仅仅在特定的环境下进行使用.但这种做法正在改变.因为备受瞩目的时间正促使监管机构和行业标准机构转向为更加积极主动地对安全提出要求.

好消息是人们对于安全的态度近年来已经产生了改变,许多供应商已经在业务流程中开始使用严格地安全性测试.许多方法已经变得常见,比如自动代码分析,安全单元测试,手动代码审计等.从这标题你可以看出,这本书主要讨论手动代码审计.

审计(auditing) 是一个分析代码的过程(无论这段代码是源代码还是二进制形式),这个过程用来发现一些可能被攻击者利用的漏洞.通过这个过程,你可以确认以及消除那些让敏感数据和商业资源陷入不必要风险的安全漏洞.

除了公司开发内部软件的明显情况外,代码审计在其他一些情况下也有意义。 表 1-1 总结了最常见的审计方法:

审计形式 描述 优势
内部软件审计(发布前) 软件公司在自己的新产品发布之前做的代码审计 漏洞能在产品进入市场前发现并填补,能节省开发和部署更新的费用,同时让公司免于潜在的骚扰
内部软件审计(发布后) 软件公司在产品发布之后进行审计 在恶意团队发现漏洞之前将其修复.这个过程可以花较长时间进行测设与检查而不是在漏洞暴露时匆忙发布
第三方产品范围比较 第三方拿相互竞争产品的某个方面进行审计 客观的第三方可以为消费者提供有价值的信息,并且帮助他们选择最安全的产品
第三方评估 第三方为客户对一个单独的软件进行审计 客户可以了解它正在考虑部署的应用程序的相对安全性。这可以为选择某个产品而不是其他提供相关参数的证明
第三方初步评估 第三方对一个还没有进入市场的产品进行评估 风险投资家可以了解未来技术在投资项目上的可行性.供应商也可以进行这种类型的评估,以确保它们所想要销售的产品的质量
独立研究 安全公司或咨询公司独立地执行软件审计 安全产品供应商可以识别扫描仪和其他安全设备的漏洞并实施保护措施。独立研究也起到行业监督的作用,为研究人员和安全公司提供了建立专业信誉的途径。

正如你所看到的,代码审计在很多情况下都适用. 尽管具备这些技能的人员的需求大,但是,很少有专业人员具有高水平执行这些审计的培训和经验。我们希望这本书能帮助填补这一空白。

审计与黑盒测试

黑盒测试(black box testing)是一种只通过操作软件所给的接口来评估软件的方法.特别地,这个过程更倾向于给一些精心设计的输入,这些输入能够让应用程序产生一些期望之外的结果,比如宕机或者暴露敏感数据.例如对一个 HTTP 服务器进行黑盒测试时可以向它发出一个字段大小异常大请求,这种请求可能造成内存崩溃的 bug(在第五章”内存崩溃”我们会详细讨论).这个测试的请求可以是合法的,例如下面的内容:

1
GET AAAAAAAAAAAAAAAAAAA...AAAAAAAAAAAAAAAAAAA HTTP/1.0

或者它也可以是非法请求,就像这样:

1
GET / AAAAAAAAAAAAAAAAAAA...AAAAAAAAAAAAAAAAAAAA/1.0

任何这种请求导致的宕机都说明这个应用程序存在严重的 bug.当考虑可以使用自动化测试应用程序的工具时,黑盒测试更有吸引力.这种自动化的黑盒测试被称为模糊测试(fuzz-testing). 而模糊测试工具包括通用的“dumb”和协议感知的“智能”fuzzer.因此你不需要手动地去尝试所有你可能想象的到的情况,你只要根据自己的设计运行这个这个工具然后收集结果就可以了.

黑盒测试的好处就是快,能迅速得到测试结果.然而,黑盒测试有几个非常关键的缺点.本质上说,黑盒测试就是丢一堆数据给应用程序然后看看他是不是做了一些设计者不想让他做的事情.你不知道这个过程中程序是如何处理数据的,所以有大量的不接触这些数据的代码你并没有进行探究.例如,回到这个 web 服务器的例子,假设请求的查询字符串中存在特定的关键词时它会具有特定的内部功能,就像下面的代码一样:请注意加粗的代码行(Markdown 代码块好像无法加粗,注意最后一个 if 语句即可 — by 译者):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct keyval {
char *key;
char *value;
};
int handle_query_string(char *query_string){
struct keyval *qstring_values, *ent;
char buf[1024];
if(!query_string)
return 0;
qstring_values = split_keyvalue_pairs(query_string);
if((ent = find_entry(qstring_values, "mode")) != NULL)
{
sprintf(buf, "MODE=%s", ent->value);
putenv(buf);
}
... more stuff here ...
}

这个 web 服务器有一个特殊的不规范的行为:如果查询字符串中含有 mode=,那么环境变量MODE就会被修改为xxx.这个特别的行为在实现上有一个缺陷,如果粗心地使用函数sprintf()就会造成缓冲区溢出.如果你不清楚为什么这段代码很危险,不必担心,缓冲区溢出漏洞会在第五章详细讨论.

从这个例子可以看出段存在 bug 的代码在黑盒测试中完全可能正常运行,简单的漏洞也无法探测出来。因此,你需要对程序代码本身进行评估而不是只靠跑几个测试例子然后记录结果就行。这也是代码审计很重要的原因。你需要能够分析代码然后发现自动测试工具无法探测到的漏洞。

幸运的是,代码审计配合黑盒测试能够利用最少的时间得到最好的修补漏洞的结果。这本书将会教会你这些知识和技术来彻底地分析一个应用程序方方面面的漏洞以及怎样应用你对这些知识的理解和创造力来发现一个应用程序的缺陷。

代码审计与开发生命周期

当你考虑应用程序会暴露给潜在的恶意用户时,应用程序安全审计的重要性不言而喻。然而你需要明确知道什么时候进行审计。一般地,你可以在系统开发生命周期(Systems Developement Life Cycle, SDLC)的任何阶段进行审计。让么人,在什么时候进行审计所花费的代价是不一样的。因此在开始之前,我们先回顾一下系统开发生命周期的每个阶段:

  1. 可行性研究。这个阶段会考虑确认完成这个项目的需求并决定什么样的开发解决方案在技术上和预算上是合适的。
  2. 需求定义。这个阶段将对于完成项目的需求进行更深入的研究,项目的目标也将确立。
  3. 设计。设计解决方案来使得目标系统能够技术性地实现并且满足需求。
  4. 实现。根据前面地设计阶段完成应用程序代码。
  5. 集成与测试。解决方案要经过一定程度的质量保证,以确保它按预期工作,并捕获软件中的任何 bug。
  6. 使用与维护。解决方案发布并投入使用,然后根据用户地反馈进行修正,更新,更正。

每个软件地开发过程都在某种程度上根据这套规则。经典的瀑布模型(waterfall models)要求系统周期严格地按照这个过程迭代一次。相反的是,新的方法论例如敏捷式软件开发(agile developement)更倾向于通过不断地迭代系统开发生命周期来改善程序。所以系统开发生命周期这个模型地应用是多样的,但基础的概念与阶段已经足够我们用于接下来地讨论。你可以用这些特点来帮助区分漏洞,在接下来的章节,你将会学到进行不同类别的审计的最佳阶段。

6.1.4 漏洞的分类

一个漏洞的类别(vulnerability class)是一系列拥有统一常见特征或者在不同软件缺陷中具有类似概念的漏洞的集合。当然,这个定义可能有点晦涩,但归根结底漏洞类别就是用来概念化软件缺陷的精神装置(mental device)。它们在我们理解问题以及将这种理解扩散到其他部分时大有帮助。但将漏洞分组到精确地不重叠的类中是不可能的。一个简单的漏洞很有可能根据代码审计者的术语,分类系统以及个人观点被分到许多类中。

这本书里不会使用一个对漏洞严格的形式化的分类,相反,这些问题会以一个一致的,实用的,适合于材料的方法进行分类。一些软件漏洞最好通过一些特殊的方法来处理。例如, 某些缺陷最好通过从高级软件组件的交互角度来看程序来解决 ; 另一种类型的缺陷最好通过将程序概念化为一系列系统调用来解决。无论采用哪种方法,本书都将解释你将在安全文献中遇到的术语和概念,以便你可以读懂安全社区在某种上下文中使用的一系列术语和分类方法。

在定义一般漏洞类时, 你可以从 SDLC 阶段的讨论中得出一些一般性的区别。有两个漏洞的类别叫设计漏洞(SDLC 阶段 1,2,3)和实现漏洞(SDLC 阶段 4,5) 。并且,这本书还会提到第三个类别:操作漏洞(operation vulnerability)(SDLC 阶段 6)。安全社区通常将设计漏洞作为软件系统体系结构和规范中的缺陷,实现漏洞时软件系统实际构建过程中的技术缺陷。操作漏洞的类别则针对在特定环境中部署和配置软件时出现的缺陷。

设计漏洞

设计漏洞来源于在软件设计时的根本的错误或者疏忽。由于设计缺陷的存在,软件本身就不会安全,因为软件做它被设计出来做得事,即错误的事情。这种缺陷时常出现, 因为对程序将在其中运行的环境所做的假设,或者程序组件在实际生产环境中所面临的暴露风险。 设计缺陷应当认定为高级别的漏洞,体系结构的缺陷,或者 程序需求或约束的问题。

在 SDLC 的简要介绍中我们知道,软件系统的设计是依托于软件需求的定义的,这些需求是一系列软件系统所必须达到的目标。特别地, 工程师获取需求集并构造设计规范, 这些设计规范关注于如何创造满足这些需求的软件。 需求通常是软件系统必须完成的任务 。例如:“允许用户从服务器中检索事务文件”。需求也可以说明软件必须要用的功能,例如:“它必须支持每小时同时下载 100 个文件。”

设计规范就是怎样构建程序来实现预期需求的计划。特别地,它包括了一个软件系统不同组成部分地描述,这些部分如何实现的信息,以及各个组成部分如何交互的信息。设计规范可以包含结构图,逻辑关系图,进程流图,接口和协议说明书,类的继承层次关系,以及其他的技术性的说明书。

当人们说起设计缺陷,它们通常不会发现软件需求方面与软件规范方面缺陷的不同。这种不同一般难以发现因为很多高层次的问题可能是需求中的疏忽造成,也可能是规范中的错误造成。

举个例子, TELNET 协议是被设计用来允许用户在远程机器中连接并像本地终端一样访问该机器。从设计的角度看,可以说 TELNET 具有一个依赖于非加密交流的漏洞。在一些环境中,如果底层网络是可信的,那么这种缺陷是可以接受的。然而,在企业网络和因特网中,非加密的交流就是一个大的缺陷,因为位于路由路径上的攻击者可以监视和劫持 TELNET 上的会话。如果管理者通过 TELNET 连接上了一个路由器然后输入了账号和密码,监视者就可以获得它们并将其记录。相反,像 SSH 这样的协议功能与 TELNET 几乎一样,但它能免于监视威胁因为所有交流信息都是经过加密的。

实现漏洞

实现漏洞中,一份代码通常做到了他应该做的事情,但在运行操作的方式上它有安全隐患。顾名思义,这种问题通常在 SDLC 的实现阶段产生,但它经常被带入集成与测试阶段。如果在解决技术差异的实现过程偏离了原有设计,这种问题就有可能发生。然而,大多数情况是由软件构件时的技术工具,平台的细微差别以及语言环境造成的。实现漏洞通常也被称作低级别缺陷或者技术缺陷。

本书会给出很多关于实现漏洞的例子,因为发现这种技术缺陷是代码评审的基础操作之一。实现漏洞又包括了几种你可能听说过的广为人知的漏洞类,例如缓冲区溢出和资料隐码攻击(SQL injection)。

回到 TELNET 的例子,你能在 TELNET 的一些特定版本软件中找到实现漏洞。一些 TELNET 先前的版本不能正确地清理用户的环境变量,允许入侵者利用 UNIX 机器的动态连接特性提高他们在这台机器上的权限。还有一些缺陷允许入侵者制造缓冲区溢出,格式化字符串攻击各种版本的 TELNET 守护进程,通常这些操作都完全未经授权。这些缺陷导致攻击者能够在远处输入任何命令,就像是特权用户一样。基本上,攻击者可以对一个 TELNET 守护程序运行一个小小的攻击程序,然后立即在服务器上得到 root 权限。

操作漏洞

操作漏洞是在特定环境中操作程序和软件的一般使用过程中出现的问题。一种区分出这种漏洞的方法就是这种漏洞并不是由于软件源代码源代码造成的,而是根植于软件如何与其运行环境。特别地,它们可以包括软件在其环境中的配置问题,支持软件和计算机的配置问题,由围绕系统的自动化和手动过程引起的问题。操作漏洞可以在用户系统中引起很多种类的攻击,例如社会工程和盗窃(social engineering and theft).这些问题一般发生在 SDLC 的使用与维护阶段,尽管它与继承和测试阶段也有一些重叠部分。

回到 TELNET 的例子,由于缺乏加密手段,它具有设计漏洞。假如你在寻找一个可以安全地实现自动化交易的软件系统。假设它需要每晚都要设定一系列权重参数来石英第二天的交易策略。更新这些数据的过程就是管理者在每个交易日的结尾通过 TELNET 登陆计算机,然后通过一个简单的应用程序输入新的变量集合。取决于环境,这个过程可能有很大的操作漏洞,因为在使用 TELNET 时有有非常多的风险,例如监视与登陆劫持。总之,维护软件的操作过程是有缺陷的,因为它使系统暴露于潜在的欺诈与攻击之下.

灰色地带

从 SDLC 的视角看,设计漏洞和实现漏洞的区别非常简单.但事实不是总如此.许多实现漏洞同样可以解释为设计漏洞,因为在设计流程中并没有正确地预料到会有问题发生.另一方面,你也可以说软件系统低层次的部分也是以某种方式设计的.一个程序员可以在实现设计规范时设计软件的很多部分.这些部分可能包括一个类,一个函数,一个网络歇息,一个虚拟机,或者一系列巧妙的循环与分支.在缺乏严格的区分下,本书对设计漏洞是这样定义的:

通常情况下,设计漏洞是程序结构中高层次的问题,比如需求,基础接口,以及核心算法.

扩展一下设计漏洞的定义,本书使用如下定义来表示实现漏洞:

在低层次设计中的安全问题,例如一些独立的函数和类的问题就会被当作实现漏洞.实现漏洞当然也包括一些更复杂的但不在设计规范中提及的逻辑单元.(这些问题通常被称为逻辑漏洞).

同样的,操作漏洞和设计漏洞与实现漏洞之间也没有清晰的区别.比如一个程序在某个不安全的环境中下载了以后,你当然可以认为它是在设计与实现上有缺陷.你可以认为一个应用程序应当在开发出来后其安全性不依赖于所运行的环境.由于缺乏严格的区分,本书关于操作漏洞是这么定义的:

通常情况下,操作漏洞用于表示软件不安全的发布以及配置问题,不健全的管理以及围绕软件的管理实践,以及支持组件,比如比如应用程序和 Web 服务器的问题,以及对软件用户的直接攻击等问题.

你能看到对于设计,实现以及操作漏洞这三个概念有非常多的解释,所以不要认为这些定义是一个标记软件缺陷的可靠的正式的系统.它们只是用来学习软件漏洞的简单且有用的方法而已.

常见的线索

在学习了一些关于审计过程,安全模型,异界三个常见的漏洞分类这些的背景知识后, 当你深入了解具体技术问题的细节时,本书将用剩下的篇幅继续讨论它们.现在,我们回过头来先看看隐藏在软件安全漏洞背后的一些常见线索,主要先关注一下漏洞最优可能在软件中出现的位置以及原因.

输入与数据流

大量的软件漏洞都来自于一个程序对有害数据处理时的预期之外的行为.所以第一格问题就是解决恶意数据时怎样被系统所接受并且造成了严重影响的.解释它的最好办法就是先从一个简单的缓冲区溢出漏洞的例子说起.

考虑一个 UNIX 程序,它包含了一个非常长的能造成缓冲区溢出的命令行变量.在这种情况下,恶意数据就是直接来自于攻击者通过命令行接口的输入.这个数据通过整个程序直到一些函数通过不安全的方式使用它,最后导致了受攻击的局面.

对于绝大多数漏洞,你都能发现一些攻击者往系统中注入一些恶意数据以触发攻击.然而,这些恶意数据可能通过比用户直接输入更迂回的方式发挥作用.这些数据可以来自不同的源头并且经过不同的接口.它也可以通过系统的多个部分,并在到达最终触发可以用条件的位置之前进行大量的修改. 因此,在检查软件系统时,需要考虑的最有用的属性之一是贯穿系统各个组件的数据流 .

例如,有一个应用可以处理大组织的会议日程表.在每个月结尾,这个应用都会生成一份本周起协调的所有会议的报告,包括一份对每个会议的简短总结.仔细检查代码就会发现,当应用程序创造这个总结时,大于 1000 个字符的会议描述会导致 可以遭受攻击的缓冲区溢出的情况.

对这个漏洞进行攻击,你可以创建一个新的会议,他的描述性文字超过 1000 个字符,然后使用这个程序安排时间表.然后你就可以等待每个月的报告创建以后看看这个攻击时如何运转的.你的恶意数据可能通过几个系统的部分然后被存入数据库,同时避免被其他系统的用户发现.相应的,作为一个安全审查人员,你必须评估这个攻击向量的可行性.这个观点涉及分析会议描述的流程,从最初的创建,到多个应用程序的组件, 最后到在易受攻击的报告生成代码中使用。 这个跟踪数据流的过程是对软件设计和实现的审查的核心。用户可塑数据对系统构成严重威胁,跟踪端到端数据流是评估这种威胁的主要方法。通常,你必须确定用户可塑数据通过外部世界的接口(例如命令行或 Web 请求)进入系统的位置。然后,你要研究用户可塑数据在系统中传输的不同方式,同时查找任何可能利用这些数据的代码。数据很可能会通过软件系统的多个组件,并在其生命周期的几个点进行验证和操作。这个观点涉及分析会议描述的流程,从最初的创建,到多个应用程序组件,最后到在易受攻击的报告生成代码中使用。

这个过程并不总是那么简单. 通常你会发现一段代码是很容易受到攻击的,但它最终是安全的,因为恶意输入的数据流在早以前就能被捕获或者过滤.通常情况下,这种攻击时通过偶然事件来组织的.例如开发认为处于完全与安全性无关的原因引入了一些代码,但这样的副作用就是在数据流的后期保护了易受攻击的组件.此外,跟踪真实应用程序中的数据流可能非常困难,复杂系统通常时有机地开发的,导致数据流高度碎片化.在处理单个用户的请求过程中,实际数据可能会遍历几十个组件,并出入于第三方代码框架中.

信任关系

软件系统中不同组件对彼此具有不同程度的信任,在分析给定软件系统的安全性时理解这些信任关系非常重要.信任关系对于数据流是不可或缺的,因为 i 组件 组件之间的信任级别通常决定了对它们之间交换的数据进行验证的数量。

设计人员和开发人员通常认为两个组件之间的接口是可信的,或者将对等组件或者支持软件的组件指定为可信的.这意味着它们通常相信受信任的组件不会受到恶意干扰,并且它们认为对组建的数据和行为进行假设是安全的. 当然,如果这种信任是错位的,并且攻击者可以访问或操作受信任的实体,系统安全性就会像多米诺骨牌一样下降(即一个部分不安全,其他地方由于传递效应也变得不安全 —by 译者)。

说到多米诺骨牌,在评估系统中的信任关系时,理解信任的传递性非常重要。例如,如果你的软件系统信任某个特定的外部组件,而该组件又信任某个网络,那么你的系统就间接地信任了该网络。如果组件对网络的信任不佳,它可能会成为攻击的受害者,最终使你的软件处于危险之中。

假设与错位信任

看待软件缺陷的另一种有用的方法是,从程序员和设计人员在构建软件时做出毫无依据的假设的角度来考虑它们.开发人员可以在一个软件的许多方面做出错误的假设.包括输入数据的有效性和格式,支撑程序的安全性,环境中潜在的敌意,攻击者和用户的能力,甚至特定应用程序接口(API)的调用以及语言特性上的行为和细微差别.

不适当的假设与错位信任这两个概念密切相关,因为你可以说对组件进行不适当的信任与对组件进行毫无根据的假设非常相似,下面几节将讨论开发人员可能会以几种方式犯下与安全性相关的错误,这些错误就包括做出毫无根据的假设以及扩展不值得信任的内容.

输入

如前所述,大多数软件漏洞都是攻击者向软件系统注入恶意数据后出发的.这些数据之所以能造成这样的麻烦,原因之一就是软件往往过于信任与它通信的人,并且对数据的潜在来源和内容做了假设.(即假设我不会输入恶意数据 —by 译者)

具体说,当开发人员编写处理数据的代码时,它们经常对提供数据的用户或者软件组件做出假设.在处理用户输入时,开发人员通常认为用户不大可能做出这样的事情,比如输入一个包含 5000 字符的街道地址,期中还包含了无法打印出来的符号(emoji 里的抽象文字? — by 译者). 类似地,如果开发人员正在为两个软件组件之间编写接口代码,它们通常会假设输入格式是好的.例如,它们可能没有预料到程序在文件中防止一个负长度地二进制记录,或者发送一个 40 亿字节长的网络请求.

相反,攻击者在查看输入处理代码时,就试图考虑每一个可能导致不一致或者意外程序状态的输入.攻击者会试图探索软件的每一个可访问接口,并专门寻找开发人员所作的假设.对于攻击者来说,任何提供意外输入的机会都是宝贵的,因为这种输入通常会对开发人员没有预料到的后续处理产生微妙的影响.通常,如果你可以对软件运行时的属性进行以外的更改,那么你通常就可以找到一种方法来利用它对程序产生更大的(负面的)影响.

接口

接口就是软件各组件相互之间以及软件与外部世界进行通信的机制.许多漏洞时由于开发人员并没有充分认识到这些接口的安全属性,从而假设只有受信任的对等放可以使用它们而造成的.如果一个程序的组件可以通过网络或者本地机器上的各种机制访问,那么攻击者可能能直接列街道该组件并且输入恶意数据.如果编写该组件时假设其对等组件是可信的,则应用程序可能会以一种能够被利用攻击的方式错误地处理输入.

使得这个漏洞更加严重的是,开发人员通常错误地估计了攻击者到达这个接口地难度,因此他们信任这个接口,而这个接口是没有保证地.例如,开发人员可能认为自己的系统有很高的安全性,因为他们使用了带有自定义加密和专有的且复杂的网络协议.他们可能错误地认为攻击者不大可能构建自己爹客户端和加密层然后以意想不到的方式操纵协议. 不幸地是,这种假设非常不合理,因为许多攻击者在专有协议地逆向工程中找到了一种独特的乐趣.

总结一下,开发者可能由于以下原因对一个接口产生错位信任:

  • 他们选择了一种暴露接口的方法,这种方法不能提供足够的保护来抵御外来攻击者
  • 他们选择了一种可靠的方法来公开接口,通常是操作系统的服务,但他们使用了错误的配置.攻击者还可能利用基础平台的漏洞获得对该接口的控制.
  • 他们假设一个接口对于攻击者来说太难访问了,这是一个危险性极高的赌博.
环境的攻击

软件系统不是在真空中运行的,他们在一个大的计算环境中作为程序来运行,这个计算环境包括操作系统,硬件架构,网络,文件系统,数据库以及用户.

尽管许多软件漏洞来自于恶意数据的处理过程,但有的软件漏洞是在攻击者更改软件所依赖的环境中发生的.这种缺陷可以当作一种对软件的底层环境所作的假设造成的漏洞.软件系统所依赖的每种支撑技术可能都有许多最佳实践和细微差别,如果应用程序开发人员没有完全了解每种技术的潜在安全问题,那么犯下一个导致安全暴露的错误就太容易了.

关于这个问题的一个经典的例子就是 UNIX 软件种常见的一个文件夹叫/tmp.当一个程序需要使用临时文件时,它会在系统公共目录中创建这个文件,一般它位于/tmp或者var/tmp.如果这个程序没有细心地编写,攻击者就可以预测程序地移动并在公共目录中设置陷阱.如果攻击者在正确的位置和正确的事件创建了一个符号连接,程序就会被欺骗,在系统其他地方以不同名称创建它的临时文件,如果易受攻击的程序以 root 权限运行,这通常就导致系统遭受攻击.

在这种情况下,漏洞不是通过攻击者提供给程序的数据触发的.相反,这是对程序运行时环境的攻击,导致程序与操作系统以一种预期之外以及不期望发生的方式进行交互.

异常条件

与处理异常情况相关的漏洞与数据和环境漏洞交织在一起。基本上,当攻击者可以通过外部手段(external measure)在程序的正常控制流中引起意外更改时,就会发生异常情况。这种行为可能导致程序的异步中断,例如信号的传递。它还可能涉及消耗全局系统资源来故意在程序的特定位置诱发故障条件。

例如,如果一个进程试图写入一个关闭的网络连接或者通道,UNIX 系统将发送一个 SIGPIPE 信号,接收到此信号时的默认行为就是终止进程.攻击者可能会是一个易受攻击的程序在适当的时候对通道进行写入操作,然后应用程序能够成功执行写操作之前关闭通道,这将导致 SIGPIPE 信号,然后导致应用程序终止,并使得整个系统处于不稳定状态.对于更具体的示例,某些 Linux 发行版的网络文件系统(Network File System,NFS)状态守护进程很容易由于在正确的事件关闭连接而崩溃.利用这个漏洞能够破坏 NFS 功能,这种情况会持续到管理员可以干预并重置守护进程为止.

6.1.6 总结

在短短的这一章中已经涵盖了很多内容,当然可能还会留下一些问题.但无需担心,后续章节将会讨论更多细节,并在你的学习过程正给出答案.闲杂,重要的时要对计算机软件中可能出现的问题有很好的了解,并理解在讨论这些问题中所使用的术语.你还应该了解应用程序安全审计的必要性,并熟悉流程的不同方面.在后面的章节中,你将在此基础上学习如何使用这个审计过程来识别你所审查的应用程序中的漏洞.

 wechat
scan and following my wechat official account.