Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

《安全编程:代码静态分析》—— Brian Chess / Jacob West #54

Open
thzt opened this issue Mar 13, 2020 · 0 comments
Open

Comments

@thzt
Copy link
Owner

thzt commented Mar 13, 2020

软件安全,是使得软件能够在恶意攻击的情况下,安全而正常运转的一种软件构建实践。

随着业界人士对软件安全的重要性愈来愈重视,人们逐步采纳并开展了一系列的最佳实践,来致力于解决这一问题。
微软公司的可信赖计算计划(Trustworthy Computing Initiative)取得了显著的成就。

无论是人工的还是使用静态分析工具的自动化方法,代码审查总是试图在软件发布之前识别出安全缺陷。
当然,哪种单一的技术都不是万能药。
对于实现安全的软件来说,代码审查是一项必要而非充分的例行工作。

最好的情况下,代码审查能揭示出 50% 左右的安全问题。
而架构的问题很难(并且很大程度上不可能)通过紧盯代码的方式找出来。
对于由无数代码行组成的现代系统来说,更是这样。
而一种综合的软件安全方式,包括了对代码审查和架构分析的全面整合运用。


没有足够的安全性,我们将无法充分发挥数字时代的潜力。
但令人奇怪的是,许多打着计算机安全旗号进行的活动,事实上根本就不是在解决安全问题,而是清理安全问题产生的混乱局面而已。
病毒扫描程序,防火墙,补丁管理以及入侵检测系统,是我们弥补软件安全不足所采用的所有手段。
而软件行业将更多的精力放在了对安全缺陷的修补上,而不是致力于一开始就创建安全的软件。

要想改变软件安全的这种状态,就要改变软件构建的方式。
这并不是件容易的事情,毕竟程序员会犯无数错误。
虽说潜在的错误可能会是无限的,但实际上,程序编写整体上却趋向于重复相同的错误。

这种熟知错误的大量重复提示我们:
当前遇到的许多安全问题是可预防的,而软件团队拥有避免这些错误出现所必需的经验技巧。

确保自己的作品是安全的,这是软件构建人员的责任。
软件安全问题不能留给系统管理员或最终用户去处理。


提高软件安全性最有效的方法,就是研究过去的安全错误,并且预防未来发生这些问题。

要构建一套健壮的系统,就必须明白这个系统可能会怎样失效。
错误是不可避免的,但要有控制错误的措施。
尽管你不能精确了解下一次会犯什么大错,但你可以控制发生错误的可能性。
你还可以控制在什么地方,什么时候,由谁来发现你的这些错误。

对安全的理解是一回事;
将你的理解应用于一种完整而始终如一的行事风格中,从而满足的你的安全目标,这完全是另外一回事。

静态分析(static analysis)是指在不执行的情况下对代码进行评估的过程。
静态分析非常强大,这是因为它允许对多种可能性进行快速考量。
一个静态分析工具,能够探查大量 “如果……将会……” 的想定情况,而不必为所有这些想定情况经过必要的计算来执行这些代码。

静态分析尤其适合用于安全方面的检查,因为许多安全问题都发生在一些隐蔽的,难以出现的情况下,
这非常难以通过实际运行代码来达到目的。

迄今为止,使用最广泛的 bug 查找方法就是动态测试,这涉及要运行这个软件,并将其输出与预期结果相比较。
大部分软件测试的目标都是将软件的实现与其需求相对比,而这种方法不足以找出软件的安全问题。

可靠的软件做其设定的事情,安全的软件做其设定的事情,而不做任何未设定的事情。

在引发安全问题的错误中,大约有一半是实现时的疏忽,遗漏或者误解造成的。

封装的作用是画出清晰的边界。


任何不需要执行代码而对其进行分析的工具,所进行的都是静态分析。

运行静态分析工具没有检查出问题,这并不能保证你的代码就是完美的;这只能说明代码中没有某些常见的问题。

最有经验,最专业的书写者,会觉得拼写检查工具是非常有用的工具。
水平较差的书写者,也能得益于拼写工具的使用,
但是使用这样的工具,并不能让他们变成优秀的书写者。

