符号执行相关
符号执行 (Symbolic Execution)是一种程序分析技术。其可以通过分析程序来得到让特定代码区域执行的输入。使用符号执行分析一个程序时,该程序会使用符号值作为输入,而非一般执行程序时使用的具体值。在达到目标代码时,分析器可以得到相应的路径约束,然后通过约束求解器来得到可以触发目标代码的具体值。
符号模拟技术(symbolic simulation)则把类似的思想用于硬件分析。符号计算(Symbolic computation)则用于数学表达式分析。
基本原理
符号执行的关键思想就是,把输入变为符号值,那么程序计算的输出值就是一个符号输入值的函数。这个符号化的过程在上一篇AEG文章中已有简要阐述,简而言之,就是一个程序执行的路径通常是true和false条件的序列,这些条件是在分支语句处产生的。在序列的 i t h i^{th} ith位置的判断如果值是 t r u e true true,那么意味着 i t h i^{th} ith条件语句走的是then这个分支;反之如果是false就意味着程序执行走的是else分支。
符号执行不关心程序的功能和实现,关心的是其分支结构(包括判断条件和分支去向).
/*ith branch*/
if(condition)
then
...
else
...
如果针对数个分支进行分析,那么程序可能执行的路径会形成一个树型结构.这种结构称为执行树.
比如:
if(condition0)
{
...//Operation0_1
if(condition1)
{
...//Operation1_1
if(condition2)
{
...//Operation2_1
}
else
{
...//Operation2_0
}
}
else
{
...//Operation1_0
}
}
else
{
...//Operation0_0
}
的树结构为
针对这样的树,很直观的得出最多需要3个输入分别针对condition0
,condition1
,condition2
判断点即可遍历.
而需要的输入方案(即真值指派)有 2 3 = 8 2^3=8 23=8种.
如果某个以分支为划分的代码块内存在致使程序崩溃或发生其他问题的部分,当遍历至这一块时程序退出并生成错误报告.包括崩溃或错误类型和此时的输入方案.这就是符号执行的引擎的主要原理.
符号执行的要求
- 尽可能的覆盖所有的可能的执行路径.
- 速度尽可能快.
路径爆炸
可以看到,对于一组分支组成的树形结构,最少有 O ( n ) O(n) O(n)种指派方案可以覆盖到所有路径,也就是说最小的复杂度是线性的.最多有 O ( 2 n ) O(2^n) O(2n)种指派方案,是指数增长类型.这就是路径爆炸.
所以对于商业级的项目,遍历所有分支就成了一项近乎不可能的工程.
符号执行要做的
符号执行实质是通过限制路径搜索空间来进行程序遍历,运行时间与所需要遍历的路径空间大小成正比。
利用可靠的程序分析技术来减小路径爆炸的复杂度
利用启发式搜索搜索最佳路径.
启发式搜索
启发式搜索算法,就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,再从这个位置进行搜索直到目标。
启发式搜索算法有点像广度优先搜索,不同的是,它会优先顺着有启发性和具有特定信息的节点搜索下去
,这些节点可能是到达目标的最好路径。
我们称这个过程为最优(best-first)或启发式搜索。
下面是其基本思想:
- 假定有一个启发式(评估)函数f ,可以帮助确定下一个要扩展的最优节点,我们采用一个约定,即f的值小或大表示找到了好的节点。这个函数基于指定问题域的信息,它是状态描述的一个实数值函数。
- 下一个要扩展的节点n是f(n)值取最*值的节点
- 当下一个要扩展的节点是目标节点时过程终止.
程序分析
在计算机科学中,程序分析是指自动分析一个程序的包括正确性、健壮性、安全性和活跃性等特征的过程。 程序分析主要研究两大领域:程序的优化(英语:Program optimization)和程序的正确性。前者研究如何提升程序性能并且降低程序的资源占用,后者研究如何确保程序完成预期的任务。
程序分析可以在不执行程序的情况下进行(静态程序分析),也可在执行时进行.
方法1:静态地合并路径,然后再使用求解器。也就是分支的归并和约简.
方法2: 在后续的计算中,记录并重用low-level function的分析结果。
方法3 : 自动化剪枝.也就是减小分析量.
约束求解
约束求解是符号执行的技术瓶颈,是最为耗时的部分,会包含大量的复杂的数据操作,大大增加约束求解的难度.其实质是对于一个布尔方程,求使其成立的真值指派方案.使用穷举的方法非常没有效率.通常使用SMT求解方法、提高约束求解器的求解速度、利用查询技术简化约束项等优化方法。
不相关的约束项的去除
程序分支主要依赖于一小部分的程序变量,即依赖于一小部分来自路径条件的约束。因此,一种有效的方法就是去掉那些与当前分支的输出不相关的路径条件。
现有路径条件:
( x + y > 10 ) a n d ( z > 0 ) a n d ( y < 12 ) a n d ( z − x = 0 ) (x+y>10)and(z>0)and(y<12)and(z-x=0) (x+y>10)and(z>0)and(y<12)and(z−x=0)
假设想生成满足
( x + y > 10 ) a n d ( z > 0 ) a n d ¬ ( y < 12 ) (x+y>10)and(z>0)and¬(y<12) (x+y>10)and(z>0)and¬(y<12)
的与y有关约束集合 { x = f ( y ) , z = g ( y ) } \{x=f(y),z=g(y)\} {x=f(y),z=g(y)}。
则(z>0)和(z-x=0)这两个约束都可以去掉。
递增求解
核心思想就是缓存已经求解过的约束.
( x + y < 10 ) a n d ( x > 5 ) → { x = 6 , y = 3 } (x+y<10)and(x>5)\rightarrow\{x=6,y=3\} (x+y<10)and(x>5)→{x=6,y=3}
对于新的约束,首先判断这个新约束的搜索空间是缓存里约束的超集还是子集。如果是新的约束的搜索空间是缓存的约束的子集,那么,就把缓存中的约束去掉多余的条件后继续求解。如果是超集,那么直接把解代入去验证。
参考链接
https://www.wikiwand.com/zh-hans/程序分析
https://zhuanlan.zhihu.com/p/26927127
https://www.wikiwand.com/zh-cn/%E5%90%AF%E5%8F%91%E5%BC%8F%E6%90%9C%E7%B4%A2
https://www.wikiwand.com/zh-cn/%E5%90%AF%E5%8F%91%E5%BC%8F%E6%90%9C%E7%B4%A2