CodeQL 文档

注意

此处描述的数据流库可从 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
 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 数据库包含以下信息:
  1. 用户输入从 user_input() 开始,流入 fill_structure
  2. 数据使用访问路径 [a, p] 写入到对象 b
  3. 对象 bfill_structure 流出,流入 process_structure
  4. 访问路径 [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 的调用

示例 2
 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 定义,数据流库将沿着以下路径跟踪流

  1. 现在,流从 write_user_input_to(...) 的传出参数开始。
  2. 流在下一行继续到 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!"

请注意,isSourceisSink 符合预期:我们正在寻找从 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 中。

因此,我们得到了所需的路徑。

考虑一个稍微不同的汇:

示例 3
 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 中的流也可以通过以下方式跟踪:
  1. 更改 isAdditionalFlowStep,使其针对表示 FieldAccess 值的数据流节点,而不是它指向的值,以及
  2. 更改 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

示例 4
 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)。因此,为了处理这种情况,我们需要将上述两种情况结合起来。
  1. 我们需要像在 使用 asExpr 部分中所述的那样对字段的值进行污染。
  2. 我们需要像在 使用 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!"

通过该查询,可以识别出数据流。

指定隐式读取

上一节演示了如何将数据流从限定符添加到字段访问,因为源隐式地污染了结构体的所有字段。本节考虑相反的情况:一个特定的字段被污染,我们希望找到任何可能从该对象读取数据的地方,包括任何读取未知字段集的地方。

为了设置场景,请考虑以下情况

示例 5
 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);
}

在这个例子中,数据流如下

  1. 我们在访问路径 [a, x] 上将用户控制的值写入对象 b 中。
  2. 之后,将 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 的字段的隐式读取(在本例中,两者都有)。这为我们提供了我们感兴趣的数据流,因为数据流库现在将看到

  1. 用户输入从 user_input() 开始。
  2. 数据流入 b,访问路径为 [a, x]
  3. 数据流向 &b 的间接寻址(即对象 b)。
  4. 在汇点处对字段 x 进行隐式读取,然后对字段 a 进行隐式读取。

因此,我们最终到达一个满足 isSink 的节点,其访问路径为空,并成功跟踪了完整的数据流路径。

  • ©GitHub, Inc.
  • 条款
  • 隐私