CodeQL 文档

在 C# 中分析数据流

您可以使用 CodeQL 跟踪数据流通过 C# 程序到其使用位置。

关于本文

本文介绍了 CodeQL 库在 C# 中如何实现数据流分析,并包含示例以帮助您编写自己的数据流查询。以下部分描述了如何使用库进行本地数据流、全局数据流和污点跟踪。有关数据流建模的更一般介绍,请参阅“关于数据流分析”。

注意

这里描述的新型模块化数据流 API 可以在 CodeQL 2.13.0 及更高版本中与之前的库一起使用。有关库如何更改以及如何将任何现有查询迁移到模块化 API 的信息,请参阅 CodeQL 查询编写的全新数据流 API

本地数据流

本地数据流是单个方法或可调用函数内的数据流。本地数据流比全局数据流更易于实现、速度更快、精度更高,并且对于许多查询来说已经足够了。

使用本地数据流

本地数据流库位于 DataFlow 模块中,该模块定义了 Node 类,表示数据可以流经的任何元素。Node 被分为表达式节点 (ExprNode) 和参数节点 (ParameterNode)。您可以使用成员谓词 asExprasParameter 在数据流节点和表达式/参数之间进行映射

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

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

 ...
}

或使用谓词 exprNodeparameterNode

/**
 * 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

例如,您可以找到从参数 source 到表达式 sink 的零个或多个本地步骤中的流

DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))

使用本地污点跟踪

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

var temp = x;
var y = temp + ", " + temp;

如果 x 是一个受污染的字符串,那么 y 也会被污染。

本地污点跟踪库位于 TaintTracking 模块中。与本地数据流类似,谓词 localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) 在从节点 nodeFrom 到节点 nodeTo 存在直接污点传播边时成立。您可以通过使用 +* 运算符递归地应用谓词,或者可以使用预定义的递归谓词 localTaint

例如,您可以找到从参数 source 到表达式 sink 的零个或多个本地步骤中的污点传播

TaintTracking::localTaint(DataFlow::parameterNode(source), DataFlow::exprNode(sink))

示例

此查询查找传递给 System.IO.File.Open 的文件名

import csharp

from Method fileOpen, MethodCall call
where fileOpen.hasQualifiedName("System.IO.File.Open")
  and call.getTarget() = fileOpen
select call.getArgument(0)

不幸的是,这只会给出参数中的表达式,而不是传递给它的值。因此,我们使用本地数据流来查找流入参数的所有表达式

import csharp

from Method fileOpen, MethodCall call, Expr src
where fileOpen.hasQualifiedName("System.IO.File.Open")
  and call.getTarget() = fileOpen
  and DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
select src

然后,我们可以使源更加具体,例如对公共参数的访问。此查询查找使用公共参数打开文件的实例

import csharp

from Method fileOpen, MethodCall call, Parameter p
where fileOpen.hasQualifiedName("System.IO.File.Open")
  and call.getTarget() = fileOpen
  and DataFlow::localFlow(DataFlow::parameterNode(p), DataFlow::exprNode(call.getArgument(0)))
  and call.getEnclosingCallable().(Member).isPublic()
select p, "Opening a file from a public method."

此查询查找对 String.Format 的调用,其中格式字符串不是硬编码的

import csharp

from Method format, MethodCall call, Expr formatString
where format.hasQualifiedName("System.String.Format")
  and call.getTarget() = format
  and formatString = call.getArgument(0)
  and formatString.getType() instanceof StringType
  and not exists(StringLiteral source | DataFlow::localFlow(DataFlow::exprNode(source), DataFlow::exprNode(formatString)))
select call, "Argument to 'string.Format' isn't hard-coded."

练习

练习 1:编写一个查询,使用本地数据流查找用于创建 System.Uri 的所有硬编码字符串。(答案

全局数据流

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

注意

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

使用全局数据流

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

import csharp

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, "Dataflow to $@.", sink, sink.toString()

使用全局污点跟踪

全局污点跟踪对于全局数据流来说,就像本地污点跟踪对于本地数据流一样。也就是说,全局污点跟踪通过添加额外的非值保留步骤来扩展全局数据流。全局污点跟踪库通过将模块 TaintTracking::Global<ConfigSig> 应用于您的配置,而不是 DataFlow::Global<ConfigSig> 来使用

import csharp

module MyFlowConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    ...
  }

  predicate isSink(DataFlow::Node sink) {
    ...
  }
}

module MyFlow = TaintTracking::Global<MyFlowConfiguration>;

生成的模块具有与从 DataFlow::Global<ConfigSig> 获得的模块相同的签名。

流源

数据流库包含一些预定义的流源。PublicCallableParameterFlowSource 类(在 semmle.code.csharp.dataflow.flowsources.PublicCallableParameter 模块中定义)表示来自公共参数的数据流,这对于查找公共 API 中的安全问题很有用。

RemoteFlowSource 类(在 semmle.code.csharp.dataflow.flowsources.Remote 模块中定义)表示来自远程网络输入的数据流。这对于查找网络服务中的安全问题很有用。

示例

此查询显示了一个数据流配置,该配置使用所有公共 API 参数作为数据源

import csharp
import semmle.code.csharp.dataflow.flowsources.PublicCallableParameter

module MyFlowConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    source instanceof PublicCallableParameterFlowSource
  }

  ...
}

类层次结构

  • DataFlow::Node - 一个充当数据流节点的元素。
    • DataFlow::ExprNode - 一个充当数据流节点的表达式。
    • DataFlow::ParameterNode - 一个参数数据流节点,表示函数入口处的参数值。
      • PublicCallableParameter - 公共类中公共方法/可调用函数的参数。
    • RemoteFlowSource - 来自网络/远程输入的数据流。
      • AspNetRemoteFlowSource - 来自远程 ASP.NET 用户输入的数据流。
        • AspNetQueryStringRemoteFlowSource - 来自 System.Web.HttpRequest 的数据流。
        • AspNetUserInputRemoveFlowSource - 来自 System.Web.IO.WebControls.TextBox 的数据流。
      • WcfRemoteFlowSource - 来自 WCF Web 服务的数据流。
      • AspNetServiceRemoteFlowSource - 来自 ASP.NET Web 服务的数据流。

示例

此数据流配置跟踪来自环境变量到打开文件的的数据流

import csharp

module EnvironmentToFileConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    exists(Method m |
      m = source.asExpr().(MethodCall).getTarget() and
      m.hasQualifiedName("System.Environment.GetEnvironmentVariable")
    )
  }

  predicate isSink(DataFlow::Node sink) {
    exists(MethodCall mc |
      mc.getTarget().hasQualifiedName("System.IO.File.Open") and
      sink.asExpr() = mc.getArgument(0)
    )
  }
}

module EnvironmentToFileFlow = DataFlow::Global<EnvironmentToFileConfiguration>;

from Expr environment, Expr fileOpen
where EnvironmentToFileFlow::flow(DataFlow::exprNode(environment), DataFlow::exprNode(fileOpen))
select fileOpen, "This 'File.Open' uses data from $@.",
  environment, "call to 'GetEnvironmentVariable'"

练习

练习 2:查找传递给 System.Uri 的所有硬编码字符串,使用全局数据流。(答案

练习 3:定义一个表示来自 System.Environment.GetEnvironmentVariable 的流源的类。(答案

练习 4:使用 2 和 3 中的答案,编写一个查询以查找来自 System.Environment.GetEnvironmentVariableSystem.Uri 的所有全局数据流。(答案

扩展库数据流

库数据流定义了数据如何在库中流动,这些库的源代码不可用,例如 .NET Framework、第三方库或专有库。

要定义新的库数据流,请扩展模块 semmle.code.csharp.dataflow.LibraryTypeDataFlow 中的类 LibraryTypeDataFlow。覆盖谓词 callableFlow 以定义数据如何在类中的方法之间流动。 callableFlow 的签名如下

predicate callableFlow(CallableFlowSource source, CallableFlowSink sink, SourceDeclarationCallable callable, boolean preservesValue)
  • callable - 执行数据流的 Callable(例如方法、构造函数、属性 getter 或 setter)。
  • source - 数据流输入。
  • sink - 数据流输出。
  • preservesValue - 流动步骤是否保留值,例如,如果 x 是一个字符串,那么 x.ToString() 保留值,而 x.ToLower() 不保留。

类层次结构

  • Callable - 可调用项(方法、访问器、构造函数等)。
    • SourceDeclarationCallable - 未构造的可调用项。
  • CallableFlowSource - 数据流进入可调用项的输入。
    • CallableFlowSourceQualifier - 数据流来自对象本身。
    • CallableFlowSourceArg - 数据流来自调用参数。
  • CallableFlowSink - 数据流从可调用项流出的输出。
    • CallableFlowSinkQualifier - 输出到对象本身。
    • CallableFlowSinkReturn - 输出从调用返回。
    • CallableFlowSinkArg - 输出是一个参数。
    • CallableFlowSinkDelegateArg - 输出通过委托参数流动(例如,LINQ)。

示例

此示例改编自 LibraryTypeDataFlow.qll。它声明通过类 System.Uri 的数据流,包括构造函数、ToString 方法以及属性 QueryOriginalStringPathAndQuery

import semmle.code.csharp.dataflow.LibraryTypeDataFlow
import semmle.code.csharp.frameworks.System

class SystemUriFlow extends LibraryTypeDataFlow, SystemUriClass {
  override predicate callableFlow(CallableFlowSource source, CallableFlowSink sink, SourceDeclarationCallable c, boolean preservesValue) {
    (
      constructorFlow(source, c) and
      sink instanceof CallableFlowSinkQualifier
      or
      methodFlow(c) and
      source instanceof CallableFlowSourceQualifier and
      sink instanceof CallableFlowSinkReturn
      or
      exists(Property p |
        propertyFlow(p) and
        source instanceof CallableFlowSourceQualifier and
        sink instanceof CallableFlowSinkReturn and
        c = p.getGetter()
      )
    )
    and
    preservesValue = false
  }

  private predicate constructorFlow(CallableFlowSourceArg source, Constructor c) {
    c = getAMember()
    and
    c.getParameter(0).getType() instanceof StringType
    and
    source.getArgumentIndex() = 0
  }

  private predicate methodFlow(Method m) {
    m.getDeclaringType() = getABaseType*()
    and
    m = getSystemObjectClass().getToStringMethod().getAnOverrider*()
  }

  private predicate propertyFlow(Property p) {
    p = getPathAndQueryProperty()
    or
    p = getQueryProperty()
    or
    p = getOriginalStringProperty()
  }
}

这定义了一个新类 SystemUriFlow,它扩展了 LibraryTypeDataFlow 以添加另一个情况。它扩展了 SystemUriClass(表示 System.Uri 的类,在模块 semmle.code.csharp.frameworks.System 中定义)以访问诸如 getQueryProperty 之类的方法。

谓词 callableFlow 声明通过 System.Uri 的数据流。第一个情况(constructorFlow)声明数据流从构造函数的第一个参数流向对象本身(CallableFlowSinkQualifier)。

第二个情况声明数据流从对象(CallableFlowSourceQualifier)流向在对象上调用 ToString 的结果(CallableFlowSinkReturn)。

第三个情况声明数据流从对象(CallableFlowSourceQualifier)流向属性 PathAndQueryQueryOriginalString 的 getter 的返回值(CallableFlowSinkReturn)。请注意,属性(getPathAndQueryPropertygetQueryPropertygetOriginalStringProperty)是从类 SystemUriClass 继承的。

在这三种情况下,preservesValue = false,这意味着这些步骤将仅包含在污点跟踪中,而不是在(正常)数据流中。

练习

练习 5:在 System.Uri 中,哪些其他属性可能公开数据?如何在 SystemUriFlow 中添加它们?(答案)

练习 6:实现类 System.Exception 的数据流。(答案)


答案

练习 1

import csharp

from Expr src, Call c
where DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(c.getArgument(0)))
  and c.getTarget().(Constructor).getDeclaringType().hasQualifiedName("System.Uri")
  and src.hasValue()
select src, "This string constructs 'System.Uri' $@.", c, "here"

练习 2

import csharp

module StringToUriConfig implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node src) {
    src.asExpr().hasValue()
  }

  predicate isSink(DataFlow::Node sink) {
    exists(Call c | c.getTarget().(Constructor).getDeclaringType().hasQualifiedName("System.Uri")
    and sink.asExpr()=c.getArgument(0))
  }
}

module StringToUriFlow = DataFlow::Global<StringToUriConfig>;

from DataFlow::Node src, DataFlow::Node sink
where StringToUriFlow::flow(src, sink)
select src, "This string constructs a 'System.Uri' $@.", sink, "here"

练习 3

class EnvironmentVariableFlowSource extends DataFlow::ExprNode {
  EnvironmentVariableFlowSource() {
    this.getExpr().(MethodCall).getTarget().hasQualifiedName("System.Environment.GetEnvironmentVariable")
  }
}

练习 4

import csharp

class EnvironmentVariableFlowSource extends DataFlow::ExprNode {
  EnvironmentVariableFlowSource() {
    this.getExpr().(MethodCall).getTarget().hasQualifiedName("System.Environment.GetEnvironmentVariable")
  }
}

module EnvironmentToUriConfig implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node src) {
    src instanceof EnvironmentVariableFlowSource
  }

  predicate isSink(DataFlow::Node sink) {
    exists(Call c | c.getTarget().(Constructor).getDeclaringType().hasQualifiedName("System.Uri")
    and sink.asExpr()=c.getArgument(0))
  }
}

module EnvironmentToUriFlow = DataFlow::Global<EnvironmentToUriConfig>;

from DataFlow::Node src, DataFlow::Node sink
where EnvironmentToUriFlow::flow(src, sink)
select src, "This environment variable constructs a 'System.Uri' $@.", sink, "here"

练习 5

所有属性都可以流动数据

private predicate propertyFlow(Property p) {
  p = getAMember()
}

练习 6

这可以从 SystemUriFlow 类中改编而来

import semmle.code.csharp.dataflow.LibraryTypeDataFlow
import semmle.code.csharp.frameworks.System

class SystemExceptionFlow extends LibraryTypeDataFlow, SystemExceptionClass {
  override predicate callableFlow(CallableFlowSource source, CallableFlowSink sink, SourceDeclarationCallable c, boolean preservesValue) {
    (
      constructorFlow(source, c) and
      sink instanceof CallableFlowSinkQualifier
      or
      methodFlow(source, sink, c)
      or
      exists(Property p |
        propertyFlow(p) and
        source instanceof CallableFlowSourceQualifier and
        sink instanceof CallableFlowSinkReturn and
        c = p.getGetter()
      )
    )
    and
    preservesValue = false
  }

  private predicate constructorFlow(CallableFlowSourceArg source, Constructor c) {
    c = getAMember()
    and
    c.getParameter(0).getType() instanceof StringType
    and
    source.getArgumentIndex() = 0
  }

  private predicate methodFlow(CallableFlowSourceQualifier source, CallableFlowSinkReturn sink, SourceDeclarationMethod m) {
    m.getDeclaringType() = getABaseType*()
    and
    m = getSystemObjectClass().getToStringMethod().getAnOverrider*()
  }

  private predicate propertyFlow(Property p) {
    p = getAProperty() and p.hasName("Message")
  }
}
  • ©GitHub, Inc.
  • 条款
  • 隐私