codeql 查询 sink-source

快照包含源文件和数据库, ql 代码查询的是数据库中的数据,然后将符合要求的结果映射到对应的源代码中

核心思想 —— 将代码作为数据处理

QL 谓词 —— 微型查询,表征数据之间的关系。已有的谓词保存在 QL 库中 .qll,可以通过 import tutorial 方式导入 tutorial 库

# 谓词定义

谓词描述 —— 给定参数和元组集合的关系

1
2
3
4
5
6
7
predicate isCountry(string country){
country = "Germany"
or
country = "Belgium"
or
country = "France"
}

谓词定义解析

  • predicate 关键字,适用于没有返回结果的谓词 或者返回结果类型,例如 int
  • isCountry 谓词名称,以小写字母开头
  • string country 谓词参数,多个以逗号间隔
  • {…} 谓词主体,逻辑表达式

带有返回结果的谓词

引入一个特殊变量 result

1
2
3
int getSucessor(int i){
result = i+1 and i in [1..9]
}

每个参数都带一个返回值(或者一个也不带)

1
2
3
4
5
6
7
8
9
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"
}
  • getANeighbor (“Germany”) 返回结果 “Austria"和"Belgium”
  • getANeighbor (“Belgium”) 无返回结果

递归谓词

谓词的返回结果直接或间接依赖于自身

1
2
3
4
5
6
7
8
9
10
11
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"
or
country = getANeighbor(result)
}
  • getANeighbor (“Belgium”) 返回 “France"和"Germany”

    简单解析,当前 4 个逻辑表达式都不正确时,判断 country = getANeighbor (result),给定的参数时 "Belgium",所以这里逻辑表达式成立的条件是 country=“Belgium”=getANeighbor (result)。

    getANeighbor () 返回值是 "Belgium", 其成立条件是传入的参数为 "France" 或者 "Germany",这里用 result 来作为 getANeighbor () 的参数,所以最终的返回结果为 "France" 和 "Germany"

将线索转换为查询语句

查询某个不秃头的人

1
2
3
from Person t
where exists(string c | t.getHairColor()=c)
select t
  • exists 关键字,是否存在
  • string c 临时变量
  • t.getHairColor () = c 逻辑表达式

# 类定义

自定义类,来找出考察对象,即住在村南的居民

1
2
3
4
5
6
7
perdicate southern(Person p) {
p.getLocation() = "south"
}

class Southerner extends Person {
Southerner() {southern(this)}
}

QL 中类用来表示一个逻辑属性 —— 当一个值满足该属性时,它是类的成员。这意味一个值可以属于多个类,例如 3 既属于 "整数" 类,也属于 "奇数" 类

上述类定义中, southern(this) 定义了该类的逻辑属性。表达式中使用了特殊变量 this,表示一个 Person 类型的值。如果一个 Person 满足 southern (this),那他属于 Southerner 类,即住在村南的居民。

当列举村南的居民,代码如下

1
2
from Southerner s
select s

部分谓词以参数传递变量,例如 southern§,部分谓词跟着某些变量后面,例如 p.getAge ()。这是因为 getAge () 是定义在类 Person 中的一个成员谓词

例如,王冠丢失后,村落实施了交通管制,孩子不允许离开居住地。即意味着谓词 isAllowedIn (string region) 不适用所有村名和所有区域,所以需要对孩子重载原来的谓词 isAllowedIn (string region)。

重新定义一个类 Child,表示所有 10 岁以下的村民,重新定义谓词 isAllowdIn (string region),表示孩子只能在自己的地盘走动,表达式为 region = this.getLocation ()

1
2
3
4
5
6
7
8
9
10
11
class Child extends Person {

/* 特征谓词 */
Child() {this.getAge() < 10}

/* 成员谓词 */
override predicate isAllowedIn(string region) {
region = this.getLocation()
}
}

操作的传递闭包

同一个操作被应用多次,被称为操作的传递闭包。在处理传递闭包时,有两个特殊的符号极其有用,即 + 和 *

  • parentOf+(p) ,对变量 p 应用一次或多次谓词
  • parentOf*(p) ,对变量 p 应用零次或者多次谓词 parentOf ()

# 深入了解递归

QL 语言中,如果谓词直接或者间接地调用了自身,称其为递归型谓词

为了求解递归谓词的返回值的集合,QL 编译器需要寻找递归的不动点,从空集开始,重复应用谓词,直到集合不发生变化,此时集合称为最小不动点。

求 0-100 之间的整数递归型谓词

1
2
3
4
5
6
7
int getANumber() {
result = 0
or
result <= 100 and result = getANumber() + 1
}

select getANumber()

求解该谓词,会得到 0-100 的集合

相互递归

递归型谓词除了调用自身外,也可以互相调用,形成一个循环

1
2
3
4
5
6
7
8
9
10
11
int getAEven() {
result = 0
or
result <= 100 and result = getAnOdd() + 1
}

int getAnOdd() {
result = getAEven
}

select getAnEven()

# Python

变量

Python 源代码中的变量可用 CodeQL 库中的 Variable 类来表示,该类有两个子类,LocalVariable 表示函数和类级别的变量,子类 GlobalVariable 用于表示模块级别的变量

控制流分析

