data-flow-analysis
CodeQL数据流lib通过对程序或函数的数据流图进行建模来实现对其的数据流分析。数据流边表示2个程序元素之间的数据流。比如 x || y
表达式中存在 x --> (x || y)
和 y --> (x || y)
的数据流边。
同时数据流也分为局部数据流(local data flow)和全局数据流(global data flow):
本地数据流只考虑函数内的数据流而忽略函数之间以及通过对象属性的数据流。
全局数据流都会考虑。
污点追踪会向数据流图中引入额外的边,这些边并不精确地对应于value flow,而是对运行时的某个值是否可以从另一个值派生进行建模,例如通过字符串操作。
计算精确和完整的数据流图存在以下挑战:
对于无法接触到源代码的标准库函数,计算通过这些函数的数据流不可行。
有些行为要到运行时才能确定,这意味着数据流lib必须采取额外的步骤来找到潜在的调用目标。
变量之间的别名可能导致一个写入操作可能更改多个指针指向的值(需要指针分析)。
数据流图可能很大,因此计算可能非常耗时。
CodeQL实现了2种数据流lib计算数据流图:
本地数据流:只需要考虑函数内的数据流。对于许多查询来说,它通常足够快速、高效和精确,并且通常可以计算CodeQL数据库中所有函数的本地数据流。
全局数据流:通常考虑程序内的所有数据流。通常比计算本地数据流更耗时,因此应优化查询语句以寻找更具体的source和sink点。
在标准库中,CodeQL对常规数据流和污点跟踪进行了区分。常规数据流库用于分析信息流,其中每个步骤都保留数据值。
在QL中,污点跟踪通过一些步骤来扩展数据流分析。在这些步骤中,数据值不一定会被保留,但潜在的不安全对象仍然会被传播。这些流步骤在污点跟踪库中使用谓词进行建模,如果污点在节点之间传播,则谓词会保持不变。
可进一步参考exploring-data-flow-with-path-queries。
关于C/C++基本分析可以参考codeql-for-cpp。这里只写数据流分析。
本地数据流库位于模块 DataFlow
中,它定义了类 Node
,表示数据可以流经的任何元素。Node
分为表达式节点(ExprNode
)和参数节点(ParameterNode
)。可以使用成员谓词 asExprasParameter
在数据流节点和表达式/参数之间进行映射。或者使用谓词 exprNode
和 parameterNode
。
class Node {
/** Gets the expression corresponding to this node, if any. */
Expr asExpr() { ... }
/** Gets the parameter corresponding to this node, if any. */
Parameter asParameter() { ... }
/**
* Gets the node corresponding to expression `e`.
*/
ExprNode exprNode(Expr e) { ... }
/**
* Gets the node corresponding to the value of parameter `p` at function entry.
*/
ParameterNode parameterNode(Parameter p) { ... }
...
}
谓词 localFlowStep(Node nodeFrom, Node nodeTo)
在存在从 nodeFrom
到 nodeTo
的直接数据流边(immediate data flow edge)时成立。谓词可以递归应用(使用 +
和 *
运算符,这里运算符的语义分别是正则表达式中至少1次和至少0次,不是算数运算符),也可以通过预定义的递归谓词localFlow
应用,localFlow
等效于 localFlowStep*
(即调用1次或多次 localFlowStep
)。
表达式 DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
可以通过0步或多步找出从形参 source
到表达式 sink
的数据流。
本地污点跟踪通过包括non-value-preserving步骤来扩展本地数据流。比如:
int i = tainted_user_input();
some_big_struct *array = malloc(i * sizeof(some_big_struct));
这种情况下,malloc
的实参被污染了。
本地污点追踪lib位于 TaintTracking
模块中。与本地数据流一样,如果存在从节点 nodeFrom
到节点 nodeTo
的直接污点传播边 ,则谓词 localTaintStep(DataFlow:Node nodeFrom,DataFlow:Node nodeTo)
成立 。谓词可以递归应用(使用 +
和 *
运算符),也可以通过预定义的递归谓词 localTaint
应用,localTaint
等效于 localTaintStep*
。
TaintTracking::localTaint(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
可以通过0步或多步找出从形参 source
到表达式 sink
的污点流。
下面查询语句可以找出传递到 fopen
中的文件名,即 fopen
调用的第1个实参。查询语句的大致意思是找到函数调用 fc
并且 fc
调用的是 fopen
,返回 fc
第1个参数。
import cpp
from Function fopen, FunctionCall fc
where fopen.hasGlobalName("fopen")
and fc.getTarget() = fopen
select fc.getArgument(0)
不过,这只会给出参数中的表达式,而不是可能传递给它的值。因此,可以使用本地数据流来查找流入实参的所有表达式。查询语句的大致意思是找到调用 fopen
的函数调用 fc
并找到所有流入 fc
第1个参数的本地数据流,返回数据流的 source
点。
import cpp
import semmle.code.cpp.dataflow.DataFlow
from Function fopen, FunctionCall fc, Expr src
where fopen.hasGlobalName("fopen")
and fc.getTarget() = fopen
and DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(fc.getArgument(0)))
select src
接着继续优化查询,查找当前函数是否有一个形参流入了 fopen
的参数
import cpp
import semmle.code.cpp.dataflow.DataFlow
from Function fopen, FunctionCall fc, Parameter p
where fopen.hasGlobalName("fopen")
and fc.getTarget() = fopen
and DataFlow::localFlow(DataFlow::parameterNode(p), DataFlow::exprNode(fc.getArgument(0)))
select p
下面查询语句查找格式化函数中格式化参数没有硬编码的情况。
DataFlow::localFlow(source, sink) and source.asExpr() instanceof StringLiteral and sink.asExpr() = formatString
意思是 source
是字符串常量,也就是该数据流不属于污点分析。import semmle.code.cpp.dataflow.DataFlow
import semmle.code.cpp.commons.Printf
from FormattingFunction format, FunctionCall call, Expr formatString
where call.getTarget() = format
and call.getArgument(format.getFormatParameterIndex()) = formatString
and not exists(DataFlow::Node source, DataFlow::Node sink |
DataFlow::localFlow(source, sink) and
source.asExpr() instanceof StringLiteral and
sink.asExpr() = formatString
)
select call, "Argument to " + format.getQualifiedName() + " isn't hard-coded."
全局数据流跟踪整个程序中的数据流,因此比本地数据流更强大。然而,全局数据流不如本地数据流精确,并且分析通常需要更多的时间和内存来执行。
全局数据流使用需要继承类 DataFlow::Configuration
。
import semmle.code.cpp.dataflow.DataFlow
class MyDataFlowConfiguration extends DataFlow::Configuration {
MyDataFlowConfiguration() { this = "MyDataFlowConfiguration" }
override predicate isSource(DataFlow::Node source) {
...
}
override predicate isSink(DataFlow::Node sink) {
...
}
}
from MyDataFlowConfiguration dataflow, DataFlow::Node source, DataFlow::Node sink
where dataflow.hasFlow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()
isSource
定义source点
isSink
定义sink点
isBarrier
可选,限制数据流
isBarrierGuard
可选,同样限制数据流
isAdditionalFlowStep
可选,添加额外flow step
hasFlow(DataFlow::Node source, DataFlow::Node sink)
用来进行 source
到 sink
的全局数据流分析。
import semmle.code.cpp.dataflow.TaintTracking
class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }
override predicate isSource(DataFlow::Node source) {
...
}
override predicate isSink(DataFlow::Node sink) {
...
}
}
isSource
定义source点
isSink
定义sink点
isSanitizer
可选,限制污点流
isSanitizerGuard
可选,同样限制数据流
isAdditionalTaintStep
可选,添加额外flow step
hasFlow(DataFlow::Node source, DataFlow::Node sink)
用来进行 source
到 sink
的全局数据流分析。
以下数据流配置跟踪从环境变量到在类Unix环境中打开文件的数据流
定义source点为 getenv
函数调用返回值。
定义sink点为 fopen
函数第1个参数。
import semmle.code.cpp.dataflow.DataFlow
class EnvironmentToFileConfiguration extends DataFlow::Configuration {
EnvironmentToFileConfiguration() { this = "EnvironmentToFileConfiguration" }
override predicate isSource(DataFlow::Node source) {
exists (Function getenv |
source.asExpr().(FunctionCall).getTarget() = getenv and
getenv.hasGlobalName("getenv")
)
}
override predicate isSink(DataFlow::Node sink) {
exists (FunctionCall fc |
sink.asExpr() = fc.getArgument(0) and
fc.getTarget().hasGlobalName("fopen")
)
}
}
from Expr getenv, Expr fopen, EnvironmentToFileConfiguration config
where config.hasFlow(DataFlow::exprNode(getenv), DataFlow::exprNode(fopen))
select fopen, "This 'fopen' uses data from $@.",
getenv, "call to 'getenv'"
以下污点分析查询跟踪从 ntohl
的调用到数组索引操作的数据。它使用Guards库来识别已进行边界检查的表达式,并定义 isSanitizer
以防止污染通过它们传播。它还使用 isAdditionalTaintStep
将流从循环边界添加到循环索引。
source点为 ntohl
函数调用,该函数将一个无符号长整形数从网络字节顺序转换为主机字节顺序。
sink点为数组索引。
import cpp
import semmle.code.cpp.controlflow.Guards
import semmle.code.cpp.dataflow.TaintTracking
class NetworkToBufferSizeConfiguration extends TaintTracking::Configuration {
NetworkToBufferSizeConfiguration() { this = "NetworkToBufferSizeConfiguration" }
override predicate isSource(DataFlow::Node node) {
node.asExpr().(FunctionCall).getTarget().hasGlobalName("ntohl")
}
override predicate isSink(DataFlow::Node node) {
exists(ArrayExpr ae | node.asExpr() = ae.getArrayOffset())
}
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(Loop loop, LoopCounter lc |
loop = lc.getALoop() and
loop.getControllingExpr().(RelationalOperation).getGreaterOperand() = pred.asExpr() |
succ.asExpr() = lc.getVariableAccessInLoop(loop)
)
}
override predicate isSanitizer(DataFlow::Node node) {
exists(GuardCondition gc, Variable v |
gc.getAChild*() = v.getAnAccess() and
node.asExpr() = v.getAnAccess() and
gc.controls(node.asExpr().getBasicBlock(), _)
)
}
}
from DataFlow::Node ntohl, DataFlow::Node offset, NetworkToBufferSizeConfiguration conf
where conf.hasFlow(ntohl, offset)
select offset, "This array offset may be influenced by $@.", ntohl,
"converted data from the network"
query编写可参考codeql-query-help。