基于源代码的漏洞分析方法

1. 基本概念

(1)基本块:基本块是指一组连续的程序指令,并且只有一个入口指令和一个退出指令(不一定是跳转指令)。一旦执行基本块中的入口指令,则其余的指令也都会执行

(2)程序执行路径:程序执行路径是指在程序执行期间所执行的一组指令序列。 如果至少存在一个输入能够使程序执行特定路径,则称该路径为可达的程序执行路径,反之则称为不可达的程序执行路径。

(3)控制流图:控制流图(Control Flow Graph)通过一个有向图来表示程序的控制流,简写为 CFG。CFG 中的节点表示程序的基本块,有向边表示两个基本块可以从前一个基本块跳转到后一个基本块。

(4)分支距离:分支距离指 CFG 中从一个基本块到另一个基本块的最短路径中的条件分支指令的数量。通常可以把 CFG 表示成加权有向图来计算,其中条件分支的每条边的权重都分配 1,而无条件分支指令的每条边的权重都分配为 0。

(5)调用图:调用图是表示程序中函数之间调用关系的有向图。每个节点对应于一个函数,边(p1, p2)表示函数p1可以调用函数p2。

(6)过程间控制流图:过程间控制流图(Inter-procedural Control-Flow Graph),简写为 iCFG,它组合了 CFG 和调用图。

过程间控制流图iCFG

(7)控制流平展化

控制流平展化

2. 静态分析流程

此部分参考《源代码软件漏洞自动挖掘关键技术_孟庆坤》

对 Java 源代码进行词法分析、语法分析构建 Java 程序的抽象语法树。并通过遍历抽象语法树收集源程序的各种信息进行代码转化,将源代码转化成等价的中间表示结构。

分析流程

语法生成树
语法生成树是语法分析器的直接产出结果,也是生成另外三个基本中间表示(抽象语法树Absrtact Dyntax Tree,AST、控制流图Control Flow Graph,CFG、数据流图Data Flow Graph,DFG)的基础。demo如下:

语法生成树示例

语法抽象树
不同于语法分析树,抽象语法树不讲究和程序的每个 token 表示完全一致,其更偏向于语义的相似性,经常被用来检验源代码的相似性。但是抽象语法树不适用于更复杂的代码分析,例如死代码和以及未初始化的变量的检测,其原因是抽象语法树不能提供明显的控制流和数据流信息。

语法抽象树示例

抽象语法树是有顺序的树结构,内部节点是操作符(例如“+”和“=”)而叶子节点是操作数(例如常量和标识符)

控制流图与数据流图
相对于抽象语法树,控制流图的每条边都有一个标签 true、false 以及 ε,分别
表示控制语句的二类出边以及连接顺序语句的边。数据流图则能够表示语句之间数据依赖关系。在数据流分析中,可以不实际运行程序,直接考察数据的转移情况,从而获取数据流属性信息。

控制流图与数据流图

3.污点分析

污点分析主要包括污点源、污点汇聚点和无害化处理等部分。其中,污点源表示将污点数据引入到系统中;污点汇聚点表示系统将污点数据输出到敏感数据区或者外界,造成敏感数据区被非法改写或者隐私数据泄露;无害化处理表示通过数据加密或重新赋值等操作使数据传播不再对系统的 完整性和保密性产生危害。以下面的代码为例说明污点分析的一般过程

污点分析模型

污点分析一般存在两个问题,即过污染和欠污染。其中,过污染是指在污染分析过程中将与污点源没有数据和控制依赖关系的数据变量标记为污点变量,即产生误报;欠污染则是 在污染分析过程中将与污点源存在数据或控制依赖关系的程 序变量标记为非污点变量,即产生漏报。对控制依赖关系的分析不足和不当可能会造成过污染和欠污染问题。

4. 符号执行

此部分参考《符号执行在软件安全领域中的研究与应用_宋雪勦》

