目录

CodeQL 基础

CodeQL 基础

1 前言

1.1 背景

CodeQL 是一个代码分析平台,在它的帮助下,安全研究人员可以利用已知的安全漏洞来挖掘类似的漏洞。

CodeQL 就是一种代码分析平台。借助这个平台,安全研究人员可以实现变种分析的自动化。这里所谓的变种分析,就是以已知的安全漏洞作为参照物,在我们的目标代码中寻找类似的安全问题的过程,也就是照葫芦画瓢的过程。

此外,为了提高安全分析人员的工作效率,CodeQL 平台还提供了许多有用的工具、脚本、查询和代码库。

1.2 相关概念

1.2.1 CodeQL 核心

  • QL 语言

在静态程序分析学科中,通常采用一种 Data-Log 的声明式语言来代替命令式语言进行结果分析,具体可以参考「静态程序分析」文章。而 QL 语言便是 Data-Log 语言的一种。

  • QL 数据库

CodeQL 数据库中存放的是使用 CodeQL 创建和分析的关系数据。 可以将其看作是目标代码的中间分析产物。

1.2.2 CodeQL 工作原理

CodeQL 工作流程:

  1. 将代码创建成数据库
  2. 编写QL查询从数据库中查询代码
  3. 解释查询结果
1.2.2.1 数据库创建

使用语言相关的 extractor 从代码中提取抽象语法树(ast)、名称绑定的语义和类型信息,把源代码转化成单关系表示(single relational representation),以 CodeQL 数据库存储。而在 CodeQL 中,是通过一种 CSV flow 模型来作为中间代码的。

此外,每种语言都有自己独特的数据库模式,用于定义创建数据库的关系。该图为提取过程中的初始词汇分析与使用 CodeQL 的实际复杂分析提供了界面。

1.2.2.2 执行查询

使用 CodeQL 专门设计的面向对象语言 QL 来查询此前创建的数据库

1.2.2.3 结果分析

将查询结果对应到源代码的上下文中去,即通过查询结果的解释找到源码中所对应的潜在漏洞

1.3 CodeQL 安装

首先需要下载 CodeQL CLI 二进制文件并安装,CLI 二进制文件支持主流的操作系统,包括 Windows、MacOS、Linux(以在 MacOS 上安装为例,Windows 上同理):

1
2
3
4
5
6
7
# 下载codeql.zip
wget https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip
# 解压
unzip codeql.zip
# 将codeql添加至path中
echo "export PATH=\$PATH:/path/to/codeql" >> ~/.zshrc
source ~/.zshrc

然后需要下载相关库文件:https://github.com/Semmle/ql。库文件是开源的,后续要做的是根据这些库文件来编写 QL 脚本。

之后,需要在 VSCode 上安装对应的扩展,在应用商店中搜索 CodeQL 即可。安装之后,需要在扩展设置里配置 CLI 文件的位置。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202141536600.png-water_print

此外,还有一种快捷配置的方式,即:start workspace 项目。

📢 注意:该工作区内含了 QL 库,因此一定要使用递归方式来下拉工作区代码。递归方式下拉该仓库后,不需要再下载 https://github.com/Semmle/ql 这个库了。

1
git clone --recursive git@github.com:github/vscode-codeql-starter.git

在配置好环境之后,就可以利用 CLI 工具来创建数据库了。以 Java 代码为例,使用如下命令创建:

1
codeql database create <database-folder> --language=java --command="mvn clean install --file pom.xml"
技巧
如果省略--command参数,则 codeQL 会自动检测并使用自己的工具来构建。但还是强烈推荐使用自己自定义的参数,尤其是大项目时。

建立好的数据库,其目录结构为:

1
2
3
4
- log/                # 输出的日志信息
- db-java/            # 编译的数据库
- src.zip             # 编译所对应的目标源码
- codeql-database.yml # 数据库相关配置

除了在本地构建数据库外,CodeQL 还提供了在线版本:LGTM.com。一方面,可以在其上面直接搜索开源项目,下载数据库;另一方面,也可以上传代码,后台会自动生成代码数据库。同时,在选定项目后,也可以在线查询,十分方便。

最后在 VSCode 中,点击「打开工作区」来打开刚刚下拉的 vscode-codeql-starter 工作区,在 CodeQL 插件里,打开刚刚生成的 database。

