CodeQL 基础
1 前言
1.1 背景
CODEQL是一个代码分析平台,它可以帮助安全研究人员利用已知的安全漏洞来探索类似的漏洞。CodeQL是一个代码分析平台。使用此平台,安全研究人员可以自动化变体分析。所谓的变体分析是,使用已知的安全漏洞作为参考,即复制它们的过程,在我们的目标代码中找到类似的安全问题的过程。
此外,为了提高安全分析师的生产率,CodeQL平台还提供了许多有用的工具,脚本,查询和代码库。
1.2 相关概念
1.2.1 CodeQL 核心
QL 语言在静态程序分析的学科中,通常使用数据模型的声明性语言而不是命令性语言进行结果分析。有关详细信息,请参阅文章“静态程序分析”。 QL语言是一种数据库语言。QL 数据库CodeQL数据库存储使用CodeQL创建和分析的关系数据。它可以视为目标代码的中间分析产品。
1.2.2 CodeQL 工作原理
CodeQl工作流程:在数据库中创建代码
从数据库写QL查询代码
解释查询结果
1.2.2.1 数据库创建
使用与语言相关的提取器提取抽象语法树,命名绑定语义和从代码键入信息,将源代码转换为单个关系表示形式,然后将其存储在CodeQL数据库中。在CodeQL中,CSV流量模型用作中间代码。此外,每种语言都有自己的独特数据库架构,可以定义创建数据库的关系。该图为提取过程中的初始词汇分析和使用CodeQL实用的复杂分析提供了一个接口。
1.2.2.2 执行查询
使用专门设计的面向对象的语言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:/path/to/codeql'〜/.zshrc
来源〜/.zshrc
然后,您需要下载相关库文件:https://github.com/semmle/ql。库文件是开源的,稍后需要做的是基于这些库文件编写QL脚本。
之后,您需要在VSCODE上安装相应的扩展名,并在App Store中搜索CodeQl。安装后,您需要在扩展设置中配置CLI文件的位置。

