在 C# 中分析数据流¶
您可以使用 CodeQL 跟踪数据流通过 C# 程序到其使用位置。
关于本文¶
本文介绍了 CodeQL 库在 C# 中如何实现数据流分析,并包含示例以帮助您编写自己的数据流查询。以下部分描述了如何使用库进行本地数据流、全局数据流和污点跟踪。有关数据流建模的更一般介绍,请参阅“关于数据流分析”。
注意
这里描述的新型模块化数据流 API 可以在 CodeQL 2.13.0 及更高版本中与之前的库一起使用。有关库如何更改以及如何将任何现有查询迁移到模块化 API 的信息,请参阅 CodeQL 查询编写的全新数据流 API。
本地数据流¶
本地数据流是单个方法或可调用函数内的数据流。本地数据流比全局数据流更易于实现、速度更快、精度更高,并且对于许多查询来说已经足够了。
使用本地数据流¶
本地数据流库位于 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
。
例如,您可以找到从参数 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."
全局数据流¶
全局数据流跟踪整个程序中的数据流,因此比本地数据流更强大。但是,全局数据流比本地数据流精度更低,并且分析通常需要更长的执行时间和更多的内存。
注意
您可以通过创建路径查询来对 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'"
扩展库数据流¶
库数据流定义了数据如何在库中流动,这些库的源代码不可用,例如 .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
方法以及属性 Query
、OriginalString
和 PathAndQuery
。
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
)流向属性 PathAndQuery
、Query
和 OriginalString
的 getter 的返回值(CallableFlowSinkReturn
)。请注意,属性(getPathAndQueryProperty
、getQueryProperty
和 getOriginalStringProperty
)是从类 SystemUriClass
继承的。
在这三种情况下,preservesValue = false
,这意味着这些步骤将仅包含在污点跟踪中,而不是在(正常)数据流中。
答案¶
练习 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"
练习 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")
}
}