静态分析也是这样的道理:
好的程序员能发挥出静态分析工具的最佳效果,
而差的程序员不管使用什么工具,都仍然写出劣质的程序。

  • 类型检查

最为广泛的静态分析的应用形式,也是大多数程序员熟悉的形式,就是类型检查。

  • 风格检查

风格检查程序也是静态分析工具。
很难在一个大型编程项目的中途采用一个风格检查工具,因为对于 “正确” 的风格,不同的程序员很可能已经形成了某种程度上各不相同的看法。
在项目开始之后,纯粹为了减少风格检查的输出而重新检查一遍代码以对代码进行调整,这实现的只是一些边缘利益,而且会以很大的麻烦作为代价。

仔细检查大量的代码并纠正其中风格检查程序所给出的警告,这有点像给大量滋生白蚁的木屋刷漆。
在项目的开始采用风格检查是最容易的。

  • 程序理解

程序理解工具能帮助用户搞懂代码库中的大量代码。
集成开发环境(IDE)一般至少都包含有某些程序理解功能。
其中简单的一些例子如:“查找本方法的所有应用” 或 “找出全局变量的声明”,等等。
更高级一点的分析可支持自动进行程序重新分解组合的功能,例如对变量重命名,或者将一个函数分割为多个函数。

  • 程序验证 和 属性检查

程序验证工具接受一份规格说明和一份代码,而后尝试证明该代码忠实实现了这份规格说明。
如果这份规格说明完整描述了程序应做的任何事情,那么该程序的验证工具可执行等价检查,以确认该程序的代码与这份规格说明确切匹配。

程序员很少能有一份用来进行等价检查的,足够详细的规格说明,而且创建一份这样的规格说明,所要做的工作最终比编写代码的工作量还要大,因此这类正式的验证很少会发生。

更为常见的情况是,验证工具将根据只描述部分程序行为的部分规格说明,对软件进行检查。
这种方式的检查,有时候称做属性检查(property checking)。
多数属性检查工具或者通过应用逻辑推论,或者通过执行模型检查来实现其功能。

许多属性检查工具,都将重点放在临时性(时序)安全属性(temporal safety properties)上。
时序安全属性,规定了一系列有序的事件,在程序中绝对不能发生这些事件。

只要有问题存在,属性检查工具就会报告这个问题,那么这个属性检查工具被称做是健全的(sound),遵守了相应的规格说明。
换句话说,这个工具将永远不会出现漏报的情况。

在学术环境中,健全性(soundness)是一个重要的特性,在这种环境中,这方面任何程序的不足都可能被贴上 “没有原则” 的标记。
但是,对于现实世界中大量的代码来说,几乎不可能满足属性检查工具所规定的条件,所以其健全性保证就没有什么意义。
由于这种原因,从业界人士的角度来看,健全性很少会成为一种对此类工具的需求。

由于追求健全性或者其他复杂的原因,属性检查工具可能会产生误报。
在误报的情况下,反例中将包含一个或多个实际上并不会发生的事件。

形式验证(即验证工具)采用一种严格的数学方法来进行其验证任务,具有悠久而充满传奇的历史。

  • Bug 查找

Bug 查找工具的目的,既不是像风格检查程序那样抱怨格式化问题,也不是像程序验证工具那样执行完全而彻底的程序和规格说明之间的比较。
与这些工具不同,Bug 查找程序只是指出以程序员设想之外的方式运转的一些地方。

Bug 查找工具通常关注的是产生较少误报情况 —— 即使这意味着会有较高的漏报率。
理想的 Bug 查找工具就反例而论是健全的。
换句话说,当此工具产生一份 Bug 报告时,其随附的反例总是表现出一个程序中切实可行的事件序列。
学院派有时也将就反例而言健全的工具,称做是完备的。


从计算的角度来看,静态分析是一种不可判定的(undecidable)问题。
要知道某个程序的行为,唯一有保证的途径就是运行这个程序。

