一、安装部分
1、下载对应版本的codeql:Releases · github/codeql-cli-binaries · GitHub
保存到本地codeql-cli文件夹内,cli即command-line interface。主要用来执行codeql的命令。
2、安装codeql仓库保存到本地codeql-repo文件夹,codeql仓库是包含多种语言的ql查询文件
3、总之,建立一个CodeQL的文件夹,文件夹里面新建3个文件,分别是:codeql-cli、codeql-repo、databases。
codeql-cli:里面放的就是codeql-osx64.zip的解压内容。
codeql-repo:里面放的是GitHub - github/codeql: CodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security下载的压缩包解压内容。
databases:空文件夹,后面用来放codeql生成的数据库文件,现在暂时是空文件夹。
4、安装vscode(https://code.visualstudio.com/)
5、配置codeql环境变量
open -e ~/.zshrc
exportPATH="/Users/xxx/Desktop/tools/CodeQL/codeql-cli:$PATH"
source ~/.zshrc
6、在终端直接输入codeql -h,出现下面的信息,说明成功了
7、下载maven(https://maven.apache.org/download.cgi)并且配置路径
二、测试部分
1、下载测试靶场(GitHub - l4yn3/micro_service_seclab: Java漏洞靶场)
2、生成数据库文件,出现Successfully,说明生成数据库文件成功.
codeql database create /Users/xxx/Desktop/tools/9-codeql/databases/test_database --language="java" --command="mvn clean install -DskipTests --file pom.xml" --source-root=/Users/xxx/Desktop/tools/9-codeql/micro_service_seclab
生成的数据库文件:
3、vscode配置ql数据库
安装codeql插件
将生成的数据库的文件夹放到vscode上面的codeql插件上,如下:
至此,实现了codeql静态代码分析
三、codeql基本语法
CodeQL 使用一种专门的查询语言,名为 QL。它是一种声明式的、面向对象的逻辑编程语言。别被这些名词吓到,它的核心思想其实非常直观:将代码视为数据,通过写查询来从这些数据中寻找特定模式(比如漏洞)。
一个标准的 CodeQL 查询结构非常类似于 SQL,主要由三个部分组成:from
、where
和 select
。
1. 核心结构:from
, where
, select
这是所有查询的基础骨架,规定了“从哪里查、满足什么条件、输出什么结果”。
from /* 1. 声明变量和类型 */
where /* 2. 定义逻辑条件和约束 */
select /* 3. 指定输出内容 */
from
子句:声明变量和类型
from
的作用是引入我们想查询的“东西”的变量,并指定它们的“类型”。你可以把它理解为:“我要从代码的数据库里,找出所有类型为 A 的东西,并给它们起个名字叫 a”。
类型(Classes): CodeQL 为不同语言预定义了大量的类型。例如:
Method
:代表一个方法或函数。MethodCall
:代表一次方法或函数调用。IfStmt
:代表一个if
语句。Expr
:代表一个表达式(Expression)。Parameter
:代表一个方法参数。
变量(Variables): 你为指定类型的实例起的名字,方便在后面引用。
示例:
// 引入两个变量:
// - m 是一个方法 (Method)
// - call 是一次方法调用 (MethodCall)
from Method m, MethodCall call
where
子句:定义逻辑条件
where
是查询的核心,用来定义变量之间必须满足的逻辑关系。只有满足 where
中所有条件的组合,才会被 select
出来。你可以使用 and
, or
, not
等逻辑连接词。
断言(Predicates): 这是 CodeQL 的“内置函数”,用来描述代码元素之间的关系。它们通常没有返回值,而是用来“断定”一个事实是否成立。
call.getCallee() = m
: 这断定call
这次方法调用所调用的目标方法正是m
。m.hasName("execute")
: 这断定方法m
的名字是 "execute"。ifstmt.getCondition()
: 获取if
语句的条件表达式。param.isType("string")
: 判断参数param
的类型是否是string
。
示例:
from Method m, MethodCall call
where
call.getCallee() = m and // 这次调用所调用的方法是 m
m.hasName("println") // 并且 m 的名字叫 "println"
这个 where
子句筛选出了所有调用名为 "println" 方法的调用事件。
select
子句:指定输出结果
select
用来定义最终的输出内容。你可以输出在 from
中声明的变量,也可以输出一段描述性的文字,或者两者的组合。
select
后面跟上变量名和你想展示的信息。你可以使用
as
关键字给输出的列起一个别名。
示例:
from Method m, MethodCall call
where
call.getCallee() = m and
m.hasName("println")
select call, "这是一个对 println 方法的调用"
这个查询最终会输出所有 MethodCall
(即 call
变量)的实例,并在第二列附带一段固定的描述文字。
2. Putting It All Together: 一个完整的例子
让我们结合上面的知识,写一个完整的查询。
目标:在 Java 代码中,找到所有对 System.out.println
方法的调用。
import java // 导入 Java 相关的 CodeQL 库
from MethodCall call, Method printlnMethod // 声明变量:call 和 printlnMethod
where
// 条件1: 我们要找的方法属于类 "PrintStream"
printlnMethod.getDeclaringType().hasQualifiedName("java.io", "PrintStream") and
// 条件2: 这个方法的名字叫 "println"
printlnMethod.hasName("println") and
// 条件3: call 这次调用实际调用的就是上面找到的方法
call.getCallee() = printlnMethod
select call, "发现一次对 System.out.println 的调用" // 输出这次调用的实例和一段描述
3. 其他重要语法概念
类型和继承 (Classes and Inheritance)
CodeQL 的类型系统是面向对象的,支持继承。这非常强大,因为你可以查询一个更抽象的类型。
Expr
(表达式) 是一个抽象类型。MethodCall
(方法调用),VariableAccess
(变量访问),Literal
(字面量) 等都继承自Expr
。
所以,如果你 from Expr e
,那么 e
就可以匹配代码中任何一种表达式。
断言 (Predicates)
断言是 CodeQL 的核心。你可以把它们看作是可重用的查询片段或自定义函数。断言分为两种:
无返回值的断言 (Predicate without result): 用来检查一个条件是否为真,常用于
where
子句。// 定义一个断言,判断一个方法是否是构造函数 predicate isConstructor(Method m) { m.hasName(m.getDeclaringType().getName()) } from Method m where isConstructor(m) // 在 where 中使用 select m
有返回值的断言 (Predicate with result): 类似于一个有返回值的函数,可以返回计算结果。
// 定义一个断言,返回一个方法的所有调用点 MethodCall getACallTo(Method m) { result.getCallee() = m } from Method dangerousMethod where dangerousMethod.hasName("eval") select getACallTo(dangerousMethod) // 在 select 中使用
这里的
result
是一个特殊关键字,代表该断言的返回值。
量词 (any
, count
, sum
...)
量词可以让你对一组值进行聚合计算或检查。
any()
: 任意一个。count()
: 计数。sum()
: 求和。avg()
: 平均值。min()
/max()
: 最小值/最大值。
示例:找到被调用超过10次的方法。
from Method m
where count(MethodCall call | call.getCallee() = m) > 10
select m, count(MethodCall call | call.getCallee() = m) as "调用次数"
这里的 |
用来分隔量词内部的变量声明和逻辑条件。
总之,我们可以根据我们自己的需求写ql脚本对代码进行审计,具体可以看看codeql官方文档学学怎么写CodeQL documentation。
4、简单编写
我们现在从零开始,一步步为我自己的 Java 代码写一个简单的 QL 查询,目标是学会这个流程,理解基本结构,并明白每个文件的作用。
我们将写一个查询,用来寻找你 Java 代码中所有调用 System.out.println()
的地方。因为几乎所有 Java 项目里都有 println,而且它
只涉及到最核心的“方法调用”概念,比较简单,方便我们理解;在很多生产项目中,直接使用 System.out.println
而不是专用的日志框架,通常被认为是一种不良实践。
第一步,创建一个 Java 示例项目 如果手上没有,可以快速创建一个。比如新建一个 MyTestApp
文件夹,在里面放一个 Main.java
文件
注意:我们需要一个规范的文件夹结构来存放我们的查询。CodeQL 不是单个 .ql
文件就能运行的,它需要一个“包”的概念。
第一步:创建一个MyTestApp的文件夹,在再 MyTestApp/src/main/java/com/example路径下创建一个Main.java文件。
// 文件: MyTestApp/src/main/java/com/example/Main.java
package com.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, CodeQL!"); // 这是我们的目标
String user = "admin";
System.out.println("User is: " + user); // 这是另一个目标
}
public void uselessLog() {
System.out.println("This is a useless log."); // 第三个目标
}
}
为该项目创建 CodeQL 数据库 在 VS Code 中打开这个
MyTestApp
文件夹,然后用命令面板 (⌘+⇧+P
) 运行CodeQL: Create Database
,选择 Java。创建你的查询包 (Query Pack) 这是最关键的一步。在另外一个地方(不要在你的 Java 项目里),创建一个新的文件夹,专门用来存放你的自定义查询。比如,我们叫它
my-java-queries
。
在这个 my-java-queries
文件夹里,创建两个文件:
a) qlpack.yml
(包配置文件) 这个文件是查询包的“身份证”,它告诉 CodeQL 这个包的名字、依赖项等信息。
# 文件: my-java-queries/qlpack.yml
name: my-custom-java-pack # 给你的包起个名字
version: 0.0.1
library: false
dependencies:
codeql/java-queries: ^0.8.0 # !!!至关重要:声明依赖官方Java查询库
dependencies
是最重要的部分。它表示我们的查询要构建在官方的 java-queries
库之上,这样我们才能使用像 MethodCall
这种预定义的 Java 类型。
b) FindPrintlnCalls.ql
(QL 查询文件) 这是我们马上要编写查询逻辑的地方。现在可以先创建一个空文件。
将查询包添加到 VS Code 工作区
在 VS Code 中,选择
File > Add Folder to Workspace...
。选择你刚刚创建的
my-java-queries
文件夹。现在你的 VS Code 左侧应该能同时看到
MyTestApp
(你的Java代码) 和my-java-queries
(你的QL代码) 这两个文件夹了。
至此,环境准备完毕!我们知道了 qlpack.yml
是项目定义文件,.ql
是具体的查询文件。
第二步:编写 QL 查询语句(“基本结构”)
现在,打开 my-java-queries/FindPrintlnCalls.ql
文件,我们来一步步写查询。
1. 导入库和元数据
在文件最上方,我们先写一些“标准开头”。
/**
* @name Find System.out.println calls
* @description Finds all calls to the method System.out.println.
* @kind problem
* @id java/find-println-calls
*/
import java // 导入 CodeQL 的 Java 标准库
/** ... */
里的内容是元数据 (Metadata)。@name
和@description
会显示在 VS Code 的界面里,方便你识别。@kind problem
告诉 VS Code 这是一个寻找“问题”的查询,结果会以告警的形式展示。@id
是这个查询的唯一标识符。
import java
是导入语句,它让我们能够使用所有 CodeQL 预先定义好的、用于分析 Java 的类和断言。
2. 编写 from-where-select
核心逻辑
接下来就是我们的 from-where-select
三部曲。
from
MethodCall call // 1. from: 我们要找的东西是“方法调用”,我们给它起个名字叫 call
where
// 2. where: 这个“方法调用”必须满足下面的条件
call.getCallee().hasName("println") and // a) 它调用的方法,名字必须是 "println"
call.getCallee().getDeclaringType().hasQualifiedName("java.io", "PrintStream") // b) 并且,声明这个方法的类的全限定名是 "java.io.PrintStream"
select
call, "Avoid using System.out.println() in production code." // 3. select: 如果满足条件,就选中这个调用(call),并附带一句提示信息
逻辑分解:
from MethodCall call
:MethodCall
是 CodeQL Java 库里预定义的一个类,代表了代码中所有的方法调用。我们声明一个变量call
来代表MethodCall
的每一个实例。where
: 这是筛选条件。call.getCallee()
: 这个断言 (predicate) 可以获取到call
这个调用所指向的具体方法。.hasName("println")
: 这是Method
类的一个断言,判断方法名是否是 "println"。.getDeclaringType()
: 获取声明这个方法的类。.hasQualifiedName("java.io", "PrintStream")
: 这是Type
类的一个断言,判断类的完整包名和类名。我们都知道System.out
是java.io.PrintStream
类型。
select call, "..."
: 这是输出。第一个参数
call
告诉 CodeQL 高亮显示代码中找到的方法调用。第二个参数是字符串,将作为结果的描述信息显示出来。
第三步:运行查询并查看结果
确保你当前选中的数据库是
MyTestApp
的数据库。在你编写的
FindPrintlnCalls.ql
文件编辑器窗口中,单击右键。从菜单中选择
CodeQL: Run Query on Selected Database
(和你截图中高亮的那个一样)。等待几秒钟,VS Code 的 "CodeQL Results" 窗口就会弹出结果。
你应该能看到 3 个结果,点击其中任何一个,VS Code 都会自动跳转到
Main.java
文件里对应的System.out.println(...)
那一行代码!
恭喜!你已经成功编写并运行了你的第一个自定义 CodeQL 查询!
学习参考:zangcc的【作者踩坑总结0错版】vscode配置codeql-MacBook(M1/M2芯片-arm).