华为云开发者社区

华为云开发者社区

本文分享自华为云社区《静态分析工具的评估测试》,作者: Uncle_Tom。

1. 垂直极限

还是先说故事。那是 2014 年参加的一个测试驱动(TDD)的培训,培训是 TDD 推广的志愿者组织的,在一个咖啡馆里搞的,周末两天的免费培训。

培训过程中的一张图和一个视频让我至今记忆尤新。

1.1. 一张图

万字带你熟悉静态分析工具的评估测试-LMLPHP

上面的两个图(原来培训的那个图找不到了,自己随手涂鸦了一下)。

左边是经过完整的系统的测试的软件产品,每个节点都通过测试,这样一层层的搭建起来的系统。看着就坚实可靠。

右边的测试则是随意的,很多地方都缺失了。任何一个风吹草动,一个异常都可能造成整个大厦倾覆。

不用说大家立刻就可以看懂,那个软件产品更可靠,更让人放心。

1.2. 一段影像

视频是电影《垂直极限》中开头的一段。虽然过去 20 多年了,但这部电影仍然被奉做山难电影的经典之作,有兴趣的朋友还是可以找来看一看的。

万字带你熟悉静态分析工具的评估测试-LMLPHP

一个风和日丽的日子里,老爸带着儿子、女儿在一个高耸、陡峭的山峰上攀岩。他们已经来到了山峰的一半,在他们的上面个还有另一群攀岩爱好者。

老爸一边整理着自己的安全栓(攀岩过程中打在岩石中,然后利用登山绳固定自己的安全扣),一边对儿子说:“检查下你妹妹的安全栓。”

儿子看向下面的妹妹,妹妹说:“告诉他我们已经不是小孩子了。”

儿子打趣的对老爸说:“爸,她还需要一个安全栓。”

爸爸看向下面的兄妹两人说:“万一出事,那样可撑不住。安妮,我不管你有多老练,聪明的攀岩者都会做好安全措施,两个栓才安全,三个更好。在岩壁上再放个安全栓,我们才继续攀。”

女儿生气的对爸爸说:“爸,你别听哥哥的,他在开玩笑,我放了三个安全栓。”

电影总是这样,一段舒缓的节奏后面,总会有段紧张的让人喘不过气来的场面。不出意外,意外就会发生。

话音未落,先是一个登山包从山上面坠落下来,快速的经过三人。上面的菜鸟在登山的过程中背包意外脱落,帮着的圣山包的登山绳造成一连串的连锁反应,两个登山者也不幸地被连带着坠落下去。老爸、儿子也被着一连串的变故拖累到挂在了半空中,女儿的三个安全栓无法承担三个人的重量,一个、两个先后崩飞,只靠着最后的一个安全栓勉强的维持着女儿,以及挂在半空的老爸和儿子。最后在老爸的要求下,儿子不得不割断了绳索,女儿和儿子看着老爸坠了下去。

看到这里,大家都不禁须臾不已,再看看上面的图,对系统的完整的测试有了更深刻的认识。

1.3. 思考

在软件的开发过程中,每一个节点的测试都是在为系统增加一个安全栓。由这样一层层搭建起来的系统,整个系统才是安全、可靠的。否则任何一个意外都可能将整个系统带飞。做工具和做单点能力验证不同,需要更多的测试节点来保障工具的稳定性和可靠性。这个从学院里出来的大部分朋友还没有意识到这个问题,加之项目的管理的问题,就会把各种坑坑洼洼带到生产中,使后期维护陷入无尽的深渊。

大家都喜欢做 0 到 1 的事情,出彩啊。很少有人愿意做为 0 到 1 填坑的基础工作,但一个工具要生存,能够赢得最终的胜利,需要无数的这些幕后英雄。就像长津湖战役一样,指挥很重要,但更多的是需要后面无数的无名英雄,那些人才是撑起整个胜利的英雄。

那么对于静态分析工具,我们该如何构建一个稳定、可靠的静态分析工具?如何评价一个静态分析工具的检查能力?

