书名:抢读版-代码审计——C/C++实践
ISBN:978-7-115-60104-9
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 曹向志 李 炼 陈能技 等
责任编辑 谢晓芳
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e60104”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。
本书旨在介绍代码审计及缺陷剖析的相关知识。本书首先介绍了代码检测技术和代码检测工具;然后讲述了C/C++安全标准,展示了与标准不兼容的案例和兼容的案例,并对案例对应的知识点进行讲解,以帮助开发人员、评测人员理解和运用标准;接着分析了 C/C++语言源代码漏洞测试,展示了包含安全漏洞的案例,以及修复安全漏洞的案例;最后介绍了常见运行时缺陷,主要基于C/C++案例代码进行剖析,这些运行时缺陷是在对C/C++项目进行代码检测和代码审计时需要重点关注的。
本书不仅适合开发人员、测试人员阅读,还适合作为相关培训机构的教材。
感谢微信时代,让我们有机会进入各个行业群,结识各行业和高校的老师们。我有幸加入北京大学软件工程国家工程研究中心的马森博士带领的北大库博团队,接触代码安全领域,拓展了该领域的技术和知识,了解了该领域的国内外工具,尤其是检测工具中的各种检测器,这些检测器可用于发现安全漏洞及运行时缺陷。
在惠普公司工作期间,在安全解决方案中,常用的安全工具有Fortify SCA工具,但是当时我并没有特别关注这款工具。在北京大学实验室,我接触了国外的代码检测工具Checkmarx、Klocwork、Coverity等,以及国内的Pinpoint、Cobot、Wukong等工具。在代码安全领域中,跟着马森博士、张世琨主任,我学到了很多技术和知识,这拓展了我的知识面。我也整理了上百页的学习资料,在这些资料的基础上,编写了本书。本书中的很多案例来自网络资料,非常感谢这些博主无私的奉献,没有他们的资料,我会花费更多的时间编写案例。后来我加入中国科学院计算技术研究所(简称“中科院计算所”)的中科天齐团队。李炼博士和高琳博士原来在澳大利亚甲骨文实验室工作,在代码安全领域有很深的造诣,后被中国科学院聘请,在国内带领中科院计算所的编译团队和代码安全团队研发Wukong产品。
代码安全在国外已经比较成熟,随着国内对代码安全越来越重视,国内代码检测工具厂商也越来越重视与代码安全相关的工具的研发。作为具有专利技术、自主可控的工具,这些工具逐渐成熟,部分技术指标已经具有了可以与国外工具媲美的程度,但是还有一段路要走。
马森博士让我有机会接触到代码安全领域,我学到了很多知识。本书引用了马森的论文“基于依赖值分析的C程序缺陷静态分析方法”。感谢北京大学实验室的各位同事,在工作期间我得到了他们的支持。
感谢中科天齐的老师在工作期间对我的指导,这拓展了我在代码安全领域的视野。
感谢赵智海、黄金菊对本书中关于GJB 8114的案例进行了校对、修正,并为案例添加了相应的技术知识点分析。
感谢我的妻子和孩子,我在编著本书时前后用了一年多的时间,她们承担了更多的家务,给了我很大的支持。本书的出版历经波折,终于面世,同时感谢为此付出努力的各位编辑。
曹向志
近些年来,随着社会的发展和科技的进步,在军事、航天、航空、能源、金融、公共安全等众多领域,国家大型关键基础设施正在向着更高的水平跃升,呈现出超大型化、复杂化、安全化的特征。软件在系统中起到核心的作用。随着软件规模的日益增大,代码数量由几万行,发展到现在的几十万行,甚至几百万行的规模,系统的逻辑结构越来越复杂,仅靠人工基本上无法满足代码检测对时效、成本等各方面的要求。
代码审计的目的是通过对源代码进行审查,找出并修复代码中各种可能影响系统安全的潜在风险,通过提高代码自身的质量,达到降低系统风险的目的。由于有些系统的代码量大,因此代码审计人员一般会借助静态分析类工具,对代码进行自动检测,并生成代码安全审计报告。
如果代码检测由人工完成,则代码审计质量主要由人的技能和经验决定,不可复用和传承。当前,由于软件安全问题越来越突出,国家对软件安全问题也越来越重视,国家或行业协会出台了很多与软件安全相关的标准或指南,示例如下。
● 《航天型号软件C语言安全子集》(GJB 5369—2005)。
● 《C/C++语言编程安全子集》(GJB 8114—2013)。
● 《C/C++语言源代码缺陷控制与测试规范》(SJ/T 11682—2017)。
● 《Java语言源代码缺陷控制与测试指南》(SJ/T 11683—2017)。
● 《C/C++语言源代码漏洞测试规范》(GB/T 34943—2017)。
● 《Java语言源代码漏洞测试规范》(GB/T 34944—2017)。
● 《C#语言源代码漏洞测试规范》(GB/T 34946—2017)等。
2019年5月13日,国家标准新闻发布会正式发布与网络安全等级保护制度2.0标准相关的《信息安全技术 网络安全等级保护基本要求》《信息安全技术 网络安全等级保护测评要求》《信息安全技术 网络安全等级保护安全设计技术要求》等国家标准。在此背景下,各企业对软件质量的要求越来越高,与软件测试相关的工作尽量“左移”,注重提升代码的交付质量。在开发阶段,测试人员要对研发工程师每天提交的代码进行检测,及时发现和修复缺陷。尤其是很多企业正在建设DevOps或DevSecOps,其中代码扫描是整个工具链中的重要一环,需要自动化源代码安全检测工具对代码在不同阶段按照不同的安全策略进行扫描。
软件测试技术的不断发展,以及对软件缺陷模式和安全漏洞模式的研究,促进了软件静态分析技术的发展。对程序代码进行静态分析能够发现很多语义缺陷、运行时缺陷和安全漏洞。静态测试在软件测试中的重要性越来越突出,加上代码规模的增加,伴随而来的是越来越复杂的代码结构。完全依靠人工审查代码,成为制约研发效率提升的关键因素。采用代码自动化分析技术,能够提升开发和测试效率,降低程序风险,同时降低研发成本。强有力的代码审查工具能够在非运行状态下全面审查代码中的安全风险。
本书全面介绍了代码审计及缺陷剖析的相关知识,既包括代码检测的技术,也包括代码检测的工具。为了让读者学以致用,本书在讲解相关技术和工具时,结合实际案例代码讲解,以便读者可以将所学知识应用到实际项目中。
本书适合以下人员阅读。
● 代码检测人员、开发人员、测试人员,本书介绍的案例与工具可以帮助他们验证所开发的程序是否有缺陷和安全漏洞。
● 软件开发经理、软件采购商,本书不仅可以帮助他们建立适合本企业的专门安全编码规范,还可以作为其内部的安全培训材料等。
● 大专院校和培训机构的学生,本书可以帮助他们提升程序开发水平。
本书由异步社区出品,社区(https://www.epubit.com/)为您提供后续服务。
您还可以扫码二维码, 关注【异步社区】微信公众号,回复“e60104”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“发表勘误”,输入勘误信息,单击“提交勘误”按钮即可,如下图所示。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区投稿(直接访问www.epubit.com/contribute即可)。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。
异步社区
微信服务号
随着国际形势越来越复杂,国家之间的竞争已由物理空间逐渐扩大到网络空间。据统计,公开的已知安全漏洞已超过20万个,还有大量未公开的安全漏洞,这些安全漏洞的价格往往从几万美元到几十万美元。Google、Microsoft等公司一般用重金奖励发现其产品中安全漏洞的人员。国内外每年举办多场CTF(Capture The Flag,夺旗)大赛和各种安全漏洞挖掘大赛。例如,2018年,阿里巴巴集团组织了软件供应链安全大赛。国内对网络安全和软件安全越来越重视,鼓励发展自主、可控的国产化软件。当然,国产化软件更需要关注安全,尤其是国产软件可能使用大量的第三方框架、开源组件、第三方库等。这些第三方软件中往往被当作安全软件集中到所研发的系统中,但是这些软件也存在大量的安全漏洞。
在检测手段尚未普及的情况下,这些安全漏洞为国产化软件带来了潜在的风险。网络安全主要是由软件安全决定的,而软件安全主要是由代码安全决定的,所以要真正做到网络安全,须先保证代码安全,进而保证软件安全,这就是内生安全的概念。
网络安全等级保护制度2.0标准于2019年12月1日正式生效。其中重要一点是将可信验证列入各级别和各环节的主要功能要求。可信1.0关注主机,可信2.0关注PC,可信3.0关注网络。在移动管理、生产管理层控制方面,提出了对恶意代码检测的管理要求;在网络和通信安全以及设备和计算安全方面,强调对“未知攻击”的检测分析要求、对恶意代码的防范要求、对可信计算基数防范恶意代码的控制要求;IaaS、SaaS安全建设管理强化了对自行软件开发和服务供应商的要求,包括安全性测试、恶意代码检测的管理要求。该标准要求安装防恶意代码软件,并定期进行恶意代码扫描,及时更新防恶意代码软件版本和恶意代码库。代码审计越来越受到重视。
在源代码的安全审计中,人的因素对审计效果影响很大。该标准的主旨在于尽可能降低人的因素对审计效果的影响,提高工作质量,真正帮客户找到问题并解决问题。
在代码检测工作中,我们常常要借助一些自动化的静态检测工具来解决部分显式问题,而人工审计更注重弥补工具的不足,需要对工具检测的结果进行审核,这在一定程度上也减轻了审计人员的工作量。认证、授权、输入/输出验证、会话管理、错误处理、加密、工作环境、日志审计及业务等往往很难通过自动化工具审计,更多需要人工审计完成。
在代码检测过程中,审计人员首先需要根据用户提供的测试环境、设计文档、使用手册,对应用系统的业务功能进行学习,对业务数据流进行梳理,检查关键环节是否进行了业务安全控制,检测完成后,需要从业务角度对威胁进行分析并归类。
代码审计是指依据CVE(Common Vulnerabilities & Exposures,通用漏洞和风险)、开放Web应用程序安全项目(Open Web Application Security Project,OWASP)Web漏洞,以及设备、软件厂商公布的漏洞库,结合专业源代码扫描工具对以各种程序语言编写的源代码进行安全审计。代码审计能够为客户提供安全编码规范咨询、源代码安全现状测评、源代码安全漏洞定位、漏洞风险分析、修改建议等一系列服务。
要做代码审计,相关人员需要具备一定的技术知识;具有代码开发能力,能够熟练阅读各种开发语言的代码,对于Java语言项目,还要了解各种开源框架、商业框架和自定义框架;理解安全漏洞、运行时缺陷的代码层面上的原理。在拿到审计任务时,相关人员需要了解本次审计的质量要求和目标。阅读系统的配置文件,例如,梳理Application-context.xml、struts.xml、web.xml等,这在一定程度上有助于理解系统的组成。从业务输入开始,分析业务数据处理流程,有助于分析污点的整个轨迹,并根据每类安全漏洞的特征,直接定位代码和分析代码。
对C/C++语言源代码进行审计时,审计人员最好先画出函数调用图、每个函数的流程图或控制流图。从主函数开始分析,分析代码中的缺陷,检查程序源代码是否存在安全隐患,或者编码是否有不符合安全标准规则的地方。借助自动化检测工具,绘制函数调用图和控制流图,辅助进行代码审计。C/C++在安全漏洞方面的问题相对较少,主要存在内存泄漏、数组越界、空指针解引用等运行时缺陷,这些缺陷在编译时是无法被编译器发现的,只有等程序执行一段时间后,才能暴露出来。审计人员最好事先通过检测工具,找到缺陷线索,然后根据缺陷线索深入分析代码。
代码审计的步骤如图1-1所示。
▲图1-1 代码审计的步骤
从上面的代码审计步骤可以看到,代码审计工作的关键环节如下。
(1)确定审计策略:主要考虑代码开发语言、架构、安全审计质量规则或出口规则、检测效率等。安全审计质量规则或出口规则需要考虑代码检测工具是否满足检测质量要求,也就是代码检测工具能否覆盖检测项,同时代码检测工具应该可弹性扩展。
(2)部署环境:主要考虑代码检测工具能否适合所有的物理环境,包括网络、服务器、工具兼容性等。
(3)工具扫描:主要考虑代码检测工具的技术指标,如工具支持的开发语言、检测精度、效率,是否支持迭检测、CI模式下的检测等。
(4)人工复核:主要考虑人工复核的工作量,检测结果是否容易复核。这就要求代码检测工具精度高,结果容易检查,误报少。
(5)审计结果和报告:主要考虑代码检测工具能否根据需要自动产生审计结果、审计报告,能否根据需要定制审计报告内容。审计结果和报告以中文显示,便于阅读。检测过程能够跟踪,回溯。
(6)出口规则:能够根据代码检测需要明确审计范围、审计安全漏洞的严重程度;检测完成,能够判断代码是否满足相应的出口规则,以便于快速决策。
(7)开发修复:主要考虑检测结果是否便于开发人员修复代码、能否快速定位安全漏洞、提示信息是否准确,甚至能否自动修复或给出修复检查代码。检测精度高,误报少,漏报少,这就要求代码检测工具比较成熟,且得到国际公认组织的认证、认可,符合国际、国内主流标准,同时对开发团队确认和修复代码可以提供培训与支持。
OWASP提供有关计算机和互联网应用程序的公正、实际、有成本效益的信息。其目的是协助个人、企业和机构发现并使用可信赖软件。
OWASP Top 10是针对开发人员和Web应用程序安全性的标准意识文档。它代表了对Web应用程序最严重的安全风险的广泛共识。被开发人员全球认可是迈向更安全编码的第一步。这是一个OWASP组织的一个重要项目。
公司应采用此文档并确保其Web应用程序将安全风险降至最低。使用OWASP Top 10可能是将组织内的软件开发文化转变为产生更安全代码的文化的第一步。
C/C++语言中与OWASP Top 10对应的安全漏洞相对于Java等语言中的少。
SANS(SysAdmin, Audit, Network, Security)研究所是美国一家信息安全培训与认证机构,CWE(Common Weakness Enumeration,通用缺陷列表)是MITRE公司维护的源代码缺陷列表。CWE/SANS在2009年首次联合发布了25个危险的编程错误(以下简称CWE Top 25),每年更新,最新的版本号是2019。
CWE Top 25由MITRE发布,是对可能导致严重软件安全事件的最广泛、最严重的安全漏洞的汇总之一。这些漏洞使黑客能够控制受影响的系统,窃取敏感数据并导致拒绝服务情况。CWE Top 25能够提示并帮助开发人员降低源代码缺陷带来的开发风险。
CWE Top 25是SANS研究所、MITRE和许多欧美软件安全专家合作的成果,综合了在SANS 20大攻击向量和CWE的开发中的经验,对这些编程错误给出了详细描述,并给出了权威性的指导以减少和避免这些错误,它们可能导致严重的安全漏洞。这些错误频繁发生,往往容易被攻击者发现且利用。这些错误之所以是危险的,是因为它们会经常让攻击者完全接管软件、窃取数据,或让软件系统停止工作。
CWE Top 25列表的主要目标是在软件发布之前,帮助程序员消除十分常见的错误,从源头上排除源代码缺陷。软件用户可以用同样的清单,获取更安全的软件。软件管理人员则可用它们来衡量软件的安全程度。
2020年发布的CWE Top 25如表1-1所示。
表1-1 2020年发布的CWE Top 25
序号 |
ID |
名称 |
---|---|---|
1 |
CWE-79 |
跨站点脚本攻击 |
2 |
CWE-787 |
越界写 |
3 |
CWE-20 |
输入验证不正确 |
4 |
CWE-125 |
越界读取 |
5 |
CWE-119 |
内存缓冲区范围内的操作限制不当 |
6 |
CWE-89 |
SQL注入 |
7 |
CWE-200 |
未经授权公开敏感信息 |
8 |
CWE-416 |
指针释放后使用 |
9 |
CWE-352 |
跨站请求伪造(Cross-Site Request Forgery,CSRF) |
10 |
CWE-78 |
OS命令注入 |
11 |
CWE-190 |
整数溢出或环绕 |
12 |
CWE-22 |
路径遍历 |
13 |
CWE-476 |
空指针解引用 |
14 |
CWE-287 |
身份验证不正确 |
15 |
CWE-434 |
不受限制地上传危险类型的文件 |
16 |
CWE-732 |
关键资源的权限分配不正确 |
17 |
CWE-94 |
对代码生成的控制不当(“代码注入”) |
18 |
CWE-522 |
凭据保护不足 |
19 |
CWE-611 |
XML外部实体参考的限制不当 |
20 |
CWE-798 |
硬编码凭证的使用 |
21 |
CWE-502 |
不可信数据的反序列化 |
22 |
CWE-269 |
权限管理不当 |
23 |
CWE-400 |
不受控制的资源消耗 |
24 |
CWE-306 |
缺少关键功能的验证 |
25 |
CWE-862 |
缺少授权 |
编码标准(ISO/IEC TS 17961)的目的是为静态分析器(包括静态分析工具和C语言编译器)建立一套基准要求,供希望诊断超出标准范围之外的不安全代码的供应商使用。所有规则均应通过静态分析强制执行。选择这些规则的标准是,实施这些规则的分析器必须能够有效发现安全的编码错误,而不会产生过多的误报。
迄今为止,静态分析在安全性上的应用已由不同的供应商以临时方式进行,从而导致对重大安全问题的覆盖范围不一致。ISO/IEC TS 17961列举了安全编码规则,并要求分析引擎根据规范诊断是否违反了这些规则。这些规则可以以与实现相关的方式进行扩展,从而为符合条件的客户提供最小的覆盖保证。
ISO/IEC TS 17961指定了使用C编程语言进行安全编码的规则,并包括每个规则的代码示例。不合规的代码示例演示了具有缺陷的语言结构,这些缺陷具有潜在的可利用的安全隐患。这些示例有望从符合条件的分析器中诊断出受影响的语言结构。预期兼容的示例不会引起诊断。ISO/IEC TS 17961没有规定这些规则的实施机制或要实施的任何特定编码样式。
其他的安全审计标准如下。
● MISRA(The Motor Industry Software Reliability Association,汽车工业软件可靠性联会)标准(如MISRA 2004、MISRA 2008和MISRA 2012标准)。
● 《C/C++语言源代码漏洞测试规范》(GB/T 34943—2017)。
● 《C/C++语言源代码缺陷控制与测试规范》(SJ/T 11682—2017)。
在代码审计过程中,常见的概念如下。
● 验证(valiation):保证输入的数据在预先设定好的有效的程序输入范围之内。
● 净化(sanitization):当数据从另一个受信任域直接传递到组件时,确保数据符合接收该数据的子系统对数据的要求。净化可能会涉及数据泄露、数据输出跨出受信边界、敏感数据泄露。净化可以通过消除不必要的字符输入(如对字符进行删除、更换、编码、转义等操作)实现。净化可以在数据输入之后或数据跨受信边界传输之前进行。数据净化和输入验证经常并存,且是相互补充的。
● 标准化(canonicalization)和归一化(normalization):标准化是指将输入以最小损失还原成等价的最简单的已知形式;归一化是一个有损转换的过程,在这个过程中,输入数据会用最简单的形式来表现。标准化和归一化必须在数据验证之前进行,从而防止攻击者利用验证例程来除去不合法的字符,并由此构建一个不合法的字符序列。在把数据发送到另一个受信域组件时,发送者必须确保数据经过适当的编码处理,保证数据在穿过受信边界时,满足数据接收者受信边界的要求。例如,虽然一个系统可能已经被恶意代码或数据渗透了,但是系统输出是经过适当转义和编码处理的,还可以避免许多攻击。
● 敏感数据泄露:泄露身份证号码、信用卡号码、用户名、密码等敏感数据。在不同等级受信域组件中共享数据时,称这些数据是跨域受信边界的。因为在Java环境中允许同一个程序中处在不同受信域的两个组件进行数据通信,所以会出现那些跨域受信边界的数据传输。因此,如果在域中存在一个授权用户,而该用户没有数据接收权限,那么系统必须保证这些数据不会发送给处于该域的组件。
● 共享内存和共享变量:可以在线程之间共享的内存称为共享内存或内存堆,可以在不同的线程中共享的变量称为共享变量。由于共享变量中的数值是缓存的,因此把这些数值写入主存会有延迟,有可能会导致其他线程会读取这个变量过时的数值。所有的实例字段、静态字段及数组元素作为共享变量存储在共享内存中。局部变量、形式方法参数及异常例程参数不在线程之间共享。
目前,在代码审计中,对一个系统进行审计需要关注方方面面,为了检查每一项需要消耗大量的时间,很难用一个工具对所有项都进行审计,往往需要借助多个工具辅助进行审计。其中,针对软件代码本身的审计工具(或者称为源代码检测的工具)是一个非常重要的工具。
现在系统的代码量非常大,往往达到几十万行,甚至几百万行,仅仅靠人工审计需要消耗很长时间,几乎不太可能满足项目时间的要求,所以针对源代码检测往往需要使用一款自动化代码检测工具进行代码扫描,扫描完成后,再通过人工复核,去掉工具的误报。然而,采用的工具是否有漏报,往往很难评估。
代码安全审计工具应该具有的功能如下。
● 支持主流开发语言,至少支持C、C++、Java、JSP、JavaScript、HTML、Python、PHP、C#、.NET等语言。
● 支持尽可能多的安全检测项,尤其是能够支持对典型安全漏洞进行深度检测,尽量正确报告代码中的缺陷、漏洞和潜在的编码风险(如bad smell),这也是考查工具可用性的必要条件。
● 不管是测评中心、开发团队、安全团队,还是代码安全审计人员,有时候都可能无法编译获得的审计代码,因此工具最好既支持对编译后的代码进行检测,又能直接对源代码本身进行检测。
● 要在国内使用,操作和审计报告全部是中文也是必要的。
● 具备定制功能,即能够根据不同的代码检测要求,选择审查范围,确定检测项,最好能够快速定制检测项。
● 目前,在软件开发过程中,大量引入开源组件和第三方库,能够支持对开源组件和第三方库的检测也应纳入必要条件,这就要求工具具备字节码、二进制文件检测能力,同时能够快速响应客户需求,支持快速升级,这能够在一定程度上减少0day漏洞的危害。
代码安全审计是一项新兴的业务。之前,除军工单位比较重视代码审计之外,金融、互联网等企业没有给予其足够的重视,对于代码审计到底怎么做,检测出什么结果是对的,工具漏报、误报是怎么度量的,审计人员可能不太清楚,所以厂商需要提供完整的解决方案,包括咨询服务、培训服务等。
代码检测工具属于静态应用安全测试(Static Application Security Testing,SAST)工具。静态应用安全测试技术通常通过在编码阶段分析应用程序的源代码或二进制文件的语法、结构、过程、接口等发现程序代码存在的安全漏洞。
相对于动态分析或运行时测试方案,SAST工具能在开发阶段检测出源码中的安全漏洞,从而大大降低修复安全问题的成本。它们还能找到许多动态分析工具通常无法找到的漏洞。得益于自动化的特性,SAST工具能在成百上千款应用间实现伸缩,而这是仅靠人为分析方法无法企及的。检测人员往往使用SAST工具对一个或两个项目进行检测,就能体验到SAST工具的优缺点,因为其检测效果立竿见影。
采用静态分析方法的很多代码检测工具覆盖了绝大多数软件安全漏洞,或诸如 OWASP Top 10的重要高风险漏洞,因此它们已经足够好了。开发公司往往不会在软件开发初期就考虑安全问题,而止步于在应用部署到生产环境之前,获得一份来自扫描工具的“无瑕疵报告”。其实,这种做法非常危险,因为它忽视了SAST技术的基本限制。
静态分析工具检测出的缺陷数量的多少与工具本身有多少条检测规则有关,这些检测规则包括安全编码规范、安全编码标准、OWASP Top 10安全漏洞、CWE Top 25缺陷、与国家安全漏洞相关的标准、与行业安全漏洞相关的标准等。每家工具厂商尽量设计更多的检测规则(或者称为检测器),以期望检测出更多的缺陷或安全漏洞。
静态分析工具相当复杂。为了正常发挥功能,它们需要从语义上理解程序的代码、依赖关系、配置文件及可能没有用同一种语言写的组件。与此同时,它们还必须保持一定的分析速度及准确性,从而降低误报的数量。此外,JavaScript、Python之类的动态类型语言在编译时往往无法决定对象所属的类或类型,因此这进一步影响了静态分析工具的效率。于是,找到大多数软件安全漏洞不是不切实际的,就是不可能的。
NIST SAMATE项目力求测量静态分析工具的效率,从而帮助公司改善该技术的使用情况。它们对一些开源软件包分别执行了静态分析及人工代码检查,并对比两者的结果。分析显示,在所发现的全部漏洞中,1/8~1/3的漏洞属于简单漏洞。进一步的研究发现,这些工具只能发现简单的实现错误,对于需要深入理解代码结构或设计的漏洞往往束手无策。在流行的开源工具Tomcat上运行时,面对26个常见漏洞,静态分析工具只对其中4个(15.4%)漏洞发出了警告。
这些统计数据与 Gartner 论文“应用安全:大胆想象,从关键入手”(Application Security: Think Big, Start with What Matters)中的发现相互呼应。在这篇论文中,作者提出,“有趣的是,通常认为SAST只能覆盖10%~20%的代码问题,DAST覆盖另外的10%~20%。”按照这种观点,如果一个公司自主开发了一个类似于Tomcat的工具,并以静态分析为主要手段进行应用安全测试,这意味着会有22个常见的安全漏洞原封不动地遗留在该公司部署的应用中。
静态分析可能无法找出的问题包括以下4种。
● 机密数据的存储与传输,尤其是当关于这些数据的程序设定与关于非机密数据的无异时。
● 与身份认证相关的问题,如蛮力攻击敏感系数、密码重置效力等。
● 与非标准数据随机选择的熵相关的问题。
● 与数据保密性相关的问题,如数据保持及其他合规性问题(例如,确保信用卡号在显示时是部分掩盖的)。
与普遍观点相反,许多静态分析工具的覆盖缺口隐含着巨大的组织风险。多数公司测试团队或安全团队无法接触源代码,导致SAST工具可能无法理解某种特定的语言或框架,再加上大规模部署这一技术及处理错误警报带来的挑战,这一风险变得更高了。尽管静态分析是确保安全开发的重要技术,但是它比不上从建立应用之初就考虑安全问题的策略。公司只有将安全理念融入产品需求与设计,并在研发生命周期的各个阶段以静态分析等技术加以验证,才最有可能创造出安全的软件。
代码检测技术经历了大概4个发展阶段。
编译技术中的数据流和模式匹配是早期静态检测工具经常采用的分析技术,包括到达定值分析、支配分析、活跃变量分析、静态单赋值技术等。
这类技术的优点是效率高,算法的复杂度低。静态分析算法对一些基本信息的分析是很有效的,但是这些算法基本上是函数内的,无法处理跨函数分析。此外,算法本身是路径不敏感的,所以整体上说精度较低。为了克服这一问题,出现了很多辅助技术,如函数内联、摘要技术,尤其是后者使用得相对比较广泛。对于不同的缺陷类型,需要给出的摘要信息往往不一样,并且对于状态比较复杂的程序,摘要信息很难表达完善,可能会丢掉一些信息,并且提高了算法的复杂度。整体来说,数据流分析的精度相对于其他方法还是偏低的。
符号执行指将程序源代码中变量的值以抽象化符号的形式表示,模拟程序执行,它适用于对路径敏感的程序分析。分析给定路径的有关变量和谓词,依次使用输入的符号表达式替换路径上赋值语句的左部变量及分支谓词的变量,使该路径的分支谓词为有关输入的符号值的等式或不等式,求解这些路径的限制条件,即可产生测试数据。符号执行的目的是分析程序中变量之间的约束关系,不需要指定具体的输入数据,将变量作为代数中的抽象符号处理,结合程序的约束条件进行推理,结果是一些描述变量间关系的表达式。
符号执行是形式化的分析方法,如果不对其加以改进,算法的复杂度会非常高,对于复杂程序会发生状态爆炸。为了兼顾精度与效率,研究领域提出了很多改进,典型的包括Saturn。它采用布尔可满足性的方式对缺陷进行计算求解。对变量值的跟踪指将每个变量的范围均表达成二进制数的形式,这可以简化更多种类型运算。Saturn的分析是函数内的路径敏感分析,但函数间采用摘要的方式。Clang采用函数内的符号执行方法进行缺陷检测,对于函数间分析给出简单的摘要。函数内检测精度尚可,但跨函数分析精度较差。
整体来看,采用符号执行作为基本分析方法是目前大多数静态缺陷工具选择的方法,比基于数据流的方法精度更高,尽管效率(基本每小时可分析十万行代码到百万行代码)有所下降,但是大多数使用者认为这是可以接受的。
抽象解释理论是由P. Cousot和R. Cousot在1977年提出的关于程序静态分析的一种近似理论,其基本方法基于格论。该理论可以看作程序执行中对所有可能语义信息的计算、提取。为了简化该计算过程,抽象解释会将受检代码的每条语句的影响简单模型化为一个抽象机器的状态变化。相对于受检程序实际系统,抽象机器更容易分析,但其代价是丧失了一定程度的分析完备性(并不是原始受检程序系统中的每种性质在抽象机器中都得以保留)。“对受检程序进行抽象解释”实际上使用一个基于“抽象对象域”的较低代价的计算过程抽象逼近基于“受检程序指称对象域(具体对象域)”的计算过程,从而使程序抽象执行的结果能够尽量反映出程序真实运行过程中的部分信息。
抽象解释本质上是在计算效率与精度之间取得均衡,通过损失部分计算精度保证计算可行性,再通过多次迭代计算增强计算精度的一种抽象逼近方法。与定理证明和模型检测技术相比,它既能处理自动定理证明无法解决的不可判定的问题,又能为模型检测束手无策的非常复杂问题的逼近求解提供系统性的构造方法和有效算法。总体上说,抽象解释有3种实现方式——多面体、区间分析及八面体。虽然多面体的分析精度很高,可以表达多个变量之间的关系,但是分析效率较低。
抽象解释理论的形式化方法被广泛应用于大规模软硬件系统,尤其是嵌入式系统的自动分析。
Thomas Reps在1995年提出了用上下文无关的可达性方式解决函数间数据流分析问题。经过证明,该方法的时间复杂度为节点数目的三次方,精度比传统的数据流分析更高,能较好地兼顾精度与效率。2008年,康奈尔大学的Cherem等人提出了值流模型,并以此模型作为基本的分析模型,进行内存泄漏检测。该方法是图可达性方法在缺陷检测方面的一个典型应用,虽然精度较符号执行的略有差距,但分析效率明显更高,能较好地兼顾精度与效率。新南威尔士大学的Sui等人在2012年对值流图进行了扩展,提出了全稀疏值流图。值流分析模型结合控制流分析、数据流分析中的定值使用及调用关系分析构建了值流图,由此,值流模型本身包含的信息更多,且通过点与点之间的连线表达变量的定值使用关系,每个值流子图均表达了某个变量到其值发生改变之前的生命周期。相对于值流模型,值依赖分析模型通过结合指向分析、区间分析等分析方法,使程序模型能够更加准确地表达变量值之间的依赖关系,为缺陷检测提供了更精化的模型,但精度与效率仍有提高的余地。
程序代码的静态分析就是通过检查程序的源代码推测程序运行时的行为信息。静态分析除能够检查指定程序中存在的错误和安全漏洞以外,还能够将优化算法加入代码编译器中,用于程序的优化。那么以何种方式才能够将静态分析用于优化呢?关键的技术就是流分析技术。流分析技术是比较传统的编译器优化技术,能够在保证程序内容真实性的状态下确定一个指定程序节点的相对路径。流分析技术大体上分为数据流分析和控制流分析。
1)数据流分析
数据流分析是一项在编译时使用的技术,它能从程序代码中收集程序的语义信息,并通过代数的方法在编译时确定变量的定义和使用方法。在代码静态分析中,数据流分析是一项可用于分析数据如何沿着程序可能的执行路径转移的程序分析技术。数据流分析的前提条件就是基于IR(Intermediate Representation,中间表示)构造CFG(Control Flow Graph,控制流图)。
基于数据流分析,我们可以实现多种全局优化。
数据流分析的通用方法是在控制流图上定义一组方程并迭代求解,一般分为正向传播和逆向传播。正向传播就是沿着控制流路径,向前传递状态,将前驱块的值传到后继块。逆向传播就是逆着控制流路径,将后继块的值反向传给前驱块。这里有两个术语——传递函数与控制流约束。传递函数是指基本块的入口与出口的数据流值为两个集合,满足函数关系f。若正向传播时入口值集为X,则出口值集为f(X);若逆向传播时出口值集X,则入口值集为f(X)。控制流约束是在一条路径两端的前驱块与后继块的数据流值的传递关系。
通过数据流分析,我们不必实际运行程序就能够发现程序运行时的行为,这就可以帮助我们理解程序。数据流分析用于解决编译优化、程序验证、调试、测试、并行、向量化和片行编程环境等问题。
2)控制流分析
控制流分析(Control Flow Analysis,CFA)是一种确认程序控制流程的静态代码分析技术。控制流程会以控制流图来表示。对于函数编程语言及面向对象程序设计,控制流分析都是指计算控制流程的算法。
控制流分析旨在生成程序的控制流图,在编译器设计、程序优化分析、代码缺陷分析等领域都有重要应用。对程序的控制流分析是对源程序或者源程序的中间表示形式的直接操作,形成控制流图;数据流分析是在控制流分析后得出的控制流图的基础上,沿着控制流图的路径,对程序中包含数据的变量进行赋值并传递,直至程序完成,变量回收或者未被回收。从逻辑关系来看,控制流分析是先于数据流分析的,控制流分析对数据流分析有着先导性和支持性的作用。
控制流分析从程序的特点上可以分为两个大类——过程内的控制流分析和过程间的控制流分析。过程内的控制流分析可以简单地理解为对一个函数内部的程序执行流程的分析,而过程间的控制流分析一般情况下指的是对函数的调用关系的分析。从控制流分析的特点上看,主要的分析还是过程内的控制流分析。针对过程内的控制流分析,有以下两种主要的方法。
● 利用某些程序执行过程中的必经点查找程序中的环,根据程序优化的需求,对这些环增加特定的注释。这种方法最理想的使用方式是迭代数据流优化器。
● 区间分析。这里定义的区间包含子程序整体结构的分析和嵌套区域的分析。根据分析结果,对源程序进行抽象语法树(Abstract Syntax Tree,AST)的构造。AST在源程序的基础上按照执行的逻辑顺序,为程序构造一棵与源程序对应的树形数据结构。AST可以在数据流分析阶段发挥关键的作用。当然,不是所有的控制流分析都是简单的,较复杂的控制流分析是基于复杂区间的结构分析,这样的分析可用于确定子程序块中所有的控制流结构。
无论采用上述哪种方法进行控制流分析,都需要先确定子程序的基本块,再根据基本块进行程序控制流图的构造。
首先,利用编译器为待测试程序生成中间指令集合,并根据指令之间的跳转关系划分基本块、生成函数内的控制流图。
其次,从函数内部控制流图出发,依据函数调用指令分析调用关系构造过程间的控制流图。
最后,逆转基本块的指向关系,构造双向控制流图,出于对大规模程序的分析效率考虑,从缺陷语句所在的基本块开发反向搜索能够有效减少遍历的节点数目。
分析程序的循环结构,循环结构中代码的效率是整个程序的效率关键。绘制程序的控制流图,从控制流图中找出循环。
抽象语法树简称语法树,是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。例如,嵌套括号隐含在树的结构中,并没有以节点的形式呈现;而类似于if-condition-then这样的条件跳转语句可以使用带有两个分支的节点表示。编译器前端、一般代码优化工具、代码检测工具及现在比较流行的Fuzzing Test工具都是基于抽象语法树的。
生成抽象语法树的工具是与开发语言相关的,对于C/C++语言,使用GNU提供的标准编译器生成工具lex和yacc。通过某种开发语言的语法规则,在lex和yacc中编写规约规则,当输入一段基于该开发语言的源代码时,则输出基于该规约规则的语法树。对于Java语言,基于javaparser工具生成抽象语法树。
抽象语法树由表示非保留字终结符的叶子节点和表示语法结构的中间节点组成。抽象语法树可以进一步完善,它可以包含表现语义关系的连接,如定义链、类型信息和符号表,并将其视为抽象语法树的节点。这样,最后的抽象语法树将包含所有的编译器前端从源代码得到的相关信息,并且能够完全体现源程序的语法结构。
在整个分析过程中,我们都不断地使用抽象语法树的节点。词法分析(lexical analysis)提供了抽象语法树所需要的符号节点,如常量和名字。语法分析(parsing)则提供了含有代表相应语法结构的中间节点的抽象语法树。语义分析(semantic analysis)最后经过对名字和操作符的处理,将抽象语法树转变为一种包含表示类型信息和符号表的标准形式,并将它们连接成树形结构。
抽象语法树是编译器前端基本的输出。一般使用面向对象的方法生成抽象语法树。抽象语法树由几种不同类型的节点组成,每种节点代表一种不同的语法结构。定义分层结构的根节点为translation_unit,产生它的目的仅仅是使抽象语法树有一个清楚的根节点。每个节点都包含一个名字来体现类型、一个子链,以及该节点所对应的源代码中的起始位置、结束位置和一系列标志。这些标志标明该节点是否可以响应一个语法或语义错误,以及是否可以在抽象语法树中重复使用。
抽象语法树的节点分为3类:
● 在语法分析阶段构造抽象语法树的节点;
● 实现语义分析的节点;
● 实现输出的节点。
我们通过构造抽象语法树的节点可以对其子节点进行增加、删除或替代。
在组织抽象语法树时,我们不仅可以将源代码的位置信息连接到节点上,还可以设置错误标志。这种组织抽象语法树的方法的好处是便于进行语义定义,向编译器前端进行输出。用不同的类封装不同类型的抽象语法节点,有利于存放对应特定语法结构或输出的语义定义。抽象语法树用类的方法自然地将编译器前端分成几个部分。语法分析的输出仅仅是多棵未完成的抽象语法树,它是语义分析的输入。语义分析的输出就是一棵包含符号表和类型信息的完整抽象语法树。该抽象语法树同时又能生成各种形式的输出。
区间分析(interval analysis)或值域分析(range analysis)提供程序中变量的取值范围。该分析实现方法在值依赖图(Value Dependence Graph,VDG)上检测一定范围内节点的数据流分析,并对循环、条件节点等进行一系列特殊处理。区间分析的结果用于缓冲区溢出、整数溢出、除以零异常等情况的检测。
在区间运算中,常量和变量并不表示为一个单独、精确的值,而表示为一个有上界和下界的区间或范围。在普通的运算中,一个数量可以表示为数轴上的一个点;而在区间运算中,一个数量表示数轴上的一段,例如,[3,5]表示数轴上从3到5的一段。当将精确的数值表示为区间时,上界与下界是相同的,例如,5表示为区间,即[5,5]。
在静态代码分析过程中,可以记录常量的影响路径,并用常量替代与之等价的变量,从而提升后续分析工作的精度,提供更确切的分析依据。复制传播常与常量折叠组合出现,用于编译优化领域。
例如,进行形如u=v的赋值之后,用变量v代替变量u。
若每次运行时表达式的值是常量,则用此常量代替表达式的值。
如果表达式a+b多次出现而且值都相同,那么可以只计算一次该表达式的值。
值依赖图(见图1-2)是一种基于程序值依赖分析的、路径敏感的空指针解引用检测方法,主要分为守卫值依赖分析和非守卫值依赖分析。值依赖分析方法通过结合数据流分析中的到达定值分析、区间分析及指向分析创建值依赖图。值依赖图刻画了可能产生空指针的语句到其解引用语句的值依赖关系。值依赖图中的边采用守卫标注,即描述相邻点之间的到达条件,降低误报率。
▲图1-2 值依赖图
值依赖的边连接两个存在依赖关系的值依赖节点。值依赖边是有向边,边的头节点称为定义节点,尾节点称为使用节点。定义节点和使用节点中分别存在一个导致依赖关系的变量,它们分别称为定义变量和使用变量。此外,值依赖边还存储了定义节点和使用节点之间的到达条件。
值依赖分析技术以程序源代码作为输入,采用数据流分析、指向分析、区间分析等分析技术给出每个变量的值的依赖关系,并以此为基础构建程序表达模型。值依赖分析方法是一种解决跨函数问题的静态分析求精技术,它改进了值流模型的不足,使模型的表达语义更加丰富,模型表达能力更加强大,使分析结果更加准确。
广义的指向分析包括别名分析、指针分析、形态分析、逃逸分析等。其中,别名分析可用于分析程序中不同的引用是否可能指向相同的内存区域;指针分析可用于分析某个指针指向的对象或存储位置;在形态分析中,通过构建形态图,分析指针可能指向的对象和对象之间的关系;逃逸分析用于分析变量的可达边界。例如,分析某个变量是否可以在创建它的函数之外访问的问题。
指向分析是一种用于分析指针和内存引用所指向的变量或内存地址的静态代码分析技术。指向分析技术是很多更复杂的代码分析技术(如编译优化、代码缺陷检测及指针修改影响分析)的基础。
两种公认的经典指向分析算法为Steensgaard算法和Andersen算法。Steensgaard算法的效率较高,其时间复杂度几乎是线性的,但精度低;Andersen算法则有较高的精度,而其时间复杂度接近O(n3)。后来的很多指向分析算法可视为这两种算法在精度和效率上的折中。库博工具中所使用的指向分析算法以2010年Manuvir Das提出的基于归一的指向分析算法为基础,结合C/C++语言特性,实现了跨函数的指向分析和等价类计算方法。
指向分析是静态分析中的一个难点。对于任何一个指针/引用,能否在编译阶段就知道它会指向内存中哪块位置(位置在这里并不是0xFFFF这样的具体位置,而是指栈/堆上的本地对象)呢?我们不能准确地分析出任何指针会确切指向哪里,但是可以求出近似解,从而在某些情况下做出优化。
在代码静态分析中,指向分析通常与区间分析、别名分析、符号执行等分析方法一同使用,用来发现非法计算、空指针解引用、内存泄漏、变量未初始化、数组越界等缺陷模式。
别名分析用于判断一个存储区域是否被几个不同的途径访问或者改写。C语言中的指针,Java语言中的继承、虚函数、反射,以及Java变量都是引用。对指针和引用的分析就是别名分析。编译器中的每一种静态分析其实都用于回答特定的问题,别名分析回答哪些别名指向同一个对象,而数据流分析回答与变量的值相关的问题,并且这些问题还与变量在程序中的位置相关。
符合执行是一种使用抽象化的符号代替具体的值来表示程序中的变量、模拟程序执行的程序分析方法。通过构建表示程序执行路径的约束表达式、符号、执行将程序分析问题规约为逻辑域问题。
符号执行可用于测试用例生成、安全漏洞检测等领域。采用符号执行的方法收集指令路径的一组路径约束(path constraint)。将得到的路径约束中的真值依次取反,由约束求解器求解得到新的输入。不断重复该过程,最终生成一组具有较高代码覆盖率的测试用例。符号执行可以应用到二进制代码中,利用对软件缺陷的检测,为软件缺陷自动生成攻击向量。
符号执行采用抽象化符号的形式表示程序源代码中变量的值,模拟程序执行的目的是分析程序中变量之间的约束关系,不需指定具体的输入数据,将变量作为代数中的抽象符号处理,并结合程序的约束条件进行推理,结果是一些描述变量间关系的表达式。
符号执行技术的核心思想是使用符号值来表示程序的输入数据,并将程序的运算过程逐指令或逐语句地转换为数学表达式,在CFG的基础上生成符号执行树,并为每一条路径建立一系列以输入数据为变量的符号表达式。
在符号执行过程中,每当遇到判断语句与跳转语句时,符号执行工具便会将当前执行路径的约束收集到该路径的约束集合中。其中,路径约束是指程序分支指令中与输入符号相关的分支条件的取值,是一系列不具有量词的布尔型公式。路径约束集合则用于存储每一条程序路径上收集到的约束,用“与”操作进行连接,通过使用约束求解器(constraint solver)对约束集合进行求解,可以得到该条路径的可达性。如果有解,表示该条路径可达;否则,表示该条路径不可达。在时间与计算资源足够的理想情况下,符号执行能够遍历目标程序的所有路径并判断其可达性。
符号执行方法存在路径爆炸和环境交互等问题,难以直接应用于大规模的程序代码分析。路径爆炸由程序中的分支语句导致,传统的符号执行方法需要保存和跟踪每条可能的程序分支状态。随着程序规模的增长,程序分支数目呈指数级增长,对于大规模程序而言,跟踪每条分支在实际应用中是不可行的。因此,基于符号执行的分析方法往往需要路径选择算法以确定所需要跟踪的程序分支。然而,其代价是降低分析过程的程序代码覆盖率,使一些缺陷不能发现。符号执行方法的另一个问题是环境交互,即如何处理涉及系统调用等程序行为的问题。若由操作系统内核直接处理系统调用,符号执行引擎难以获取系统调用结果,其保存的程序状态可能不准确。若符号执行引擎模拟系统调用过程,则需要实现和维护各种复杂的系统调用过程。
程序切片技术是一种重要的程序分析技术,被广泛应用于程序的调试、测试、维护等领域。程序中的某个输出只与这个程序的部分语句及控制谓词有关系,因此删除其他的语句或者控制谓词将对这个输出没有任何影响。也就是说,对于一个特定的输出,源程序的作用与删除不相关的语句和控制谓词后所得的程序的作用是相同的。
程序切片主要通过寻找程序内部的相关特性,分解程序,然后对分解所得的程序切片进行分析研究,以此达到理解和认识整个程序的目的。动态程序切片主要是指在某个给定输入的条件下,源程序执行路径上所有对程序某条语句或兴趣点上的变量有影响的语句。
敏感性分析包括流敏感性分析、流不敏感性分析、上下文敏感分析、路径敏感分析等。一款代码检测工具的检测深度主要取决于其敏感性分析技术的运用。
流敏感(flow-sensitive)指的是考虑程序语句执行的顺序。例如,在数据流分析的指针别名分析中,非流敏感指针别名分析得出的结论是“变量x和y可能会指向同一位置”,而流敏感指针别名分析得出的结论类似于“在执行第20条指令后,变量x和y可能会指向同一位置”。因此,非流敏感指针别名分析不考虑控制流,并认为所发现的别名在程序中所有位置均成立。
流敏感性指在数据流分析中考虑语句的执行顺序,或使用控制流图提供的控制流信息。
流不敏感性指不考虑语句的执行顺序。由于流不敏感分析方法不考虑控制流信息,因此它难以发现与程序控制流相关的缺陷。流不敏感分析方法的精度往往较低,而效率较高。
上下文敏感(context-sensitive)指在过程间分析( interprocedural analysis)时,考虑函数调用的上下文信息。通过对函数的返回值分析,进行过程间调用分析。
上下文敏感性指在分析时考虑全局变量及在分析函数时考虑函数调用的实参的影响。上下文敏感分析也称为跨函数分析。
路径敏感(path-sensitive)指的是依据条件分支语句的不同谓词,计算不同的分析信息,也就是说,路径敏感将跟踪程序控制流的每一个分支,以记录两个分支路径的不同程序状态。相应地,非路径敏感并不考虑分支之间的区别。简单的路径敏感存在路径爆炸(path explosion)或无穷搜索空间(infinite search space)的问题。路径敏感性指在分析过程考虑程序的每个分支,分别处理程序的每个分支;路径不敏感性指不对程序分支进行区分。
约束求解是程序分析问题的一种常用处理手段。许多程序分析问题可以转换为约束求解问题,也就是将程序代码转化为一阶或二阶的约束方程,然后进一步将其转换为符合约束求解器输入的格式,最终得到问题的解。常见的约束求解工具是SAT Solver,目前还有更进一步的精化约束分析方法。
前后支配分析是数据流分析的一种,是采用位向量表示支配节点集合,描述采用迭代方式计算控制流图上支配节点集合的算法,也是在支配节点集合的基础上分析直接支配节点、支配边界节点的算法。
在计算机科学中,布尔可满足性问题(Boolean Satisfiability Problem,SAT,有时也称为命题可满足性问题)是确定是否存在满足给定布尔公式的解释的问题。换句话说,它询问给定布尔公式的变量是否可以一致地用值TRUE或FALSE替换。如果公式计算结果为TRUE,则称公式可满足。如果不存在这样的赋值,即对于所有可能的变量赋值,公式计算结果为FALSE,则称公式不可满足。例如,公式“a AND NOT b”是可以满足的,因为可以找到值a = TRUE且b = FALSE,使得(a AND NOT b)= TRUE。相反,公式“a AND NOT a”是不可满足的。
程序依赖图(Program Dependence Graph,PDG)是程序的一种图形表示,它是带标记的有向多重图。程序依赖图是有向图。程序依赖图是软件程序间控制依赖关系和数据依赖关系的图形表示。面向方面的程序是基于面向方面的思想,使用相关的框架或语言工具,实现系统中横切关注点的清晰模块化的程序。程序依赖图是分析和理解程序的基础工具之一,它在面向对象的程序上的应用渐趋成熟,而在面向方面的程序上的应用才刚刚开始。
处理方法是以程序的控制流图为基础,去掉CFG的控制流边,加入数据和控制流边。程序依赖图包括数据依赖图和控制依赖图。数据依赖图定义了数据之间的约束关系,控制依赖图定义了语句执行情况的约束关系。
基于SAST技术的静态分析技术将程序依赖图算法应用于动态切片,从而达到缩小程序分析范围的目的。
循环摘要是对函数中循环信息的总结。对于循环内的ICFGNode、getLoopInfo( )方法,程序返回所在最内层循环的循环摘要。为了统一循环格式,检测工具大多把while和for统一转换成do…while形式。
代码检测工具的主要功能如表1-2所示。
表1-2 代码检测工具的主要功能
功 能 |
说 明 |
---|---|
支持多种检测语言 |
支持C/C++、Java、JSP、Python、PHP、C#、HTML、JavaScript、Objective-C、Swift、SQL(非开发语言)等主流开发语言 |
支持多种编译器 |
支持C/C++、Java等语言的编译器 |
支持的检测方式 |
支持基于源代码的检测或支持编译后的目标代码检测 |
支持安全编码规则检测 |
支持国内外常见的编码规范、标准检测,如GJB 8114、GB/T 34944、GB/T 34943、ISO 17961、Misra 2012等 |
支持运行时缺陷检测 |
支持数组越界、内存泄漏、空指针解引用、不可达代码、未初始化变量使用、无限循环、缓冲区溢出、线程死锁等 |
支持安全漏洞检测 |
支持历届发布的OWASP Top 10、CWE Top 25和更多CWE安全漏洞,如SQL注入、XSS、信息泄露、弱密码、弱哈希、LDAP注入、资源注入等 |
支持的检测器 |
支持用户选择检测器组合成检测集,进行检测 |
支持缺陷/安全漏洞的详细描述 |
检测结果以全中文的形式进行展现,包括污点轨迹跟踪、所在函数、检测规则、严重等级、违背规则的示例代码、正确的示例代码等信息 |
定位缺陷/安全漏洞 |
可直接定位该缺陷发生的位置,显示相应的步骤描述 |
自动跟踪和标记缺陷修复状态,调整严重级别 |
能够自动跟踪缺陷修复情况,标记缺陷修复状态 |
查询检测结果 |
根据关键字、文件、规则、严重级别等查询检测结果 |
生成检测报告 |
支持生成PDF、Word格式检测报告 |
管理功能 |
支持缺陷/安全漏洞管理、缺陷/管理统一展示等 |
增量检测 |
支持增量检测功能 |
接口功能 |
支持与Jira、禅道等缺陷管理系统与项目管理系统对接 |
支持IDE插件 |
支持Eclipse、Visual Studio等编辑器插件安装检测功能 |
与CI平台集成 |
支持Jenkins等持续集成平台的插件安装,定时检测等功能 |
支持配置管理系统 |
支持SVN/GIT等配置管理对接 |
检测效率高 |
每小时检测50万以上代码 |
误报率低 |
误报率控制在20%以内 |
漏报率低 |
相对漏报率控制在20%以内 |
Fortify、Checkmarx、Klocwork、Coverity、Codesonar、SonarQube、Wukong、CoBOT等工具都可以作为代码检测工具。目前,很多工具已经单独为做代码检测做了一些优化。例如,Wukong检测完成,可以生成审计报告,对缺陷进行修复后,再次进行审计,生成复审报告。
一款工具满足以下条件可以作为代码检测工具。
● 支持C/C++、Java等主流开发语言。
● 支持主要的国家和行业中关于代码安全漏洞的标准、指南等。
● 审计的范围可以设定,以根据各种设定审计内容;
● 支持对工具检测结果的人工复核,修改缺陷级别,识别误报等;
● 能够审计报告,报告内容包括详细的缺陷描述。
下面对国内外几款常见的代码检测工具进行简单介绍。
Fortify的全称是Fortify Static Code Analyzer。Fortify于2006年进入中国市场,在国内知名度较高。Fortify目前支持多种主流开发语言检测。Fortify具有5类分析引擎——数据流、语义、结构、控制流、配置流。Fortify在检测过程中对产生的中间语法树(Normal Syntax Tree,NST)进行分析,匹配所有规则库中的漏洞特征。
Fortify的优势如下:
● 支持的检测语言多达26种;
● 支持8大类、800多小类安全漏洞检测;
● 侧重于安全漏洞检测;
● 支持规则自定义,包括合规信息的识别;
● 每个漏洞有发生的可能性和严重性两个分类标识;
● 支持注解,通过注解消除误报;
● 支持Eclipse、Visual Studio、WSAD等集成。
Fortify的劣势如下:
● 编译型语言需要编译通过才能检测;
● 不支持增量检测;
● 误报率较高,大多数情况下可能超过50%;
● 漏报率也较高。
Checkmarx提供了一个全面的白盒测试安全审计解决方案,帮助企业在软件开发过程中查找、识别、追踪绝大部分主流编码中的技术漏洞和逻辑漏洞。
Checkmarx的优势如下:
● 无须编译,直接扫描源代码;
● 规则可定制,内置查询语言;
● 支持增量扫描;
● 支持漏洞图解、最佳修复点分析。
Checkmarx的劣势如下:
● 自定义规则不灵活;
● 只能部署在Windows平台上。
Klocwork支持C、C++、Java、C#开发语言检测。Klocwork更侧重于缺陷检测,在安全漏洞检测方面较弱。
Klocwork的优势如下:
● 即使编译不通过也允许检测;
● 支持GIT/SVN下的增量检测;
● 支持场外分析结果导入后分析;
● 可以定制检测器;
● 支持程序质量度量分析。
Klocwork的劣势如下:
● 支持的检测语言较少;
● 自定义规则难以使用;
● 基于C-S架构展示。
Covertity是一款综合能力较强的检测工具。
Covertity的优势如下:
● 与各种外围工具有很多对接类型;
● 内置函数式编程语言,可以自定义检测器;
● 支持的检测语言超过20种;
● 支持超过70种框架;
● 支持超过20种编译器(主要基于C/C++语言);
● 支持编译不通过情况下的检测;
● 整合了Blackduck,支持SCA分析。
Covertity的劣势是需要配置编译器。
Codesonar在国内市场的销售量不多,主要由于它支持的标准在国内能够使用的只有交通行业。
Codesonar不仅可以用于增量分析,还支持MISRA 2012、ISO 26262、DO-178B、US CERT、CWE。
SonarQube是一款社区版商业工具软件,通过插件形式,支持超过20种检测语言。
检测器数量多,但是大多属于安全编码规则类型。
Wukong软件源代码安全漏洞修复平台是中国科学院计算技术研究所下属企业研发的一款SAST类工具,具有自主专利技术,可以深度检测安全漏洞。Wukong不仅支持OWASP Top 10、CWE常见缺陷的检测,还支持GB/T 34943、GB/T 34944,支持CERT Java等。
CoBOT是北京大学软件工程国家工程研究中心与北京北大软件工程股份有限公司共同研发的一款SAST类工具,支持10种左右语言,能够在编译不通过的情况下进行检测。CoBOT不仅支持GJB 8114、GJB 5369、ISO 17961、MISRA 2004、MISRA 2008、MISRA 2012,支持CERT C、CERT Java标准,还支持历届OWASP Top 10、CWE Top 25及常见CWE缺陷。
虽然代码检测工具起步较早,但是行业从业人员对其认知不足,对代码升级工具选择片面。例如,有人认为工具报告出的缺陷越多越好,而国内有些企业利用正则表达式逐行检索代码,实际上不仅无法做到跨文件、跨函数的上下文敏感分析,还无法做到污点轨迹分析,这造成大量的误报。这类工具是不可用的代码检测工具。主流的代码检测引擎则通过模式匹配技术与复写传播等技术开发检测引擎。下面是一个OWASP案例说明。
OWASP是开放式Web应用安全项目。它提供一个OWASP Benchmark,是一个SAST工具评价标准套件。它在GitHub上有开源的程序,当前版本为1.2beta。它共有2740个真假安全漏洞案例代码,包含了11类OWASP安全漏洞类型(见表1-3)。其中包括安全漏洞的错误示例总计1415个,假安全漏洞(即正确示例)总计有1325个。
表1-3 OWASP安全漏洞类型
安全漏洞类型 |
中文名称 |
错误示例数 |
正确示例数 |
---|---|---|---|
Command Injection (cmdi) |
命令行注入 |
126 |
125 |
Weak Cryptography (crypto) |
弱密码 |
130 |
116 |
Weak Randomness (hash) |
弱哈希 |
129 |
107 |
LDAP Injection (ldapi) |
LDAP注入 |
27 |
32 |
Path Traversal (pathtraver) |
路径遍历 |
133 |
135 |
Secure Cookie Flag (securecookie) |
Cookie安全 |
36 |
31 |
SQL Injection (sqli) |
SQL注入 |
272 |
232 |
Trust Boundary Violation (trustbound) |
违反信任边界 |
83 |
43 |
Weak Randomization (weakrand) |
弱随机 |
218 |
275 |
XPATH Injection (xpathi) |
XPATH注入 |
15 |
20 |
Cross Site Scripting (xss) |
XSS |
246 |
209 |
作者从OWASP Benchmark的SQL注入漏洞中选择两个案例。
BenchmarkTest00043是真实漏洞,BenchmarkTest00052是假漏洞。这两个案例位于Benchmark 1.2beta根目录中,如图1-3所示。
▲图1-3 Benchmark 1.2beta中的两个案例
首先,打开BenchmarkTest00043.java文件,查看代码,如图1-4所示。
▲图1-4 BenchmarkTest00043.java文件中的代码片段
从上述的程序可以看到,一般检测工具会直接定位到第54行,因为这里出现一个特征方法executeUpdate( ),执行SQL语句。如果没有上下文分析,则并不能确定这是否为一个真实漏洞。主流检测工具会通过代码切片,在抽象语法树上进行向后遍历,分析SQL参数是否进行注入处理,找到第50行,第50行不仅实现SQL字符串的拼接,还引入param变量。在抽象语法树上回溯param变量的取值,找到第46行和第47行。第47行无入侵可能。在第46行中,param变量的值通过scr对象的getTheParameter( )方法传递vector字符串获得。再向上找到src对象,src对象来自其他类的定义,把post报文的请求参数request传递给SeparateClassRequest( )方法。
打开SeparateClassRequest.java文件,如图1-5所示。
▲图1-5 SeparateClassRequest.java文件中的代码片段
在构造函数中,传入了request参数,当src调用getTheParameter( )方法时,把vector传给形参p,该方法返回p对应的参数值,并没有对post传入的参数进行任何处理。经过一系列的回溯和上下文分析,最后确认程序存在着SQL注入漏洞。一般单文件漏洞扫描无法跨文件去回溯和上下文分析确认漏洞是否为真实漏洞,但是通过简单的函数名称判断也会报出这个漏洞。作为一个专业的评价基准项目,OWASP Benchmark也考虑了工具对漏洞的分析能力,对于假漏洞,是否能识别出来呢?
打开BenchmarkTest00052.java这个文件,如图1-6所示。
▲图1-6 BenchmarkTest00052.java文件中的代码片段
与上面的示例一样,在第 55 行发现可能的污点后,回溯到第 46 行。对应的另一个文件中的getTheValue( )方法如图1-7所示。
▲图1-7 SeparateClassRequest.java文件中的getTheValue( )方法
getTheValue( )方法返回的是固定字符串bar。具有上下文和跨文件分析能力的工具分析到这里,并不会报出漏洞,因为这不存在SQL注入的可能。而一般扫描工具无法完成上下文分析,没有生成抽象语法树,更难以完成跨文件分析,自然会报出这个漏洞,但是这是一个假漏洞。
代码检测工具属于SAST工具,SAST工具相对比较成熟,所以针对这类工具,国际上已经有了测试基准和评价标准。工具评价采用约登指数(youden index),也称为正确指数。约登指数等于灵敏度与特异度之和减去1,即约登指数=灵敏度+特异度−1。
灵敏度(sensitivity)是指检测工具把真实漏洞正确报出的比例。
特异度(specificity)是指检测工具将假漏洞错误报出的比例。
基准测试有4种可能的测试结果:
● 工具正确识别真实漏洞(True Positive,TP),称为真阳性;
● 工具无法识别真实漏洞(False Negative,FN),称为假阴性,也就是所谓的漏报;
● 工具正确识别假漏洞(True Negatvie,TN),称为真阴性;
● 工具错误地报出假漏洞(False Positive,FP),称为假阳性,也就是所谓的误报。
灵敏度的计算公式是TPR= TP / (TP + FN) ,也就是检出率/真阳性率,是错误小示例报出数占所有错误小示例总数的比例,该值越大越好。
特异度的计算公式是1−FPR= FP / (FP + TN),也就是误报率/假阴性率,是正确小示例报出数占所有正确小示例总数的比例,该值越小越好。
约登指数 = TPR+(1−FPR)−1=TPR−FPR,该值越大越好。
假设某工具的TPR为0.98,而FPR=0.05,也就是灵敏度=0.98,特异度=1−FPR=0.95,则约登指数=0.98+0.95−1=0.93,相当于93分。实际上,约登指数=TPR−FPR=0.93,即约登指数等于漏洞检出率减漏洞误报率。
国际上也有一套C/C++测试套件(也就是Juliet测试套件),用于对SAST工具进行基准测试。该测试套件由NIST(National Institute of Standards and Technology,美国国家标准与技术研究)研发和维护。当前版本为Juliet C/C++ 1.3,全部使用C/C++编写,共有64000个测试案例文件,包含118个比较典型的CWE缺陷类型。
软件成分分析(Software Composition Analysis,SCA)专门用于分析开发人员使用的各种源码、模块、框架和库,以识别和清点开源软件(Open Source Software,OSS)的组件及其构成和依赖关系,并识别已知的安全漏洞或者潜在的许可证授权问题,把这些风险排查在应用系统投产之前,也适用于应用系统运行中的诊断分析。SCA在2016—2018年Gartner发布的DevSecOps中均出现,但每年的关注点不同。在2016年的报告中,Gartner强调的是DevSecOps的安全测试和RASP(Runtime Application Self-Protection,运行时应用自我保护)。2017年,Gartner侧重面向开源软件进行安全扫描和软件成分分析。2018年,Gartner继续强调针对开源软件的软件成分分析。
在编写程序的过程中,开发人员可能会使用第三方框架,如在Java开发中使用Spring Boot框架、SSH或SSM框架等,在C/C++开发中使用QT框架等。这些框架有些是开源的,有些是闭源的或称为商业的框架,还可能使用第三方库,以及引入开源组件。对于这些非程序员自行编写的代码,测试人员往往认为它们是正确的,无须做代码检测,而通常采用黑盒测试手段对其进行测试,测试用例往往覆盖不全面。这就导致在非程序员自行编写代码检测方面存在着盲区。
根据Gartner的统计,全球使用开源代码的比例以每年30%的速度增长,80%~90%的企业代码实际上由公共仓库导入的开源代码构成,开发人员下载开源代码8次,就有1次引入已知安全漏洞。Synnopsys公司统计,98%的公司甚至不知道它们开发的系统引用的第三方库有哪些,无法轻易追踪和监督纳入项目的开源代码。而对于开源代码,每年报出4000个以上的安全漏洞。
在对第三方库和开源框架、开源组件安全漏洞的检测上,前些年,美国的Black Duck Software、Veracode和瑞典的FOSSID占据大部分市场。2017年,BlackDuck Software公司被Synopsys收购,Blackduck被整合到Synopsys的Coverity产品中。国内推向市场的成熟工具只有CoBOT SCA、FossCheck、开源卫士、KeySwan及SmartRocket Scanner。目前这些工具存在一个普遍的缺陷,就是针对开源组件检测的支持较好,往往对第三方库和框架的支持不足。开源组件可以从GitLib和GitHub等开源网站下载并对它们分析,但是第三方库往往是收费的商业库,并不开源。框架更新速度较快。
相对于SAST类工具,软件成分分析工具主要采用已知安全漏洞的模式检测代码中的未知安全漏洞,而SCA类工具的原理相对比较简单,主要采用已知安全漏洞所在的开源组件、第三方库、框架等检索代码是否由于引用了这些软件成分而存在已知的安全漏洞。
由于SCA类工具的原理对比较简单,因此研发该类工具的企业相对较多。研发的难点主要在于开源组件、第三方库、框架的积累,研发人员需要通过爬虫技术从开源网站下载海量的开源组件,以及同一组件的不同版本,检测每一个版本是否存在安全漏洞。当然,如果保证代码库全面,也需要把大量的开源或闭源的第三方库、开源框架和商业家纳入代码库。下载代码主要的网站是GitLib、GitHub、SourceForge,国内网站主要是OSCHINA。由于安全漏洞和代码分别存在于不同的网站,主要来自美国NVD(National Vulnerability Database,国家漏洞数据库)、CNVD(China National Vulnerability Database,中国国家漏洞数据库)、CNNVD(China National Vulnerability Database of Information Security,中国国家信息安全漏洞数据库),因此需要把安全漏洞与代码库的软件成分版本对应起来,这样在对被检测程序检测时,对比代码库中代码文件的特征码与被检测程序中的文件特征码,如果目的和源的某一个代码成分相关文件的特征码一致,则认为它们来自同一个软件组件,软件成分分析工具会把对应的安全漏洞报出来。
软件成分分析工具由开源和二进制代码数据层、软件成分数据采集与识别层、软件成分知识层、软件成分分析平台层、代码可控性及风险分析层组成。
系统为代码库中的软件代码提取特征值。特征值的提取分为5个级别,具体包括函数级、类级、文件级、包级和项目级。根据识别粒度的精度选项,系统可以根据不同的特征值算法建立起相应的特征值索引库,这些特征值索引库为后面的匹配提供基础。
基于建立的分布式存储代码资源库,代码特征库生成技术从函数、类、文件、包、项目5个方面提取相应级别的特征。提取的特征具有很好的区分性,称为代码指纹。
在函数级别抽取的特征包括函数编号、函数代码的摘要值、函数包含的词法标识、函数的抽象语法树的特征向量、函数的程序依赖图的特征向量、对应源文件开始的行号、对应源文件结束的行号、函数描述文本。原始代码会首先通过词法分析器,提取标识符、常量、关键字、运算符等标识单元,然后利用符号表进行规范化处理,生成函数级指纹。
在类级别抽取的特征包括类编号、类代码的摘要值、类包含的词法标识、类的抽象语法树的特征向量、类的程序依赖图的特征向量、对应源文件开始的行号、对应源文件结束的行号、类描述文本。
在文件级别抽取的特征包括文件编号、文件代码的摘要值、文件包含的词法标识、文件的程序依赖图的特征向量、文件的描述文本。
包是一个汇总特征。在包级别抽取的特征包括包编号、包下属的文件、类和函数特征,以及包的描述文本。
项目也是一个汇总特征。在项目级别抽取的特征包括项目编号、项目下属的包级别的特征,以及项目的描述文本。
基于特征库的匹配算法则从文本、标识、语法、语义4个方面进行匹配,称为指纹匹配算法。在每个方面存在3种匹配方式:
● 依据摘要的匹配方式;
● 依据特征向量或特征向量哈希值的匹配方式;
● 针对标识袋的部分索引的匹配方式。
对于摘要特征,匹配算法可以精确匹配到相同摘要值的函数、类、文件,以及包含该函数、类、文件的包和项目。
对于词法分析生成的标识袋特征,匹配算法可以利用其部分索引快速定位相似的函数、类、文件,以及包含该函数、类、文件的包和项目。
对于语法分析生成抽象语法树的特征向量或其哈希值特征,匹配算法可以近似定位目标函数、类,以及包含该函数、类的文件、包和项目。
对于语义分析生成程序依赖图或值依赖图的特征向量或其哈希值特征,匹配算法可以近似定位目标函数、类,以及包含该函数、类的文件、包和项目。
各个级别对象所使用的特征提取方法如表1-4所示。其中,对应级别对象直接使用的特征提取方法用“√”表示,通过推导方式提取特征的用“〇”表示,不适用相应级别对象的特征提取方法用“×”表示。
表1-4 各个级别对象所使用的特征提取方法
对象 |
特征提取方法 |
|||
---|---|---|---|---|
文本 |
标识 |
语法 |
语义 |
|
函数 |
× |
√ |
√ |
√ |
类 |
× |
〇 |
〇 |
〇 |
文件 |
√ |
〇 |
〇 |
〇 |
包 |
〇 |
〇 |
〇 |
〇 |
项目 |
〇 |
〇 |
〇 |
〇 |
函数级别的特征通过标识、语法和语义等特征提取方法提取。
首先,通过标识计算函数特征。将源代码仓库中的代码片段解析为<符号,频数>的集合并存储。而对于二进制代码,首先,要对二进制代码进行反汇编,依据汇编代码提取代码片段集合。每个代码片段有全局唯一的ID,通过统计所有的符号,生成全局符号频数表。接着,基于“子块重叠过滤”思想,建立部分索引。具体过程如下。
(1)设相似度为θ,为平衡精确度和召回率,θ可取70%。
(2)查询全局符号频数表,将代码片段中的符号按全局频数升序排列。
(3)设代码片段中总符号数为N,选取前N−θN + 1个符号,建立符号-代码片段的反向索引。
其次,通过语法计算函数特征。通过构建源代码函数级别的抽象语法树,以及二进制代码对应汇编代码的控制流图,构建语法级别的特征向量,并将特征向量存入数据库。抽象语法树上的特征分为树的节点个数、深度及树的前向遍历顺序等类别。
函数控制流图上的特征分为如下两类。
● 跳转指令。具体地,将跳转指令分为个数、位置、跳转目标位置3个维度,其中位置和跳转目标位置使用相对位置进行编码,能够较准确地抽象控制流图中的控制流特征。
● 函数调用指令。该特征能够在一定程度上刻画函数的功能。例如,如果仅调用字符串拼接函数,可能执行的是字符串操作功能。基于控制流图中的跳转指令特征和函数调用特征,以函数为单位计算特征值。这一步将特征向量表示为局部敏感的哈希值。
最后,通过语义计算函数特征。首先,对源代码进行值依赖分析。对于二进制代码而言,仅反汇编是不够的,因为汇编代码在不同的指令架构及不同的操作系统中均有不同的结构,这为构建统一的值依赖模型造成困难。为解决这个问题,基于中间语言为二进制代码建立值依赖模型。国际上已存在很多中间语言,如RREIL、Vine、BAP、LLVM IR等。这里将基于RREIL语言实现二进制代码到中间语言的提升,从而消除指令架构、操作系统等在值依赖上的差异,使用统一的框架分析值依赖关系。对源代码或中间代码对应的中间表示进行进一步的值依赖分析,计算常量值并去除不可达路径,从而将中间代码进一步归一化,按照值依赖的依赖关系(依赖图中的指向关系),构建各依赖子图上的特征向量,存入数据库。
对于二进制代码而言,根据编译选项,同一段源代码可以有多个二进制目标码的表现形式。通常要考虑到几个重要的编译选项类别,如目标平台、调试与否、动态或静态链接,以及其他一些优化选项,而且不同编译器的特点也不尽相同。因此,要就二进制的多种编译状态进行函数特征提取,从而判断多个编译器、多个编译选项的二进制文件,实现识别匹配。
如果分析的是面向对象语言的源代码,则类级特征通过函数特征推导出。计算匹配的函数占类中所有函数的比例,根据阈值决定类级的推导结果。
文件级别的特征通过文本特征提取方法提取。具体而言,首先将该项目分解为包和文件,并将包进一步分解为多个文件。文本特征通过MD5求得。对于二进制代码,基于字节流的方式计算MD5值。
此外,通过类和函数级别的特征推导文件级别的特征。计算匹配的类和函数占文件中所有类与函数的比例,根据阈值决定文件级的推导结果。
包和项目级别的特征通过文件特征推导。根据上一级别得到的匹配的文件个数,计算匹配的相似度。根据预设的阈值,确定包与项目级别的推导结果。
将源代码文件降维为一个特定的指纹,实质上定义了一个f维的空间。在这个空间中,每一个源代码文件的特征映射到一个向量。结合各自的权重对代码的所有特征向量进行加权求和,得到的向量则可以表征源代码的特点。考虑到大规模数据的特点,对这个和向量做进一步的压缩转化,最终得到一个64位的二进制签名值,即该源代码文件的指纹值。
基于相似哈希的代码特征构建技术主要分为3个阶段——预处理阶段、指纹索引阶段、相似匹配阶段。
在预处理阶段,对代码进行特征的筛选和提取,输出指纹值。在指纹索引阶段,根据特定的索引策略将指纹存入相似哈希指纹库中,以便于快速匹配。在相似匹配阶段,对待测工程文件进行一系列处理,查询出溯源检测的结果。
代码中一些无关因素(如空行、空格、注释等)会影响生成的哈希结果。为了提高相似性匹配结果的精确度,需要对源代码进行统一的格式化处理。
经过多次实验和改进,为了解决高频特征对低频特征的湮没问题,同时提高整体运行效率,在特征提取粒度上选择最符合数据特点的行级别,以代码格式化后的每一行作为源代码文件的一个特征值。在特征提取的过程中,通过正则表达式对特征进行筛选,排除一些不含语义的纯符号行。
使用传统的哈希算法MurMurhash,针对源代码文件的每一个特征计算出一个对应的哈希值(64位的二进制数串)。
以每个特征出现的频次作为特征的权重进行加权求和,哈希值每一位的0或者1决定了其与权重值是正相乘还是负相乘,将加权后的每一个哈希值逐位相加即可得到结果序列串。
降维指的是对加权求和后的结果序列串进行变换。对于每一位,若它为正数,则将其变换为1;否则,将其变换为0。通过降维,得到最终的相似哈希指纹值,如图1-8所示。
▲图1-8 降维分析
在指纹索引阶段,对于两个源代码文件,判断它们是否相似的标准是它们之间的汉明距离的大小。在海量的数据集中,寻找与被测代码指纹相近的指纹值的过程是非常耗时的,因此本项目使用了索引优化方法,通过分段索引的方式大幅提高了查询效率。
X位的指纹值可以表达的数据长度为2X,库中总共包含的指纹数目为2Y,基于数值的随机性,库中的指纹会相对均匀地分布在大小为2X的空间中。以数的高Y位作为计数器和索引,就能在每次查询中快速地将需要查询的结果定位到一个很小的范围内。最后,依次查看所有符合条件的候选结果,计算汉明距离。
结合抽屉原理和上述理论,这里使用分段索引方法,将每个指纹分成5段,长度分别为13,13,13,12,12,并通过排列组合将其重组作为索引,分别记录在数据库的字段中。这使在相似匹配阶段中被测项目的哈希值能够通过索引快速映射到可能匹配的候选序列,进一步得到溯源结果。在大小为264的数据集上,这样的索引方法能够将每次相似搜索的候选序列的范围降低到26,大幅提高检索效率。
完成以上步骤后,相似哈希指纹库构建完毕,在相似匹配阶段对待测项目进行溯源检测,该阶段的输入为待测项目的源代码,输出为所有指纹库中查询到的与输入项目中代码相似的结果。
首先,与预处理阶段相似,要对文件进行标准化、特征提取及相似哈希指纹生成的操作。然后,类似于指纹索引阶段,要将待查询的相似哈希指纹值分成5段,两两排列组合后得到10个索引片段,分别与数据库中对应的字段进行精确匹配,匹配的文件即为可能与待测文件相似的文件。最后,为所有候选序列中的哈希值计算海明距离,并汇总最终计算出来的结果。
输入可能同时包含源代码和二进制代码。其中,二进制代码可能由开源代码编译而来,或者是第三方闭源的二进制代码库。为保证高效、高精度的代码识别匹配,使用流水线的方式,分级、分层进行代码的成分识别。
首先,对代码进行反混淆预处理。然后,按目录结构划分为多个包,每个包下有多个文件。按文件、函数粒度,在文本、标识、语法、语义等方面使用相应方法提取特征。根据特征,进行匹配。最后,在类、文件、包、项目级别综合上一级别的匹配结果,根据相似度阈值,给出该级别的匹配结果。详细流程如图1-6所示。
下面分别描述针对文本分析、标识分析、语法分析、语义分析等方法提取的特征对应的匹配方法。
在文本分析方面,对非代码文本进行模糊特征匹配,对代码文本进行精确特征匹配。如果匹配成功,则直接返回结果,并在包和项目层分别判断匹配结果。如果包中的所有文件完全匹配,项目中的所有包和文件完全匹配,则项目直接匹配成功;否则,进行标识分析。
在标识分析方面,首先将待匹配的代码片段转化为<符号,频数>的表示,并按照全局频数升序排列,选取前N−θN + 1个符号,依次查询部分索引,获得可能相似的代码片段并以它们作为候选代码片段。然后,一一验证候选代码片段,获取其<符号,频数>的表示,进一步根据包含的符号数目、重叠符号的位置及符号重叠数目的上界估计过滤。最后剩下的代码片段经验证后输出为待匹配的代码片段的副本。
在语法分析方面,结合抽象语法树或二进制代码对应中间代码的控制流图,提取被检项目文件中语法级别的特征向量,具体包括函数跳转和函数调用等特征,通过值的方式表示,并和库中的特征向量进行匹配,进行相似度计算。
在语义分析方面,对源代码及二进制代码对应中间代码进行值依赖分析,并基于依赖的切片进行子图特征计算,和库中的相应特征进行匹配。无论是否匹配成功,都把匹配结果返回类级和文件级,综合文本、标识和语法级别的匹配结果,依次计算类、文件、包、项目的相似度和匹配结果,从而给出最后的报告。
被检项目源代码的识别在多个语言解析器的支持下工作。根据匹配算法,计算与特征值索引数据库的匹配情况。针对强匹配算法,源代码的特征值必须与索引数据库的特征值一致,才可被视为该开源组件;针对非强匹配算法,如混淆后的代码,则需要计算特征值之间的相似度,根据相似度的阈值确定是否是该开源组件。
目前常见的混淆方法可以分为如下3类。
● 布局混淆技术:通过修改代码的排列方式等混淆代码。布局混淆技术通过重叠二进制代码的指令改变了代码布局。例如,对于一段二进制代码0x8B0x440x240x04而言,如果从0x8B开始反汇编,则结果为mov指令;而如果从0x44开始反汇编,则结果为inc指令和and指令。解析起始位置的不同将导致汇编代码的不同。
● 控制流混淆技术:通过修改代码的控制流结构混淆代码。例如,通过使用恒为真的条件对已有控制流进行包装,并向假的分支中插入很多垃圾代码,修改控制流的结构。反汇编工具如果不能识别恒为真的条件,就可能进入另一个分支,继续执行反汇编,而该分支中的垃圾代码可能导致反汇编失败。
● 数据流混淆技术:例如,通过缩减、扩大、分割、合并数组等方式修改数组结构,从而对数据进行混淆。通过加减等操作对已有数据进行等价变换,从而对变量进行混淆。
虽然代码混淆技术有效,但是完美的混淆技术是不存在的。目前反混淆技术主要分为一般性反混淆技术和基于混淆模式的反混淆技术。一般性反混淆技术不假设代码使用了混淆技术,而对二进制代码进行更精确的解析。基于混淆模式的反混淆技术是对混淆技术的一个逆向工程与反向混淆过程,从而有针对性地恢复混淆之前的代码。
使用一般性反混淆技术与基于混淆模式的反混淆技术相结合的方法,实现代码反混淆。
在一般性反混淆技术方面,基于值依赖模型进行代码反混淆,通过在汇编代码上建立值依赖模型,计算出不可达路径、数据常量值等控制流和数据流的信息,从而提高反汇编和反编译的精度。
在基于混淆模式的反混淆技术方面,基于搜索算法优化反混淆精度,通过有机组合各种混淆模式以达到最优混淆效果。虽然当前的反混淆技术考虑了特定的混淆模式,但是在混淆模式组合在一起时无法高精度地进行反混淆。我们使用多种反混淆技术相结合的思路,基于搜索算法对反混淆技术进行有效组合,从而实现代码反混淆。
值依赖关系可以反映变量的生命周期及变化情况,因此值依赖关系可以以变量追踪的形式分析代码中引入的各个变量,从而构建代码掌控度模型。基于值依赖分析的代码掌控度综合分析分为构建值依赖模型和基于值依赖模型的函数内分析两部分。
构建值依赖模型主要分3步。
(1)非守卫值依赖分析,目标是建立变量定义与使用的依赖关系。这种依赖关系可能由函数内的定值与使用构成,别名关系也可能导致定义与使用关系并不是一个变量,而这种别名关系有可能是跨函数的,因此在此阶段引入跨函数的Mod-Effect分析。此外,为了解决全局变量对值依赖(对象域)的影响,采用全局分析的方法。
(2)守卫值依赖分析,主要进行值依赖图的守卫构建。在初步值依赖图的基础上,通过分析分支语句及控制流对每条边进行守卫计算,并将计算结果标注在相应的边上。如果未标注,则表示守卫值为true。
(3)精化值依赖分析,主要进行值依赖图的精化构建。相对于原来的值依赖分析,本项目中,增加了常量传播与常量折叠、对新的依赖关系的约减分析及针对循环的约减分析。
完成值依赖模型构建后,需要进行基于值依赖模型的函数内分析。这主要包括如下部分。
● 基于值依赖模型的程序切片。由于跨函数的特性,值依赖模型并没有建立每个函数的摘要信息。因此,在该模型上进行分析,需要首先获取与漏洞相关的节点及路径,去掉与漏洞无关的节点,即对值依赖模型进行切片,以提高分析的效率。
● 约束表达式预处理。在计算可满足性之前,约束表达式可以通过一些预处理进一步提高精度和效率,如复制传播分析、常量值传播与折叠、别名指针替换、运算律化简等方法。
● 函数内路径分析。为了降低误报率,在检测算法中必须考虑不合适路径(infeasible path)问题。对于不合适路径的处理,采用符号执行的方法,往往效果较好,但是吞吐量存在问题。因此,我们在值依赖模型的基础上提出一种新的方法,该方法能够通过扩展约束表达式引入新的约束公式,以增强原有语义的方式,去掉不合适路径所产生的误报。
根据引入代码的组成鉴别结果和值依赖图分析代码自主研发率,首先确定成功匹配的行数,以这些作为非自主研发的代码部分,然后分别以文件和函数为单位,对非自主研发的部分代码与完整代码求比值。非自主研发的部分代码不在理解、掌握范围内,因此通过分别计算文件级和函数级的自主研发率,得到不同层次的代码掌控度。
分析层(由代码掌控度组成)代表的是代码中掌控的代码程度及具体内容,包括基于值依赖图的文件级和函数级代码自主研发率分析计算、许可证分析、代码中自主研发部分的前台展示,基于代码成分分析结果,向用户展示用户代码与匹配代码的比对结果。
根据代码识别结果分析代码自主研发率,首先确定成功匹配的行数,以这些作为非自主研发的代码部分,然后分别以文件和函数为单位,对非自主研发的部分代码与完整代码求比值。非自主研发的部分代码不在理解、掌握范围内,因此通过分别计算文件级和函数级的自主研发率,结合代码许可证分析计算,得到不同层次的代码掌控度。
代码来源风险和安全风险分析技术从代码的成分分析结果出发,分析代码的闭源、授权和开源部分后,得到代码掌控度。对不同来源的代码进行分析,对来源不明的代码进行标记,得到代码的来源风险;对不同来源和不同掌控程度的代码进行漏洞挖掘与分析,得到代码的安全风险。
在这里,我们实现基于搜索引擎的代码与漏洞自动化对齐,将由开发商与项目名称构成的键值对映射至指定的一个或者多个下载链接,以此来标识项目是否包含安全漏洞。
具体步骤如下。
(1)抓取当前流行的安全漏洞发布网站发布的安全漏洞信息,并进行实时更新,建立公开安全漏洞资源数据库。该数据库包含已知的安全漏洞信息和相关的安全漏洞的软件信息。
(2)根据公开安全漏洞资源数据库,建立包含公开安全漏洞的软件信息数据库。在建立最初,数据库只包含由开发商和项目名称构成的键值对、项目版本及指向的安全漏洞。
(3)以上述键值对为关键词,在指定网站范围(如某些著名开源社区或者二进制仓库)内进行搜索。抓取搜索引擎推荐的搜索结果,进一步完善软件信息数据库,将对应键值对的搜索结果插入数据库。
(4)根据完善之后的软件信息数据库进行过滤,筛选其中符合对齐要求的软件信息并将其设置为可信,标识其为对齐的数据。
(5)每次更新安全漏洞资源数据库时查找并更新软件信息数据库,以保证其实时性。
通过上述流程,自动建立一个实时更新的安全漏洞信息数据库和软件信息数据库,二者之间以公开的安全漏洞编号作为关联键值。对齐的结果来自搜索引擎的加权结果,取自大量用户的搜索频率和点击频率,经过抽样验证,正确率较高。同时,过滤掉名称相同或者名称格式不规范所带来的文本匹配困难,有效降低误报率。
软件代码搜索的大规模并行处理技术分为高速采集同步技术、分布式存储技术、大规模并行检索技术。
若使用高速采集同步技术,我们需要设计基于知识图谱的软件代码智能目录,通过分布式网络爬虫集群,进行源代码的采集和更新,同时存储基于开源代码本体的元数据。
分布式存储技术实现了软件代码的数据存储机制、分布式数据管理机制、低成本的水平扩展机制,以及分布式数据操作平台和数据仓库平台。代码和代码元数据涉及结构化数据与非结构化数据的存储与融合,对于它们,要研究基于机器学习的异构数据融合机制。为了提供代码版本管理和分布式操作的高层服务,要在Git集群和SSH服务集群的基础上,结合基于Redis和消息队列的分布式缓存机制,设计基于微服务的Git API。
对于大规模并行检索技术,首先要设计特征库生成方案,在中间表示(文本、标识、抽象语法树、程序依赖图)和特征粒度(函数、类、文件、包、项目)上,设计特征匹配算法;在处理大规模并发上,要将Spark引入特征匹配算法中进行并行优化,结合智能的查询分发机制和基于Nginx的负载均衡机制,形成一套大规模分级搜索的算法架构。
SCA类工具的主要功能如下。
● 支持20种以上检测语言,支持二进制文件、jar包和动态链接库检测,支持闭源的二进制文件(.jar、.dll、.exe)检测。
● 代码库具有500多万个项目、10亿多次提交、超过30亿个文件、5000亿行代码,并且上述数据需要在本地安装、部署。
● 安全漏洞库具有NVD、CNVD、CNNVD,可防止10万多个安全漏洞。
● 对于被检测项目,能够实现提供检测项目的版本号、发布时间、检测时间、代码行数及占用的硬盘空间等概要信息。
● 对于被检测项目,能够提供自主研发率、开源组件占比分析、组件匹配情况、部分匹配情况和外部依赖情况信息。
● 对于被检测项目,能够提供包含的全部组件名称、每个组件匹配的文件个数、组件来源及包含的安全漏洞数量,并为用户查看安全漏洞提供向导功能。
● 对于被检测项目,能够提供包含的全部安全漏洞数量及高、中、低等级的安全漏洞数量或占比。
● 对于检测出的每个组件成分,不仅提供当前版本号、发布日期、包含的安全漏洞数量(包括高、中、低等级的安全漏洞数量)、组件类型(开源、闭源),还提供组件对应的最新版本信息,以及建议升级到相应版本的信息,并为用户提供升级下载的链接。
● 对于检测出的每个组件成分,提供组件许可协议的详细信息,包括简称、风险等级、使用范围、影响项目等,支持60种以上的许可证类型。
● 对于检测出的每个组件成分,提供组件包含的安全漏洞详情,包括安全漏洞CVE官方名称、发布日期,安全漏洞对应的CWE漏洞类型,至少提供CVSS 2.0或CVSS 3.0中一个通用安全漏洞评分系统评分,评分包括严重程度、可利用性和影响项目信息。对于每个安全漏洞,提供安全漏洞详情描述、导向CWE官方网站的详情页面链接,让用户能够快速掌握安全漏洞信息。
● 支持项目、组件、安全漏洞三者之间的关联分析,既能够通过项目找到组件及其包含的安全漏洞,也能够通过安全漏洞找到存在的组件,以及影响的项目数量、项目名称等信息。
● 支持检索功能,能够根据检测项目的开发语言、检测状态、项目名称、包含安全漏洞级别等信息项目检索。
● 能够对两个相似项目进行对比检测,通过设定基准项目,分析相同文件、相似文件。
● 能够统计当前用户项目数量和状态信息、组件语言等信息,所有组件以及其安全漏洞信息、组件开发语言信息等,所有安全漏洞及其严重级别统计、影响项目统计、安全漏洞类型统计等信息。
● 当构件安全漏洞数据更新后,系统根据更新安全漏洞数据检测当前已检测项目,查看是否存在新的安全漏洞。
● 系统能够支持使用压缩包、SVN、Git等代码版本管理工具导入被检测项目。
● 能够进行不同级别分析,包括文件级、函数级及标识级,检测速度达到100万行/小时。
随着安全形势的日益严峻,各企业对代码安全越来越重视,对代码安全人才的需求也日益增多。要成为一名合格的代码安全检测工程师,需要一些基础条件并不断地学习。
基础条件如下。
● 所学专业是与计算机相关的专业,理解缺陷和安全漏洞原理。
● 了解C、C++、Java等语言,能够读懂代码。
具有上面的基础以后,代码安全检测工程师还需要怎么提升自己呢?
(1)阅读关于OWASP Top 10的资料。OWASP Top 10安全漏洞类型是行业事实上的参考依据,是企业重点关注的安全漏洞类型。阅读这些资料对理解安全漏洞非常有帮助。
(2)浏览CWE网站。CWE网站上有大量的缺陷分类信息,包括CWE Top 25,这些缺陷类型是企业普遍关注的缺陷类型,这些缺陷类型相对于OWASP Top 10更侧重于运行时缺陷。
(3)使用本书介绍的代码安全审计/检测练习靶场,学习分析代码中的缺陷。最佳学习方法是根据安全漏洞表现形式,分析源代码为什么被攻击,找到代码的污点轨迹,再通过源代码检测审计工具扫描源代码/目标码,检测出代码缺陷,报出对应的缺陷类型。这非常有利于理解问题的原因,提升人工分析能力。
(4)学习目前CNAS认证的代码检测标准——GB/T 34943、GB/T 34944和GB/T 34946,它们分别是面向C/C++、Java、C#语言的代码检测标准,每个标准包括几十条缺陷/安全漏洞。
OWASP基准测试项目是OWASP组织下的一个开源项目,又叫作OWASP基准测试项目,是免费且开放的测试套件。它可以用来评估SAST类自动化静态分析工具的覆盖范围和准确性。当前版本为1.2 Beta版,共有2740个Java真假安全漏洞案例,都有对应的源代码,也可以进行编译,产生字节码,且可以部署演示。
无论是针对源代码的扫描分析类工具,还是面向中间码的扫错分析工具,它们都可以进行用来进行项目案例扫描。通过部署,在应用级别上进行对照渗透测试的输入,从对应代码中查找污点轨迹,这对练习代码检测非常有帮助。可以练习的安全漏洞包括命令行注入、弱密码、弱哈希、LADP注入、路径穿透、安全Cookie、SQL注入、违反信任边界、弱随时值、XPATH注入、跨站点脚本攻击(Cross Site Scripting,XSS)。从OWASP官方网站下载OWASP Benchmark最新包。
WebGoat是OWASP组织研制出的用于进行Web安全漏洞实验的应用平台,用来说明Web应用中存在的安全漏洞。WebGoat运行在带有JVM的平台上。
某些课程也给出了视频演示,指导用户利用这些安全漏洞进行攻击。可演示的安全漏洞包括XSS、访问控制、线程安全、操作隐藏字段、操纵参数、弱会话Cookie、SQL盲注、数字型SQL注入、字符串型SQL注入、Open Authentication失效、危险的HTML注释等。部署后,从应用上验证安全漏洞,也可以对应到源代码上。WebGoat是开源项目,用户可以从网站上下载OWASP Webgoat的源代码,并可以对其进行编译。
OWASP Juice Shop也是OWASP项目,以JavaScript、HTML语言为主,典型安全漏洞不多,包含OWASP的十大安全漏洞。
在PHP/MySQL环境中写的一个Web靶场可演示暴力破解、命令注入、跨站请求伪造、文件包含、文件上传、不安全的验证码、SQL注入、SQL盲注、反射型跨站脚本攻击、存储型跨站脚本攻击类型的安全漏洞。
Juliet由美国国家标准与技术研究院(National Institute of Standards and Technology,NIST)研发和维护。其中有一套C/C++测试套件和一套Java测试套件,用于对SAST类工具进行基准测试。其中的源代码可以编译。当前版本为Juliet C/C++ 1.3版本,全部使用C/C++编写,共有64000个测试案例文件,包含118个比较典型的CWE缺陷类型。
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e60104”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。