注意
此处描述的数据流库可从 CodeQL 2.12.5 开始使用。随着 CodeQL 2.13.0 的发布,该库使用了新的模块化数据流 API。有关库之前版本的详细信息,请参见分析 C 和 C++ 中的数据流,有关新模块化 API 以及如何将任何现有查询迁移到更新的数据流库的信息,请参见CodeQL 查询编写的新数据流 API。
C/C++ 的高级数据流场景¶
C 和 C++ 的数据流区分了指针的值和指针指向的值。我们称之为指针的“间接寻址”。跟踪指针及其间接寻址作为单独的实体对于精确的数据流非常重要。但是,这也意味着您需要指定要建模的数据流节点。如果您选择了错误的数据流节点,那么分析就会存在缺陷。本文讨论了一些重要的场景,需要考虑数据流是否应该在指针的值或其间接寻址上进行计算。
概述¶
对于几乎所有情况,我们只需要实例化一个数据流配置并指定源和汇,数据流库将为我们处理所有事项。
但是,当对字段的写入对 CodeQL 不可見(例如,因为它是发生在没有定义在数据库中的函数中)时,我们需要跟踪限定符,并告诉数据流库应该将流从限定符传输到字段访问。这可以通过向数据流模块添加 isAdditionalFlowStep
谓词来实现。
当您编写额外的流步骤来跟踪指针时,您必须决定数据流步骤应该从指针流出还是从其间接寻址流出。同样,您必须决定额外的步骤应该针对指针还是其间接寻址。
相反,如果对字段的读取对 CodeQL 不可見,您可以添加一个 allowImplicitRead
谓词来对数据流进行建模。
常规数据流分析¶
考虑以下场景:我们有来自 user_input()
的数据,我们想弄清楚这些数据是否可以到达 sink
的参数。
void sink(int);
int user_input();
类似以下查询的常规数据流查询
/**
* @kind path-problem
*/
import semmle.code.cpp.dataflow.new.DataFlow
import Flow::PathGraph
module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source.asExpr().(Call).getTarget().hasName("user_input")
}
predicate isSink(DataFlow::Node sink) {
exists(Call call |
call.getTarget().hasName("sink") and
sink.asExpr() = call.getAnArgument()
)
}
}
module Flow = DataFlow::Global<Config>;
from Flow::PathNode source, Flow::PathNode sink
where Flow::flowPath(source, sink)
select sink.getNode(), source, sink, "Flow from user input to sink!"
将捕获大多数内容,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | struct A { const int *p; int x; }; struct B { A *a; int y; }; void fill_structure(B* b, const int* pu) { // ... b->a->p = pu; } void process_structure(const B* b) { sink(*b->a->p); } void get_and_process() { int u = user_input(); B* b = (B*)malloc(sizeof(B)); // ... fill_structure(b, &u); // ... process_structure(b); free(b); } |
- 此数据流很容易匹配,因为 CodeQL 数据库包含以下信息:
- 用户输入从
user_input()
开始,流入fill_structure
。 - 数据使用访问路径
[a, p]
写入到对象b
。 - 对象
b
从fill_structure
流出,流入process_structure
。 - 访问路径
[a, p]
在process_structure
中被读取,并且最终该值会进入汇。
- 用户输入从
从限定符到字段访问的流¶
有时,字段访问对 CodeQL 不可見(例如,因为函数的实现未包含在数据库中),因此数据流无法将所有存储与读取匹配。这会导致丢失(误报)结果。
例如,考虑另一种设置,其中我们的数据源作为函数 write_user_input_to
的传出参数开始。我们可以在数据流库中使用以下 isSource
来对这种设置进行建模
predicate isSource(DataFlow::Node source) {
exists(Call call |
call.getTarget().hasName("write_user_input_to") and
source.asDefiningArgument() = call.getArgument(0)
)
}
这将匹配以下示例中对 write_user_input_to
的调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | void write_user_input_to(void*); void use_value(int); void* malloc(unsigned long); struct U { const int* p; int x; }; void process_user_data(const int* p) { // ... use_value(*p); } void get_and_process_user_input_v2() { U* u = (U*)malloc(sizeof(U)); write_user_input_to(u); process_user_data(u->p); free(u); } |
使用此 isSource
定义,数据流库将沿着以下路径跟踪流
- 现在,流从
write_user_input_to(...)
的传出参数开始。- 流在下一行继续到
u->p
。
但是,因为 CodeQL 在读取 u->p
之前没有观察到对 p
的写入,因此数据流将在 u
处停止。我们可以通过添加一个额外的流步骤来纠正数据流可用信息中的这一差距,该步骤将通过字段读取。
/**
* @kind path-problem
*/
import semmle.code.cpp.dataflow.new.DataFlow
import Flow::PathGraph
module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(Call call |
call.getTarget().hasName("write_user_input_to") and
source.asDefiningArgument() = call.getArgument(0)
)
}
predicate isSink(DataFlow::Node sink) {
exists(Call call |
call.getTarget().hasName("use_value") and
sink.asExpr() = call.getAnArgument()
)
}
predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2) {
exists(FieldAccess fa |
n1.asIndirectExpr() = fa.getQualifier() and
n2.asIndirectExpr() = fa
)
}
}
module Flow = DataFlow::Global<Config>;
from Flow::PathNode source, Flow::PathNode sink
where Flow::flowPath(source, sink)
select sink.getNode(), source, sink, "Flow from user input to sink!"
请注意,isSource
和 isSink
符合预期:我们正在寻找从 write_user_input_to(...)
的传出参数开始,并作为 isSink
的参数结束的流。有趣的部分是添加了 isAdditionalFlow
,它指定了从 FieldAccess
的限定符到访问结果的额外流步骤。
在实际的查询中,isAdditionalFlowStep
步骤将以各种方式受到限制,以确保它不会添加过多的流(因为从字段限定符到字段访问的流通常会产生大量虚假流)。例如,可以限制 fa
为一个针对特定字段的字段访问,或者一个针对某个 struct
类型定义的字段的字段访问。
这里我们有一个重要的选择:n2
应该是与 fa
的指针值相对应的节点,还是 fa
的间接寻址(即 fa
指向的内容)?
使用 asIndirectExpr¶
如果我们使用 n2.asIndirectExpr() = fa
,我们指定示例 2 中的流会移动到 fa
指向的内容。这允许数据通过后面的解引用流動,这正是我们跟踪数据流从 p
到 *p
的方式 process_user_data
中。
因此,我们得到了所需的路徑。
考虑一个稍微不同的汇:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | void write_user_input_to(void*); void use_pointer(int*); void* malloc(unsigned long); struct U { const int* p; int x; }; void process_user_data(const int* p) { // ... use_pointer(p); } void get_and_process_user_input_v2() { U* u = (U*)malloc(sizeof(U)); write_user_input_to(u); process_user_data(u->p); free(u); } |
此示例与前一个示例唯一的区别是,我们的数据最终会进入对 use_pointer
的调用,该调用接受 int*
而不是 int
作为参数。由于我们的 isAdditionalFlowStep
实现已经迈向了 FieldAccess
的间接寻址,因此我们已经跟踪了字段指向的内容。因此,我们可以通过使用 sink.asIndirectExpr()
来指定我们想要跟踪的数据是最终被传递给 use_pointer
的参数指向的值,从而找到此流。
predicate isSink(DataFlow::Node sink) {
exists(Call call |
call.getTarget().hasName("use_pointer") and
sink.asIndirectExpr() = call.getAnArgument()
)
}
使用 asExpr¶
- 或者,示例 2 中的流也可以通过以下方式跟踪:
- 更改
isAdditionalFlowStep
,使其针对表示FieldAccess
值的数据流节点,而不是它指向的值,以及 - 更改
isSink
,指定我们想要跟踪的是传递给use_pointer
的参数的值(而不是参数指向的值)。
- 更改
更改后,我们的查询变为:
/**
* @kind path-problem
*/
import semmle.code.cpp.dataflow.new.DataFlow
import Flow::PathGraph
module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(Call call |
call.getTarget().hasName("write_user_input_to") and
source.asDefiningArgument() = call.getArgument(0)
)
}
predicate isSink(DataFlow::Node sink) {
exists(Call call |
call.getTarget().hasName("use_pointer") and
sink.asExpr() = call.getAnArgument()
)
}
predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2) {
exists(FieldAccess fa |
n1.asIndirectExpr() = fa.getQualifier() and
n2.asExpr() = fa
)
}
}
module Flow = DataFlow::Global<Config>;
from Flow::PathNode source, Flow::PathNode sink
where Flow::flowPath(source, sink)
select sink.getNode(), source, sink, "Flow from user input to sink!"
当我们到达 u->p
时,额外步骤会将流从限定符指向的内容传输到 FieldAccess
的结果。之后,数据流继续到 p
use_pointer(p)
中,并且由于我们在 isSink
中指定了我们想要跟踪参数的值,因此我们的数据流分析会找到结果。
将变量的地址传递给 use_pointer
¶
考虑另一种情况,其中 U
包含一个 int
数据,我们如下所示将数据地址传递给 use_pointer
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | void write_user_input_to(void*); void use_pointer(int*); void* malloc(unsigned long); struct U { int data; int x; }; void process_user_data(int data) { // ... use_pointer(&data); } void get_and_process_user_input_v2() { U* u = (U*)malloc(sizeof(U)); write_user_input_to(u); process_user_data(u->data); free(u); } |
由于 data
字段现在是 int
而不是 int*
,因此该字段不再具有任何间接寻址,因此在 isAdditionalFlowStep
中使用 asIndirectExpr
已经没有意义(因此额外步骤将不会有任何结果)。因此,关于是否污染字段的值或其间接寻址,没有选择:它必须是值。
- 然而,由于我们在第 12 行将
data
的地址传递给use_pointer
,因此受污染的值是指向use_pointer
参数所指向的内容(因为&data
所指向的值恰好是data
)。因此,为了处理这种情况,我们需要将上述两种情况结合起来。 - 我们需要像在 使用 asExpr 部分中所述的那样对字段的值进行污染。
- 我们需要像在 使用 asIndirectExpr 部分中所述的那样选择参数的间接寻址。
通过这些更改,查询看起来像这样
/**
* @kind path-problem
*/
import semmle.code.cpp.dataflow.new.DataFlow
import Flow::PathGraph
module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(Call call |
call.getTarget().hasName("write_user_input_to") and
source.asDefiningArgument() = call.getArgument(0)
)
}
predicate isSink(DataFlow::Node sink) {
exists(Call call |
call.getTarget().hasName("use_pointer") and
sink.asIndirectExpr() = call.getAnArgument()
)
}
predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2) {
exists(FieldAccess fa |
n1.asIndirectExpr() = fa.getQualifier() and
n2.asExpr() = fa
)
}
}
module Flow = DataFlow::Global<Config>;
from Flow::PathNode source, Flow::PathNode sink
where Flow::flowPath(source, sink)
select sink.getNode(), source, sink, "Flow from user input to sink!"
通过该查询,可以识别出数据流。
指定隐式读取¶
上一节演示了如何将数据流从限定符添加到字段访问,因为源隐式地污染了结构体的所有字段。本节考虑相反的情况:一个特定的字段被污染,我们希望找到任何可能从该对象读取数据的地方,包括任何读取未知字段集的地方。
为了设置场景,请考虑以下情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | struct A { const int *p; int x; }; struct B { A *a; int z; }; int user_input(); void read_data(const void *); void *malloc(size_t); void get_input_and_read_data() { B b; b.a = (A *)malloc(sizeof(A)); b.a->x = user_input(); // ... read_data(&b); free(b.a); } |
在这个例子中,数据流如下
- 我们在访问路径
[a, x]
上将用户控制的值写入对象b
中。- 之后,将
b
传递给read_data
,我们在数据库中没有它的定义。
现在我们想跟踪这个用户输入流入 read_data
。
数据流库有一个专门的谓词来处理这种情况,因此我们不需要使用 isAdditionalFlowStep
添加任何额外的流步骤。相反,我们告诉数据流库 read_data
是一个汇点,并且可能隐式地从它所传递的对象中的字段读取数据。为此,我们在数据流模块中实现 allowImplicitRead
/**
* @kind path-problem
*/
import semmle.code.cpp.dataflow.new.DataFlow
import Flow::PathGraph
module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(Call call |
call.getTarget().hasName("user_input") and
source.asExpr() = call
)
}
predicate isSink(DataFlow::Node sink) {
exists(Call call |
call.getTarget().hasName("read_data") and
sink.asIndirectExpr() = call.getAnArgument()
)
}
predicate allowImplicitRead(DataFlow::Node n, DataFlow::ContentSet cs) {
isSink(n) and
cs.getAReadContent().(DataFlow::FieldContent).getField().hasName(["a", "x"])
}
}
module Flow = DataFlow::Global<Config>;
from Flow::PathNode source, Flow::PathNode sink
where Flow::flowPath(source, sink)
select sink.getNode(), source, sink, "Flow from user input to sink!"
谓词 allowImplicitRead
指定如果我们处于一个满足 isSink
的节点,那么我们允许假设存在对名为 a
或名为 x
的字段的隐式读取(在本例中,两者都有)。这为我们提供了我们感兴趣的数据流,因为数据流库现在将看到
- 用户输入从
user_input()
开始。- 数据流入
b
,访问路径为[a, x]
。- 数据流向
&b
的间接寻址(即对象b
)。- 在汇点处对字段
x
进行隐式读取,然后对字段a
进行隐式读取。
因此,我们最终到达一个满足 isSink
的节点,其访问路径为空,并成功跟踪了完整的数据流路径。