然后编写自己的 CodeQL 脚本,并将脚本保存至 vscode-codeql-starter/codeql-custom-queries-java 处,这样 import 模块时就可以正常引用。将编写的 ql 脚本在 VSCode 中打开,之后点击 CodeQL 插件中的 Run Query,即可开始查询。

2 QL 语法

2.1 谓词

在 CodeQL 中,函数并不叫“函数”,叫做 Predicates(谓词)。为了便于说明,下文中的函数与谓词都是指代同一个内容。

谓词的定义方式如下:

1
2
3
4
predicate name(type arg)
{
  statements
}

定义谓词有三个要素:

  • 关键词 predicate(如果没有返回值),或者结果的类型(如果当前谓词内存在返回值)
  • 谓词的名称
  • 谓词的参数列表
  • 谓词主体

2.1.1 无返回值的谓词

  • 无返回值的谓词以predicate关键词开头。若传入的值满足谓词主体中的逻辑,则该谓词将保留该值。
  • 无返回值谓词的使用范围较小,但仍然在某些情况下扮演了很重要的一个角色
  • 举一个简单的例子
1
2
3
4
5
6
7
8
predicate isSmall(int i) {
  i in [1 .. 9]
}

from int i 
where isSmall(i) // 将整数集合i从正无穷大的数据集含,限制至 1-9
select i
// 输出 1-9 的数字

若传入的 i 是小于 10 的正整数,则 isSmall(i) 将会使得传入的集合 i 只保留符合条件的值,其他值将会被舍弃。

2.1.2 有返回值的谓词

当需要将某些结果从谓词中返回时,与编程语言的 return 语句不同的是,谓词使用的是一个特殊变量 result。谓词主体的语法只是为了表述逻辑之间的关系,因此务必不要用一般编程语言的语法来理解。

1
2
3
4
5
6
7
int getSuccessor(int i) {
  // 若传入的 i 位于 1-9 内,则返回 i+1
  result = i + 1 and i in [1 .. 9]
}
  
select getSuccessor(3)  // 输出4
select getSuccessor(33) // 不输出任何信息

在谓词主体中,result 变量可以像一般变量一样正常使用,唯一不同的是这个变量内的数据将会被返回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
string getANeighbor(string country) {
    country = "France" and result = "Belgium"
    or
    country = "France" and result = "Germany"
    or
    country = "Germany" and result = "Austria"
    or
    country = "Germany" and result = "Belgium"
}

select getANeighbor("France")
// 返回两个条目,"Belgium"  "Germany"

谓词不允许描述的数据集合个数不限于有限数量大小的。举个例子:

1
2
3
4
5
6
// 该谓词将使得编译报错
int multiplyBy4(int i) {
  // i 是一个数据集合,此时该集合可能是「无限大小」
  // result 集合被设置为 i*4,意味着 result 集合的大小有可能也是无限大小
  result = i * 4
}

但如果我们仍然需要定义这类函数,则必须限制集合数据大小,同时添加一个 bindingset 标注。该标注将会声明谓词 plusOne 所包含的数据集合是有限的,前提是 i 绑定到有限数量的数据集合。

1
2
3
4
5
6
7
8
bindingset[x] bindingset[y]
predicate plusOne(int x, int y) {
  x + 1 = y
}

from int x, int y
where y = 42 and plusOne(x, y)
select x, y

2.2 类

在 CodeQL 中的类,并不意味着建立一个新的对象,而只是表示特定一类的数据集合,定义一个类,需要三个步骤:

  • 使用关键字class
  • 起一个类名,其中类名必须是首字母大写的。
  • 确定是从哪个类中派生出来的

其中,基本类型 booleanfloatintstring 以及 date 也算在内。

如下是官方的一个样例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class OneTwoThree extends int {
  OneTwoThree() { // characteristic predicate
    this = 1 or this = 2 or this = 3
  }
 
  string getAString() { // member predicate
    result = "One, two or three: " + this.toString()
  }

  predicate isEven() { // member predicate
    this in [1 .. 2] 
  }
}

from OneTwoThree i 
where i = 1 or i.getAString() = "One, two or three: 2"
select i
// 输出 1  2

其中,特征谓词类似于类的构造函数,它将会进一步限制当前类所表示数据的集合。它将数据集合从原先的 Int 集,进一步限制至 1-3 这个范围。this 变量表示的是当前类中所包含的数据集合。与 result 变量类似,this 同样是用于表示数据集合直接的关系。

