codeql 查询 sink-source
快照包含源文件和数据库, ql 代码查询的是数据库中的数据,然后将符合要求的结果映射到对应的源代码中
核心思想 —— 将代码作为数据处理
QL 谓词 —— 微型查询,表征数据之间的关系。已有的谓词保存在 QL 库中 .qll,可以通过 import tutorial
方式导入 tutorial 库
# 谓词定义
谓词描述 —— 给定参数和元组集合的关系
1 | predicate isCountry(string country){ |
谓词定义解析
- predicate 关键字,适用于没有返回结果的谓词 或者返回结果类型,例如 int
- isCountry 谓词名称,以小写字母开头
- string country 谓词参数,多个以逗号间隔
- {…} 谓词主体,逻辑表达式
带有返回结果的谓词
引入一个特殊变量 result
1 | int getSucessor(int i){ |
每个参数都带一个返回值(或者一个也不带)
1 | string getANeighbor(string country) { |
- getANeighbor (“Germany”) 返回结果 “Austria"和"Belgium”
- getANeighbor (“Belgium”) 无返回结果
递归谓词
谓词的返回结果直接或间接依赖于自身
1 | string getANeighbor(string country) { |
-
getANeighbor (“Belgium”) 返回 “France"和"Germany”
简单解析,当前 4 个逻辑表达式都不正确时,判断 country = getANeighbor (result),给定的参数时 "Belgium",所以这里逻辑表达式成立的条件是 country=“Belgium”=getANeighbor (result)。
getANeighbor () 返回值是 "Belgium", 其成立条件是传入的参数为 "France" 或者 "Germany",这里用 result 来作为 getANeighbor () 的参数,所以最终的返回结果为 "France" 和 "Germany"
将线索转换为查询语句
查询某个不秃头的人
1 | from Person t |
- exists 关键字,是否存在
- string c 临时变量
- t.getHairColor () = c 逻辑表达式
# 类定义
自定义类,来找出考察对象,即住在村南的居民
1 | perdicate southern(Person p) { |
QL 中类用来表示一个逻辑属性 —— 当一个值满足该属性时,它是类的成员。这意味一个值可以属于多个类,例如 3 既属于 "整数" 类,也属于 "奇数" 类
上述类定义中, southern(this)
定义了该类的逻辑属性。表达式中使用了特殊变量 this,表示一个 Person 类型的值。如果一个 Person 满足 southern (this),那他属于 Southerner 类,即住在村南的居民。
当列举村南的居民,代码如下
1 | from Southerner s |
部分谓词以参数传递变量,例如 southern§,部分谓词跟着某些变量后面,例如 p.getAge ()。这是因为 getAge () 是定义在类 Person 中的一个成员谓词
例如,王冠丢失后,村落实施了交通管制,孩子不允许离开居住地。即意味着谓词 isAllowedIn (string region) 不适用所有村名和所有区域,所以需要对孩子重载原来的谓词 isAllowedIn (string region)。
重新定义一个类 Child,表示所有 10 岁以下的村民,重新定义谓词 isAllowdIn (string region),表示孩子只能在自己的地盘走动,表达式为 region = this.getLocation ()
1 | class Child extends Person { |
操作的传递闭包
同一个操作被应用多次,被称为操作的传递闭包。在处理传递闭包时,有两个特殊的符号极其有用,即 + 和 *
parentOf+(p)
,对变量 p 应用一次或多次谓词parentOf*(p)
,对变量 p 应用零次或者多次谓词 parentOf ()
# 深入了解递归
QL 语言中,如果谓词直接或者间接地调用了自身,称其为递归型谓词
为了求解递归谓词的返回值的集合,QL 编译器需要寻找递归的不动点,从空集开始,重复应用谓词,直到集合不发生变化,此时集合称为最小不动点。
求 0-100 之间的整数递归型谓词
1 | int getANumber() { |
求解该谓词,会得到 0-100 的集合
相互递归
递归型谓词除了调用自身外,也可以互相调用,形成一个循环
1 | int getAEven() { |
# Python
变量
Python 源代码中的变量可用 CodeQL 库中的 Variable 类来表示,该类有两个子类,LocalVariable 表示函数和类级别的变量,子类 GlobalVariable 用于表示模块级别的变量
控制流分析
每个作用域类(Class,Function,Module)都包含了一个由 ControlFlowNode 构成的图,每个作用域都有一个入口点和至少一个的出口点。为了提高分析控制流和数据流的速度,控制流节点会被分组为基本构造块。一个基本块就是一个没有任何分支的代码序列。
AST 节点和控制流节点存在一对多的关系。
查找针对特定函数的调用
通过 Call 和 Name 两个类,查找对函数 eval 的调用
1 | import python |
# 语句和表达式分析
语句
对于 Python 中各种类型的语句,CodeQL 都提供了相应的类加以表示
Stmt 类 —— 语句
-
Assert 类 —— assert 语句
-
Assign 类
-
AssignStmt 类 —— 赋值语句,如 x = y
-
ClassDef —— 类定义语句
-
FunctionDef —— 函数定义语句
-
AugAssign —— 增量赋值 (augmented assignment) 语句,如 x += y
-
Break 类 —— break 语句
-
Continue 类 —— continue 语句
-
Delete 类 —— del 语句
-
ExceptStmt 类 —— try 语句的 except 部分
-
Exec 类 —— exec 语句
-
For 类 —— for 语句
-
Global 类 —— global 语句
-
If 类 —— if 语句
-
ImportStar 类 —— from xxx import * 语句
-
Import 类 —— 其他类型的 import 语句
-
Nonlocal 类 —— nonlocal 语句
-
Pass 类 —— pass 语句
-
Print 类 —— print 语句 (仅限于 python 2 版本)
-
Raise 类 —— raise 语句
-
Return 类 —— return 语句
-
Try 类 —— try 语句
-
While 类 —— while 语句
-
With 类 —— with 语句
表达式
对于 Python 中各种类型的表达式,CodeQL 都提供了相应的类来加以表示。
Expr 类 —— 表达式
-
Attribute —— 属性,例如 obj.attr
-
BinaryExpr —— 二进制运算,例如 x+y
-
BoolExpr —— Short circuit logical operations, 例如 x and y, x or y
-
Bytes —— 字节,例如 b"x"
-
Call —— 函数调用,例如 f (arg)
-
Compare —— 比较运算,0<x<10
-
Dict —— 字典,例如
-
DictComp 类 —— 字典推导式,如
-
Ellipsis 类 —— 省略号表达式,如…
-
GeneratorExp 类 —— 生成器表达式
-
IfExp 类 —— 条件表达式,如 x if cond else y
-
ImportExpr 类 —— 表示导入模块的表达式
-
ImportMember 类 —— 表示从模块导入某些成员的表达式(from xxx import * 语句的一部分)
-
Lambda 类 —— Lambda 表达式
-
List 类 —— 列表,如 [‘a’, ‘b’]
-
ListComp 类 —— 列表推导式,如 [x for …]
-
Name 类 —— 对变量 var 的引用
-
Num 类 —— 数字,如 3 或 4.2
-
Floatliteral
-
ImaginaryLiteral 类
-
IntegerLiteral 类
-
Repr 类 —— 反引号表达
-
Set 类 —— 集合,如
-
SetComp 类 —— 集合推导式,如
-
Slice 类 —— 切片;如表达式 seq [0:1] 中的 0:1
-
Starred 类 —— 星号表达式,如 y, *x = 1,2,3(仅限于 Python 3)
-
StrConst 类 —— 字符串。 在 Python2 中,可以是字节或 Unicode 字符。 在 Python3 中,只能是 Unicode 字符。
-
Subscript 类 —— 下标运算,如 seq [index]
-
UnaryExpr 类 —— 一元运算,如 - x
-
Unicode 类 —— Unicode 字符,如 u"x" 或(Python 3 中的)“x”
-
Yield 类 —— yield 表达式
-
YieldFrom 类 —— yield from 表达式(Python 3.3+)
# 控制流分析
ControlFlowNode 类
抽象语法树节点和控制流节点存在一对多的关系,每个语法元素,即 AstNode 类,可以映射到零个、一个或多个 ControlFlowNode 类,每个 ControlFlowNode 类只映射到一个 AstNode
1 | try: |
访问 close_resource () 的路径有三条,一正常执行,二引发 might_raise,三 break
查找不可达语句,每条通过 AstNode 的路径都有一个 ControlFlowNode,所以所有不可达的 AstNode 都没有通过 ControlFlowNode 的路径,因此没有 ControlFlowNode 的 AstNode 是不可达的
1 | import python |
执行上述代码可能会得到大量返回结果,因为 Module 类是 AstNode 的子类,所以查询结果中包含用 C 语言实现的模块以及不包含源代码的模块。所以,最好还是由查找不可达 AstNode 转换为查找不可达语句
1 | import python |
# 污点跟踪和数据流分析
污点跟踪库位于 TaintTracking 模块中,另外,用于污点跟踪和数据流分析的所有查询都有三个显式组件(其中一个是可选的),以及一个隐式组件。显式组件包括
- 一个或多个可能存在不安全数据的源点,它们由 TaintTracking::Source 类表示。
- 由 TaintTracking::Sink 类表示的一个或多个数据或污点可能流向的接收点。
- 零个或多个清洗器,由 Sanitizer 类表示。
在数据从源点流向接收点的过程中,如果没有遭到清洗器的拦截的话,用于污点跟踪或数据流分析的查询就会返回相应的分析结果。
这三个组件是通过 TaintTracking::Configuration 绑定在一起的,以便明确特定查询与哪些源点和接收点相关。
最后一个隐式组件是污点的 “kind”,由 TaintKind 类表示。污点的类型决定了,除了执行内置的、针对 “保留值” 的处理之外,还执行哪些针对 “非保留值” 的分析步骤。例如,对于上面讲过的 dir = path + “/”,当污点表示字符串的时候,则污点数据会从 path 流向 dir,但如果污点为 None 的话,则不会出现这种情况。
查询所有对于 eval 函数的调用
1 | import python |
上面查询存在两个问题
- 对于所有名为’eval’的调用都会被视为内置函数 eval 的调用,导致假阳性
- 其假设 eval 不能被其他名称引用,导致假阴性
改进 —— 通过谓词 Value::named 来准确识别 eval 函数
1 | import python |
准确识别 eval 函数后,通过 Value.getACall () 来识别对 eval 函数的调用
1 | import python |
# 使用 VSCode 配置 CodeQL 工作区
# 使用 "starter" 工作区
"starter" 工作区实际上是一个 Git 存储库,包含如下内容
- 用于存放分析 Python 代码的 CodeQL 库和查询的存储库
# 编写路径查询
1 | /** |
- Paths 从标准 CodeQL 库中导入的路径图模块
- source,sink 为路径图中的节点
路径查询元数据
其必须包含属性 @kind path-problem,以确保查询结果可以正确解释和显示
生成路径解释
为了生成路径解释,我们的查询需要计算路径图。因此,我们需要定一个谓词 edges,用于计算与查询生成结果相关的谓词。从标准库导入预定义的 edges 谓词
Python
1 | import semmle.python.security.Paths |
自定义谓词
1 | query predicate edges(PathNode a, PathNode b){ |
定义流动条件
在编写路径查询时,通常包含一个谓词,该谓词仅在数据从源点流向接收点时成立
1 | where source.flowsTo(sink) |
# CodeQL CLI
# 创建 CodeQL 数据库
执行 codeql database create
1 | codeql database create [database-path] --language python --source-root ./demo-root |
其中,必须指定
-
–language: 为其创建数据库的语言的标识符。CodeQL 支持为以下语言创建数据库
语言 标识符 C/C++ cpp C# csharp Go go Java java JavaScript/TypeScript javascript Python python -
–source-root: 用于创建数据库的源文件的根目录。默认情况会将当前目录认为是源文件的根目录
# 使用 CodeQL CLI 分析数据库
实际利用 CodeQL 分析代码的过程,就是在从代码中提取的数据库上运行查询的过程。
运行查询
-
database analyze
-
database run-queries —— 该命令将以中间二进制格式(通常为 BQRS 格式)输出非解释型结果
-
query run —— 该命令既可以输出 BQRS 格式的文件,也可以将结果直接输出至命令行
1
codeql query run ./query.ql --databse=./demo-query --output=./result.bqrs
解码 bqrs
1 | codeql bqrs decode [--output=<file>] [--result-set=<name>] [--sort-key=<col>[,<col>...]] <options>... -- <file> |
codeql database analyze
运行 database analyze 命令时,其完成下述工作
- 运行一个或多个查询文件,在 CodeQL 数据库上运行相应的查询代码
- 根据某些查询元数据来解释结果,以便在源代码中相应位置显示警示信息
1 | codeql database analyze --format=csv --output=./path |
- –format —— 分析过程中生成的结果文件的格式
- –output —— 分析过程中生成的结果文件输出路径
使用自定义查询
-
编写有效查询,并将其保存到扩展名为.ql 的文件中
-
提供查询元数据
查询元数据通常位于每个查询文件的顶部,其作用时向用户提供有关该查询的说明信息,并告诉 CodeQL 如何处理查询结果
必须提供的两个属性
- 查询标识符 (@id): 由小写字母和数字组成的单词序列,用 “/” 或 "-" 作为分隔符,用于对查询进行标识和分类
- 查询类型(@kind):用以表示查询结果时警报(@kind problem)还是路径(@kind path-problem)
创建自定义 QL 包
编写自定义的查询代码时,应将其保存在自定义的 QL 包目录中。QL 包提供了一种组织 CodeQL 分析过程中所用文件的方法。自定义 QL 包目录下必须提供一个名为 qlback.yml
文件。
qlback.yml
文件用于告诉 CodeQL 如下信息:如何编译相应的查询代码,这个包依赖哪些库,在哪里找到查询套件信息。https://help.semmle.com/codeql/codeql-cli/reference/qlpack-overview.html#qlpack-yml
-
Q1: VSCode 中可以执行得到结果,命令行中执行 codeql query run 无法得到结果?
A: 在 VSCode 使用了工作区,所以执行查询时会自动在工作区目录下检索 ql 文件中导入的第三方库。而命令行执行时,ql 文件所在位置则无法检索到相应的第三方库。目前做法,把 ql 文件放在 VSCode-CodeQL-starter (工作区) 的 codeql-custom-queries-python/ 目录下
后续改进可以打包相关第三方库,这个方式还需要学习
https://docs.github.com/zh/code-security/codeql-cli/codeql-cli-manual/pack-packlist
# 更新 CodeQL 数据库
1 | codeql database upgrade |