注意
本文中使用的数据流库已被改进的库取代,该库从 CodeQL 2.12.5 开始可用,请参阅 分析 C 和 C++ 中的数据流(新)。旧库已在 CodeQL 2.14.1 中弃用,将在以后的版本中删除。随着 CodeQL 2.13.0 的发布,两个库都使用新的模块化数据流 API。
分析 C 和 C++ 中的数据流¶
可以使用数据流分析来跟踪可能导致代码库中漏洞的潜在恶意或不安全数据流。
关于数据流¶
数据流分析计算变量在程序中各个点可能具有的值,确定这些值如何在程序中传播以及它们在哪里使用。在 CodeQL 中,可以建模局部数据流和全局数据流。有关建模数据流的更一般介绍,请参阅“关于数据流分析。”
局部数据流¶
局部数据流是在单个函数内的数据流。局部数据流通常比全局数据流更容易、更快且更精确,并且对于许多查询来说已经足够了。
使用局部数据流¶
局部数据流库位于模块 DataFlow
中,该模块定义了类 Node
,表示数据可能流经的任何元素。 Node
被分成表达式节点 (ExprNode
) 和参数节点 (ParameterNode
)。可以使用成员谓词 asExpr
和 asParameter
在数据流节点和表达式/参数之间映射
class Node {
/** Gets the expression corresponding to this node, if any. */
Expr asExpr() { ... }
/** Gets the parameter corresponding to this node, if any. */
Parameter asParameter() { ... }
...
}
或者使用谓词 exprNode
和 parameterNode
/**
* 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
存在直接数据流边,则成立。可以使用递归 (使用 +
和 *
运算符) 或通过预定义的递归谓词 localFlow
来应用此谓词,它等效于 localFlowStep*
。
例如,找到从参数 source
到表达式 sink
的零个或多个局部步骤中的流,可以按如下方式实现
DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
使用局部污染跟踪¶
局部污染跟踪通过包含非值保留流步骤来扩展局部数据流。例如
int i = tainted_user_input();
some_big_struct *array = malloc(i * sizeof(some_big_struct));
在这种情况下,malloc
的参数被污染了。
局部污染跟踪库位于模块 TaintTracking
中。与局部数据流一样,谓词 localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo)
如果从节点 nodeFrom
到节点 nodeTo
存在直接污染传播边,则成立。可以使用递归 (使用 +
和 *
运算符) 或通过预定义的递归谓词 localTaint
来应用此谓词,它等效于 localTaintStep*
。
例如,找到从参数 source
到表达式 sink
的零个或多个局部步骤中的污染传播,可以按如下方式实现
TaintTracking::localTaint(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
示例¶
以下查询找到传递给 fopen
的文件名。
import cpp
from Function fopen, FunctionCall fc
where fopen.hasGlobalName("fopen")
and fc.getTarget() = fopen
select fc.getArgument(0)
不幸的是,这只会给出参数中的表达式,而不是可能传递给它的值。因此,使用局部数据流来查找所有流入参数的表达式
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
然后可以更改源,例如,使用函数的参数。以下查询查找参数在打开文件时使用的位置
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
以下示例查找对格式化函数的调用,其中格式字符串不是硬编码的。
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."
全局数据流¶
全局数据流跟踪整个程序中的数据流,因此比局部数据流更强大。但是,全局数据流不如局部数据流精确,并且执行分析通常需要更多时间和内存。
注意
可以通过创建路径查询在 CodeQL 中建模数据流路径。要查看 VS Code 的 CodeQL 生成的路径查询,需要确保它具有正确的元数据和
select
子句。有关更多信息,请参阅 创建路径查询。
使用全局数据流¶
全局数据流库通过实现签名 DataFlow::ConfigSig
并应用模块 DataFlow::Global<ConfigSig>
来使用,如下所示
import semmle.code.cpp.dataflow.DataFlow
module MyFlowConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
...
}
predicate isSink(DataFlow::Node sink) {
...
}
}
module MyFlow = DataFlow::Global<MyFlowConfiguration>;
以下谓词在配置中定义
isSource
—定义数据可能流出的位置isSink
—定义数据可能流入的位置isBarrier
—可选,限制数据流isAdditionalFlowStep
—可选,添加额外的流步骤
数据流分析使用谓词 flow(DataFlow::Node source, DataFlow::Node sink)
执行
from DataFlow::Node source, DataFlow::Node sink
where MyFlow::flow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()
使用全局污染跟踪¶
全局污染跟踪对于全局数据流就像局部污染跟踪对于局部数据流一样。也就是说,全局污染跟踪通过额外的非值保留步骤扩展全局数据流。全局污染跟踪库通过将模块 TaintTracking::Global<ConfigSig>
应用于配置而不是 DataFlow::Global<ConfigSig>
来使用,如下所示
import semmle.code.cpp.dataflow.TaintTracking
module MyFlowConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
...
}
predicate isSink(DataFlow::Node sink) {
...
}
}
module MyFlow = TaintTracking::Global<MyFlowConfiguration>;
生成的模块具有与从 DataFlow::Global<ConfigSig>
获得的模块相同的签名。
示例¶
以下数据流配置跟踪从环境变量到在类 Unix 环境中打开文件的流
import semmle.code.cpp.dataflow.DataFlow
module EnvironmentToFileConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists (Function getenv |
source.asExpr().(FunctionCall).getTarget() = getenv and
getenv.hasGlobalName("getenv")
)
}
predicate isSink(DataFlow::Node sink) {
exists (FunctionCall fc |
sink.asExpr() = fc.getArgument(0) and
fc.getTarget().hasGlobalName("fopen")
)
}
}
module EnvironmentToFileFlow = DataFlow::Global<EnvironmentToFileConfiguration>;
from Expr getenv, Expr fopen
where EnvironmentToFileFlow::flow(DataFlow::exprNode(getenv), DataFlow::exprNode(fopen))
select fopen, "This 'fopen' uses data from $@.",
getenv, "call to 'getenv'"
以下污染跟踪配置跟踪从对 ntohl
的调用到数组索引操作的数据。它使用 Guards
库来识别已进行边界检查的表达式,并定义 isBarrier
来阻止污染通过它们传播。它还使用 isAdditionalFlowStep
来添加从循环边界到循环索引的流。
import cpp
import semmle.code.cpp.controlflow.Guards
import semmle.code.cpp.dataflow.TaintTracking
module NetworkToBufferSizeConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node node) {
node.asExpr().(FunctionCall).getTarget().hasGlobalName("ntohl")
}
predicate isSink(DataFlow::Node node) {
exists(ArrayExpr ae | node.asExpr() = ae.getArrayOffset())
}
predicate isAdditionalFlowStep(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)
)
}
predicate isBarrier(DataFlow::Node node) {
exists(GuardCondition gc, Variable v |
gc.getAChild*() = v.getAnAccess() and
node.asExpr() = v.getAnAccess() and
gc.controls(node.asExpr().getBasicBlock(), _)
)
}
}
module NetworkToBufferSizeFlow = TaintTracking::Global<NetworkToBufferSizeConfiguration>;
from DataFlow::Node ntohl, DataFlow::Node offset
where NetworkToBufferSizeFlow::flow(ntohl, offset)
select offset, "This array offset may be influenced by $@.", ntohl,
"converted data from the network"
答案¶
练习 1¶
import semmle.code.cpp.dataflow.DataFlow
from StringLiteral sl, FunctionCall fc
where fc.getTarget().hasName("gethostbyname")
and DataFlow::localFlow(DataFlow::exprNode(sl), DataFlow::exprNode(fc.getArgument(0)))
select sl, fc
练习 2¶
import semmle.code.cpp.dataflow.DataFlow
module LiteralToGethostbynameConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source.asExpr() instanceof StringLiteral
}
predicate isSink(DataFlow::Node sink) {
exists (FunctionCall fc |
sink.asExpr() = fc.getArgument(0) and
fc.getTarget().hasName("gethostbyname"))
}
}
module LiteralToGethostbynameFlow = DataFlow::Global<LiteralToGethostbynameConfiguration>;
from StringLiteral sl, FunctionCall fc
where LiteralToGethostbynameFlow::flow(DataFlow::exprNode(sl), DataFlow::exprNode(fc.getArgument(0)))
select sl, fc
练习 3¶
import cpp
class GetenvSource extends FunctionCall {
GetenvSource() {
this.getTarget().hasGlobalName("getenv")
}
}
练习 4¶
import semmle.code.cpp.dataflow.DataFlow
class GetenvSource extends DataFlow::Node {
GetenvSource() {
this.asExpr().(FunctionCall).getTarget().hasGlobalName("getenv")
}
}
module GetenvToGethostbynameConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof GetenvSource
}
predicate isSink(DataFlow::Node sink) {
exists (FunctionCall fc |
sink.asExpr() = fc.getArgument(0) and
fc.getTarget().hasName("gethostbyname"))
}
}
module GetenvToGethostbynameFlow = DataFlow::Global<GetenvToGethostbynameConfiguration>;
from DataFlow::Node getenv, FunctionCall fc
where GetenvToGethostbynameFlow::flow(getenv, DataFlow::exprNode(fc.getArgument(0)))
select getenv.asExpr(), fc
进一步阅读¶
- 使用路径查询探索数据流 在 GitHub 文档中。