符号执行是一种程序分析技术,最早由 King 等人在 1975 年提出,用于在逻辑上推理程序的正确性。符号执行以符号值作为输入来执行程序,其中符号值由符号以及它所对应的具体值集所组成,例如 表示一个 32 位整数的符号,那么其值可以是所有 32 位整数集合中的任意一个值。
符号执行引擎在任何时候都会维护一个state (stmt,α,π),stmt表示下一条要执行的语句,α表示当前的符号执行存储状态(把程序中的变量与具体值或αi进行关联),π表示路径约束,表示程序执行到stmt时,符号αi需要满足的一组逻辑公式。

void foobar(int a, int b){
    int x=1,y=0
    if(a!=0) {
        y=x+3
        if(b==0){
            x=2*(a+b)
        }
        assert(x-y !=0) //断言
    }
}

假设我们的目标是确定哪些输入能够使 foobar 函数中的第 8 行的断言失败。初始状态 A 的路径约束π=true ,变量a和b分别用符号αa和αb表示,符号在每一个条件分支语句会执行出不同的状态,如C、D、G、H等。如果要使断言失败就是要让约束x-y=0成立,其实也就是需要使2(αa+αb)-4=0∧αa≠0∧αb=0成立,然后使用 SMT(Satisfiability Modulo theories)求解器对路径约束表达式进行求解得到输入为α=2,b=0时就可以使第 8 行的断言失败。整体过程如下:

符号执行树示例

此Demo是一个静态符号执行的分析,但是在实际应用中库函数或系统调用等可能无法跟踪,如果跟踪了存储状态会呈指数级增长,很难达到特定代码部分。符号执行引擎会不断调用SMT求解器,较为影响性能。因此也催生出了动态符号执行,使用具体执行来驱动静态符号执行。

如果是动态符号执行,会假设以a=1,b=1作为输入,那么具体执行路径即为执行树中的A->B->C->E->G,在执行完第八行断言成功后,可以把最后一个路径约束取反,例如把αb≠0取反,那么得到新的输入a=1,b=0,并且执行可以得到新的路径A->B->C->E->F。但是既然有选择输入,就会存在路径约束误报和路径偏差等劣势。动态符号执行也是一种白盒Fuzzing

路径选择是动态符号执行中的一个关键技术,指在我们收集约束条件后,采用一定的策略对其中的约束进行取反,这个策略称为路径约束策略,一般有两种:随机路径选择、覆盖率优化策略。前者保证了“公平”但可能存在路径空间爆炸。后者是以提高覆盖率为前提采取对路径中的节点附加权值来实现。

还有一种情况是,用户只想运行程序中的特定部分,也衍生出选择性符号执行方法。此方法在运行该特定部分时会采用多路径模式,而执行其他代码时采用单路径模式。

符号执行引擎对比分析

5. 插桩技术

程序插桩技术是指在被测试过程中插入探针,接着执行程序的同时探针也被执行,获取程序执行中的信息,如控制流信息或数据流信息,插桩技术也可以分为:目标代码插桩、源代码插桩。插桩的目的是得到程序运行时的一些动态信息。插桩技术也是动态符号执行的关键技术之一。

目标代码插桩又称为动态插桩技术,一般情况下处理的是二进制代码或字节码。在可执行程序运行时,要监控内存,当检测到下一条要执行的语句应该插桩时,就执行探针函数,暂停程序,待探针函数执行完再继续程序的执行。这种方式的插桩针对执行代码,程序的源代码也不用处理。而源代码插桩不用经过程序执行,是直接对源文件进行插桩。它先对源文件进行扫描,进行词法和语法分析,找到插桩点,在该点植入探针。这些操作是在程序执行之前进行。

web应用测试脚本插桩流程

6. Fuzzing

Fuzzing最初是由威斯康辛大学的巴顿·米勒在20世纪90年代提出的,源于软件测试中的黑盒测试技术。它的基本思想是把一组随机数据作为程序的输入,并监视程序运行过程中的任何异常,通过记录导致异常的输入数据进一步定位软件中缺陷的位置。