此外,还有一种快速配置方法,即:Start Workspace项目。
注意:此工作空间包含QL库,因此您必须使用递归方法来拉下工作区代码。在以递归方式取下递归存储库后,您不再需要下载https://github.com/semmle/ql库。
1
git clone-recursive [email protected]:github/vscode-codeql-starter.git
配置环境后,您可以使用CLI工具来创建数据库。以Java代码为例,使用以下命令创建它:
1
codeql数据库创建数据库折叠器-language=java -command='mvn clean naster-file pom.xml'
技能
如果省略了- 命令参数,则CodeQL将自动检测并使用其自己的工具构建。但是,仍然强烈建议使用您自己的自定义参数,尤其是在大型项目方面。
一个好的数据库具有目录结构:
1
2
3
4
- 日志/#输出日志信息
-DB-Java/#编译数据库
-src.zip#编译相应的目标源代码
-CodeQl-Database.yml#数据库相关配置
除了本地构建数据库外,CodeQL还提供了一个在线版本:lgtm.com。一方面,您可以直接搜索其上的开源项目并下载数据库;另一方面,您还可以上传代码,背景将自动生成代码数据库。同时,选择项目后,您也可以在线查询,这非常方便。
最后,在vscode中,单击“打开工作区”以打开刚刚拉下的VSCODE-CODEQL启动工作区,然后在CodeQL插件中打开刚刚生成的数据库。
然后写下您自己的codeql脚本,然后将脚本保存到vscode-codeql-starter/codeql-custom-queries-java,以便可以正常引用该模块。在VSCODE中打开书面QL脚本,然后单击CodeQL插件中的运行查询以启动查询。
2 QL 语法
2.1 谓词
在CodeQl中,一个函数不称为“函数”,而是称为谓词(prepicates)。为了易于解释,以下功能和谓词是指相同的内容。谓词定义如下:
1
2
3
4
谓词名称(类型ARG)
{
语句
}
有三个要素可以定义一个谓词:
关键字谓词(如果没有返回值)或结果的类型(如果当前谓词中有返回值)
谓词的名称
谓词参数列表
谓词主题
2.1.1 无返回值的谓词
没有返回值的谓词以谓词关键字开始。如果传递值满足谓词主体中的逻辑,则谓词保留值。非返回值谓词的使用范围很少,但在某些情况下仍然起着非常重要的作用。
举一个简单的例子
1
2
3
4
5
6
7
8
预测ISSMALL(INT I){
我在[1 . 9]中
}
来自int i
iSSMALL(i)//包含来自正无穷基数据集至1-9的整数集i
选择我
//输出1-9个数字
如果通过I中的i是一个较小的积极整数,则ISSMALL(i)将导致IN集合中通过的IN i仅保留符合条件的值,而其他值将被丢弃。
2.1.2 有返回值的谓词
当需要从谓词返回某些结果时,与编程语言中的返回语句不同时,谓词使用特殊的变量结果。谓词主题的语法仅是为了表达逻辑之间的关系,因此不要使用一般编程语言的语法来理解很重要。1
2
3
4
5
6
7
int getuccessor(int i){
//如果即将到来的i在1-9之内,请返回i+1
结果=i + 1,在[1 . 9]中
}
选择getuccessor(3)//输出4
选择getuccessor(33)//未输出信息
在谓词主体中,结果变量可以像普通变量一样正常使用,唯一的区别是将返回该变量中的数据。
1
2
3
4
5
6
7
8
9
10
11
12
字符串getaneighbor(字符串乡村){
country='法国'和结果='比利时'
或者
country='法国'和结果='德国'
或者
country='德国'和结果='奥地利'
或者
country='德国',结果='比利时'
}
选择Getaneighbor(“法国”)
//返回两个条目,“比利时”和“德国”
谓词不允许由不限于有限数量大小描述的数据集数量。例如:
1
2
3
4
5
6
//此谓词将导致编译错误
int multiplyby4(int i){
//我是一个数据收集,目前可能是“无限大小”
//结果集合设置为i*4,这意味着结果集合的大小也可能是无限尺寸的。
结果=i * 4
}
但是,如果我们仍然需要定义此类功能,则必须同时添加bindingset注释。该注释将声明,只要我绑定到有限数量的数据集,谓词加上中包含的数据集是有限的。
1
2
3
4
5
6
7
8
bindingset [x] bindingset [y]
谓词plusone(int x,int y){
x + 1=y
}
从int x,int y
其中y=42和plusone(x,y)
选择x,y
2.2 类
codeql中的类别为限制集合数据大小,但仅表示特定类的数据集。定义课程需要三个步骤:使用关键字类
给出一个班级名称,在首字母中必须大写班级名称。
确定从哪个类得出的课程
其中还包括基本类型的布尔,浮点,int,字符串和日期。
这是一个官方例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class onetwothree扩展了int {
onetwothree(){//特征预测
这个=1或this=2或this=3
}
字符串getAstring(){//成员预测
结果='一个,两个或三个或三个或三个:' + this.tostring()
}
预测iseven(){//成员预测
这在[1 . 2]中
}
}
来自Onetwothree i
其中i=1或i.getAstring()='一个,两个或三个或三个或三个或三个: 2'
选择我
//输出1和2
其中,并不意味着建立一个新的对象与类的构造函数相似,该构造将进一步限制当前类所示的数据收集。它进一步将数据集从原始INT组限制为范围1-3。该变量表示当前类中包含的数据集。与结果变量类似,这也用于表示数据集的直接关系。
此外,在特征谓词中,存在一个更常用的关键字。此关键字的语法如下:
1
2
3
4
存在(可变声明|公式)
//以下两个存在是等效的。
存在(变量声明|公式1 |公式2
存在(可变声明|一级方程式和公式2
此关键字使用引入一些新变量。如果在变量中至少一组值可以使公式保持,则将保留该值。
一个简单的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
导入CPP
类nevertentbyteswap扩展了expr {
NetworkBytesWap()
{
//对于大型发电类别的数据收集,
存在(大量发育Mi |
//如果存在宏调用,则其宏名称满足特定的正则表达式
mi.getmacroname()。regexpMatch('ntoh(s | l | ll)')和
//将此类型的数据保存到当前类中
此=mi.getExpr()
)
}
}
来自NetworkBytesWap n
选择N,“网络字节交换”
3 CodeQL U-Boot Challenge
在GitHub Learning Lab中,有一个学习Codeql-codeql U-Boot挑战(C/C ++)的入门课程(C/C ++)](3https://lab.github.github.com/githubtraining/codeql-u-u-u-boot-boot-boot-challenge-ccc++))编写一个简单的查询来查询strlen函数的定义位置。
1
2
3
4
5
导入CPP
从功能f
其中f.getName()='strlen'
选择f,“名为strlen'的函数”
分析此简单查询,然后查询memcpy函数
1
2
3
4
5
导入CPP
从功能f
其中f.getName()='memcpy'
选择f,“名为memcpy”的函数
使用不同的类和不同的谓词。在这里,我们编写QL以找到名为NTOHS,NTOHL和NTOHLL的宏定义。
1
2
3
4
5
6
导入CPP
来自宏观宏
#where macro.getName()='ntohs'或macro.getName()='ntohl'或macro.getName()='ntohll'
wery acro.getName()。regexpMatch('ntoh(s | l | ll)')
选择宏
使用双变量。通过使用多个变量描述复杂的代码关系来查询特定函数的呼叫位置。
1
2
3
4
5
导入CPP
从函数c,函数f
其中c.getTarget()=f and f.getName()=='memcpy'
选择c
使用Step6的技巧查询宏定义的呼叫位置。
1
2
3
4
5
导入CPP
来自宏观发育MI
其中mi.getMacro()。getName()。regexpMatch('ntoh(s | l | ll)')
选择MI
更改选择的输出。找到这些宏调用扩展的顶级表达式(宏扩展)。
1
2
3
4
5
导入CPP
来自宏观发育MI
其中mi.getMacro()。getName()。regexpMatch('ntoh(s | l | ll)')
选择mi.getExpr()#注意.getExpr()此处
实施一个类。使用“存在的关键字”引入临时变量以设置当前类的数据集;当声明以确定当前类的范围时,将调用特征谓词,类似于C ++构造函数。
当声明以确定当前类的范围时,将调用特征谓词,类似于C ++构造函数。在查询语句中的类中,通过存在的量词来创建一个临时变量MI,以表示所谓的宏的名称。如果将所谓的宏观扩展并且当前代码片段相等,则此表达式属于此集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
导入CPP
类nevertentbyteswap扩展了expr {
networkbyteswap(){
存在(大量发育Mi |
mi.getmacroname()。regexpMatch('ntoh(s | l | ll)')和
此=mi.getExpr()
)
}
}
来自NetworkBytesWap n
选择N,“网络字节交换”
污渍跟踪
在先前的步骤的帮助下,基本上描述了CodeQL的使用。最后一个测试是使用CodeQL进行污点跟踪。这里使用了CodeQL的全局污染跟踪。新定义的配置类从tainttracking :Configuration继承。类中的超载Issource谓词定义为污点的来源,而ISSINK定义为污点收敛点。
有时,远程输入数据可以通过NTOH函数处理,并且通过转换endians获得相应的数字。如果memcpy的第二个参数未正确控制,则可能导致数据溢出。结合上述两个结论,如果通过字节转换获得远程输入的数据,则将其用作Memcpy的第二个参数而无需通过验证,则可能会导致数据溢出。
在Issource中,判断源Expr是否是确定污渍来源的NetworkBytesWap类。
在ISSINK中,我们使用辅助类功能表来确定函数调用是否为memcpy,以及接收器的代码段是否是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
导入CPP
导入semmle.code.cpp.dataflow.tainttracking
导入dataFlow:3360 Pathergraph
#设置用于交换网络数据的类
类nevertentbyteswap扩展了expr {
networkbyteswap(){
存在(大量发育Mi |
mi.getmacroname()。regexpMatch('ntoh(s | l | ll)')和
此=mi.getExpr()
)
}
}
#设置污点跟踪的分析信息
类配置扩展tainttracking:Configuration {
config(){this='NetworkTomemFunClength'}
覆盖预测ISSOURCE(dataFlow:Node source){source.asexpr()instance of NetworkByTesWap}
Override预测ISSINK(DataFlow:Node接收器){
存在(函数电路调用|
call.getTarget()。getName()='memcpy'和
sink.asexpr()=call.getArgument(2)和
不调用。GetArgument(1).isconstant()
)
}
}
# 询问
来自Config CFG,DataFlow: PatherNode源,dataflow: PatherNode接收器
其中cfg.hasflowpath(源,接收器)
选择接收器,源,接收器,“网络字节交换流向memcpy'
4 CodeQL for Java
4.1 基本查询
在if语句中搜索冗余代码,例如空的分支。示例代码如下:1
如果(错误){}
编写查询语句如下:
1
2
3
4
5
6
7
8
9
10
11
#介绍Java标准查询库
导入Java
#定义查询变量,并声明IFSTMT变量表示IF语句
#声明blockstmt变量代表然后代码块
来自IFSTMT IFSTMT,BlockStmt块
#定义查询的限制
ifstmt.getThen()=block和
block.getNumstmt()=0
#将结果返回到控制台选择程序元素“警报消息”
选择IFSTMT,“此”语句是多余的。
010-1011编写QL代码的过程是一个迭代过程。初始查询结果可能会出现一些“意外”结果,因此需要通过连续修改来改进QL查询代码。
在下面的示例代码中,如果分支确实具有自己的目的,则其他的其他内容进行了优化:当if语句中有其他分支时,人们相信空分支具有自己的功能并忽略空分支。
1
2
3
4
5
6
7
如果(.) {
.
} else if('-verbose'.equals(option)){
//无事可做- 之前处理
} 别的{
错误(“未识别的选项”);
}
查询语句优化:
1
2
3
ifstmt.getThen()=block和