此外,在特征谓词中,比较常用的一个关键字是 exists。该关键字的语法如下:

1
2
3
4
exists(<variable declarations> | <formula>)
// 以下两个 exists 所表达的意思等价。
exists(<variable declarations> | <formula 1> | <formula 2>
exists(<variable declarations> | <formula 1> and <formula 2>

这个关键字的使用引入了一些新的变量。如果变量中至少有一组值可以使 formula 成立,那么该值将被保留。

一个简单的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import cpp

class NetworkByteSwap extends Expr{
    NetworkByteSwap()
    {
        // 对于MacroInvocation这个大类的数据集合来说
        exists(MacroInvocation mi |
            // 如果存在宏调用,其宏名称满足特定正则表达式
            mi.getMacroName().regexpMatch("ntoh(s|l|ll)") and
            // 将这类数据保存至当前类中
            this = mi.getExpr()
          )
    }
}

from NetworkByteSwap n
select n, "Network byte swap"

3 CodeQL U-Boot Challenge

在 Github Learning Lab 中,有一个用于学习 CodeQL 的入门课程 - CodeQL U-Boot Challenge (C/C++)](https://lab.github.com/GitHubtraining/codeql-u-boot-challenge-(cc++))

  • 编写一个简单的查询,用于查询strlen函数的定义位置。
1
2
3
4
5
import cpp

from Function f
where f.getName() = "strlen"
select f, "a function named strlen"
  • 分析这个简单的查询,之后查询一下memcpy函数
1
2
3
4
5
import cpp

from Function f
where f.getName() = "memcpy"
select f, "a function named memcpy"
  • 使用不同的类以及不同的谓词。这里我们编写 QL 查找名为 ntohsntohl 以及 ntohll的宏定义。
1
2
3
4
5
6
import cpp 

from Macro macro
# where macro.getName() = "ntohs" or macro.getName() = "ntohl" or macro.getName() = "ntohll"
where macro.getName().regexpMatch("ntoh(s|l|ll)")
select macro
  • 使用双变量。通过使用多个变量来描述复杂的代码关系,查询特定函数的调用位置。
1
2
3
4
5
import cpp

from FunctionCall c, Function f
where c.getTarget() = f and f.getName() == "memcpy"
select c
  • 使用 Step6 的技巧,查询宏定义的调用位置。
1
2
3
4
5
import cpp

from MacroInvocation mi
where mi.getMacro().getName().regexpMatch("ntoh(s|l|ll)")
select mi
  • 改变 select 的输出。查找这些宏调用所扩展到的顶级表达式(宏展开)。
1
2
3
4
5
import cpp

from MacroInvocation mi
where mi.getMacro().getName().regexpMatch("ntoh(s|l|ll)")
select mi.getExpr() # 注意这里的.getExpr()
  • 实现一个类。用 exists 关键字来引入一个临时变量,以设置当前类的数据集合;特征谓词在声明时会被调用以确定当前类的范围,类似于 C++ 构造函数。

特征谓词在声明时会被调用以确定当前类的范围,类似于 C++ 构造函数。查询语句中的类中,先通过 exists 量词创建一个临时变量 mi 来表示被调用的宏的名字,如果被调用的的宏展开后和当前代码片段相等,则这个表达式属于这个集合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import cpp

class NetworkByteSwap extends Expr {
  NetworkByteSwap() {
    exists(MacroInvocation mi |
      mi.getMacroName().regexpMatch("ntoh(s|l|ll)") and
      this = mi.getExpr()
    )
  }
}

from NetworkByteSwap n
select n, "Network byte swap"
  • 污点追踪

借助前面几步,基本描述了 CodeQL 的使用。最后一个测试是使用 CodeQL 进行污点追踪。这里使用了 CodeQL 的全局污点追踪 (Global taint tracking)。新定义的 Config 类继承于 TaintTracking::Configuration。类中重载的 isSource 谓语定义为污点的源头,而 isSink 定义为污点汇聚点。

有时候,远程输入的数据可能经过 ntoh 函数处理,通过转换字节序得到相应的数字。而 memcpy 的第 2 个参数如果控制不当,可造成数据溢出。将上面两个结论结合起来,如果有一个远程输入的数据通过字节序变换得到的数字,在未经过校验的情况下,作为了 memcpy 的第二个参数,那么就有可能造成数据溢出。

isSource 中,判断 sourceExpr 是否是 NetworkByteSwap 这个类,来判断污点的源头。

isSink 中,我们使用了辅助类 FunctionCall 判断函数调用是否为 memcpysink 的代码片段是否为 memcpy 的第二个参数;最后一句则是判断函数的第一个参数是否为常量,如果为常量的话基本不可能出现问题,所有忽略。

 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
29
30
31
32
33
import cpp
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow::PathGraph

# 设置用于交换网络数据的类
class NetworkByteSwap extends Expr {
  NetworkByteSwap() {
    exists(MacroInvocation mi |
      mi.getMacroName().regexpMatch("ntoh(s|l|ll)") and
      this = mi.getExpr()
    )
  }
}

# 设置污点跟踪的分析信息
class Config extends TaintTracking::Configuration {
  Config() { this = "NetworkToMemFuncLength" }

  override predicate isSource(DataFlow::Node source) { source.asExpr() instanceof NetworkByteSwap }

  override predicate isSink(DataFlow::Node sink) {
    exists(FunctionCall call |
      call.getTarget().getName() = "memcpy" and
      sink.asExpr() = call.getArgument(2) and
      not call.getArgument(1).isConstant()
    )
  }
}

# 查询
from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "Network byte swap flows to memcpy"

4 CodeQL for Java

4.1 基本查询

对 if 语句中的冗余代码进行搜索,例如空的 then 分支,示例代码如下:

1
if (error) {}

编写查询语句如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 引入 Java 标准查询库
import java

# 定义查询变量,声明 IfStmt 变量代表 if 语句
# 声明 BlockStmt 变量代表 then 代码块
from IfStmt ifstmt, BlockStmt block
# 定义查询的限制条件
where ifstmt.getThen() = block and
  block.getNumStmt() = 0
# 将结果返回到控制台 select <program element>, "<alert message>"
select ifstmt, "This 'if' statement is redundant."

查询优化

编写 QL 代码的过程是一个迭代的过程,在最初的查询结果中可能会出现一些「非预期」的结果,因此需要通过不断修改,来完善 QL 查询代码。

在如下示例代码中,空的 else if 分支的确有着自己的用途,因此优化查询:当 if 语句中具有 else 分支时,认为空分支有自己的作用,忽略空分支。

1
2
3
4
5
6
7
if (...) {
  ...
} else if ("-verbose".equals(option)) {
  // nothing to do - handled earlier
} else {
  error("unrecognized option");
}

查询语句优化:

1
2
3
where ifstmt.getThen() = block and
  block.getNumStmt() = 0 and
  not exists(ifstmt.getElse())

4.2 CodeQL 中的 Java 库

在分析一个 Java 程序时,可以利用 CodeQL 库中提供的大量类的集合。该库用于分析从 Java 项目中提取的 CodeQL 数据库。这个库中的类以面向对象的形式呈现数据库中的数据,并提供抽象和谓词来完成常见的分析任务。

这个库是作为一组 QL 模块实现的,也就是扩展名为 .qll 的文件。java.qll 模块导入了所有的核心 Java 库模块,因此可以通过在查询中使用 import java 来使用该库。

4.2.1 五大类库

  • Program Elements,程序元素,例如类和方法
  • AST nodes,抽象树节点,例如语句和表达式
  • Metadata,元数据,例如注解和注释
  • metrics,计算指标,例如循环复杂度
  • Call Gragh,调用图

4.2.2 程序元素

这些类包括:包(Package)、编译单元(CompilationUnit)、类型(Type)、方法(Method)、构造函数(Constructor)和变量(Variable)。

它们共同的超类是 Element,它提供了常用的成员谓词,用于确定程序元素的名称和检查两个元素是否相互嵌套。

因此可以方便的引用一个方法或构造函数的元素。此外,Callable 类是 MethodConstructor 的共同超类,可以用于此目的。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202202181451240.png-water_print

类型:Types

类 Type 有许多子类,用于表示不同种类的类型。

  • PrimitiveType 表示原始类型,即 boolean, byte, char, double, float, int, long, short;QL 也将 voidnulltype 归为原始类型。
  • RefType 是非原始类型,它又有几个子类。
    • Class
    • interface
    • enum
    • Array

例如,要查询程序中所有的 int 类型的变量:

1
2
3
4
5
6
import java

from Variable v, PrimitiveType pt
where pt = v.getType() and
    pt.hasName("int")
select v

引用类型也是根据它们的声明范围来分类的。

  • TopLevelType 代表在编译单元(一个 .java 文件)的顶层声明的类。
  • NestedType 是一个在另一个类型内声明的类型。
    • LoadClass:在成员方法或构造方法中声明的类
    • AnonymousClass:匿名类

例如,如下查询可以找到所有名称与其编译单元不一致的顶层类型。

1
2
3
4
5
import java

from TopLevelType tl
where tl.getName() != tl.getCompilationUnit().getName()
select tl

最后,该库还有一些单例子类,如:TypeObjectTypeCloneableTypeRuntimeTypeSerializable、TypeString、TypeSystemTypeClass。每个 CodeQL 类都代表其名称所暗示的标准 Java 类。

写一个找到所有直接继承 Object 的嵌套类的查询

1
2
3
4
5
import java

from NestedClass nc
where nc.getASupertype() instanceof TypeObject
select nc
泛型:Generics

GenericTypeGenericInterfaceGenericClass。它代表了一个泛型型声明,如 Java 标准库中的接口 java.util.Map:

1
2
3
4
5
6
package java.util.;

public interface Map<K, V> {
    int size();
    // ...
}

类型参数,如本例中的 K 和 V,由 TypeVariable 类表示。

一个泛型的参数化实例提供了一个具体实现该类型的参数,如 Map<String, File>。这样的类型由 ParameterizedType 表示,它与 GenericType 不同。要从 ParameterizedType 到其相应的 GenericType,可以使用谓词 getSourceDeclaration

例如,我们可以使用下面的查询来找到 java.util.Map 的所有参数化实例。

1
2
3
4
5
6
import java

from GenericInterface map, ParameterizedType pt
where map.hasQualifiedName("java.util", "Map") and
    pt.getSourceDeclaration() = map
select pt

一般来说,泛型需要限制类型参数可以与哪些类型绑定。例如,一个从字符串到数字的映射类型可以被声明如下:

1
2
3
class StringToNumMap<N extends Number> implements Map<String, N> {
    // ...
}

这意味着 StringToNumberMap 的参数化实例只能使用 Number 或它的一个子类型来实例化类型参数 N,而不能用其它类,如说 File。我们说 N 是一个有界的类型参数,Number 是它的上界。在 QL 中,一个类型变量可以用谓词 getATypeBound 来查询它的类型边界。类型边界本身由 TypeBound 类表示,它有一个成员谓词 getType 来检索变量被约束的类型。

如下的查询找到所有以 Number 类型为界限的变量:

1
2
3
4
5
6
import java

from TypeVariable tv, TypeBound tb
where tb = tv.getATypeBound() and
    tb.getType().hasQualifiedName("java.lang", "Number")
select tv

为了处理那些在泛型出现之前的遗留代码,每个泛型都有一个没有任何类型参数的 「原始」版本。在 CodeQL 库中,原始类型用 RawType 类表示,它有预期的子类 RawClassRawInterface。同样,有一个谓词 getSourceDeclaration 用于获得相应的通用类型。如下的查询可以找到(原始)类型 Map 的变量。实际上,现在仍然有许多项目在使用原始类型的 Map。

1
2
3
4
5
6
import java

from Variable v, RawType rt
where rt = v.getType() and
    rt.getSourceDeclaration().hasQualifiedName("java.util", "Map")
select v

上述的查询语句,针对如下代码片段,只能查找到 m1 而不能查找到 m2。

1
2
Map m1 = new HashMap();
Map<String, String> m2 = new HashMap<String, String>();
变量:Variable

Variable 表示 Java 中的变量,它可以是一个类的成员字段(无论是否静态),也可以是一个局部变量,或者是函数的参数。因此,有三个子类来满足这些特殊情况的需要。

  • Field :字段
  • LocalVariableDecl :本地变量.
  • Parameter :方法或构造函数的参数。

4.2.3 抽象语法树:Abstract Syntax Tree

该类中包含了抽象语法树的节点,也就是语句(QL 中的类 Stmt)和表达式(QL 中的类 Expr)。关于标准 QL 库中可用的表达式和语句类型的完整列表,可以参考该链接

ExprStmt 都提供了成员谓词来获取程序的抽象语法树:

  • Expr.getAChildExpr 返回一个给定表达式的子表达式。
  • Stmt.getAChild 返回直接嵌套在给定语句中的语句或表达式。
  • Expr.getParentStmt.getParent 返回 AST 节点的父节点。

下面的查询可以找到所有父类为返回语句的表达式。

1
2
3
4
5
import java

from Expr e
where e.getParent() instanceof ReturnStmt
select e

因此,程序中如果包含:return x + y 子表达式,QL 的查询结果将会返回:x + y

下面的查询可以找到某个表达式的父级为 if 语句:

1
2
3
4
5
import java

from Stmt s
where s.getParent() instanceof IfStmt
select s

这个查询将找到程序中所有 if 语句的 then 分支和 else 分支。

最后,一个查询的例子,可以找到方法体。

1
2
3
4
5
import java

from Stmt s
where s.getParent() instanceof Method
select s

如上的这些例子可知,表达式的父节点并不总是表达式:它也可能是一个语句,例如 IfStmt。同样,语句的父节点也不总是一个语句:它也可能是一个方法或构造函数。为了抓住这一点,QL Java 库提供了两个抽象类 ExprParentStmtParent,前者代表可能是表达式的父节点的任何节点,后者代表可能是语句的父节点的任何节点。

4.2.4 元数据:Metadata

除了 Java 程序代码本身之外,Java 程序还有几种元数据。其中包括有注解(Annotations) 和 Javadoc 注释。由于这些元数据对于加强代码分析或者是作为分析目标本身都很有用处,因此,QL 库定义了用于访问这些元数据的类。

对于注解(Annotations),类 Annotatable 是所有可以被注解的程序元素的超类。这包括包、引用类型、字段、方法、构造函数和声明的局部变量。对于每个这样的元素,类中的谓词 getAnAnnotation 可以检索该元素可能有的任何注释。例如,下面的查询可以找到构造函数上的所有注解。

1
2
3
4
import java

from Constructor c
select c.getAnAnnotation()

LGTM.com 的示例结果中都使用了注解。其中的一些返回结果,它们被用来取消警告和标记代码为废弃的。这些注释是由类 Annotation 表示的。一个注释是一个表达式,其类型是 AnnotationType。例如,通过修改查询,使其只返回废弃的构造函数。

1
2
3
4
5
6
7
import java

from Constructor c, Annotation ann, AnnotationType anntp
where ann = c.getAnAnnotation() and
    anntp = ann.getType() and
    anntp.hasQualifiedName("java.lang", "Deprecated")
select ann

对于 Javadoc 类型的注释,类 Element 有一个成员谓词 getDoc,它返回 Documentable 对象,然后可以查询其附加的 Javadoc 注释。例如,下面的查询可以找到私有字段的 Javadoc 注释。

1
2
3
4
5
6
import java

from Field f, Javadoc jdoc
where f.isPrivate() and
    jdoc = f.getDoc().getJavadoc()
select jdoc

Javadoc 类将整个 Javadoc 注释转换为一棵 JavadocElement 节点的树,可以使用成员谓词 getAChildgetParent 对其进行遍历。例如,编辑查询,使其找到 Javadoc 注释中所有关于私有字段的 @author 标签。

1
2
3
4
5
6
7
import java

from Field f, Javadoc jdoc, AuthorTag at
where f.isPrivate() and
    jdoc = f.getDoc().getJavadoc() and
    at.getParent+() = jdoc
select at

4.2.5 指标:Metrics

标准的 QL Java 库提供了对 Java 程序元素计算度量的广泛支持。为了避免代表这些元素的类因有太多与度量计算有关的成员谓词而负担过重,这些谓词在委托类中可用。

总共有六个这样的 QL 类。MetricElementMetricPackageMetricRefTypeMetricFieldMetricCallableMetricStmt。相应的元素类都提供了一个成员谓词 getMetrics,可以用来获取委托类的一个实例,然后对其进行度量计算。

查询举例:查询循环复杂度大于 40 的方法。

1
2
3
4
5
6
import java

from Method m, MetricCallable mc
where mc = m.getMetrics() and
    mc.getCyclomaticComplexity() > 40
select m

4.2.6 调用图:Call graph

从 Java 程序代码中生成的 CodeQL 数据库包括关于程序调用图的预计算信息,也就是说,一个给定的调用在运行时可能分配 (dispatch) 给哪些方法或构造函数。

上面介绍的 Callable 类,包括方法和构造函数。调用表达式是用类 Call 来抽象的,包括方法调用、 new 表达式和使用 thissuper 的显式构造函数调用。

通过使用谓词 Call.getCallee 来找出一个特定的调用表达式所指向的方法或构造函数。例如,下面的查询 所有名为 println 的调用方法。

1
2
3
4
5
6
import java

from Call c, Method m
where m = c.getCallee() and
    m.hasName("println")
select c

此外,Callable.getAReference 会返回一个指代它的 Call。因此,可以用这个查询找到那些从未被调用的方法和构造函数。代码示例:查询未被引用过的调用:

1
2
3
4
5
import java

from Callable c
where not exists(c.getAReference())
select c

4.3 数据流分析

本小节描述了如何在 CodeQL 中的 Java 库进行数据流分析的,并包括几个使用数据流查询的例子。下面几节描述了如何使用库进行本地数据流、全局数据流和污点跟踪。

4.3.1 本地数据流

使用本地数据流

本地数据流的作用域限定在一个方法或调用内。本地数据流相比全局数据流更容易,更快速,更准确。本地数据流相关的库位于 DataFlow 模块中,需要手动导入。

1
import semmle.code.java.dataflow.DataFlow

数据流节点(Node)可以分为 ExprNodeParamterNodeDataFlow::Node 的两个谓词,可以将数据流节点转 ExprParameter 的形式。

1
2
3
4
5
6
7
class Node {
  // 获取数据流节点Expr的形式
  Expr asExpr() { ... }
	
  // 获取数据流节点Parameter的形式
  Parameter asParameter() { ... }
}

DataFlow 的两个谓词,可以将 ExprParameter 转数据流节点的形式:

1
2
3
4
5
// 获取 Expr 对应的数据流节点形式
ExprNode DataFlow::exprNode(Expr e)

// 获取 Parameter 对应的数据流节点形式
ParameterNode DataFlow::parameter(Parameter p)

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

例如,可以通过 DataFlow 的谓词 localFlowStep 限定从 nodeFrom 流向 nodeTo 的数据流。

1
DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
使用本地污点追踪

如下示例代码中,如果 x 被定义为污点,那么 y 也将是污点。

1
2
String temp = x;
String y = temp + ", " + temp;

本地污点跟踪库在 TaintTracking 模块中。与本地数据流一样,可以使用 TaintTracking 的谓词 localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) 限定从 nodeFrom 流向 nodeTo 的数据流。

可以通过使用 +* 操作符递归地应用该谓词,或者使用预定义的递归谓词 localTaint,它等同于 localTaintStep*

1
TaintTracking::localTaint(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
示例

如下查询可以找到传递给 new FileReader(..) 的文件名。

1
2
3
4
5
6
7
import java

from Constructor fileReader, Call call
where
  fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and
  call.getCallee() = fileReader
select call.getArgument(0)

但是,该查询的结果只给出了参数中的表达式,而不是可以传递给它的值。所以使用本地数据流来找到所有流入参数的表达式。

1
2
3
4
5
6
7
8
9
import java
import semmle.code.java.dataflow.DataFlow

from Constructor fileReader, Call call, Expr src
where
  fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and
  call.getCallee() = fileReader and
  DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
select src

然后我们可以使来源更具体,例如对一个公共参数的访问。这个查询可以找到一个公共参数被传递给 new FileReader(...) 中。

1
2
3
4
5
6
7
8
9
import java
import semmle.code.java.dataflow.DataFlow

from Constructor fileReader, Call call, Parameter p
where
  fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and
  call.getCallee() = fileReader and
  DataFlow::localFlow(DataFlow::parameterNode(p), DataFlow::exprNode(call.getArgument(0)))
select p

4.3.2 全局数据流

全局数据流比本地数据流更强大,但是执行时也更消耗时间与内存。

使用全局数据流

需要继承 DataFlow::Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import semmle.code.java.dataflow.DataFlow

class MyDataFlowConfiguration extends DataFlow::Configuration {
  MyDataFlowConfiguration() { this = "MyDataFlowConfiguration" }

  override predicate isSource(DataFlow::Node source) {
    ...
  }

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

Configuration 内置的几个谓词

  • isSource:定义数据流的来源
  • isSink:定义数据流的终点
  • isBarrier:可选,限制数据流
  • isAdditionalFlowStep:可选,限制数据流的步数

然后通过调用 Configuration 的谓词 hasFlow(DataFlow::Node source, DataFlow::Node sink) 来执行数据流分析。

1
2
3
from MyDataFlowConfiguration dataflow, DataFlow::Node source, DataFlow::Node sink
where dataflow.hasFlow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()
使用全局污点追踪

需要继承 TaintTracking::Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import semmle.code.java.dataflow.TaintTracking

class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
  MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }

  override predicate isSource(DataFlow::Node source) {
    ...
  }

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

Configuration 内置的几个谓词:

  • isSource:污染源
  • isSink:污染汇聚点
  • isSanitizer:过滤器
  • isAdditionalTaintStep:数据流拼接

全局污点追踪的执行与全局数据流的执行相似,也是通过调用 ConfigurationhasFlow 谓词。

4.4 Java 类型

4.4.1 关于 Java 中的类型

此小结是 https://codeql.github.com/docs/codeql-language-guides/types-in-java/ 对 内容的进一步阐述

CodeQL 中提供了 Type 类以及子类,来表示 Java 中的各种类型。

正如前文所述,PrimitiveType 类用来表示 Java 中所有的主数据类型,例如 boolean、int。RefType 类用来表示 Java 中所有的引用类型,例如 Array、字符串等。

RefType 类提供了两个成员谓词 getASupertypegetASubtype 来查找该引用类型对应的超类和子类。

1
2
3
4
5
class A {}

interface I {}

class B extends A implements I {}

如上所示:A 的超类是 java.lang.Object,子类是 B;B 的超类有 A 和 I,没有子类。

以获取B类所有的超类为例:

1
2
3
4
5
import java

from Class B
where B.hasName("B")
select B.getASupertype+()

除了类层次建模之外,RefType 还提供了成员谓词 getAMember 用于访问类型中声明的成员(即字段、构造函数和方法),谓词 inherits(Method m) 用于检查类型是声明还是继承了方法 m。

4.4.2 示例

寻找可能存在问题的数据转换

作为如何使用类层次 API 的示例,我们可以编写一个查询来查找数组的向下转换,即某种类型 A[] 的表达式 e 转换为类型 B[] 的情况,使得 B 是 (不一定是直接的)A 的子类型。

这种类型的转换是有问题的,因为向下转换数组会导致运行时异常,即使每个单独的数组元素都可以向下转换。 例如,以下代码会引发 ClassCastException

1
2
Object[] o = new Object[] { "Hello", "world" };
String[] s = (String[])o;

另一方面,如果表达式 e 恰好实际计算为 B[] 数组,则强制转换将成功:

1
2
Object[] o = new String[] { "Hello", "world" };
String[] s = (String[])o;

在本教程中,我们不会尝试区分这两种情况。 我们的查询应该仅仅查找从某个类型源转换为另一个类型目标的转换表达式 ce,例如:

  • 源和目标都是数组类型。
  • 源的元素类型是目标的元素类型的递归超类。

转换为查询语句:

1
2
3
4
5
6
7
import java

from CastExpr ce, Array source, Array target
where source = ce.getExpr().getType() and
    target = ce.getType() and
    target.getElementType().(RefType).getASupertype+() = source.getElementType()
select ce, "Potentially problematic array downcast."

Array 类的成员谓词 getElementType 可以获得该数组中元素的数据类型,result 是 Type 类型的

1
Type Array::getElementType()

在第 6 行中,将 Type 类型向下转换为 RefType 类型,然后调用 RefType 类的成员谓词 getASupertype

1
target.getElementType().(RefType).getASupertype+()
标识方法

Java 支持重载,可以通过限制形参的数据类型来标识固定的某个方法:

标识 java.util.Collection.contains(Object) 方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class JavaUtilCollection extends GenericInterface {
    JavaUtilCollection() {
        this.hasQualifiedName("java.util", "Collection")
    }
}

class JavaUtilCollectionContains extends Method {
    JavaUtilCollectionContains() {
        this.getDeclaringType() instanceof JavaUtilCollection and
        this.hasStringSignature("contains(Object)")
    }
}

如上所示的 hasStringSignature 谓词有如下作用:

  1. 限制方法名为 contains,可以使用谓词 hasName 来替代
  2. 限制参数为 1 个,可以使用谓词 getNumberOfParameters 来替代
  3. 限制参数的类型是 Object 类型,可以使用如下方式来替代
1
getParameter(0).getType() instanceof TypeObject.

参考