不同技术的比较

模糊测试是目前主流的漏洞挖掘方法,它通过随机地生成大量测试用例来执行程序,同时在执行时监控程序是否崩溃,再根据测试用例判断是否由于漏洞导致程序崩溃。测试用例(种子)对于Fuzzing的影响很大,所以Fuzzing面临的主要挑战包含种子初始化、种子选择、种子变异、高速测试、代码覆盖率、违规检查、漏洞分析等。


Fuzzing通用流程
自动化漏洞挖掘框架

6.1 分类

Fuzzer分类有很多方法,如以测试用例生成办法分类、以项目探查方法分类(定向Fuzzing、基于覆盖的Fuzzing)、以是否存在反馈机制分类(Dumb Fuzz、Smart Fuzz)

(1)根据测试用例生成方式分类有如下几种:
基于变异:对正常输入文件或参数的一部分进行随机性的变异,将修改后的值作为测试用例输入。这种方法需要已知的有效种子文件,尤其是包含所有程序分支的种子文件,最大程度的使程序的所有代码都被执行。这是 Fuzzing 测试中最简单、直接的方法,许多 Fuzzing 测试人员使用这种方法生成大量测试用例去测试软件。通常是在对格式或者协议有所了解的前提下,对获得的样本数据中的某些域进行变化,从而产生新的变异数据。该方法对初始值有着很强的依赖性,不同的初始值会带来差异很大的代码覆盖率,从而会产生差异很大的Fuzzing效果。

基于生成:根据程序语法的模版自动化的生成测试用例,而不是随机性的改变可用的种子文件。该方法需要对程序的文件结构和被测协议有详细的了解,对其进行建模,然后自动化的生成测试用例。 通常是给出文件格式或者网络协议具体的描述规则,然后依据此规则产生测试数据。该方法需要用户对格式或者协议有非常深的了解,并需要大量的人工参与

优缺点比较

(2)以项目探查方法分类
定向Fuzzing:生成覆盖目标代码和程序目标路径的测试用例。首先通过静态分析技术定位目标程序中的脆弱语句的位置,通过动态分析技术找到输入元素和潜在脆弱语句之间的影响映射关系,针对每个潜在脆弱点变异影响它的输入元素不断地生成新的测试用例以触发潜在漏洞。

基于覆盖的fuzzing:生成覆盖尽可能多的程序代码的测试用例,期望更全面的测试,并尽可能多地检测错误

6.2 AFL

AFL即American Fuzzy Lop是当今使用最广泛的Fuzzer,基于变异和边覆盖。在执行过程中,AFL向被测程序中进行输入,然后获取程序的覆盖率,将覆盖率大的input保留下来进行变异,然后在下一轮测试中向被测程序中输出这些变异后的输入,一直到程序的覆盖率在较长的一段时间不能继续增大为止。

(1)AFL通用流程

AFL通用流程

无论改善哪个以上步骤,都可促进fuzzer的有效性和效率。(测试速度、覆盖率的准
确度、种子选择策略、种子突变策略和对安全违规的敏感性等)

基于突变的fuzzer测试效果很大程度上依赖于初始种子的质量。好的初始种子可以显著提高fuzzing的效果,如提供格式良好的种子输入可以节省构建一个种子的大量cpu时间,良好的种子可以满足复杂文件格式的要求,这在变异阶段很难猜到,基于良好格式种子的突变更有可能产生可能达到更深且难以到达的路径的测试用例,良好的种子可以在多次测试中重复使用。

(2)常见种子收集方法
Standard benchmarks:开源应用程序通常使用标准基准测试发布,可以免费用于测试项目。 提供的基准是根据应用程序的特征和功能构建的,自然地构建了一组良好的种子输入
Crawling from Internet:考虑到目标应用程序输入的多样性,从Internet上爬取是最直观的方法。对于一些常用的文件格式,网络上有许多提供免费测试数据集的开放测试项目。