自从程序的诞生,程序分析便紧随其后,人们视图通过一个程序来分析编制的程序,保证编制程序运行结果的正确性。尽管后面的莱斯定理给出了这个问题的“不可判定性”,但并不会妨碍程序分析在这方面的卓越表现。

万字带你熟悉静态分析工具的评估测试-LMLPHP

尽管现在大模型能够帮助我们生成程序代码和对已有程序进行问题检查,这似乎可以绕过我们一值以来使用的模式匹配的检查方式,可以简化模式的提炼,和根据模式再编写检查规则。 但如何保障生成代码的安全性和可靠性,以及如何评估大模型的能力,仍然将是人工智能在今后很长一段时间需要解决的问题。

通过下面对三组测试用例的分析,希望能够给静态分析工具的测试和评估给大家一个指导性的启发。

2. Juliet Java、C/CPP 测试用例集

2.1. 创建背景

2005 年美国国家标准与技术研究院(National Institute of Standards and Technology (NIST)),简称NIST,下属的软件质量组,成立了软件保障指标和工具评估项目(Software Assurance Metrics And Tool Evaluation (SAMATE)),简称SAMATE项目。项目组的主要目的是通过开发支持软件工具评估的方法、衡量工具和技术的有效性以及识别工具和方法中的差距来改善软件保障,主要工作包括定义错误类,收集具有已知错误的程序语料库,以及更好地了解工具的有效性。

围绕这个目标,软件保障指标和工具评估项目(SAMATE)建立了两个子项目:

  • 软件保障参考数据集(Software Assurance Reference Dataset (SARD)),简称SARD,用于收集整理C、C++、Java、PHP 和 C#针对软件弱点的测试用例集;

  • 软件工具博览会(Static Analysis Tool Exposition (SATE)),简称SATE,用于工具制造商对用例进行测试和工具研讨。截至 2021 年,已经举办了六场 SATE 活动。

2.2. Juliet 用例集简介

Juliet 测试用例集就是软件保障参考数据集(SARD)下的一个是用于检测 C/C++ 和 Java 程序已知缺陷的集合。

2010 年 12 月 1.0版本。Juliet 测试用例集最早的 1.0 发布于2010 年 12 月,由软件保障指标和工具评估项目(SAMATE)的开发团队完成,名字选取了当时软件保障参考数据集(SARD)的第十个贡献者:国际无线电(International Radiotelephony)的字母表中的第十个单词 “Juliet” 而的得名。

2011 年 1.1 - 1.1.1 版本。Juliet 1.1 版本 是 Juliet 的开发团队根据多个因素为选定的缺陷创建了测试用例,包括团队的经验、缺陷的重要性或严重性以及其出现的频率。测试用例涵盖了 2011 年 CWE/SAN TOP 25 个最危险的编程错误中的 14 个。剩下的 11 个缺陷是设计问题,例如 CWE-862 授权机制缺失 和 CWE-250 带着不必要的权限执行,这些问题不适合用静态分析的方式来检测,所以未包含在测试用例中。

2012 年 1.2 版本。2012 年的 Juliet 1.2 版本。基本形成了现在使用版本。是我们现在使用最多的版本,程序分析、自动修复、深度学习的论文基本上都使用了这里面的用例,用于证明理论、工具的有效性。

2017 年 1.3 版本。只对1.2 版本的个别错误进行了修正。

  • Juliet C/C++ 涵盖 118 个 CWE 问题,1689 个场景,用例数: 181,140, 其中正例:117,041, 反例:64,099。
  • Juliet Java 涵盖 112 个 CWE 问题,933 个场景,用例数: 96,537, 其中正例:67,656, 反例:28,881。
2022 年 1.3.1 版本。增加了SARIF 的支持,试图通过SARIF 精确的给出告警的位置信息,但对数据流的支持的并不好,不能完全依靠现有的SARIF版本。

注:关于 SARIF请参考:

2.3. Juliet 用例构造的特点

2.3.1. 统一明确的命名方式