赖斯定理(Rice's theorem):
静态分析不能完美的确定一般程序的任何重要属性。

在实践中,重要的是静态分析工具提供了有用的结果。
对于静态分析工具来说,其静态分析的不可判定天性,并不会真正的成为太大的限制因素。

想一下物理领域的类似情况,对于一辆新汽车来说,光速为其最大车速潜在的设置了一种限制,
但是,在光速的限制成为真正的问题之前,许多工程上的难点就已经限制了汽车的速度。

编程语言中每个独有的情形都会给静态分析工具出一个新的小难题。
就单个问题来说,这些小难题都不是太难解决,但是,把这些问题放在一起,就使得程序解析成为一项艰巨的任务。
某些大型组织还会通过向程序语言中引进一些新的语法,来创建其自己的语言风格,这就使得情况变得更为不妙。
这将构成解析问题。

一个分析工具要识别出确实需要分析的代码,最好的方式就是将这个工具平滑的集成到该程序所用的编译系统中。

针对静态分析工具的实践挑战主要包括:

  • 对程序的理解(构建一个准确的程序模型)
  • 在精度,深度和可伸缩性之间做出合理的折中
  • 找到恰当的缺陷集合
  • 提供易于理解的结果和错误陈述
  • 容易集成到程序的构建系统和集成开发环境中

为了将静态分析整合到现有的开发过程之中,整个组织需要一个工具采用计划。
整个计划应规划出谁将运行此工具,何时运行工具,以及结果如何处理。
静态分析工具本身是不关心过程的,但工具采用之路却是与过程相关的。
在你编制一项工具采用计划时,要充分考虑到风格和文化的问题。


认为事情不可为的人,不要妨碍努力而为的人。

静态分析工具需要做的第一件事情,就是将要进行分析的代码转换成一种程序模型,即一组代表此代码的数据结构。

  • 语义分析

在生成 AST 的时候,分析工具将同时生成一张符号表(symbol table)。
对于程序中的每个标识符,符号表将其类型和声明或定义的指针与标识符相关联。

对象的类型决定了该对象可调用方法的集合,所以对于一个面向对象语言来说,类型信息是至关重要的。
此外,通常至少需要将源文本中的隐含类型变换,转换为 AST 中明确的类型变换。

在编译器领域,符号分析和类型检查都是作为语义分析(semantic analysis)的概念而提的。
这是因为编译器会给程序中发现的这些符号赋予一定的意义。
使用这些数据结构的静态分析工具,比起那些没有使用的工具来说,具有明显的优势。

在语义分析之后,编译器和较为高级的静态分析工具就将分道扬镳了。
现代编译器使用 AST 和符号以及类型信息来生成一种中间的表示法,这是一种通用版本的机器码,适用于进行优化,而后转换为平台特有的目标代码。

静态分析的路就没有那么清楚和直接了。
根据所执行分析类型的不同,静态分析工具可能需要在 AST 之上执行额外的转换,或者可能生成自己的中间表示法变种,以适应其自己的需要。

  • 跟踪控制流

当函数执行的时候,许多静态分析算法(以及编译器优化技术)都会探究其可能采取的执行路径。
为了使算法更加高效,绝大多数工具都会在 AST 或者中间表示法之上生成一个控制流图(control flow graph)。

控制流图中的节点是一些基本块(basic block):
指令序列总是从第一条指令开始执行,并且连续执行到最后一条指令,任何指令都不可跳过。
在此控制流图的边界指向或者代表着基本块之间潜在的控制流路径。
控制流图的返回边界代表着潜在的循环。

调用图(call graph)描述了函数或者方法间潜在的控制流。
在缺少函数指针或者虚方法的情况下,构建调用图就只是查找每个函数中引用的函数指针而已。

如果程序在运行时动态载入代码模块,那么,由于程序可能会运行那些在分析时不可见的代码,
所以这种情况下没有什么方法能确保控制流图是完整的。

  • 跟踪数据流

数据流分析算法,将检查数据在程序中移动的情形。
编译器执行数据流分析,从而定位寄存器,移除无用代码,并执行许多其他的优化。

数据流分析通常包括遍历某个函数的控制流图,并记录数据的值在哪里产生,在哪里使用,将函数变换为静态单赋值形式(static single assignmen,SSA),这对许多数据流问题来说都是有益的。

SSA 形式的函数只允许对每个变量进行一次赋值。
为适应这种约束,就必须将新的变量引入到程序中。

SSA 形式是颇有价值的,这是因为给定程序中的任意一个变量,可以很容易的判断出这个变量的值是从哪里来的。
这种特性有许多应用,例如,如果某个 SSA 变量永远被赋值为一个常量,那么就可以使用这个常量来替换所有对这个 SSA 变量的使用。这种技术称之为 常量传递(constant propagation)。

常量传递本身对于查找安全问题就非常有用,比如查找硬编码到程序中的口令或密钥。

如果某个变量分别沿着不同的控制流图被赋予了不同的值,那么在 SSA 形式中,这个变量必须在控制流路径融合的那个位置进行合并处理。
SSA 引入一个新型变量,并将来自 2 条控制流路径之一的值赋给这个新型变量,通过这种方法来完成上述融合。
用于融合点的这种简化符号称做一个 ψ-函数。
这个 ψ-函数 的加入,是为了依据执行的控制流路径而选择适当的变量值。


使用高级静态分析算法的动机,是为了提高上下文环境敏感度 —— 判断特定代码在何种环境和何种条件下运行。
更好的上下文环境敏感度,能够更好的评估目标代码带来的危险。

任何高级分析策略都至少由 2 个主要部分构成:
一个用来分析单个函数的程序内分析(intraprocedural analysis)组件,
另一个用于分析函数间交互行为的程序间分析(interprocedural analysis)组件。

由于程序内(intraprocedural)和程序间(interprocedural)的名字很相似,
所以我们使用更为通俗的属于 “本地分析(local analysis)” 来表示程序内分析,
而使用 “全局分析(global analysis)” 来表示程序间分析。

分析算法:

  • 本地分析:AST,控制流图
  • 全局分析:调用图

本地分析方法:

  • 抽象解释(abstract interpretation)
  • 谓词转换器(predicate transformers)
  • 模型检查(model checking)

抽象解释(abstract interpretation)是一种通用技术,这种方法首先将程序中与感兴趣的属性无关的方面抽取出去,而后使用选中的程序抽象执行一种解释。

谓词转换器(predicate transformers),程序的最弱前置条件(weakest precondition,WP)是对程序调用者最少(也就是最弱)的需求,这些需求是实现最终状态(后置条件,postcondition)所必需的。
程序中的语句可以视作是对后置条件执行转换。
从程序中的最后一条语句开始倒退处理到的第一条语句,程序所需的后置条件被转换为该程序要成功实现所必须的前置需求。

谓词转换器之所以如此吸引人,是因为通过为一段代码产生一个前置条件,谓词转换器抽取掉程序的细节而创建了一份此程序对调用者的需求总结。

对于临时性的安全属性 —— 比如 “内存应该只释放一次” 以及 “应该只有非空指针才能被解除引用”,可以很容易的将所检查的属性表示为一个小型的有限状态自动机。
模型检查(model checking)方法将此属性作为一种规范来接受,将要检查的程序转换为一种自动机(称做模型),而后将规范与此模型加以对比。

如果模型检查程序,能找出导致规范自动机达到其错误状态的一个变量,或者程序中的一条路径,
那么模型检查程序就把这个潜在的问题,识别为一种 2 次释放(内存)漏洞。


好的静态分析工具会尽可能在程序外部实现其检查所需的规则,这样,规则就能在无需更改工具本身的情况下添加,减少,或者修改。
最好的静态分析工具会将所有要检查的规则都放在外部实现。
优秀的静态分析工具是规则驱动的。


没有战术的战略,是取得胜利最慢的途径;
没有战略的战术,只是失败前的聒噪罢了。


没有什么工具能替代得了安全培训,优秀设计或者优秀编程技巧,
但是,静态分析工具是已详尽知识和对细节的警惕来武装程序员的一种重要方式,
而详尽的知识和对细节的警惕,是程序员在并不完美的语言,库和组件基础上创建他们自己的安全代码所需要的东西。

我们已经看到这样一种变化:
人们已经从试图只找到并减轻那些证明为可被利用错误的,这种反应式策略,
朝着主动的推广最佳实践的方向发展。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant