CodeQL 文档

注意

此处描述的数据流库可从 CodeQL 2.12.5 开始使用。随着 CodeQL 2.13.0 的发布,该库使用新的模块化 API 进行数据流。有关库的先前版本的信息,请参阅 分析 C 和 C++ 中的数据流,有关新模块化 API 以及如何将任何现有查询迁移到更新的数据流库的信息,请参阅 CodeQL 查询编写的新数据流 API

分析 C 和 C++ 中的数据流(新)

您可以使用数据流分析来跟踪可能存在恶意或不安全数据流,这些数据流会导致代码库中的漏洞。

关于数据流

数据流分析计算变量在程序中的各个点可能具有的值,确定这些值如何在程序中传播以及它们在何处使用。在 CodeQL 中,您可以对本地数据流和全局数据流进行建模。有关数据流建模的更一般介绍,请参阅“关于数据流分析。”

本地数据流

本地数据流是单个函数内的数据流。本地数据流通常比全局数据流更容易、更快且更精确,并且对于许多查询来说已经足够了。

使用本地数据流

本地数据流库位于模块 DataFlow 中,该模块定义了表示数据可以流经的任何元素的类 NodeNode 分为表达式节点 (ExprNode, IndirectExprNode) 和参数节点 (ParameterNode, IndirectParameterNode)。间接节点表示经过固定数量的指针解引用后的表达式或参数。

可以使用成员谓词 asExprasIndirectExprasParameter 在数据流节点和表达式或参数之间进行映射。

class Node {
  /**
   * Gets the expression corresponding to this node, if any.
   */
  Expr asExpr() { ... }

  /**
   * Gets the expression corresponding to a node that is obtained after dereferencing
   * the expression `index` times, if any.
   */
  Expr asIndirectExpr(int index) { ... }

  /**
   * Gets the parameter corresponding to this node, if any.
   */
  Parameter asParameter() { ... }

  /**
   * Gets the parameter corresponding to a node that is obtained after dereferencing
   * the parameter `index` times.
   */
  Parameter asParameter(int index) { ... }

  ...
}

谓词 localFlowStep(Node nodeFrom, Node nodeTo) 如果从节点 nodeFrom 到节点 nodeTo 存在立即数据流边,则为真。该谓词可以递归地应用(使用 +* 运算符),或者通过预定义的递归谓词 localFlow 应用,该谓词等效于 localFlowStep*

例如,可以按以下方式查找从参数 source 到表达式 sink 在零个或多个本地步骤中的流,其中 nodeFromnodeTo 的类型为 DataFlow::Node

nodeFrom.asParameter() = source and
nodeTo.asExpr() = sink and
DataFlow::localFlow(nodeFrom, nodeTo)

使用本地污点跟踪

本地污点跟踪通过包括非值保留流步骤来扩展本地数据流。例如

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 在零个或多个本地步骤中的污点传播,其中 nodeFromnodeTo 的类型为 DataFlow::Node

nodeFrom.asParameter() = source and
nodeTo.asExpr() = sink and
TaintTracking::localTaint(nodeFrom, nodeTo)

示例

以下查询查找传递给 fopen 的文件名

import cpp

from Function fopen, FunctionCall fc
where
  fopen.hasGlobalName("fopen") and
  fc.getTarget() = fopen
select fc.getArgument(0)

但是,这只会给出参数中的表达式,而不是可能传递给它的值。相反,我们可以使用本地数据流来查找流入参数的所有表达式,其中我们使用 asIndirectExpr(1)。这是因为我们感兴趣的是传递给 fopen 的字符串的值,而不是指向它的指针

import cpp
import semmle.code.cpp.dataflow.new.DataFlow

from Function fopen, FunctionCall fc, Expr src, DataFlow::Node source, DataFlow::Node sink
where
  fopen.hasGlobalName("fopen") and
  fc.getTarget() = fopen and
  source.asIndirectExpr(1) = src and
  sink.asIndirectExpr(1) = fc.getArgument(0) and
  DataFlow::localFlow(source, sink)
select src

然后,我们可以更改源,例如使用函数的参数。以下查询查找在打开文件时使用参数的位置

import cpp
import semmle.code.cpp.dataflow.new.DataFlow

from Function fopen, FunctionCall fc, Parameter p, DataFlow::Node source, DataFlow::Node sink
where
  fopen.hasGlobalName("fopen") and
  fc.getTarget() = fopen and
  source.asParameter(1) = p and
  sink.asIndirectExpr(1) = fc.getArgument(0) and
  DataFlow::localFlow(source, sink)
select p

以下示例查找格式化函数的调用,其中格式字符串不是硬编码的。

import semmle.code.cpp.dataflow.new.DataFlow
import semmle.code.cpp.commons.Printf

from FormattingFunction format, FunctionCall call, Expr formatString, DataFlow::Node sink
where
  call.getTarget() = format and
  call.getArgument(format.getFormatParameterIndex()) = formatString and
  sink.asIndirectExpr(1) = formatString and
  not exists(DataFlow::Node source |
    DataFlow::localFlow(source, sink) and
    source.asIndirectExpr(1) instanceof StringLiteral
  )
select call, "Argument to " + format.getQualifiedName() + " isn't hard-coded."

练习

练习 1:编写一个查询,该查询使用本地数据流查找通过 gethostbyname 创建 host_ent 时使用的所有硬编码字符串。(答案

全局数据流

全局数据流跟踪整个程序中的数据流,因此比本地数据流更强大。但是,全局数据流不如本地数据流精确,并且分析通常需要更长的时间和更多的内存来执行。

注意

您可以通过创建路径查询来在 CodeQL 中对数据流路径进行建模。要在用于 VS Code 的 CodeQL 中查看路径查询生成的数据流路径,您需要确保它具有正确的元数据和 select 子句。有关更多信息,请参阅 创建路径查询

使用全局数据流

全局数据流库通过实现签名 DataFlow::ConfigSig 并应用模块 DataFlow::Global<ConfigSig> 来使用,如下所示

import semmle.code.cpp.dataflow.new.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.new.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 cpp
import semmle.code.cpp.dataflow.new.DataFlow

module EnvironmentToFileConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    exists(Function getenv |
      source.asIndirectExpr(1).(FunctionCall).getTarget() = getenv and
      getenv.hasGlobalName("getenv")
    )
  }

  predicate isSink(DataFlow::Node sink) {
    exists(FunctionCall fc |
      sink.asIndirectExpr(1) = fc.getArgument(0) and
      fc.getTarget().hasGlobalName("fopen")
    )
  }
}

module EnvironmentToFileFlow = DataFlow::Global<EnvironmentToFileConfiguration>;

from
  Expr getenv, Expr fopen, DataFlow::Node source, DataFlow::Node sink
where
  source.asIndirectExpr(1) = getenv and
  sink.asIndirectExpr(1) = fopen and
  EnvironmentToFileFlow::flow(source, sink)
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.new.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(), _) and
      not exists(Loop loop | loop.getControllingExpr() = gc)
    )
  }
}

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"

练习

练习 2:编写一个查询,该查询使用全局数据流查找通过 gethostbyname 创建 host_ent 时使用的所有硬编码字符串。(答案

练习 3:编写一个类来表示来自 getenv 的流源。(答案

练习 4:使用 2 和 3 中的答案,编写一个查询,该查询查找从 getenvgethostbyname 的所有全局数据流。(答案

答案

练习 1

import cpp
import semmle.code.cpp.dataflow.new.DataFlow

from StringLiteral sl, FunctionCall fc, DataFlow::Node source, DataFlow::Node sink
where
  fc.getTarget().hasName("gethostbyname") and
  source.asIndirectExpr(1) = sl and
  sink.asIndirectExpr(1) = fc.getArgument(0) and
  DataFlow::localFlow(source, sink)
select sl, fc

练习 2

import cpp
import semmle.code.cpp.dataflow.new.DataFlow

module LiteralToGethostbynameConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    source.asIndirectExpr(1) instanceof StringLiteral
  }

  predicate isSink(DataFlow::Node sink) {
    exists(FunctionCall fc |
      sink.asIndirectExpr(1) = fc.getArgument(0) and
      fc.getTarget().hasName("gethostbyname")
    )
  }
}

module LiteralToGethostbynameFlow = DataFlow::Global<LiteralToGethostbynameConfiguration>;

from
  StringLiteral sl, FunctionCall fc, DataFlow::Node source, DataFlow::Node sink
where
  source.asIndirectExpr(1) = sl and
  sink.asIndirectExpr(1) = fc.getArgument(0) and
  LiteralToGethostbynameFlow::flow(source, sink)
select sl, fc

练习 3

import cpp
import semmle.code.cpp.dataflow.new.DataFlow

class GetenvSource extends DataFlow::Node {
  GetenvSource() { this.asIndirectExpr(1).(FunctionCall).getTarget().hasGlobalName("getenv") }
}

练习 4

import cpp
import semmle.code.cpp.dataflow.new.DataFlow

class GetenvSource extends DataFlow::Node {
  GetenvSource() { this.asIndirectExpr(1).(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.asIndirectExpr(1) = fc.getArgument(0) and
      fc.getTarget().hasName("gethostbyname")
    )
  }
}

module GetenvToGethostbynameFlow = DataFlow::Global<GetenvToGethostbynameConfiguration>;

from
  Expr getenv, FunctionCall fc, DataFlow::Node source, DataFlow::Node sink
where
  source.asIndirectExpr(1) = getenv and
  sink.asIndirectExpr(1) = fc.getArgument(0) and
  GetenvToGethostbynameFlow::flow(source, sink)
select getenv, fc
  • ©GitHub, Inc.
  • 条款
  • 隐私