测试用例使用CWE作为命名和组织的基础。测试用例力求对目标缺陷使用最具体的CWE条目。每个测试用例文件与一个CWE条目相关联。

2.3.1.1. 测试用例文件名命名规范

测试用例名由四个元素的组合唯一标识:

例如:

单文件用例:

CWE190_Integer_Overflow__byte_console_readLine_add_01.java
  • CWE190: CWE 的编号,可以在CWE网站上查询到这个编号对应的问题:https://cwe.mitre.org/data/definitions/190.html;
  • Integer_Overflow:CWE190 描述的简写,整型溢出。CWE190 的完整描述是:整型溢出或越界折返;
  • byte_console_readLine_add: 场景描述:通过readLine函数从控制界面读取字节流;
  • 01:代表基础用例。

多文件用例, 下面的一组文件构成了一个用例:

  • CWE190_Integer_Overflow__byte_console_readLine_add_81_bad.java
  • CWE190_Integer_Overflow__byte_console_readLine_add_81_base.java
  • CWE190_Integer_Overflow__byte_console_readLine_add_81_goodB2G.java
  • CWE190_Integer_Overflow__byte_console_readLine_add_81_goodG2B.java
  • CWE190_Integer_Overflow__byte_console_readLine_add_81a.java
    • CWE190: CWE 的编号,可以在CWE网站上查询到这个编号对应的问题:https://cwe.mitre.org/data/definitions/190.html;
    • Integer_Overflow:CWE190 描述的简写,整型溢出。CWE190 的完整描述是:整型溢出或越界折返;
    • byte_console_readLine_add: 场景描述:通过readLine函数从控制界面读取字节流;
    • 81: 数据流案例,案例将参数传递给通过引用调用的抽象方法;
    • _bad: 表示这是个反例的文件;
    • _base: 表示这个是用例的主文件;
    • _goodB2G: good 表示这是个正例的文件,同时 B2G 表示污点(Bad)会经过清洗到达(2(to))爆发点,但由于做了清洗,变成了无污染的,不会引起问题(Good);
    • _goodG2B: good 表示这是个正例的文件,同时 G2B 表示污点经过清洗变成无污染(Good)到达(2(to))爆发点(Bad),但由于是无污染的数据,也不会引起问题;
    • a: 表示这是个辅助文件。

2.3.1.2. 测试用例函数名命名规范

有问题(反例)函数,通常以bad命名, 并可使用正则表达式匹配:^bad$;

没问题(正例)的函数,通常以good命名, 并可使用正则表达式匹配:^good$;

对于用例中存在多个正例的函数,有以下三种命名方式,可使用正则表达式匹配:^good(\d+|G2B\d*|B2G\d*)$;
  • 默认或通用的方式,采用例如: good1()、good2()、good3()的命名方式;
  • 当一个好的源将安全数据传递到一个潜在的坏接收器时,采用例如:goodG2B()、goodG2B1()、goodG2B2()、goodG2B3() 的方式命名;
  • 当不良源将不安全或潜在不安全的数据传递给良好源时,采用例如:goodB2G()、goodB2G1()、goodB2G2()、goodB2G3()的方式命名;
  • 对于数据流用例还遵守以下命名方式:有问题的污染源,采用正则表达式:^badSource$;有问题的爆发点,采用正则表达式:^badSink$;无问题的污染源,采用正则表达式:^good(G2B\d*|B2G\d*)?Source$;无问题的爆发点,采用正则表达式:^good(G2B\d*|B2G\d*)?Sink$

2.3.1.3. 命名规则在测试中带来的好处

从文件名就能判断出检测问题、场景、适用的案例类型,有的还可以知道文件是正例、还是反例;

从发生问题的函数名就可以知道,所处的函数是正例,还是反例;

通常检测工具都会给出问题所在的文件和函数,这样在查看问题的时候能够立刻判断出检测结果的有效性;

严格的命名方式,也便于使用程序自动化的检查结果做出快速的判断。

2.3.2. 每个问题给出发生问题的可能场景