POC samples:AFL可提取达到相同代码覆盖率的最小输入集合。

良好的种子选择策略,可以帮助fuzzer覆盖更多的代码,触发更多漏洞。种子选择策略包括AFL: smaller and faster、AFLFast:low-frequent paths、AFLGo:directed selection、SlowFuzz:characteristics of known vulnerabilities。对于种子变异也有相应的策略,主要分为对于源种子、变异位置和变异值进行改变。

(3)代码覆盖率
由于程序行为的不确定性,对程序状态不存在一个能在程序运行时轻易测探的度量标准。代码覆盖率便成为了一个解决方案,但它是一个近似的度量,不能完全反映程序状态的信息

image.png

代码覆盖率的计算方式:
在程序分析中,程序由基本块组成。基本块是只有一个入口和出口点的代码片段,基本块中的指令将顺序执行,并且只执行一次。目前通常以基本块作为最佳单位计算代码覆盖率。基本块是程序执行的最小相干单位,测量功能或指令会导致信息丢失或冗余。通过第一个指令的地址可以识别基本块,通过代码检测可以方便地提取基本块信息。

覆盖粒度包含了路径覆盖、块覆盖和边覆盖。路径覆盖跟踪完整和准确的路径覆盖将导致较高的开销,所以一般使用粗略的覆盖信息,如块覆盖和边覆盖。块覆盖是在测试期间跟踪每个块的命中次数。这种方式被VUzzer、libFuzzer、honggfuzz等fuzzer广泛采用。边覆盖是跟踪每个边的命中次数、它不跟踪边执行的顺序。AFL采用的就是静态/动态插桩跟踪边覆盖。根据是否提供源代码,AFL提供编译器内置插桩方式和外置插桩方式。前者根据我们使用的编译器提供gcc模式和llvm模式,在生成二进制文件时对代码片段进行插桩。后者当基本块转换为TCG块时,qemu模式将对代码片段进行插桩。

AFL代码覆盖率计算

使用位图(默认大小为64KB)跟踪应用程序的边覆盖率。位图的每个字节表示特定边的统计信息(例如,命中次数)。然后计算每条边的哈希值,并将其用作位图的键。Hash计算:cur ⊕ (prev ≫ 1) 此方式速度快但容易发生哈希碰撞。key值得随机性导致两条不同的边可能会有同一个hash值。

位图是64KB,因此最多可以存储65.5K条边,而不会产生冲突。但当程序的边数大于65.5K条时,必然会存在边与其他边碰撞。

统计表

因为AFL对每条边使用固定的哈希工时进行计算,存在哈希碰撞的问题,CollAFL对其进行了改进,通过对不同边应用不同的哈希公式Fmul(cur, prev) = (cur ≫ x) ⊕ (prev ≫ y) + z
CollAFL代码覆盖率计算

综上,覆盖率整体概况可以用一张图来表示


覆盖率概况

(4)违规检查
Fuzzers监视目标程序执行期间的执行状态,期望异常和崩溃。可以使用不同的机制,例如静态检测、动态二进制检测、调试甚至系统仿真,来检测目标应用程序并跟踪有用的信息。

常用的异常监视方法包括对特定系统信号、崩溃和其他违规行为的监视。对于没有直观程序异常行为的违规,可以使用很多工具,包括AddressSanitizer 、DataFlowsanitizer 、ThreadSanitizer 等。当捕捉到违规时,Fuzzers会存储相应的测试用例,以备重播和分析。

AFL下载
http://lcamtuf.coredump.cx/afl/
简单例子
https://blog.csdn.net/yalecaltech/article/details/88752848
https://xz.aliyun.com/t/4314
练习
https://www.freebuf.com/articles/system/191543.html
https://blog.csdn.net/youkawa/article/details/45696317

你可能感兴趣的:(基于源代码的漏洞分析方法)