每个作用域类(Class,Function,Module)都包含了一个由 ControlFlowNode 构成的图,每个作用域都有一个入口点和至少一个的出口点。为了提高分析控制流和数据流的速度,控制流节点会被分组为基本构造块。一个基本块就是一个没有任何分支的代码序列。

AST 节点和控制流节点存在一对多的关系。

查找针对特定函数的调用

通过 Call 和 Name 两个类,查找对函数 eval 的调用

1
2
3
4
5
import python

from Call call, Name name
where call.getFunc() = name and name.getId() = "eval"
select call, "call to 'eval'"

# 语句和表达式分析

语句

对于 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
2
3
4
5
6
try:
might_raise()
if cond:
break
finally:
close_resource()

访问 close_resource () 的路径有三条,一正常执行,二引发 might_raise,三 break

查找不可达语句,每条通过 AstNode 的路径都有一个 ControlFlowNode,所以所有不可达的 AstNode 都没有通过 ControlFlowNode 的路径,因此没有 ControlFlowNode 的 AstNode 是不可达的

1
2
3
4
5
import python

from AstNode node
where not exists(node.getAFlowNode())
select node

执行上述代码可能会得到大量返回结果,因为 Module 类是 AstNode 的子类,所以查询结果中包含用 C 语言实现的模块以及不包含源代码的模块。所以,最好还是由查找不可达 AstNode 转换为查找不可达语句

1
2
3
4
5
import python

from Stmt s
where not exists(s.getAFlowNode())
select s

# 污点跟踪和数据流分析

污点跟踪库位于 TaintTracking 模块中,另外,用于污点跟踪和数据流分析的所有查询都有三个显式组件(其中一个是可选的),以及一个隐式组件。显式组件包括

  1. 一个或多个可能存在不安全数据的源点,它们由 TaintTracking::Source 类表示。
  2. 由 TaintTracking::Sink 类表示的一个或多个数据或污点可能流向的接收点。
  3. 零个或多个清洗器,由 Sanitizer 类表示。

在数据从源点流向接收点的过程中,如果没有遭到清洗器的拦截的话,用于污点跟踪或数据流分析的查询就会返回相应的分析结果。

这三个组件是通过 TaintTracking::Configuration 绑定在一起的,以便明确特定查询与哪些源点和接收点相关。

最后一个隐式组件是污点的 “kind”,由 TaintKind 类表示。污点的类型决定了,除了执行内置的、针对 “保留值” 的处理之外,还执行哪些针对 “非保留值” 的分析步骤。例如,对于上面讲过的 dir = path + “/”,当污点表示字符串的时候,则污点数据会从 path 流向 dir,但如果污点为 None 的话,则不会出现这种情况。

查询所有对于 eval 函数的调用

1
2
3
4
5
import python

from Call call, Name name
where call.getFunc() = name and name.getId() = "eval"
select call, "call to 'eval'"

上面查询存在两个问题

  1. 对于所有名为’eval’的调用都会被视为内置函数 eval 的调用,导致假阳性
  2. 其假设 eval 不能被其他名称引用,导致假阴性

改进 —— 通过谓词 Value::named 来准确识别 eval 函数

1
2
3
4
5
import python

from Name name
where name = Value::named('eval')
select name

准确识别 eval 函数后,通过 Value.getACall () 来识别对 eval 函数的调用

1
2
3
4
5
6
import python

from ControlFlowNode call, Value val
where eval = Value::named('eval') and
call = eval.getACall()
select call, "call to 'eval'"

# 使用 VSCode 配置 CodeQL 工作区

# 使用 "starter" 工作区

"starter" 工作区实际上是一个 Git 存储库,包含如下内容

  • 用于存放分析 Python 代码的 CodeQL 库和查询的存储库

# 编写路径查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
*...
*@kind path-problem
*...
*/

import python
import semmle.python.security.Paths

...

from TaintedPathSource source, TaintedPathSink sink
where source.flowsTo(sink)
select sink.getNode(), source, sink, ''
  • Paths 从标准 CodeQL 库中导入的路径图模块
  • source,sink 为路径图中的节点

路径查询元数据

其必须包含属性 @kind path-problem,以确保查询结果可以正确解释和显示

生成路径解释

为了生成路径解释,我们的查询需要计算路径图。因此,我们需要定一个谓词 edges,用于计算与查询生成结果相关的谓词。从标准库导入预定义的 edges 谓词

Python

1
import semmle.python.security.Paths

自定义谓词

1
2
3
query predicate edges(PathNode a, PathNode b){
/** Logical conditions which hold if `(a,b)` is an edge in the data flow graph */
}

定义流动条件

在编写路径查询时,通常包含一个谓词,该谓词仅在数据从源点流向接收点时成立

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 命令时,其完成下述工作

  1. 运行一个或多个查询文件,在 CodeQL 数据库上运行相应的查询代码
  2. 根据某些查询元数据来解释结果,以便在源代码中相应位置显示警示信息
1
codeql database analyze --format=csv --output=./path
  • –format —— 分析过程中生成的结果文件的格式
  • –output —— 分析过程中生成的结果文件输出路径

使用自定义查询

  1. 编写有效查询,并将其保存到扩展名为.ql 的文件中

  2. 提供查询元数据

    查询元数据通常位于每个查询文件的顶部,其作用时向用户提供有关该查询的说明信息,并告诉 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
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

chaihj15 微信支付

微信支付

chaihj15 支付宝

支付宝