Juliet 还为每个问题(CWE)枚举了一些发生场景,一些场景还使用模板结合控制流和数据流进行了枚举,从而测试更加广泛的问题发生的可能性,以提高问题的覆盖率。

Juliet 一共采用了三类模板:

  • 控制流模板(point-flaw)
  • 单一污点源和爆发点模板(source-sink);
  • 多污点源和爆发点模板(sources-sinks);

Juliet Java

  • Juliet Java 一共 122 个 CWE, 覆盖 933 个场景。
  • 用模板生成的场景 823 个,这包括:point-flaw: 92 个场景;sources-sink: 197 个场景;sources-sinks: 534 个场景;
  • 单一场景 110 个。

Juliet C/C++

  • Juliet C/C++ 一共 118 个 CWE, 覆盖 1689 个场景。
  • 用模板生成的场景 1509 个,这包括:

而单一场景 180 个。

2.3.3. 每个场景使用不同的案例进行覆盖

Juliet 用例还真对每个场景结合控制流和数据流进行了枚举,以达到每个场景在不同代码条件下的检测能力的覆盖。

从下表我们看到类型基本上涵盖了编程语言通常所需要的基本语法,例如条件判断(if),条件分支(swith),循环(while,for),函数间和程序间不同层数的调用,以及不同类型的参数传递方式。

案例类型明细

2.3.4. 缺点

Juliet 可以说是堪称完美的测试用例集,但随着时间的推移,编程语言的迭代,它也显现出一些缺点:

  • 一些用例的写法开始变得老旧,可能不能胜任新的场景;
  • 没有对语言的所有语法特点做覆盖,特别是新的一些语法类型;比如:lamda 表达式;
  • 用例的设计存在大量的重叠,特别是对数据流的检测,每个场景都使用了相同的模板,产生了大量的用例。其实对于数据流的处理,完全可以分成:污染源、污染传递、污染清理、以及爆发四个不同的维度进行分别测试,这样可以大大提升测试效率。

3. OwaspBenchmark 测试用例集

3.1. 用例背景

OWASP(Open Worldwide Application Security Project (OWASP)) 基金会致力于通过其社区主导的开源软件项目、全球数百个分会、数万名成员以及举办当地和全球会议来提高软件的安全性。

3.2. 用例简介

OWASP Benchmark Project 是一个 Java 测试套件,旨在评估自动化软件漏洞检测工具的准确性、覆盖率和速度。

  • 1.0 版本于 2015年4月15日发布,共有 20983 个测试用例。
  • 1.1 版本于 2015年5月23日发布。1.1版本在前一版本的基础上进行了改进,确保每个漏洞区域都有真阳性和假阳性。
  • 1.2 版本于 2016年6月5日首次发布(1.2测试版于2015年8月15日发布)。自那以后,v1.2版本一直在不断调整。1.2 版本将测试用例数量控制在小于3000个测试用例,以便快速得到测试结果。

3.3. 用例设计和组成方式

下面以 1.2 版本为例进行说明。

3.3.1. 用例问题的覆盖

从下面这个表可以看出 Benchmark 更多的注重覆盖 Web 类的问题的检查,同时重点覆盖了 OWASP TOP 10 中的主要能够通过静态检查工具检查的问题。关于 OWASP TOP 10 可以参考:CWE 4.6 和 OWASP TOP10(2021)

Benchmark 用例集主要以加密问题,以及注入类问题为主,这也巧合的与静态检查技术相互匹配。加密问题多用抽象语法树(AST)的遍历来返现加密函数,并对其做出判断。而注入类问题多用数据流的污点分析技术来追踪外部输入是否会对爆发点形成可达的路径。有关污点分析技术,可以参考:使用污点分析检查log4j问题

3.3.2. 问题场景的覆盖

Benchmark 用例对每个问题采用: 场景枚举 + 组合的方式完成用例的设计,并通过此方法形成问题的覆盖。

这里以我们熟悉的:CWE89 SQL注入问题来说明这种用例的设计方式。用例集中CWE 89 SQL注入问题一共有 232 个正例,272 个反例,共计504 个用例。

