敏感性在静态代码分析中的作用
日期:2023年03月31日 阅:130
敏感性概要
敏感性作为代码分析精度的重要指标,在学术界经常被研究人员提及。但工业界或者工具使用者对这些概念可能是不清楚的,导致在选择静态分析工具或者评价静态分析工具的优劣仅从功能、效率、是否支持CI/CD等角度,我们不否认这些功能对于易用性很重要,但是静态分析工具最有价值的地方在于其分析引擎的能力,就像评价汽车的发动机引擎的能力是衡量一辆车价值的重要指标。代码分析引擎的优劣的评价标准是其检测缺陷时,支持的各种敏感分析的能力。本文就一些基本概念进行一下简单介绍,以供代码分析工具使用者们参考。敏感性是指分析方法对程序特征的敏感程度。这些特征包括程序的控制流、数据流、动态执行所有可能的路径、函数调用上下文以及对象的属性等。敏感性是衡量静态代码分析精度的关键因素,不同的敏感性方法对代码的检测能力和精度产生重要影响。举一个非敏感性的例子,如静态分析工具在系统中发现与SQL相关的API语法时如java.sql.Statement.executeQuery都报出SQL注入问题,而不检查SQL命令是否来自不受信任的输入以及真正执行时到达的可能性。这种方法使工具永远虽然不会有漏报,但报告每一个与SQL相关的API使用情况只会使工具可用性降低,产生大量不需要程序员修复的误报。为了提高安全工具的可用性,支持和实现各种分析敏感性是非常重要的。
考虑以下代码片段GetUserInput返回一个由外界输入的字符串,也就是是不可信的外界输入源。如果该输入最终成为Execute方法的参数,该函数将给定的参数作为操作系统命令运行,这就意味着在这里存在命令行注入风险。
1 a = 3; b = 2;
2 input = String.Empty;
3 if (a < b)
4 {
5 input = GetUserInput();
6 }
7 else
8 {
9 input = String.Empty;
10 }
11 Execute(input);
人工分析代码,我们可以看到Excute方法不可能执行来自于GetUserInput的不受信任的输入,因为只有当a小于b时,才会调用GetUserInput。如果静态代码分析工具报出了命令行注入缺陷,那么开发人员可能不会去修改。这个例子很简单,因为a和b的值是常量。但如果它们是变量,代码分析工具需要去计算其可能的数值范围,这对代码分析功能的能力才是真正的挑战,不同问题的难度也不同。在这个例子中,不报出警报的工具,比报出该漏洞的工具更有用,这就是敏感性分析的价值。
敏感性的类型
对于任何静态分析器,检测到缺陷和漏洞的准确性和完整性都取决于分析器的敏感性。我们将简单概括静态代码分析中常见的敏感性定义,以便了解提高分析结果准确性的多种途径。
第一个是数据流分析(DFA)的内部属性-流敏感性。流敏感分析考虑了代码执行的顺序,它会分析代码中变量的使用和赋值顺序,以便更准确地检测潜在问题。下面的例子展示了我们从最终用户处得到的输入通过GetUserInput的一个非常基本的污染流到一个危险的Execute方法。由于我们将一个空字符串分配给污染变量input,然后将其提供给Execute方法,因此这里没有漏洞。
input= GetUserInput();
input= String.Empty;
Execute(input);
具有流敏感性的自动分析也不会在此处报出漏洞,因为语句的顺序是导致前面的input值被后面覆盖。然而,一个流不敏感的自动分析会在这里报出漏洞,因为他不去考虑第一句和第二句的先后执行顺序,导致了明显的误报。
再看下面的代码。作为静态分析工具,如果我们不关心条件语句(这将是我们下面将讨论的第二种敏感性,即路劲敏感),考虑语句的顺序,是否应该报出漏洞呢?
1 input = String.Empty;
2 shouldAllow = false;
3 if(shouldAllow)
4 {
5 input = GetUserInput();
6 }
7 else
8 {
9 input = String.Empty;
10 }
11 Execute(input);
流敏感可以区分变量在不同执行阶段的状态,从而更准确地检测潜在问题。流敏感分析可以帮助检测变量使用前是否已初始化,以避免使用未初始化变量的问题。在常见的代码分析实践中,对于C/C++指向分析采用流敏感分析会极大提高检测精度,但效率会显著下降,这就需要优秀的算法能力。
路径敏感性是在分析过程中考虑条件语句的影响。下面的代码没有执行路径将污染源从GetUserInput传递到Execute。因为第一个if块包含一个语句,确保a < b不会在第二个if条件语句中成立。
1 a = 3; b = 5;
2 input = String.Empty;
3 if (a < b)
4 {
5 b = a – 1;
6 input = GetUserInput();
7 }
8 else
9 {
10 input = String.Empty;
11 }
12 if (a < b)
13 Execute(input);
因此,如果一个静态分析工具在这里报出该漏洞,那就是误报,因为它没有考虑路径敏感性。路径敏感分析可以提高静态分析的精度,尤其是在处理条件判断和循环结构时。通过分析所有可能的执行路径,路径敏感分析可以准确地检测潜在问题,避免误报和漏报。路径敏感分析的缺点是计算复杂度较高,因为需要分析所有可能的执行路径。在实际应用中,可能需要采用一些优化策略来降低计算复杂度,例如路径合并和路径抽象等。
这个简单的代码块让我们来看看上下文敏感性。它从GetUserInput方法获取一个不可信的输入,然后通过使用不同的参数(input和String.Empty)调用相同的Identity方法生成两个独立的变量output1和output2。Identity方法所做的只是返回它接收到的参数,没有进行任何修改。
很容易看出output1是被污染的,而output2则没有。因此,在具有output2作为参数的危险Execute方法中不存在安全问题。
1 input = GetUserInput();
2 string output1 = Identity(input);
3 string output2 = Identity(String.Empty);
4 Execute(output2);
5 string Identity(string id)
6 {
7 return id;
8 }
如果自动化分析具有上下文敏感性,它可以将Identity的两个调用位置视为具有两个不同的输入(两个不同的上下文)的不同调用,这种区分将防止分析工具在此处发生误报。否则,分析工具将认为在代码块中只有一个Identity函数调用,输出output2也可能来自于output1和output2,即包含受污染值。
对于静态分析来说,区分上下文的计算成本非常高,因为一个函数有多个调用和返回值,静态分析工具必须按不同的“调用、返回”对来区分不同的上下文。上下文敏感分析的缺点是计算复杂度
较高,因为需要分析函数在不同调用点的行为。在实际应用中,可能需要采用一些优化策略来降低计算复杂度,例如使用摘要或者缓存技术。
静态代码分析中的另一个敏感性是域敏感性。为了增加精度,能够单独考虑对象的不同字段变得非常重要。
让我们看看下面的代码。问题是否存在漏洞。答案很简单。没有。因为传递给执行方法的foo对象的字段不是污染的。虽然它是相同的对象,但ParamA字段是污染的,而ParamB字段不是。
1 Foo foo = new Foo();
2 foo.ParamA = GetUserInput();
3 foo.ParamB = String.Empty;
4 Execute(foo.ParamB);
5 class Foo
6 {
7 public string ParamA;
8 public string ParamB;
9 }
这种区分被称为域敏感性,并且易于理解。对于数据流分析,跟踪所有这些不同的成员,考虑所有嵌套对象会显著降低分析效率。但显然,它也增加了报告的警报的准确性。
相同的思想在数组中也同样适用,如下述代码片段。域敏感分析的缺点是计算复杂度较高,因为需要分析对象的属性赋值和访问情况。在实际应用中,可能需要采用一些优化策略来降低计算复杂度,例如使用摘要或者逃逸分析技术。
1 string [] myArray = new string[len];
2 myArray[0] = GetUserInput();
3 myArray[1] = String.Empty;
4 Execute(myArray[1]);
对象敏感分析,关注程序中对象的创建、使用和属性,以及对象之间的交互。该分析方法可以提高代码分析的准确性,特别是在处理面向对象编程(OOP)的情况下。对象敏感分析的主要特点是它将分析过程与特定的对象实例关联起来。这意味着分析不仅关注程序的全局结构和行为,还关注特定对象实例的局部行为。这使得对象敏感分析能够识别和跟踪对象的状态和属性,从而在许多情况下提高分析的精度。对象敏感分析的一种常见方法是通过k-对象敏感性(k-object sensitivity)来实现。k-对象敏感性是一种上下文敏感性方法,它将对象的创建点(allocation site)作为上下文信息,并根据这些信息来区分不同的对象实例。k-对象敏感性中的k表示用于构建上下文的对象创建点的数量。例如,当k=1时,仅考虑最近的一个对象创建点;当k=2时,考虑最近的两个对象创建点,以此类推。
下面的代码包含两个Foo类的实例;foo1和foo2。其中一个包含污染值,而另一个没有。由于Execute方法使用了没有污染值的foo2的字段,因此我们在这里没有漏洞。
1 Foo foo1 = new Foo();
2 foo1.ParamA = GetUserInput();
3 Foo foo2 = new Foo();
4 foo2.ParamA = String.Empty;
5 Execute(foo2.ParamA);
6 class Foo
7 {
8 public string ParamA;
9 }
对象敏感分析特别适用于处理面向对象的程序,因为它关注对象的创建、使用和属性。由于需要跟踪对象的创建和使用,对象敏感分析可能导致计算复杂度增加,特别是对于大型程序。由于计算复杂度较高,对象敏感分析可能需要较长的分析时间。在实际应用中,可能需要采用优化策略以平衡精度和效率。
结论
选择静态代码分析工具时,精度非常重要。精度越高,误漏报率越低,敏感程度就越高。然而,实现高精度的分析工具需要耗费大量的时间和资源,对算法要求很高,这也是大多数国内公司在短时间内很难写出高精度引擎的重要原因,既没有人才积累也没有技术积累。在判断静态工具优劣的时,不能简单地以拥有多少个检测器,是否支持某标准或者支持集成的方式,来评价静态代码分析工具的好坏,因为这些都是短时间内很容易实现的,静态分析工具难点在于引擎支持的敏感度能否和有限的资源达到一种平衡。用户需要根据需求来平衡工具的精度和执行时间的成本,以选择最合适的工具。同时,开发人员和安全专家需要了解不同类型的敏感性,例如流敏感性、路径敏感性、上下文敏感性、域敏感性和对象敏感性,以了解何时应使用哪种类型的分析来提高工具的精度。做到所有维度的敏感,就可以做到无误漏报,但理论上这是不可能的,全部敏感的算法复杂度太高以致在有限的资源和时间内,无法完成,所以大部分工具选择折中的方案(例如某F工具选择在C/C++上不做路径敏感,某C工具不考虑全局变量导致缺陷)。即为了保证效率,在某些情况上选择低敏感度,某些情况选择高敏感度。
鸿渐SAST工具采用基于程序复杂度进行自动折中的多敏感性分析技术–值依赖分析技术,如流敏感性、路径敏感性、上下文敏感性、域敏感性和对象敏感性,能够根据当前代码的复杂度和机器当前的资源,采用智能化的方法自动进行分析敏感性折中,也就是对于结构简单程序采用高敏感分析,对于当前资源少的机器采用低敏感度的分析以保证效率和精度的最优平衡。
敏感性问题对于当下流行的AI解决方案GPT是否是个挑战,我们在下一篇文章中继续分析
➦扫码关注我们
文章来源:鸿渐科技