因为 SQL 是注入是通过外部不可信数据,传播到 SQL 脚本执行的位置而导致的安全问题。这个外部数据传播的过程可以分为:

  • 接收数据

用例使用了我们常用的从 http 请求中得到外部数据,然后将数据以不同方法存入不同类型的字段。用例中列举了下面 9 种不同的方法。例如放入:字符串、枚举、数组等。

万字带你熟悉静态分析工具的评估测试-LMLPHP

  • 数据传递

用例接收到数据后,使用不同的传递方式,向程序内传递,并对信息采用不同的操作方式进行加工。用例中列举了下面 10 种不同的方法。例如:通过 创建一个新类然后调用函数传递、if 条件表达式、内部类等。

万字带你熟悉静态分析工具的评估测试-LMLPHP

  • 问题爆发

最终数据会拼装成 SQL 语句,并通过不同的调用方式执行。用例中列举了 3 类,28 种不同的执行方式。

万字带你熟悉静态分析工具的评估测试-LMLPHP

  • 场景组合

得到上面三种基本节点后,通过组合的方式形成用例。下表列举了 CWE89 SQL注入 272个反例组合的场景:

万字带你熟悉静态分析工具的评估测试-LMLPHP

3.3.3. OwaspBenchmark 用例集的缺点

OwaspBenchmark 应为涵盖了Web应用的主要安全问题,使用例集基本上成为了Web 应用安全测试的基本用例集。但它也存在一些缺点。

  • 用例名只采用了简单的编号方式,从用例看不出测试目标:用例反应的问题、场景、正例、反例这些基本信息,而不得不给每个用例加了一个xml文件来说明这些用例的基础信息。
  • 用例缺少场景的描述,工具测试后,无法得到覆盖场景和非覆盖场景的统计信息,只知道覆盖率。具体哪些场景缺失,要一个个用例去自己分析。
  • 缺少检测语言语法级别的场景的覆盖,例如lamda表达式,工具在数据流的分析过程中,任何一个语法的不适配都会导致分析中断。

4. Alipay 测试用例集

4.1. 用例背景

针对xAST领域缺乏有效衡量技术能力标准的业界痛点,蚂蚁安全团队联合蚂蚁程序分析团队、浙江大学网络空间安全学院的20余位专家学者,共同设计了xAST评价体系及其测试样本套件Benchmark,致力于成为应用安全测试工具的“度量衡”。

  • 目标:打造具备行业共识的xAST能力评价体系技术标准

  • 价值:衡量xAST产品技术能力,指引xAST技术发展方向,辅助企业产品选型

4.2. 用例设计和组成方式

用例设计的核心思想是:分层设计,降低评价复杂度。

从底层到上层分成引擎能力、规则能力和产品化能力这三层。对这三层分别设计评价体系和测试样本,既降低了每一层评价的复杂度,又使测试结果可以直接反映问题出在哪一层。

看的出,用例集的设计者试图希望结合Juliet、Owaspbenchmark 的优点,在形成一种分层的评估测试方式。来完善前面两个用例集在语法层面的不足。

目前用例集只推出了一个雏形,还在建设中。

5. 理想的测试用例集

最后再来总结下,理想的测试用例集应该是怎样的。

  • 能够从用例的命名上,清楚的反映:测试问题、场景、正例、反例;或从目标函数上明确知道在这个函数内的告警是正确的告警,还是误报;
  • 能够覆盖业界主要的安全问题,例如:CWE TOP 25、OWASP TOP 10等常见的问题;
  • 能够覆盖检测语言的主要语法和语言的主要使用方式;
  • 能够有一定的场景枚举和组合,以增加测试用例的复杂度,这有点类似 fuzzy 测试了。

写在最后,测试用例集的结果,只能反映一个工具的基础能力,并不能取代通过实际的工程来打磨检查工具。

一般的程序员只需要在问题和实现上建立一条通道就好,但程序分析的程序员却需要考虑各种程序员实现问题的可能性。

6. 参考

点击关注,第一时间了解华为云新鲜技术~

12-27 18:23